packages feed

registry-0.6.0.0: src/Data/Registry/Rio.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}

-- | Utilities for working with resources
module Data.Registry.Rio
  ( module Data.Registry.Rio,
    singleton,
    cacheAt
  )
where

import Control.Monad.Morph
import Control.Monad.Trans.Resource
import Data.Dynamic
import Data.Registry.Internal.Cache
import Data.Registry.Make (make)
import Data.Registry.Registry
import Protolude

-- | This data type provides some support for creating effectful components with resources
--   You can use the regular MonadResource functions like allocate to make sure that resources are cleaned up
--   You can also use the 'cacheAt' function
newtype Rio a = Rio {rioRun :: ReaderT Cache (ResourceT IO) a}
  deriving (Functor, Applicative, Monad, MonadReader Cache, MonadIO, MonadResource, MonadUnliftIO)

-- | Run a Rio action by providing an empty cache and allocating / destroying resources
runRio :: MonadIO m => Rio a -> m a
runRio = liftIO . runResourceT . runCache

-- | Run a Rio action by providing an empty cache and allocating / destroying resources
execRio :: MonadIO m => Rio a -> m (a, Cache)
execRio = liftIO . runResourceT . execCache

-- | Use the value created by a Rio action so that resources are properly allocated and cached
withRio :: MonadIO m => Rio a -> (a -> IO b) -> m b
withRio action f = liftIO . runResourceT $ do
  a <- runCache action
  liftIO $ f a

-- | Use the value created by a Rio action so that resources are properly allocated and cached
--   inside a monad transformer
withRioM :: (MonadResource (m (ResourceT IO)), MFunctor m) => Rio a -> (a -> m IO b) -> m IO b
withRioM action f = hoist runResourceT $ do
  a <- liftResourceT (runCache action)
  hoist lift (f a)

-- | Run a Rio action by providing an empty cache
runCache :: Rio a -> ResourceT IO a
runCache (Rio action) = do
  cache <- liftIO newCache
  runReaderT action cache

-- | Run a Rio action by providing an empty cache, and return the final cache
--   for inspection
execCache :: Rio a -> ResourceT IO (a, Cache)
execCache (Rio action) = do
  cache <- liftIO newCache
  (,cache) <$> runReaderT action cache

-- | Lift a resourceful value into Rio
liftRio :: ResourceT IO a -> Rio a
liftRio = Rio . lift

-- | This function must be used to run services involving resources
--   The value a is created using the registry, used with the function 'f'
--   and all resources are freed at the end
withRegistry :: forall a b ins out m. (Typeable a, MonadIO m, MakeSingletons out) => Registry ins out -> (a -> IO b) -> m b
withRegistry registry f =
  liftIO $ runResourceT (runRegistryT @a registry >>= liftIO . f)

-- | This function works like 'withRegistry' for a higher-order monad, typically `PropertyT IO` when
--   writing property tests with Hedgehog
withRegistryM ::
  forall a b ins out m.
  (Typeable a, MonadResource (m (ResourceT IO)), MFunctor m, MakeSingletons out) =>
  Registry ins out ->
  (a -> m IO b) ->
  m IO b
withRegistryM = withRioM . make @(Rio a) . singletons

-- | Create a function of type a with a given registry
--   Return a ResourceT value to control resource allocation
runRegistryT :: forall a ins out. (Typeable a, MakeSingletons out) => Registry ins out -> ResourceT IO a
runRegistryT = runCache . make @(Rio a) . singletons

-- | Make singletons for all the output types of a registry
--   but only if they not specialized values
singletons :: forall ins out. (MakeSingletons out) => Registry ins out -> Registry ins out
singletons r = _singletonsRegistry $ makeSingletons (startSingletonsRegistry r)

-- | Registry where all Rio values are singletons
newtype SingletonsRegistry (todo :: [Type]) (ins :: [Type]) (out :: [Type]) = SingletonsRegistry {_singletonsRegistry :: Registry ins out}

-- | Prepare a Registry for making singletons
startSingletonsRegistry :: Registry ins out -> SingletonsRegistry out ins out
startSingletonsRegistry = SingletonsRegistry

-- | Prepare a Registry for making singletons on a specific list of types
makeSingletonsRegistry :: forall todo ins out. Registry ins out -> SingletonsRegistry todo ins out
makeSingletonsRegistry = SingletonsRegistry @todo

-- | This typeclass take an existing registry and makes a singleton for each Rio output type
class MakeSingletons ls where
  makeSingletons :: SingletonsRegistry ls ins out -> SingletonsRegistry '[] ins out

-- | If the list of types is empty there is nothing to do
instance MakeSingletons '[] where
  makeSingletons = identity

-- | If the type represents an effectful value, make a singleton for it and recurse on the rest
instance {-# OVERLAPPING #-} (Typeable a, MakeSingletons rest) => MakeSingletons (Rio a : rest) where
  makeSingletons (SingletonsRegistry r) =
    makeSingletons $ SingletonsRegistry @rest (tweakUnspecialized @(Rio a) singleton r)

-- | If the type represents a pure value, make singletons for the rest
instance (MakeSingletons rest) => MakeSingletons (a : rest) where
  makeSingletons (SingletonsRegistry r) = makeSingletons (makeSingletonsRegistry @rest r)