packages feed

contra-tracers-1.0.0: src/Data/Generics/Tracers.hs

--  This Source Code Form is subject to the terms of the Mozilla Public
--  License, v. 2.0. If a copy of the MPL was not distributed with this
--  file, You can obtain one at http://mozilla.org/MPL/2.0/.

{-# LANGUAGE TypeOperators #-}

{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
{-# OPTIONS_HADDOCK prune #-}

-- | This module provides a way to generically define and configure multiple
-- tracers for multiple logging components.
--
-- This is useful in order to associate separate configuration (e.g. minimum
-- severity) to tracers corresponding to different parts of an application. For
-- example, in a webserver, one may imagine wanting to lower the verbosity of a
-- tracer used for all HTTP requests, but keep the database tracer at a debug
-- level.
--
-- While this is possible to do without generics, it usually requires a bit of
-- boilerplate and repetitions. This module makes it easy to define one-single
-- data object as a record, for representing both severities configurations and
-- concrete instantiated tracers. Type signatures are a bit frightening, but the
-- usage is pretty simple (see 'Control.Tracer' for examples)
--
module Data.Generics.Tracers
    ( -- * Definition
      IsRecordOfTracers
    , TracerDefinition (..)
    , type TracerHKD

    -- * Construction
    , configureTracers
    , defaultTracers

    -- Internal
    , SomeMsg (..)
    ) where

import Prelude

import GHC.Generics

import Control.Tracer
    ( Tracer (..), nullTracer )
import Data.Aeson
    ( ToJSON (..) )
import Data.Functor.Const
    ( Const (..) )
import Data.Functor.Contravariant
    ( contramap )
import Data.Kind
    ( Type )
import Data.Severity
    ( HasSeverityAnnotation (..), Severity )

-- * Definition

-- | A constraint-alias defining generic record of tracers.
type IsRecordOfTracers tracers m =
    ( GConfigureTracers m (Rep (tracers m MinSeverities)) (Rep (tracers m Concrete))
    , Generic (tracers m MinSeverities)
    , Generic (tracers m Concrete)
    )

-- | A data kind to control what's defined in a generic record of tracer.
--
-- Use 'configureTracers' to turn a record of configuration into a record of
-- concrete tracers.
data TracerDefinition = Concrete | MinSeverities

-- | A high-kinded type family to make definitions of tracers more readable. For
-- example, one can define a multi-component set of tracers as such:
--
--     data Tracers m (kind :: TracerDefinition) = Tracers
--         { tracerHttp  :: TracerHKD kind (Tracer m HttpLog)
--         , tracerStart :: TracerHKD kind (Tracer m StartLog)
--         }
type family TracerHKD (definition :: TracerDefinition) (tracer :: Type) :: Type where
    TracerHKD MinSeverities tracer = Const (Maybe Severity) tracer
    TracerHKD Concrete tracer = tracer

-- * Construction

-- | Convert a 'MinSeverities' of tracers into a 'Concrete' record of tracers,
-- using a base opaque 'Tracer'.
configureTracers
    :: ( IsRecordOfTracers tracers m )
    => tracers m MinSeverities
    -> Tracer m SomeMsg
    -> tracers m Concrete
configureTracers f tr =
    to . gConfigureTracers mempty tr . from $ f

class GConfigureTracers m (f :: Type -> Type) (g :: Type -> Type) where
    gConfigureTracers
        :: String
        -> Tracer m SomeMsg
        -> f (Const (Maybe Severity) tr)
        -> g tr

instance GConfigureTracers m f g => GConfigureTracers m (D1 c f) (D1 c g) where
    gConfigureTracers field tr = M1 . gConfigureTracers field tr . unM1

instance GConfigureTracers m f g => GConfigureTracers m (C1 c f) (C1 c g) where
    gConfigureTracers field tr = M1 . gConfigureTracers field tr . unM1

instance (Selector s, GConfigureTracers m f g) => GConfigureTracers m (S1 s f) (S1 s g) where
    gConfigureTracers _ tr = M1 . gConfigureTracers field tr . unM1
      where
        field = selName (undefined :: S1 s f a)

instance (GConfigureTracers m f0 g0, GConfigureTracers m f1 g1)
    => GConfigureTracers m (f0 :*: f1) (g0 :*: g1)
  where
    gConfigureTracers field tr (f0 :*: f1) =
        gConfigureTracers field tr f0
        :*:
        gConfigureTracers field tr f1

instance (ToJSON msg, HasSeverityAnnotation msg, Applicative m)
    => GConfigureTracers m (K1 i (Const (Maybe Severity) (Tracer m msg))) (K1 i (Tracer m msg))
  where
    gConfigureTracers field tr = \case
        K1 (Const (Just minSeverity)) ->
            K1 (contramap (SomeMsg minSeverity field) tr)
        K1 (Const Nothing) ->
            K1 nullTracer

-- | Assign a default value to a record of tracers.
--
-- This is useful when used in conjunction with 'Const' and a record of tracer
-- severities:
--
--     defaultTracers Info == Tracers
--         { tracerHttp = Const (Just Info)
--         , tracerDb = Const (Just Info)
--         }
--
defaultTracers
    :: forall tracers (m :: Type -> Type).
        ( Generic (tracers m MinSeverities)
        , GDefaultRecord (Rep (tracers m MinSeverities))
        , GDefaultRecordType (Rep (tracers m MinSeverities)) ~ Maybe Severity
        )
    => Maybe Severity
    -> tracers m MinSeverities
defaultTracers =
    to . gDefaultRecord

-- | This generic class allows for defining a default value to a record of
-- functors wrapping some type 'a'.
class GDefaultRecord (f :: Type -> Type) where
    type GDefaultRecordType f :: Type
    gDefaultRecord :: GDefaultRecordType f -> f (Const (GDefaultRecordType f) b)

instance GDefaultRecord f => GDefaultRecord (D1 c f) where
    type GDefaultRecordType (D1 c f) = GDefaultRecordType f
    gDefaultRecord a = M1 (gDefaultRecord a)

instance GDefaultRecord f => GDefaultRecord (C1 c f) where
    type GDefaultRecordType (C1 c f) = GDefaultRecordType f
    gDefaultRecord a = M1 (gDefaultRecord a)

instance GDefaultRecord f => GDefaultRecord (S1 c f) where
    type GDefaultRecordType (S1 c f) = GDefaultRecordType f
    gDefaultRecord a = M1 (gDefaultRecord a)

instance (GDefaultRecord f, GDefaultRecord g, GDefaultRecordType f ~ GDefaultRecordType g) => GDefaultRecord (f :*: g) where
    type GDefaultRecordType (f :*: g) = GDefaultRecordType f
    gDefaultRecord a = gDefaultRecord a :*: gDefaultRecord a

instance GDefaultRecord (K1 i (Const a b)) where
    type GDefaultRecordType (K1 i (Const a b)) = a
    gDefaultRecord a = K1 (Const a)

-- Internal

-- A GADT capturing constraints as existential.
data SomeMsg where
    SomeMsg
        :: forall msg. (ToJSON msg, HasSeverityAnnotation msg)
        => Severity
        -> String
        -> msg
        -> SomeMsg