diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..451bb46 --- /dev/null +++ b/COPYING @@ -0,0 +1,27 @@ +Copyright 2019 Chloé Kekoa + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..be647b0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "contralog" +version = "0.0.1" + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2d04ada --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "contralog" +version = "0.0.1" +license-file = "COPYING" + +authors = ["Chloé Kekoa"] +description = "Composable logging with monoids and contravariant functors." +homepage = "https://github.com/chloekek/contralog.git" +documentation = "https://docs.rs/contralog" +repository = "https://github.com/chloekek/contralog.git" diff --git a/README b/README new file mode 100644 index 0000000..61ae1e2 --- /dev/null +++ b/README @@ -0,0 +1,11 @@ +Composable logging with monoids and contravariant functors. + +If you want to use this library there are at least two ways to obtain it: you +could depend on it with Cargo by adding contralog as a dependency in +Cargo.toml, or you could copy the src/lib.rs file into your own project under +a different name. + +When developing this library please use Nix to ensure the build is +reproducible: + + nix run -ic cargo test diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..8ec9a47 --- /dev/null +++ b/default.nix @@ -0,0 +1,7 @@ +let + pkgs = import ./nix/pkgs.nix {}; +in + [ + pkgs.cargo + pkgs.gcc + ] diff --git a/nix/pkgs.nix b/nix/pkgs.nix new file mode 100644 index 0000000..e5b2a73 --- /dev/null +++ b/nix/pkgs.nix @@ -0,0 +1,8 @@ +let + tarball = fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/f5cc5ce8d60fe69c968582434fbfbf8f350555cb.tar.gz"; + sha256 = "025773zp9hvizwf4frimm7mnr6cydmckw7kayqmik6scisq0mfk5"; + }; + config = {}; +in + {}: import tarball {inherit config;} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4dca380 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,247 @@ +//! Composable logging with monoids and contravariant functors. +//! +//! A logger is a routine that takes input and has side-effects. +//! Any routine that has the appropriate type will do. +//! A logger can be seen as the opposite or dual of an infinite iterator. +//! +//! The core trait of this crate is [Logger]. +//! It has only a single method that must be implemented: log. +//! To log something, pass it to this method. +//! It is up to the logger to decide what to do with the value. +//! +//! Loggers are composable: +//! given two loggers with compatible types, +//! a new logger can be created that forwards +//! its input to both loggers. +//! +//! Loggers can also be transformed using +//! methods such as [map] and [filter]. +//! +//! [Logger]: trait.Logger.html +//! [map]: trait.Logger.html#method.map +//! [filter]: trait.Logger.html#method.filter + +use std::convert::Infallible; +use std::iter; +use std::marker::PhantomData; + +/// A logger is a routine that takes input and has side-effects. +/// +/// See the documentation for the [contralog](index.html) crate +/// for a general overview of loggers. +pub trait Logger +{ + type Error; + fn log(&mut self, item: I) -> Result<(), Self::Error>; + + /// Create a “by reference” adaptor for this logger. + fn by_ref(&mut self) -> &mut Self + { + self + } + + /// Combine two loggers, creating a new logger + /// that logs each input to both loggers. + fn chain(self, other: L) -> Chain + where Self: Sized + { + Chain{fst: self, snd: other} + } + + /// Apply a function to each input and + /// pass it to the logger + /// only if the function returns true for it. + fn filter(self, f: F) -> Filter + where Self: Sized, F: FnMut(&I) -> bool + { + Filter{inner: self, f} + } + + /// Apply a function to each input + /// before passing it to the logger. + fn map(self, f: F) -> Map + where Self: Sized + { + Map{inner: self, f, _phantom: PhantomData} + } + + /// Return a logger that + /// silently drops errors reported by this logger. + fn safe(self) -> Safe + where Self: Sized + { + Safe{inner: self, _phantom: PhantomData} + } +} + +impl<'a, I, L> Logger for &'a mut L + where L: Logger +{ + type Error = L::Error; + fn log(&mut self, item: I) -> Result<(), Self::Error> + { + (**self).log(item) + } +} + +/// Returned from the [Logger::chain](trait.Logger.html#method.chain) method. +pub struct Chain +{ + fst: L, + snd: M, +} + +impl Logger for Chain + where L: Logger, M: Logger, I: Clone +{ + type Error = L::Error; + fn log(&mut self, item: I) -> Result<(), Self::Error> + { + self.fst.log(item.clone())?; + self.snd.log(item) + } +} + +/// A logger that ignores all input. +pub fn empty() -> Empty +{ + Empty{_phantom: PhantomData} +} + +/// Returned from the [empty](fn.empty.html) function. +pub struct Empty +{ + _phantom: PhantomData (I, E)>, +} + +impl Logger for Empty +{ + type Error = E; + fn log(&mut self, _item: I) -> Result<(), E> + { + Ok(()) + } +} + +/// A logger that collects values into a container. +pub fn extender(container: C) -> Extender +{ + Extender{container, _phantom: PhantomData} +} + +/// Returned from the [extender](fn.extender.html) function. +pub struct Extender +{ + pub container: C, + _phantom: PhantomData I>, +} + +impl Logger for Extender + where C: Extend +{ + type Error = Infallible; + fn log(&mut self, item: I) -> Result<(), Self::Error> + { + let from = iter::once(item); + self.container.extend(from); + Ok(()) + } +} + +/// Returned from the [Logger::filter](trait.Logger.html#method.filter) method. +pub struct Filter +{ + inner: L, + f: F, +} + +impl Logger for Filter + where L: Logger, F: FnMut(&I) -> bool +{ + type Error = L::Error; + fn log(&mut self, item: I) -> Result<(), Self::Error> + { + if (self.f)(&item) { + self.inner.log(item) + } else { + Ok(()) + } + } +} + +/// Returned from the [Logger::map](trait.Logger.html#method.map) method. +pub struct Map +{ + inner: L, + f: F, + _phantom: PhantomData B>, +} + +impl Logger for Map + where L: Logger, F: FnMut(B) -> I +{ + type Error = L::Error; + fn log(&mut self, item: B) -> Result<(), Self::Error> + { + let new_item = (self.f)(item); + self.inner.log(new_item) + } +} + +/// Returned from the [Logger::safe](trait.Logger.html#method.safe) method. +pub struct Safe +{ + inner: L, + _phantom: PhantomData E>, +} + +impl Logger for Safe + where L: Logger +{ + type Error = E; + fn log(&mut self, item: I) -> Result<(), Self::Error> + { + let result = self.inner.log(item); + drop(result); + Ok(()) + } +} + +#[cfg(test)] +mod tests +{ + use super::*; + + #[test] + fn test_chain() + { + let mut fst = extender(Vec::new()); + let mut snd = extender(Vec::new()); + let mut thd = fst.by_ref().chain(&mut snd); + thd.log(0).unwrap(); + assert_eq!(&fst.container, &[0]); + assert_eq!(&snd.container, &[0]); + } + + #[test] + fn test_filter() + { + let mut fst = extender(Vec::new()); + let mut snd = fst.by_ref().filter(|&i| i >= 0); + snd.log(-1).unwrap(); + snd.log(0).unwrap(); + snd.log(1).unwrap(); + assert_eq!(&fst.container, &[0, 1]); + } + + #[test] + fn test_map() + { + let mut fst = extender(Vec::new()); + let mut snd = fst.by_ref().map(|i: i32| i.abs()); + snd.log(-1).unwrap(); + snd.log(0).unwrap(); + snd.log(1).unwrap(); + assert_eq!(&fst.container, &[1, 0, 1]); + } +}