packages feed

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 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