packages feed

di 0.3 → 1.0

raw patch · 8 files changed

+199/−600 lines, 8 filesdep +df1dep +di-coredep +di-df1dep −QuickCheckdep −bytestringdep −didep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: df1, di-core, di-df1, di-handle, di-monad, exceptions

Dependencies removed: QuickCheck, bytestring, di, stm, tasty, tasty-hunit, tasty-quickcheck, time, transformers

Dependency ranges changed: base

API changes (from Hackage documentation)

- Di: contralevel :: (level -> level') -> Di level' path msg -> Di level path msg
- Di: contramsg :: (msg -> msg') -> Di level path msg' -> Di level path msg
- Di: contrapath :: (path -> path') -> Di level path' msg -> Di level path msg
- Di: filter :: (level -> Bool) -> Di level path msg -> Di level path msg
- Di: flush :: MonadIO m => Di level path msg -> m ()
- Di: log :: (MonadIO m, Monoid path) => Di level path msg -> level -> msg -> m ()
- Di: mkDi :: MonadIO m => (UTCTime -> level -> path -> msg -> IO ()) -> m (Di level path msg)
- Di: mkDiStringHandle :: (MonadIO m) => Handle -> m (Di String [String] String)
- Di: mkDiStringStderr :: MonadIO m => m (Di String [String] String)
+ Di: alert :: MonadDi Level path Message m => Message -> m ()
+ Di: attr :: MonadDi level Path msg m => Key -> Value -> m a -> m a
+ Di: class Monad m => MonadDi level path msg (m :: * -> *) | m -> level path msg
+ Di: critical :: MonadDi Level path Message m => Message -> m ()
+ Di: data DiT level path msg (m :: * -> *) a
+ Di: data Key
+ Di: data Message
+ Di: data Segment
+ Di: data Value
+ Di: debug :: MonadDi Level path Message m => Message -> m ()
+ Di: emergency :: MonadDi Level path Message m => Message -> m ()
+ Di: error :: MonadDi Level path Message m => Message -> m ()
+ Di: hoistDiT :: () => forall x. () => n x -> m x -> forall x. () => m x -> n x -> DiT level path msg m a -> DiT level path msg n a
+ Di: info :: MonadDi Level path Message m => Message -> m ()
+ Di: key :: Text -> Key
+ Di: new :: (MonadIO m, MonadMask m) => (Di Level Path Message -> m a) -> m a
+ Di: notice :: MonadDi Level path Message m => Message -> m ()
+ Di: runDiT :: MonadIO m => Di level path msg -> DiT level path msg m a -> m a
+ Di: segment :: Text -> Segment
+ Di: value :: Text -> Value
+ Di: warning :: MonadDi Level path Message m => Message -> m ()
- Di: push :: Monoid path => path -> Di level path msg -> Di level path msg
+ Di: push :: MonadDi level Path msg m => Segment -> m a -> m a

Files

CHANGELOG.md view
@@ -1,3 +1,11 @@+# Version 1.0++* BREAKING CHANGE: Most of what used to be in this library lives now in+  `di-core`. This library is now intended to be an entry point to the various+  `di-*` libraries. Consider this first release of the new ecosystem a preview+  release: The API is likely to stay stable, but extensive testing,+  formalization and tooling is due.+ # Version 0.3  * BREAKING CHANGE: `mkDiTextStderr` and `mkDiStringHandle` return a `Di String
README.md view
@@ -1,12 +1,14 @@ # di -Easy and powerful typeful logging without monad towers.+Typeful hierarchical structured logging using di, mtl and df1. +This is meta-package bringing in together things from the `di-core`,+`di-monad`, `di-handle` and `di-df1` libraries.++See the `Di` module for more documentation.+ [![Build Status](https://travis-ci.org/k0001/di.svg?branch=master)](https://travis-ci.org/k0001/di)  See the [BSD3 LICENSE](https://github.com/k0001/di/blob/master/di/LICENSE.txt) file to learn about the legal terms and conditions for this library.--Find documentation for this library in the top-level-[`Di`](https://github.com/k0001/di/blob/master/src/Di.hs) module. 
di.cabal view
@@ -1,5 +1,5 @@ name: di-version: 0.3+version: 1.0 author: Renzo Carbonara maintainer: renλren.zone copyright: Renzo Carbonara 2017-2018@@ -9,31 +9,29 @@ category: Logging build-type: Simple cabal-version: >=1.18-synopsis: Easy, powerful, structured and typeful logging without monad towers.-description: Easy, powerful, structured and typeful logging without monad towers.+synopsis: Typeful hierarchical structured logging using di, mtl and df1.+description:+  Typeful hierarchical structured logging using di, mtl and df1.+  .+  This is meta-package bringing in together things from the @di-core@,+  @di-monad@, @di-handle@ and @di-df1@ libraries.+  .+  See the "Di" module for more documentation. homepage: https://github.com/k0001/di bug-reports: https://github.com/k0001/di/issues  library-  hs-source-dirs: src+  hs-source-dirs: lib   default-language: Haskell2010   exposed-modules: Di-  other-modules: Di.Core Di.Backend-  build-depends: base >=4.9 && <5.0, stm, time, transformers+  build-depends:+    base >=4.9 && <5.0,+    df1,+    di-core,+    di-df1,+    di-handle,+    di-monad,+    exceptions   ghcjs-options: -Wall -O3   ghc-options: -Wall -O2 -test-suite test-  default-language: Haskell2010-  type: exitcode-stdio-1.0-  hs-source-dirs: test-  main-is: Main.hs-  build-depends:-    base,-    bytestring,-    di,-    stm,-    QuickCheck,-    tasty,-    tasty-hunit,-    tasty-quickcheck
+ lib/Di.hs view
@@ -0,0 +1,167 @@+{-# LANGUAGE OverloadedStrings #-}+-- | This module is a highly opinionated, basic, and yet surprisingly sufficient+-- choice of a concrete stack of logging solutions belonging to the [di+-- logging ecosystem](https://github.com/k0001/di)—an otherwise rather+-- general ecosystem, flexible and full of choices.+--+-- For most logging scenarios out there, the choices made here should suffice,+-- but if you find these are not sufficient for your particular use case, please+-- refer to other libraries of the /di logging ecosystem/ such as+-- [di-core](https://hackage.haskell.org/package/di-core),+-- [di-monad](https://hackage.haskell.org/package/di-core),+-- [di-handle](https://hackage.haskell.org/package/di-core), or+-- [di-df1](https://hackage.haskell.org/package/di-core), and you are likely+-- to find a compatible and composable solution there. For this reason, staring+-- with this package rather than one of the those other lower-level packages is+-- always recommended.+--+-- The choices made here are:+--+-- * We encourage a [mtl](https://hackage.haskell.org/package/mtl) approach+--   through a typeclass called 'Di.Monad.MonadDi', for which all of the+--   monad transformers in+--   [transformers](https://hackage.haskell.org/package/transformers) and+--   [pipes](https://hackage.haskell.org/package/pipes) have instances.+--+-- * We provide our own 'Di.Monad.DiT' monad transformer which+--   has a 'MonadDi' instance, as well as instances for all the relevant+--   typeclasses in the+--   [base](https://hackage.haskell.org/package/base),+--   [mtl](https://hackage.haskell.org/package/mtl), and+--   [exceptions](https://hackage.haskell.org/package/exceptions) libraries.+--   All of the 'MonadDi' instances exported by this package expect a+--   'DiT' transformer in the stack somewhere, and defer all work to it.+--+-- * We embrace the [df1](https://hackage.haskell.org/package/df1) hierarchical+--   structured logging format, both at the type-level and when rendering the+--   log lines as text. Most notably, this means that we embrace the /df1/+--   importance 'Df1.Level's.+--+-- * We commit logs to the outside world by printing them to 'System.IO.stderr'.+--+-- You will notice that some of the functions in this module mention the types+-- 'Df1.Level', 'Df1.Path' and 'Df1.Message', and some other functions+-- talk about @level@, @path@ and @msg@ type variables. This is+-- because even while our particular set of choices require some monomorphic+-- types, the /di logging ecosystem/ treats these values polymorphically, so+-- they will show up in the types in one way or another, either in concrete or+-- polymorphic form. This can seem a bit noisy, but the good news is that if,+-- for example, want to call a third party library that uses other types+-- for conveying the idea of a “log importance level” or a “log message”,+-- then you can do so if you can convert between these different types.+-- For more information about this, see "Di.Monad" and "Di.Core", but not today.+--+-- The intended usage of this module is:+--+-- @+-- import qualified "Di"+-- @+module Di+ ( new+ , Di.Core.Di++   -- * Monadic API+ , Di.Monad.MonadDi++   -- ** Hierarchy+ , Di.Df1.Monad.push+ , Df1.Segment+ , Df1.segment++   -- ** Metadata+ , Di.Df1.Monad.attr+ , Df1.Key+ , Df1.key+ , Df1.Value+ , Df1.value++   -- ** Messages+ , Df1.Message+ , Di.Df1.Monad.debug+ , Di.Df1.Monad.info+ , Di.Df1.Monad.notice+ , Di.Df1.Monad.warning+ , Di.Df1.Monad.error+ , Di.Df1.Monad.alert+ , Di.Df1.Monad.critical+ , Di.Df1.Monad.emergency++   -- * Basic DiT support+ , Di.Monad.DiT+ , Di.Monad.runDiT+ , Di.Monad.hoistDiT+ ) where++import Control.Monad.Catch as Ex+import Control.Monad.IO.Class (MonadIO)++import qualified Df1+import qualified Di.Core+import qualified Di.Df1+import qualified Di.Df1.Monad+import qualified Di.Handle+import qualified Di.Monad++--------------------------------------------------------------------------------++-- | Obtain a 'Di.Core.Di' that will write logs in the /df1/ format to+-- 'System.IO.stderr'.+--+-- Generally, you will want to call 'new' just once per application, right from+-- your @main@ function. For example:+--+-- @+-- main :: 'IO' ()+-- main = do+--    'new' $ \\di -> do+--       'Di.Monad.runDiT' di $ do+--           -- /The rest of your program goes here./+--           -- /You can start logging right away./+--           'Di.Df1.Monad.notice' "Welcome to my program!"+--           -- /You can use 'Di.Df1.Monad.push' to separate different/+--           -- /logging scopes of your program:/+--           'Di.Df1.Monad.push' "initialization" $ do+--               -- /something something do initialization/+--               'Di.Df1.Monad.notice' "Starting web server"+--           'Di.Df1.Monad.push' "server" $ do+--               -- /And you can use 'Di.Df1.Monad.attr' to add metadata to/+--               -- /messages logged within a particular scope./+--               'Di.Df1.Monad.attr' "port" "80" $ do+--                    'Di.Df1.Monad.info' "Listening for new clients"+--                    clientAddress <- /somehow get a client connection/+--                    'Di.Df1.Monad.push' "handler" $ do+--                       'Di.Df1.Monad.attr' "client-address" clientAddress $ do+--                          'Di.Df1.Monad.info' "Connection established"+-- @+--+-- That program will render something like this to 'System.IO.stderr' (in colors!):+--+-- @+-- 2018-05-06T19:48:06.194579393Z NOTICE Welcome to my program!+-- 2018-05-06T19:48:06.195041422Z \/initialization NOTICE Starting web server+-- 2018-05-06T19:48:06.195052862Z \/server port=80 INFO Listening for new clients+-- 2018-05-06T19:48:06.195059084Z \/server port=80 \/handler client%2daddress=192%2e168%2e0%2e25%3a32528 INFO Connection established+-- @+--+-- (Unrelated: Notice how /df1/ escapes pretty much all punctuation characters.+-- This is temporal until /df1/ is formalized and a more limited set of+-- punctuation characters is reserved.)+new+  :: (MonadIO m, Ex.MonadMask m)+  => (Di.Core.Di Df1.Level Df1.Path Df1.Message -> m a)+  -- ^ Within this scope, you can use the obtained 'Di.Core.Di' safely, even+  -- concurrently. As soon as @m a@ finishes, 'new' will block until+  -- all logs have finished processing, before returning.+  --+  -- /WARNING:/ Even while+  -- @'new' commit 'pure' :: m ('Di.Core.Di' 'Df1.Level' 'Df1.Path' 'Df1.Message')@+  -- type-checks, and you can use it to work with the 'Di.Core.Di' outside the+  -- intended scope, you will have to remember to call 'Di.Monad.flush'+  -- yourself before exiting your application. Otherwise, some log messages may+  -- be left unprocessed. If possible, use the 'Di.Core.Di' within this function+  -- and don't let it escape this scope.+  -> m a -- ^+new act = do+  commit <- Di.Handle.stderr Di.Df1.df1+  Di.Core.new commit act+
− src/Di.hs
@@ -1,66 +0,0 @@--- |------ Import this module as follows:------ @--- import Di (Di)--- import qualified Di--- @---module Di- ( -- * Usage-   -- $usage-   Di- , log- , flush- , push- , filter- , contralevel- , contrapath- , contramsg-   -- * Backends- , mkDi- , B.mkDiStringStderr- , B.mkDiStringHandle- ) where--import Prelude hiding (filter, log)--import Di.Core-  (Di, log, flush, push, filter, contralevel, contrapath, contramsg, mkDi)-import qualified Di.Backend as B----- $usage------ First, read the documentation for the 'Di' datatype.------ Second, create a base 'Di' value. You will achieve this by using 'mkDi' or,--- more likely, one of the ready-made 'B.mkDiStringStderr',--- 'B.mkDiStringHandle', etc.------ At this point, you can start logging messages using 'log'. However, things--- can be made more interesting.------ Your choice of base 'Di' will mandate particular types for the @level@,--- @path@ and @msg@ arguments to 'Di'. However, these base types are likely to--- be very general (e.g., 'String'), so quite likely you'll want to use--- 'contralevel', 'contrapath' and 'contramsg' to make those types more--- specific. For example, you can use a precise datatype like the following for--- associating each log message with a particular importance level:------ @--- data Level = Error | Info | Debug---   deriving ('Show')--- @------ Now, assuming the your base 'Di' @level@ type is 'String', you can use--- @'contralevel' 'show'@ to convert your @'Di' 'String' path msg@ to a @'Di'--- 'Level' path msg@. The same approach applies to @path@ and @msg@ as well,--- through the 'contrapath' and 'contramsg' functions respectively.------ /Hint/: If you are building a library, be sure to export your @Level@--- datatype so that users of your library can 'contralevel' your 'Level'--- datatype as necessary.-
− src/Di/Backend.hs
@@ -1,97 +0,0 @@-{-# LANGUAGE LambdaCase #-}--module Di.Backend- ( mkDiStringStderr- , mkDiStringHandle- ) where--import Control.Monad.IO.Class (MonadIO, liftIO)-import Data.Monoid (mconcat, mappend)-import Data.String (IsString(fromString))-import qualified Data.Time as Time-import Prelude hiding (log, filter)-import qualified System.IO as IO-import Data.Semigroup (Semigroup(..))--import Di.Core (Di, mkDi, contrapath)-------------------------------------------------------------------------------------- | Strings separated by a forward slash.------ The string doesn't contain any of @[\'/'\, \' \', \'\\n\', \'\\r\']@.------ Use 'fromString' (GHC's @OverloadedStrings@ extension) to construct a--- 'StringPath'.-newtype StringPath = UnsafeStringPath { unStringPath :: String }-  deriving (Eq, Ord, Show)--instance IsString StringPath where-  fromString = stringPathSingleton-  {-# INLINE fromString #-}---- | Ocurrences of one of @[\'/'\, \' \', \'\\n\', \'\\r\']@ in the given--- 'String' will be replaced by @\'_\'@.-stringPathSingleton :: String -> StringPath-stringPathSingleton = \s -> UnsafeStringPath (map f s)-  where f :: Char -> Char-        f = \case '/'  -> '_'-                  ' '  -> '_'-                  '\n' -> '_'-                  '\r' -> '_'-                  c    -> c--instance Semigroup StringPath where-  UnsafeStringPath "" <> b = b-  a <> UnsafeStringPath "" = a-  UnsafeStringPath a <> UnsafeStringPath b = UnsafeStringPath (a <> "/" <> b)--instance Monoid StringPath where-  mempty = UnsafeStringPath ""-  {-# INLINE mempty #-}-  mappend = (<>)-  {-# INLINE mappend #-}--- | 'String's are written to 'IO.Handle' using the 'IO.Handle''s locale--- encoding.------ The @level@ and @message@ are plain 'String's.------ The @path@ is a list of 'String's which will be separated by @\'/\'@ when--- rendered. Ocurrences of one of @[\'/'\, \' \', \'\\n\', \'\\r\']@ in those--- 'String's will be replaced by @\'_\'@.------ @--- > 'log' ('push' [\"cuatro\"] ('push' [\"uno dos\", \"tres\"] di)) \"WARNING\" \"Hello!\"--- WARNING 2018-05-03T09:15:54.819379740000Z uno_dos\/tres\/cuatro: Hello!--- @-mkDiStringHandle-  :: (MonadIO m)-  => IO.Handle-  -> m (Di String [String] String)-mkDiStringHandle h = liftIO $ do-    IO.hSetBuffering h IO.LineBuffering-    fmap (contrapath (mconcat . map stringPathSingleton)) $ do-       mkDi $ \ts l p m -> do-          IO.hPutStrLn h $ mconcat-             [ l, " ", renderIso8601 ts-             , if p == mempty then "" else (" " <> unStringPath p)-             , ": ", noBreaks m ]-          IO.hFlush h-  where-    noBreaks :: String -> String-    noBreaks = concatMap $ \case-      '\n' -> "\\n"-      '\r' -> "\\r"-      c -> [c]---- |--- @--- 'mkDiStringStderr'  ==  'mkDiStringHandle' 'IO.stderr'--- @-mkDiStringStderr :: MonadIO m => m (Di String [String] String)-mkDiStringStderr = mkDiStringHandle IO.stderr------------------------------------------------------------------------------------renderIso8601 :: Time.UTCTime -> String-renderIso8601 = Time.formatTime Time.defaultTimeLocale "%Y-%m-%dT%H:%M:%S.%qZ"
− src/Di/Core.hs
@@ -1,277 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Di.Core- ( Di(..)- , mkDi- , log- , flush- , push- , filter- , contralevel- , contrapath- , contramsg- ) where--import Control.Concurrent (forkFinally, myThreadId)-import Control.Concurrent.STM-import qualified Control.Exception as Ex-import Control.Monad (when)-import Control.Monad.IO.Class (MonadIO, liftIO)-import Data.Function (fix)-import Data.Monoid (mconcat, (<>))-import qualified Data.Time as Time-import Prelude hiding (log, filter)-import qualified System.IO as IO-------------------------------------------------------------------------------------- | @'Di' level path msg@ allows you to to log messages of type @msg@, with a--- particular importance @level@, under a scope identified by @path@ (think of--- @path@ as a filesystem path that you can use to group together related log--- messages).------ Each @msg@ gets logged together with its @level@, @path@ and the--- 'Time.UTCTime' stating the instant when the logging requests was made.------ Even though logging is usually associated with rendering text, 'Di' makes no--- assumption about the types of the @msg@ values being logged, nor the @path@--- values that convey their scope, nor the @level@ values that convey their--- importance. Instead, it delays conversion from these precise types into the--- ultimately desired raw representation (if any) as much as possible. This--- makes it possible to log more precise information (for example, logging a--- datatype of your own without having to convert it to text first), richer--- scope paths (for example, the scope could be a 'Data.Map.Strict.Map' that--- gets enriched with more information as we 'push' down the @path@), and--- importance @level@s that are never too broad nor too narrow. This improves--- type safety, as well as the composability of the @level@, @path@ and @msg@--- values. In particular, all of @level@, @path@ and @msg@ are contravariant--- values, which in practice means including a precise 'Di' into a more general--- 'Di' is always possible (see the 'contralevel', 'contrapath' and 'contramsg'--- functions).------ Messages of undesired importance levels can be muted by using 'filter'.------ Contrary to other logging approaches based on monad transformers, a 'Di' is a--- value that is expected to be passed around explicitly. Of course, if--- necessary you can always put a 'Di' in some internal monad state or--- environment and provide a custom API for it. That's a choice you can make.------ A 'Di' can be safely used concurrently, and messages are rendered in the--- absolute order they were submitted for logging.------ 'Di' is pronounced as \"dee" (not \"die" nor \"dye" nor \"day"). \"Di" is--- the spanish word for an imperative form of the verb \"decir", which in--- english means "to say", which clearly has something to do with logging.-data Di level path msg = Di-  { _diLog :: Time.UTCTime -> level -> path -> msg -> IO ()-    -- ^ Low level logging function.-  , _diFilter :: level -> Bool-    -- ^ Whether a particular message @level@ should be logged or not.-  , _diLogs :: TQueue (IO ())-    -- ^ Work queue. This queue keeps fully applied '_diLog' calls.-  }---- | Build a new 'Di' from a logging function.------ /Note:/ If the passed in 'IO' function throws a exception, it will be--- just logged to 'IO.stderr' and then ignored.------ /Note:/ There's no need to "release" the obtained 'Di'.-mkDi-  :: MonadIO m-  => (Time.UTCTime -> level -> path -> msg -> IO ())-  -> m (Di level path msg)  -- ^-mkDi f = liftIO $ do-   di <- Di f (const True) <$> newTQueueIO-   me <- myThreadId-   _ <- forkFinally (worker di) (either (Ex.throwTo me) pure)-   pure di- where-   worker :: Di level path msg -> IO ()-   worker di = fix $ \k -> do-     eio <- Ex.try $ atomically $ do-        io <- peekTQueue (_diLogs di)-        pure (Ex.finally io (atomically (readTQueue (_diLogs di))))-     case eio of-        Left (_ :: Ex.BlockedIndefinitelyOnSTM) -> do-           pure ()  -- Nobody writes to '_diLogs' anymore, so we can just stop.-        Right io -> do-           catchSync io $ \se -> do-              ts <- fmap renderIso8601 Time.getCurrentTime-              IO.hPutStrLn IO.stderr $ mconcat-                 [ "Logging error at ", ts-                 , ": could not log message at due to "-                 , Ex.displayException se, ". Ignoring." ]-           k---- | Log a message with the given importance @level@.------ This function returns immediately after queing the message for logging in a--- different thread. If you want to explicitly wait for the message to be--- logged, then call 'flush' afterwards.------ /Note:/ No exceptions from the underlying logging backend (i.e., the 'IO'--- action given to 'mkDi') will be thrown from 'log'. Instead, those will be--- recorded to 'IO.stderr' and ignored.-log :: (MonadIO m, Monoid path) => Di level path msg -> level -> msg -> m ()-log (Di dLog dFilter dLogs) !l = \(!m) ->-   when (dFilter l) $ liftIO $ do-      ts <- Time.getCurrentTime-      atomically $ writeTQueue dLogs $! dLog ts l mempty m-{-# INLINABLE log #-}----- | Block until all messages being logged have finished processing.------ Manually calling 'flush' is not usually necessary, but, if at some point you--- want to ensure that all messages logged until then have properly rendered to--- the underlying backend, then 'flush' will block until that happens.-flush :: MonadIO m => Di level path msg -> m ()-flush di = liftIO (atomically (check =<< isEmptyTQueue (_diLogs di)))-{-# INLINABLE flush #-}----- | Returns a new 'Di' on which only messages with a @level@ satisfying the--- given predicate—in addition to any previous 'filter's—are ever logged.------ Identity:------ @--- 'filter' ('const' 'True')    ==   'id'--- @------ Composition:------ @--- 'filter' ('Control.Applicative.liftA2' (&&) f g)   ==   'filter' f . 'filter' g--- @------ Conmutativity:------ @--- 'filter' f . 'filter' g    ==    'filter' g . 'filter' f--- @-filter :: (level -> Bool) -> Di level path msg -> Di level path msg-filter f = \di -> di { _diFilter = \l -> f l && _diFilter di l }-{-# INLINABLE filter #-}----- | Push a new @path@ to the 'Di'.------ Identity:------ @--- 'push' 'mempty'   ==   'id'--- @------ Composition:------ @--- 'push' (a <> b)   ==   'push' b . 'push' a--- @-push :: Monoid path => path -> Di level path msg -> Di level path msg-push ps = \(Di dLog dFilter dLogs) ->-  Di (\ts l p1 m -> dLog ts l (ps <> p1) m) dFilter dLogs-{-# INLINABLE push #-}----- | A 'Di' is contravariant in its @level@ argument.------ This function is used to go from a /more general/ to a /less general/ type--- of @level@. For example, @data Level = Info | Error@ is a less general type--- than @data Level' = Info' | Warning' | Error'@, since the former can only--- convey two logging levels, whereas the latter can convey three. We can--- convert from the more general to the less general @level@ type using this--- 'contralevel' function:------ @--- 'contralevel' (\\case { Info -> Info'; Error -> Error' }) (di :: 'Di' Level' ['String'] msg)---     :: 'Di' Level ['Int'] msg--- @------ Identity:------ @--- 'contralevel' 'id'   ==   'id'--- @------ Composition:------ @--- 'contralevel' (f . g)   ==   'contralevel' g . 'contralevel' f--- @-contralevel :: (level -> level') -> Di level' path msg  -> Di level path msg-contralevel f = \(Di dLog dFilter dLogs) ->-  Di (\ts l p m -> dLog ts (f l) p m) (\l -> dFilter (f l)) dLogs-{-# INLINABLE contralevel #-}---- | A 'Di' is contravariant in its @path@ argument.------ This function is used to go from a /more general/ to a /less specific/ type--- of @path@. For example, @['Int']@ is a less general type than @['String']@,--- since the former clearly conveys the idea of a list of numbers, whereas the--- latter could be a list of anything that is representable as 'String', such as--- names of fruits and poems. We can convert from the more general to the less--- general @path@ type using this 'contrapath' function:------ @--- 'contrapath' ('map' 'show') (di :: 'Di' level ['String'] msg)---     :: 'Di' ['Int'] msg--- @------ Identity:------ @--- 'contrapath' 'id'   ==   'id'--- @------ Composition:------ @--- 'contrapath' (f . g)   ==   'contrapath' g . 'contrapath' f--- @-contrapath :: (path -> path') -> Di level path' msg  -> Di level path msg-contrapath f = \(Di dLog dFilter dLogs) ->-  Di (\ts l p m -> dLog ts l (f p) m) dFilter dLogs-{-# INLINABLE contrapath #-}---- | A 'Di' is contravariant in its @msg@ argument.------ This function is used to go from a /more general/ to a /less general/ type--- of @msg@. For example, @'Int'@ is a less general type than @'String'@, since--- the former clearly conveys the idea of a numbers, whereas the latter could be--- a anything that is representable as 'String', such as names of painters and--- colors. We can convert from the more general to the less general @msg@ type--- using this 'contramsg' function:------ @--- 'contramsg' 'show' (di :: 'Di' level path 'String')---     :: 'Di' level path 'Int'--- @------ Identity:------ @--- 'contramsg' 'id'   ==   'id'--- @------ Composition:------ @--- 'contramsg' (f . g)   ==   'contramsg' g . 'contramsg' f--- @-contramsg :: (msg -> msg') -> Di level path msg' -> Di level path msg-contramsg f = \(Di dLog dFilter dLogs) ->-  Di (\ts l p m -> dLog ts l p (f m)) dFilter dLogs-{-# INLINABLE contramsg #-}------------------------------------------------------------------------------------renderIso8601 :: Time.UTCTime -> String-renderIso8601 = Time.formatTime Time.defaultTimeLocale "%Y-%m-%dT%H:%M:%S.%qZ"--catchSync :: IO a -> (Ex.SomeException -> IO a) -> IO a-catchSync m f = Ex.catch m $ \se -> case Ex.asyncExceptionFromException se of-   Just ae -> Ex.throwIO (ae :: Ex.AsyncException)-   Nothing -> f se
− test/Main.hs
@@ -1,136 +0,0 @@-{-# LANGUAGE CPP #-}-module Main where--import Control.Applicative (liftA2)-import qualified Control.Exception as Ex-import Control.Concurrent.STM-  (STM, atomically, TQueue, newTQueue, writeTQueue, tryReadTQueue)-import Data.Foldable (for_)-import Data.Function (fix)-import Data.Monoid (Sum(Sum, getSum))-import qualified Test.Tasty as Tasty-import qualified Test.Tasty.HUnit as HU-import Test.Tasty.HUnit ((@?=))-import qualified Test.Tasty.QuickCheck as QC-import qualified Test.Tasty.Runners as Tasty--import Di (Di)-import qualified Di------------------------------------------------------------------------------------main :: IO ()-main = Tasty.defaultMainWithIngredients-  [ Tasty.consoleTestReporter-  , Tasty.listingTests-  ] tt--tt :: Tasty.TestTree-tt = Tasty.testGroup "di"-  [ QC.testProperty "log" $ do-      QC.forAll QC.arbitrary $ \lpms ->-        QC.ioProperty $ do-          expect lpms $ \di0 -> do-            for_ lpms $ \(l, p, m) -> do-              Di.log (Di.push p di0) l m--  , HU.testCase "push" $ do-       let x = [("","",""), ("","",""), ("","1",""), ("","12",""), ("","12",""), ("","","")]-       expect x $ \di0 -> do-          Di.log di0 "" ""-          -- Identity-          Di.log (Di.push "" di0) "" ""-          -- Composition-          Di.log (Di.push "1" di0) "" ""-          Di.log ((Di.push "2" . Di.push "1") di0) "" ""-          Di.log (Di.push "12" di0) "" ""-          -- Checking that di0 still works-          Di.log di0 "" ""--  , HU.testCase "contralevel" $ do-       let x = [("1","",""), ("1","",""), ("1","",""), ("2","",""), ("2","",""), ("1","","")]-           n = 1 :: Int-       expect x $ \di0 -> do-          Di.log di0 "1" ""-          -- Identity-          Di.log (Di.contralevel id di0) "1" ""-          -- Composition-          Di.log (Di.contralevel show di0) n ""-          Di.log ((Di.contralevel succ . Di.contralevel show) di0) n ""-          Di.log (Di.contralevel (show . succ) di0) n ""-          -- Checking that di0 still works-          Di.log di0 "1" ""--  , HU.testCase "contrapath" $ do-       let x = [("","",""), ("","1",""), ("","1",""), ("","1",""), ("","2",""), ("","2",""), ("","","")]-           n = Sum (1 :: Int)-       expect x $ \di0 -> do-          Di.log di0 "" ""-          Di.log (Di.push "1" di0) "" ""-          -- Identity-          Di.log (Di.push "1" (Di.contrapath id di0)) "" ""-          -- Composition-          Di.log (Di.push n (Di.contrapath (show . getSum) di0)) "" ""-          Di.log (Di.push n ((Di.contrapath (mappend n) . Di.contrapath (show . getSum)) di0)) "" ""-          Di.log (Di.push n (Di.contrapath (show . getSum . mappend n) di0)) "" ""-          -- Checking that di0 still works-          Di.log di0 "" ""--  , HU.testCase "contramsg" $ do-       let x = [("","","1"), ("","","1"), ("","","1"), ("","","2"), ("","","2"),  ("","","1")]-           n = 1 :: Int-       expect x $ \di0 -> do-          Di.log di0 "" "1"-          -- Identity-          Di.log (Di.contramsg id di0) "" "1"-          -- Composition-          Di.log (Di.contramsg show di0) "" n-          Di.log ((Di.contramsg succ . Di.contramsg show) di0) "" n-          Di.log (Di.contramsg (show . succ) di0) "" n-          -- Checking that di0 still works-          Di.log di0 "" "1"--  , HU.testCase "filter" $ do-       let x = [("1","","a"), ("2","","c"), ("1","","d"), ("3","","g"), ("3","","j"), ("3","","m"), ("1","","n")]-       expect x $ \di0 -> do-          Di.log di0 "1" "a"-          -- Predicates-          Di.log (Di.filter (/= "1") di0) "1" "b"-          Di.log (Di.filter (/= "1") di0) "2" "c"-          -- Identity-          Di.log (Di.filter (const True) di0) "1" "d"-          -- Composition-          Di.log ((Di.filter (/= "1") . Di.filter (/= "2")) di0) "1" "e"-          Di.log ((Di.filter (/= "1") . Di.filter (/= "2")) di0) "2" "f"-          Di.log ((Di.filter (/= "1") . Di.filter (/= "2")) di0) "3" "g"-          Di.log (Di.filter (liftA2 (&&) (/= "1") (/= "2")) di0) "1" "h"-          Di.log (Di.filter (liftA2 (&&) (/= "1") (/= "2")) di0) "2" "i"-          Di.log (Di.filter (liftA2 (&&) (/= "1") (/= "2")) di0) "3" "j"-          -- Conmutativity (c.f., "e" "f" "g")-          Di.log ((Di.filter (/= "2") . Di.filter (/= "1")) di0) "1" "k"-          Di.log ((Di.filter (/= "2") . Di.filter (/= "1")) di0) "2" "l"-          Di.log ((Di.filter (/= "2") . Di.filter (/= "1")) di0) "3" "m"-          -- Checking that di0 still works-          Di.log di0 "1" "n"-  ]--expect :: [(String, String, String)] -> (Di String String String -> IO a) -> IO a-expect as0 k = Ex.bracket-  (do tq <- atomically newTQueue-      di <- Di.mkDi (\_ l p m -> atomically (writeTQueue tq (l, p, m)))-      pure (tq, di))-  (\(tq, di) -> do-      Di.flush di-      as1 <- atomically (drainTQueue tq)-      as1 @?= as0)-  (\(_, di) -> k di)--drainTQueue :: TQueue a -> STM [a]-drainTQueue tq = do-  let go as = maybe (pure as) (\a -> go (a:as)) =<< tryReadTQueue tq-  fmap reverse (go [])--#if !MIN_VERSION_QuickCheck(2,10,0)-instance QC.Testable () where-  property () = QC.property True-#endif