atelier-core 0.4.0.1 → 0.6.0.0
raw patch · 46 files changed
+219/−152 lines, 46 filesdep ~atelier-coredep ~atelier-preludedep ~effectfulPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: atelier-core, atelier-prelude, effectful, effectful-core, effectful-plugin
API changes (from Hackage documentation)
- Atelier.Time: instance Data.Aeson.Types.FromJSON.FromJSON Data.Time.Units.Hour
- Atelier.Time: instance Data.Aeson.Types.FromJSON.FromJSON Data.Time.Units.Microsecond
- Atelier.Time: instance Data.Aeson.Types.FromJSON.FromJSON Data.Time.Units.Millisecond
- Atelier.Time: instance Data.Aeson.Types.FromJSON.FromJSON Data.Time.Units.Minute
- Atelier.Time: instance Data.Aeson.Types.FromJSON.FromJSON Data.Time.Units.Second
- Atelier.Time: instance Data.Aeson.Types.ToJSON.ToJSON Data.Time.Units.Hour
- Atelier.Time: instance Data.Aeson.Types.ToJSON.ToJSON Data.Time.Units.Microsecond
- Atelier.Time: instance Data.Aeson.Types.ToJSON.ToJSON Data.Time.Units.Millisecond
- Atelier.Time: instance Data.Aeson.Types.ToJSON.ToJSON Data.Time.Units.Minute
- Atelier.Time: instance Data.Aeson.Types.ToJSON.ToJSON Data.Time.Units.Second
+ Atelier.Time: AsJsonMicrosecond :: unit -> AsJsonMicrosecond unit
+ Atelier.Time: AsRawUnit :: unit -> AsRawUnit unit
+ Atelier.Time: [getAsJsonMicroseconds] :: AsJsonMicrosecond unit -> unit
+ Atelier.Time: [getAsRawUnit] :: AsRawUnit unit -> unit
+ Atelier.Time: instance Data.Time.Units.TimeUnit unit => Data.Aeson.Types.FromJSON.FromJSON (Atelier.Time.AsJsonMicrosecond unit)
+ Atelier.Time: instance Data.Time.Units.TimeUnit unit => Data.Aeson.Types.ToJSON.ToJSON (Atelier.Time.AsJsonMicrosecond unit)
+ Atelier.Time: instance GHC.Classes.Eq unit => GHC.Classes.Eq (Atelier.Time.AsJsonMicrosecond unit)
+ Atelier.Time: instance GHC.Classes.Eq unit => GHC.Classes.Eq (Atelier.Time.AsRawUnit unit)
+ Atelier.Time: instance GHC.Generics.Generic (Atelier.Time.AsJsonMicrosecond unit)
+ Atelier.Time: instance GHC.Generics.Generic (Atelier.Time.AsRawUnit unit)
+ Atelier.Time: instance GHC.Real.Integral unit => Data.Aeson.Types.FromJSON.FromJSON (Atelier.Time.AsRawUnit unit)
+ Atelier.Time: instance GHC.Real.Integral unit => Data.Aeson.Types.ToJSON.ToJSON (Atelier.Time.AsRawUnit unit)
+ Atelier.Time: instance GHC.Show.Show unit => GHC.Show.Show (Atelier.Time.AsJsonMicrosecond unit)
+ Atelier.Time: instance GHC.Show.Show unit => GHC.Show.Show (Atelier.Time.AsRawUnit unit)
+ Atelier.Time: newtype AsJsonMicrosecond unit
+ Atelier.Time: newtype AsRawUnit unit
Files
- CHANGELOG.md +19/−0
- LICENSE +1/−1
- README.md +7/−22
- atelier-core.cabal +10/−10
- src/Atelier/Config.hs +4/−2
- src/Atelier/Effects/Arguments.hs +2/−1
- src/Atelier/Effects/Await.hs +2/−1
- src/Atelier/Effects/Cache.hs +7/−5
- src/Atelier/Effects/Cache/Config.hs +2/−1
- src/Atelier/Effects/Cache/Singleflight.hs +20/−18
- src/Atelier/Effects/Chan.hs +4/−1
- src/Atelier/Effects/Clock.hs +2/−1
- src/Atelier/Effects/Console.hs +2/−1
- src/Atelier/Effects/Debounce.hs +2/−1
- src/Atelier/Effects/Delay.hs +2/−1
- src/Atelier/Effects/Env.hs +2/−1
- src/Atelier/Effects/Exit.hs +2/−1
- src/Atelier/Effects/File.hs +4/−1
- src/Atelier/Effects/FileSystem.hs +2/−1
- src/Atelier/Effects/FileWatcher.hs +4/−2
- src/Atelier/Effects/Input.hs +2/−1
- src/Atelier/Effects/Internal/Coroutine.hs +2/−1
- src/Atelier/Effects/Iterator.hs +2/−1
- src/Atelier/Effects/Log.hs +2/−1
- src/Atelier/Effects/Posix/Daemons.hs +2/−1
- src/Atelier/Effects/Posix/IO.hs +7/−5
- src/Atelier/Effects/Process.hs +2/−1
- src/Atelier/Effects/Process/Internal.hs +2/−1
- src/Atelier/Effects/Publishing/Pub.hs +2/−1
- src/Atelier/Effects/Publishing/Sub.hs +4/−1
- src/Atelier/Effects/Tally.hs +2/−1
- src/Atelier/Effects/Timeout.hs +2/−1
- src/Atelier/Effects/UUID.hs +2/−1
- src/Atelier/Effects/Yield.hs +2/−1
- src/Atelier/Exception.hs +7/−5
- src/Atelier/Time.hs +27/−44
- src/Atelier/Types/Base64.hs +2/−1
- src/Atelier/Types/QuietSnake.hs +12/−2
- src/Atelier/Types/Semaphore.hs +2/−1
- src/Atelier/Types/Semaphore/STM.hs +2/−1
- src/Atelier/Types/WithDefaults.hs +2/−1
- test/Unit/Atelier/Effects/AwaitSpec.hs +2/−1
- test/Unit/Atelier/Effects/Cache/SingleflightSpec.hs +2/−1
- test/Unit/Atelier/Effects/CacheSpec.hs +8/−1
- test/Unit/Atelier/Effects/Conc/TeardownStressSpec.hs +7/−3
- test/Unit/Atelier/Effects/FileWatcherSpec.hs +11/−1
CHANGELOG.md view
@@ -7,6 +7,25 @@ ## [Unreleased] +## [0.6.0.0] - 2026-09-10++### Changed++- Require `effectful >=2.7 && <2.8`.+- Require `effectful-core >=2.7 && <2.8`.+- Require `effectful-plugin >=2.2 && <2.3`.++## [0.5.0.0] - 2026-09-09++### Changed++- `Atelier.Time`: Remove orphan `FromJSON` and `ToJSON` instances, and instead+ introduce `AsRawUnit` and `AsJsonMicrosecond` `newtype`s for `FromJSON` and+ `ToJSON` instance deriving. This fixes the issue where the time units'+ `FromJSON` and `ToJSON` instances did not match, meaning+ `decode (encode (10 :: Hour)) == Nothing` held true. Now you have to be more+ explicit about how the units will be represented in JSON.+ ## [0.4.0.1] - 2026-08-14 ### Removed
LICENSE view
@@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Christian Georgii+Copyright (c) 2026 Tweag Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
README.md view
@@ -4,29 +4,14 @@ ## Overview -`atelier-core` provides a set of composable Effectful effects and supporting types for building structured, observable applications.--| Module | Purpose |-|---|---|-| `Atelier.Component` | Structured component lifecycle (`setup → listeners → start`) |-| `Atelier.Config` | Configuration with environment variable overrides |-| `Atelier.Effects.Log` | Structured logging with hierarchical namespaces |-| `Atelier.Effects.Conc` | Thread management via [Ki](https://github.com/awkward-squad/ki) (structured concurrency) |-| `Atelier.Effects.Cache` | Caching with singleflight deduplication |-| `Atelier.Effects.Publishing` | Event publishing with context propagation |-| `Atelier.Effects.Monitoring.Tracing` | OpenTelemetry tracing |-| `Atelier.Effects.Monitoring.Metrics` | Prometheus metrics |-| `Atelier.Effects.FileWatcher` | Filesystem change notifications |-| `Atelier.Effects.Process` | External process management |--It also wraps a number of `IO`-based primitives (environment, clock, file system, console, POSIX) as effects so they can be interpreted and tested explicitly: `Atelier.Effects.Env`, `Atelier.Effects.Clock`, `Atelier.Effects.FileSystem`, `Atelier.Effects.Console`, `Atelier.Effects.Posix.*`, and more.--## Part of atelier+`atelier-core` provides a set of composable Effectful effects and supporting+types for building structured, observable applications. -- [`atelier-prelude`](https://github.com/tweag/tricorder/tree/main/atelier-prelude) — relude-based prelude with Effectful conventions-- [`atelier-core`](https://github.com/tweag/tricorder/tree/main/atelier-core) — this package-- [`atelier-db`](https://github.com/tweag/tricorder/tree/main/atelier-db) — relational database effect (Hasql/Rel8)-- [`atelier-testing`](https://github.com/tweag/tricorder/tree/main/atelier-testing) — database-backed test utilities+It also wraps a number of `IO`-based primitives (environment, clock, file+system, console, POSIX) as effects so they can be interpreted and tested+explicitly: `Atelier.Effects.Env`, `Atelier.Effects.Clock`,+`Atelier.Effects.FileSystem`, `Atelier.Effects.Console`,+`Atelier.Effects.Posix.*`, and more. ## License
atelier-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: atelier-core-version: 0.4.0.1+version: 0.6.0.0 synopsis: Foundational Effectful-based effects and utilities description: Core effects and utilities for effect-based applications, built on Effectful — part of the atelier toolkit. category: Control@@ -98,7 +98,7 @@ ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded build-depends: aeson >=2.2 && <2.4- , atelier-prelude >=0.1 && <0.3+ , atelier-prelude ==0.3.* , base >=4.18 && <4.23 , base64-bytestring ==1.2.* , bytestring >=0.11 && <0.13@@ -107,9 +107,9 @@ , daemons ==0.4.* , data-default ==0.8.* , directory ==1.3.*- , effectful ==2.6.*- , effectful-core ==2.6.*- , effectful-plugin >=2.0 && <2.2+ , effectful ==2.7.*+ , effectful-core ==2.7.*+ , effectful-plugin ==2.2.* , effectful-th ==1.0.* , filepath >=1.4 && <1.6 , fsnotify ==0.4.*@@ -190,15 +190,15 @@ tasty-discover:tasty-discover build-depends: aeson >=2.2 && <2.4- , atelier-core >=0.3 && <0.5- , atelier-prelude >=0.1 && <0.3+ , atelier-core+ , atelier-prelude ==0.3.* , base >=4.18 && <4.23 , bytestring >=0.11 && <0.13 , containers >=0.6 && <0.9 , data-default ==0.8.*- , effectful ==2.6.*- , effectful-core ==2.6.*- , effectful-plugin >=2.0 && <2.2+ , effectful ==2.7.*+ , effectful-core ==2.7.*+ , effectful-plugin ==2.2.* , hedgehog ==1.7.* , hspec ==2.11.* , hspec-hedgehog ==0.3.*
src/Atelier/Config.hs view
@@ -45,7 +45,8 @@ , extractNestedConfig , LoadedConfig (..) , runConfig- ) where+ )+where import Data.Aeson (FromJSON (..), Value (..)) import Data.Default (Default (..))@@ -155,7 +156,8 @@ -- @ -- extractNestedConfig \@"foo.bar" \@Text cfg == "test" -- @-extractNestedConfig :: forall (key :: Symbol) r. (Default r, FromJSON r, KnownSymbol key) => LoadedConfig -> r+extractNestedConfig+ :: forall (key :: Symbol) r. (Default r, FromJSON r, KnownSymbol key) => LoadedConfig -> r extractNestedConfig (LoadedConfig root) = go props root where props = T.splitOn "." $ toText $ symbolVal $ Proxy @key
src/Atelier/Effects/Arguments.hs view
@@ -14,7 +14,8 @@ -- * Interpreters , runArgumentsIO- ) where+ )+where import Effectful (Effect, IOE) import Effectful.Dispatch.Dynamic (interpret_)
src/Atelier/Effects/Await.hs view
@@ -16,7 +16,8 @@ -- * Interpreters , eachAwait , awaitYield- ) where+ )+where import Atelier.Effects.Internal.Coroutine ( Await
src/Atelier/Effects/Cache.hs view
@@ -14,7 +14,8 @@ , runCacheTtl , runCacheTtlWithWait , runCacheForever- ) where+ )+where import Data.Time (NominalDiffTime, UTCTime, addUTCTime) import Effectful (Effect)@@ -149,10 +150,11 @@ evictExpiredEntries store ttl now = ListT.fold ( \count (k, v) ->- if now >= addUTCTime ttl v.createdAt then- Map.delete k store $> count + 1- else- pure count+ if now >= addUTCTime ttl v.createdAt+ then+ Map.delete k store $> count + 1+ else+ pure count ) 0 $ Map.listT store
src/Atelier/Effects/Cache/Config.hs view
@@ -5,7 +5,8 @@ -- "Atelier.Effects.Tally". module Atelier.Effects.Cache.Config ( Config (..)- ) where+ )+where import Data.Aeson (FromJSON) import Data.Default (Default (..))
src/Atelier/Effects/Cache/Singleflight.hs view
@@ -11,7 +11,8 @@ , updateCache , removeFromCache , runSingleflight- ) where+ )+where import Effectful (Effect) import Effectful.Concurrent (Concurrent)@@ -88,24 +89,25 @@ -- Use tryPutTMVar in case updateCache already filled it filled <- STM.atomically $ STM.tryPutTMVar mvar result - if filled then do- -- We successfully filled the TMVar with our result- -- If it was an exception, remove from cache so future requests can retry- case result of- Left _exception -> do- STM.atomically $ Map.delete key cache- Right _ -> pure ()+ if filled+ then do+ -- We successfully filled the TMVar with our result+ -- If it was an exception, remove from cache so future requests can retry+ case result of+ Left _exception -> do+ STM.atomically $ Map.delete key cache+ Right _ -> pure () - -- Return the result or re-throw the exception- case result of- Left exception -> throwIO exception- Right value -> pure value- else do- -- updateCache filled it before us - read and use that value- finalResult <- STM.atomically $ STM.readTMVar mvar- case finalResult of- Left exception -> throwIO exception- Right value -> pure value+ -- Return the result or re-throw the exception+ case result of+ Left exception -> throwIO exception+ Right value -> pure value+ else do+ -- updateCache filled it before us - read and use that value+ finalResult <- STM.atomically $ STM.readTMVar mvar+ case finalResult of+ Left exception -> throwIO exception+ Right value -> pure value (False, Nothing) -> do result <- STM.atomically
src/Atelier/Effects/Chan.hs view
@@ -40,7 +40,8 @@ , InChan , OutChan , readChanBatched- ) where+ )+where import Control.Concurrent.Chan.Unagi (InChan, OutChan) import Data.Time.Units (TimeUnit, toMicroseconds)@@ -57,6 +58,8 @@ type instance DispatchOf Chan = Static WithSideEffects++ data instance StaticRep Chan = Chan
src/Atelier/Effects/Clock.hs view
@@ -13,7 +13,8 @@ , runClockConst , runClockState , runClockList- ) where+ )+where import Data.Time (UTCTime, getCurrentTime) import Data.Time.LocalTime (TimeZone, getCurrentTimeZone, utc)
src/Atelier/Effects/Console.hs view
@@ -15,7 +15,8 @@ , runConsoleHandle , runConsole , runConsoleToList- ) where+ )+where import Effectful (Effect, IOE) import Effectful.Dispatch.Dynamic (interpret_, reinterpret_)
src/Atelier/Effects/Debounce.hs view
@@ -38,7 +38,8 @@ , ensureEntry , ensureCallback , Entry (..)- ) where+ )+where import Data.Dynamic (Dynamic, fromDynamic, toDyn) import Effectful (Effect, Limit (..), Persistence (..), UnliftStrategy (..))
src/Atelier/Effects/Delay.hs view
@@ -18,7 +18,8 @@ , every , runDelay , runDelayNoOp- ) where+ )+where import Data.Time.Units (TimeUnit, toMicroseconds) import Effectful (Effect)
src/Atelier/Effects/Env.hs view
@@ -8,7 +8,8 @@ , lookupEnv , runEnv , runEnvConst- ) where+ )+where import Effectful (Effect, IOE) import Effectful.Dispatch.Dynamic (interpret_)
src/Atelier/Effects/Exit.hs view
@@ -12,7 +12,8 @@ -- * Interpreters , runExit , runExitNoOp- ) where+ )+where import Effectful (Effect, IOE) import Effectful.Dispatch.Dynamic (interpret_, reinterpret_)
src/Atelier/Effects/File.hs view
@@ -22,7 +22,8 @@ , hIsEOF , hSetBuffering , runFile- ) where+ )+where import Effectful (Dispatch (..), DispatchOf, Effect, IOE) import Effectful.Dispatch.Static@@ -45,6 +46,8 @@ type instance DispatchOf File = Static WithSideEffects++ data instance StaticRep File = File
src/Atelier/Effects/FileSystem.hs view
@@ -24,7 +24,8 @@ , runFileSystemIO , runFileSystemNoOp , runFileSystemState- ) where+ )+where import Control.Exception (bracket) import Effectful (Effect, IOE)
src/Atelier/Effects/FileWatcher.hs view
@@ -65,7 +65,8 @@ -- * Internals (exported for testing) , deduplicateDirs , matchesAny- ) where+ )+where import Control.Concurrent (threadDelay) import Control.Concurrent.STM (retry)@@ -204,7 +205,8 @@ -- Delivers all scripted events to the callback in order, then blocks -- indefinitely — matching the blocking semantics of 'runFileWatcherIO'. -- The 'Watch' specification is ignored; the caller controls what events are fed in.-runFileWatcherScripted :: (Concurrent :> es) => [(FilePath, FileEvent)] -> Eff (FileWatcher : es) a -> Eff es a+runFileWatcherScripted+ :: (Concurrent :> es) => [(FilePath, FileEvent)] -> Eff (FileWatcher : es) a -> Eff es a runFileWatcherScripted events = reinterpret (evalState events) \env -> \case WatchFilePaths _ callback -> localSeqUnlift env \unlift -> do
src/Atelier/Effects/Input.hs view
@@ -10,7 +10,8 @@ , runInputConst , toReader , fromState- ) where+ )+where import Effectful (Effect) import Effectful.Dispatch.Dynamic (interpret_)
src/Atelier/Effects/Internal/Coroutine.hs view
@@ -42,7 +42,8 @@ -- ** Interpreters , awaitYield- ) where+ )+where import Effectful (Effect, UnliftStrategy (..), inject, raiseWith) import Effectful.Dispatch.Dynamic (interpretWith_, interpret_, reinterpretWith_, reinterpret_)
src/Atelier/Effects/Iterator.hs view
@@ -10,7 +10,8 @@ , fromEvents , filter , changes- ) where+ )+where import Effectful.Concurrent (Concurrent) import Prelude hiding (filter)
src/Atelier/Effects/Log.hs view
@@ -41,7 +41,8 @@ , runLogNoOp , runLogToHandle , runLogWriter- ) where+ )+where import Data.Aeson (FromJSON (..)) import Data.Default (Default (..))
src/Atelier/Effects/Posix/Daemons.hs view
@@ -10,7 +10,8 @@ , isRunning , forceKillAndWait , runDaemons- ) where+ )+where import Data.Default (def) import Effectful (Effect, IOE, Limit (..), Persistence (..), UnliftStrategy (..))
src/Atelier/Effects/Posix/IO.hs view
@@ -19,7 +19,8 @@ module Atelier.Effects.Posix.IO ( readFdAll , readFdFrom- ) where+ )+where import Foreign.Marshal.Alloc (allocaBytes) import Foreign.Ptr (castPtr)@@ -55,10 +56,11 @@ chunk <- allocaBytes chunkSize \ptr -> do n <- Posix.fdReadBuf fd (castPtr ptr) (fromIntegral chunkSize) BS.packCStringLen (castPtr ptr, fromIntegral n)- if BS.null chunk then- return acc- else- go (acc <> Builder.byteString chunk)+ if BS.null chunk+ then+ return acc+ else+ go (acc <> Builder.byteString chunk) -- | Seek to @offset@ and read the remainder of a file descriptor into a lazy
src/Atelier/Effects/Process.hs view
@@ -39,7 +39,8 @@ -- * Interpreters , runProcessIO- ) where+ )+where import Control.Exception (IOException, catch) import Effectful (Effect, IOE)
src/Atelier/Effects/Process/Internal.hs view
@@ -5,7 +5,8 @@ -- operations may target the wrong group. Production code should not import this. module Atelier.Effects.Process.Internal ( RunningProcess (..)- ) where+ )+where import System.Process.Typed qualified as TP
src/Atelier/Effects/Publishing/Pub.hs view
@@ -9,7 +9,8 @@ , map , mapM , consume- ) where+ )+where import Effectful (Effect) import Effectful.Dispatch.Dynamic (interpret_)
src/Atelier/Effects/Publishing/Sub.hs view
@@ -12,7 +12,8 @@ , listenUntilM_ , forkListener , forkListener_- ) where+ )+where import Data.Time (UTCTime) import Effectful (Effect, inject)@@ -149,4 +150,6 @@ data OnceEx ev = OnceEx ev++ instance Show (OnceEx ev) where show _ = "OnceEx"
src/Atelier/Effects/Tally.hs view
@@ -33,7 +33,8 @@ -- * Interpreters , runTally , runTallyConst- ) where+ )+where import Effectful (Effect) import Effectful.Concurrent (Concurrent)
src/Atelier/Effects/Timeout.hs view
@@ -16,7 +16,8 @@ , timeout , timeout_ , runTimeout- ) where+ )+where import Effectful (Effect, IOE, Limit (..), Persistence (..), UnliftStrategy (..)) import Effectful.Dispatch.Dynamic (interpret, localUnliftIO)
src/Atelier/Effects/UUID.hs view
@@ -9,7 +9,8 @@ , gen , runGenUUID , runGenUUIDConst- ) where+ )+where import Data.UUID (UUID) import Data.UUID.V4 (nextRandom)
src/Atelier/Effects/Yield.hs view
@@ -28,7 +28,8 @@ , catMaybes , changes , filter- ) where+ )+where import Effectful.Dispatch.Dynamic (impose_, interpose_) import Effectful.State.Static.Shared (evalState, get)
src/Atelier/Exception.hs view
@@ -11,7 +11,8 @@ , trySyncIO , catchSyncIO , isSyncException- ) where+ )+where import Effectful.Exception (SomeAsyncException, isSyncException) @@ -37,7 +38,8 @@ catchSyncIO :: (HasCallStack) => IO a -> (SomeException -> IO a) -> IO a catchSyncIO f g = f `E.catch` \e ->- if isSyncException e then- g e- else- E.throwIO e+ if isSyncException e+ then+ g e+ else+ E.throwIO e
src/Atelier/Time.hs view
@@ -1,16 +1,5 @@-{-# OPTIONS_GHC -Wno-orphans #-}- -- | Strongly-typed time units and conversions. ----- Re-exports the duration types from "Data.Time.Units" — so a single import--- covers 'Microsecond', 'Millisecond', 'Second', 'Minute' and 'Hour' — and adds--- conversion helpers plus JSON instances for those types. Each unit serialises--- as an integer count of itself: a 'Second' as a whole number of seconds, a--- 'Millisecond' as milliseconds, and so on.------ Note: the 'FromJSON' and 'ToJSON' instances for the time-unit types are--- orphans, defined here because neither @aeson@ nor @time-units@ provides them.--- -- @ -- nominalDiffTime 1.5 :: Millisecond -- 1.5s rounded to 1500ms -- convertUnit (5 :: Minute) :: Second -- 300@@ -30,11 +19,24 @@ , fromMicroseconds , toMicroseconds , convertUnit- ) where + -- * Utility newtypes for converting time to and from JSON+ , AsJsonMicrosecond (..)+ , AsRawUnit (..)+ )+where+ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Time (NominalDiffTime)-import Data.Time.Units (Hour, Microsecond, Millisecond, Minute, Second, TimeUnit, convertUnit, fromMicroseconds, toMicroseconds)+import Data.Time.Units+ ( Hour+ , Microsecond+ , Millisecond+ , Minute+ , Second+ , TimeUnit (..)+ , convertUnit+ ) -- | Convert a 'NominalDiffTime' to any 'TimeUnit', rounding to the nearest@@ -43,44 +45,25 @@ nominalDiffTime = fromMicroseconds . round @Double . (* 1_000_000) . realToFrac --- Orphan instances for JSON deserialization of time unit types.--- All time units in Data.Time.Units wrap Integer, so we parse as Integer.--instance FromJSON Microsecond where- parseJSON = fmap fromInteger . parseJSON---instance FromJSON Millisecond where- parseJSON = fmap fromInteger . parseJSON---instance FromJSON Second where- parseJSON = fmap fromInteger . parseJSON---instance FromJSON Minute where- parseJSON = fmap fromInteger . parseJSON---instance FromJSON Hour where- parseJSON = fmap fromInteger . parseJSON+newtype AsJsonMicrosecond unit = AsJsonMicrosecond {getAsJsonMicroseconds :: unit}+ deriving stock (Eq, Generic, Show) -instance ToJSON Microsecond where- toJSON us = toJSON (toMicroseconds us)+instance (TimeUnit unit) => ToJSON (AsJsonMicrosecond unit) where+ toJSON = toJSON . toMicroseconds . getAsJsonMicroseconds -instance ToJSON Millisecond where- toJSON ms = toJSON (toMicroseconds ms `div` 1000)+instance (TimeUnit unit) => FromJSON (AsJsonMicrosecond unit) where+ parseJSON = fmap (AsJsonMicrosecond . fromMicroseconds) . parseJSON -instance ToJSON Second where- toJSON s = toJSON (toMicroseconds s `div` 1_000_000)+newtype AsRawUnit unit = AsRawUnit {getAsRawUnit :: unit}+ deriving stock (Eq, Generic, Show) -instance ToJSON Minute where- toJSON m = toJSON (toMicroseconds m `div` 60_000_000)+instance (Integral unit) => ToJSON (AsRawUnit unit) where+ toJSON = toJSON . toInteger . getAsRawUnit -instance ToJSON Hour where- toJSON h = toJSON (toMicroseconds h `div` 3_600_000_000)+instance (Integral unit) => FromJSON (AsRawUnit unit) where+ parseJSON = fmap (AsRawUnit . fromInteger) . parseJSON
src/Atelier/Types/Base64.hs view
@@ -10,7 +10,8 @@ -- @ module Atelier.Types.Base64 ( Base64 (..)- ) where+ )+where import Data.Aeson (FromJSON (..), ToJSON (..), withText)
src/Atelier/Types/QuietSnake.hs view
@@ -15,9 +15,19 @@ -- @ module Atelier.Types.QuietSnake ( QuietSnake (..)- ) where+ )+where -import Data.Aeson (FromJSON (..), GFromJSON, GToJSON, Options, ToJSON (..), Zero, genericParseJSON, genericToJSON)+import Data.Aeson+ ( FromJSON (..)+ , GFromJSON+ , GToJSON+ , Options+ , ToJSON (..)+ , Zero+ , genericParseJSON+ , genericToJSON+ ) import Data.Aeson.Types (defaultOptions, fieldLabelModifier) import Data.Default (Default (..)) import GHC.Generics (Rep)
src/Atelier/Types/Semaphore.hs view
@@ -10,7 +10,8 @@ , set , peek , withSemaphore- ) where+ )+where import Effectful.Concurrent.STM (Concurrent, atomically)
src/Atelier/Types/Semaphore/STM.hs view
@@ -9,7 +9,8 @@ , set , peek , withSemaphore- ) where+ )+where import Effectful.Concurrent.STM ( Concurrent
src/Atelier/Types/WithDefaults.hs view
@@ -17,7 +17,8 @@ -- Parsing @{\"verbose\": true}@ then yields @Config { retries = 3, verbose = True }@. module Atelier.Types.WithDefaults ( WithDefaults (..)- ) where+ )+where import Data.Aeson (FromJSON (..), ToJSON (..), Value (..)) import Data.Default (Default (..))
test/Unit/Atelier/Effects/AwaitSpec.hs view
@@ -6,6 +6,7 @@ import Effectful.Writer.Static.Shared (runWriter, tell) import Test.Hspec (Spec, describe, it, shouldBe) +import Data.List qualified as List import Effectful.Concurrent.STM qualified as STM import Atelier.Effects.Chan (runChan)@@ -70,7 +71,7 @@ (_, result) <- runTest $ Await.awaitYield @Int (Yield.yield 99 >> blockForever)- $ fmap (* 2) Await.await >>= tell . one+ $ fmap (* 2) Await.await >>= tell . List.singleton result `shouldBe` [198] describe "when the awaiter finishes before the yielder"
test/Unit/Atelier/Effects/Cache/SingleflightSpec.hs view
@@ -197,7 +197,8 @@ it "propagates exception to all concurrent waiters" $ do let action = runSingleflightTest $ do sem <- Sem.new- a1 <- Conc.fork $ withCache @Int @Int 1 (slowCompute sem 42 >> throwIO (TestException "concurrent-boom"))+ a1 <-+ Conc.fork $ withCache @Int @Int 1 (slowCompute sem 42 >> throwIO (TestException "concurrent-boom")) Delay.wait (1 :: Millisecond) a2 <- Conc.fork $ withCache @Int @Int 1 (compute 99) Sem.signal sem
test/Unit/Atelier/Effects/CacheSpec.hs view
@@ -14,7 +14,14 @@ import Hedgehog.Gen qualified as Gen import Hedgehog.Range qualified as Range -import Atelier.Effects.Cache (Config (..), cacheDelete, cacheInsert, cacheLookup, cacheModify, runCacheTtlWithWait)+import Atelier.Effects.Cache+ ( Config (..)+ , cacheDelete+ , cacheInsert+ , cacheLookup+ , cacheModify+ , runCacheTtlWithWait+ ) import Atelier.Effects.Clock (runClockState) import Atelier.Effects.Conc (runConc) import Atelier.Effects.Delay (runDelay)
test/Unit/Atelier/Effects/Conc/TeardownStressSpec.hs view
@@ -37,11 +37,15 @@ spec_ConcTeardownStress :: Spec spec_ConcTeardownStress = do- iterations <- runIO (fromMaybe defaultIterations . (>>= readMaybe) <$> lookupEnv "ATELIER_CONC_STRESS_N")- timeoutSecs <- runIO (fromMaybe defaultTimeoutSecs . (>>= readMaybe) <$> lookupEnv "ATELIER_CONC_STRESS_TIMEOUT_S")+ iterations <-+ runIO (fromMaybe defaultIterations . (>>= readMaybe) <$> lookupEnv "ATELIER_CONC_STRESS_N")+ timeoutSecs <-+ runIO (fromMaybe defaultTimeoutSecs . (>>= readMaybe) <$> lookupEnv "ATELIER_CONC_STRESS_TIMEOUT_S") runSpin <- runIO (isJust <$> lookupEnv "ATELIER_CONC_SPIN") - publishDelayUs <- runIO (fromMaybe defaultPublishDelayUs . (>>= readMaybe) <$> lookupEnv "ATELIER_CONC_STRESS_DELAY_US")+ publishDelayUs <-+ runIO+ (fromMaybe defaultPublishDelayUs . (>>= readMaybe) <$> lookupEnv "ATELIER_CONC_STRESS_DELAY_US") let testTimeoutMicros = timeoutSecs * 1_000_000
test/Unit/Atelier/Effects/FileWatcherSpec.hs view
@@ -12,7 +12,17 @@ import Hedgehog.Gen qualified as Gen import Hedgehog.Range qualified as Range -import Atelier.Effects.FileWatcher (FileEvent (..), FileWatcher, Watch, deduplicateDirs, dir, dirWhere, matchesAny, runFileWatcherScripted, watchFilePaths)+import Atelier.Effects.FileWatcher+ ( FileEvent (..)+ , FileWatcher+ , Watch+ , deduplicateDirs+ , dir+ , dirWhere+ , matchesAny+ , runFileWatcherScripted+ , watchFilePaths+ ) spec_FileWatcher :: Spec