packages feed

atelier-core-1.0.0.0: src/Atelier/Effects/Clock.hs

-- | A clock effect for reading the current time and time zone.
--
-- Putting wall-clock reads behind an effect keeps time-dependent code testable:
-- 'runClock' uses the system clock, while 'runClockConst', 'runClockState' and
-- 'runClockList' supply deterministic times for tests.
module Atelier.Effects.Clock
    ( Clock
    , UTCTime
    , TimeZone
    , currentTime
    , currentTimeZone
    , runIO
    , runConst
    , runState
    , runList
    )
where

import Data.Time (UTCTime, getCurrentTime)
import Data.Time.LocalTime (TimeZone, getCurrentTimeZone, utc)
import Effectful (Effect, IOE)
import Effectful.Dispatch.Dynamic (interpret_, reinterpret)
import Effectful.State.Static.Shared (State, evalState, get, put)
import Effectful.TH (makeEffect)


-- | Effect for reading the current time and time zone.
data Clock :: Effect where
    -- | The current wall-clock time, in UTC.
    CurrentTime :: Clock m UTCTime
    -- | The system's current time zone.
    CurrentTimeZone :: Clock m TimeZone


makeEffect ''Clock


-- | Interpret 'Clock' against the real system clock.
runIO :: (IOE :> es) => Eff (Clock : es) a -> Eff es a
runIO = interpret_ $ \case
    CurrentTime -> liftIO getCurrentTime
    CurrentTimeZone -> liftIO getCurrentTimeZone


-- | Interpret 'Clock' so 'currentTime' always returns a fixed time. The time
-- zone is reported as 'utc'.
runConst :: UTCTime -> Eff (Clock : es) a -> Eff es a
runConst time = interpret_ $ \case
    CurrentTime -> pure time
    CurrentTimeZone -> pure utc


-- | Interpret 'Clock' so 'currentTime' reads from a mutable 'State' cell,
-- letting a test advance \"now\" explicitly. The time zone is reported as 'utc'.
runState :: (State UTCTime :> es) => Eff (Clock : es) a -> Eff es a
runState = interpret_ $ \case
    CurrentTime -> get
    CurrentTimeZone -> pure utc


-- | Scripted interpreter for testing: returns times from a pre-loaded list in
-- order. Each 'currentTime' call pops the next item. Crashes if the list is
-- exhausted.
runList :: [UTCTime] -> Eff (Clock : es) a -> Eff es a
runList times = reinterpret (evalState times) $ \_ -> \case
    CurrentTime ->
        get >>= \case
            [] -> error "runClockList: no more times in queue"
            t : ts -> put ts >> pure t
    CurrentTimeZone -> pure utc