hierarchical-env-0.2.0.0: src/Control/Env/Hierarchical.hs
{-# LANGUAGE ExplicitNamespaces #-}
-- |
-- Module : Control.Env.Hierarchical
-- Description:
-- License: BSD-3
-- Maintainer: autotaker@gmail.com
-- Stability: experimental
module Control.Env.Hierarchical
( -- * Getting Fields
-- $usage:field
Has,
getL,
-- * Invoking interface methods
-- $usage:interface
Has1,
runIF,
-- * Injecting dependecies
-- ** With universal environments
-- $usage:dependency
-- ** With hierarchical environments
-- $usage:hierarchical
Environment (Super, superL),
Extends (Extends),
deriveEnv,
-- * Hiding dependencies
-- $usage:hiding
Interface (..),
mapBaseRIO,
)
where
import Control.Env.Hierarchical.Internal
( Environment (Super, superL),
Extends (Extends),
Has,
Has1,
getL,
runIF,
)
import Control.Env.Hierarchical.TH (deriveEnv)
import Control.Method (Interface (IBase, mapBase), mapBaseRIO)
-- $usage:field
--
-- If a method depends on some value of type @T@,
-- use @'Has' T env@ constraints, and get the value from the environment
-- with @view 'getL'@.
--
--
-- @
-- newtype ServerName = ServerName String
-- printServerName :: ('Has' ServerName env) => RIO env ()
-- printServerName = do
-- ServerName n <- view 'getL'
-- liftIO $ putStrLn $ "ServerName is " <> n
-- @
-- $usage:interface
-- We call a record type whose fields are methods as an interface.
--
-- In the following example, @UserRepo@ is an interface.
--
-- Dependency to an interface @F@ is represented as type constraint @Has1 F env@,
-- methods of the interface can be invoked inside @'runIF'@.
--
-- @
-- data UserRepo env = UserRepo {
-- _createUser :: UserName -> RIO env UserId,
-- _setPassword :: UserId -> Password -> RIO env ()
-- }
-- makeLenses ''UserRepo
--
-- data User = User {
-- _userName :: UserName,
-- _userId :: UserId,
-- } deriving(Eq, Ord, Show)
-- makeLenses ''User
--
-- signup :: ('Has1' UserRepo env) => UserName -> Password -> RIO env User
-- signup name passwd = 'runIF' $ \\userRepo -> do
-- userId <- view createUser userRepo name
-- view setPassword userRepo userId passwd
-- pure User { _userName = name, _userId = userId}
-- @
-- $usage:dependency
--
-- First, define environment @Env@ that
-- contains all dependencies as fields.
--
-- @
-- data Env = Env !ServerName !(UserRepo Env)
-- @
--
-- Then, the following boilerpolate will derive required type constraints.
-- (e.g. @'Has' ServerName Env@, @'Has1' UserRepo Env@)
--
-- @
-- 'deriveEnv' ''Env
-- @
--
-- Now, you can inject dependency by specifying the actual value of @Env@
-- to the argument of 'runRIO'.
--
-- @
-- mkUserRepo :: DBConfig -> UserRepo Env
-- mkUserRepo = ...
--
-- runApp :: ServerName -> DBConfig -> [UserName] -> IO ()
-- runApp serverName dbConfig users = do
-- let env = Env serverName (mkUserRepo dbConfig)
-- runRIO env $ do
-- printServerName
-- forM_ users $ \userName ->
-- user <- signup userName "password"
-- print user
-- @
-- $usage:hierarchical
-- Instead of resolving the dependency universally,
-- you can extend environments by adding @Extend T@ as a field.
--
-- In the following example @ExtEnv@ inherits @BaseEnv@.
-- The extended environment is a nominal sub-type of its super environment,
-- that is,
--
-- * @'Has' T ('Super' E)@ implies @'Has' T E@, and
-- * @'Has1' F ('Super' E)@ implies @'Has1' F E@.
--
-- @
-- data BaseEnv = BaseEnv !ServerName !ConnectionPool
--
-- 'deriveEnv' ''BaseEnv
--
-- data ExtEnv = ExtEnv !(Extends BaseEnv) !(UserRepo ExtEnv)
--
-- 'deriveEnv' ''ExtEnv
-- @
--
-- Then, @ExtEnv@ resolves the dependencies.
--
-- @
-- userRepoImpl :: 'Has' ConnectionPool env => UserRepo env
-- userRepoImpl = UserRepo createUserImpl setPaswordImpl
-- where
-- createUserImpl userName = ...
-- setPasswordImpl uid passwd = ...
--
-- runApp :: ServerName -> ConnectionPool -> [UserName] -> IO ()
-- runApp serverName pool users = do
-- let baseEnv = BaseEnv serverName pool
-- extEnv = ExtEnv (Extends baseEnv) userRepoImpl
-- runRIO extEnv $ do
-- printServerName
-- forM_ users $ \usernm -> do
-- user <- signup usernm "password"
-- liftIO $ print user
-- @
-- $usage:hiding
-- Suppose that we are implementing an interface @AuthHandler@,
-- which handle signin and signup business logic.
--
-- @
-- data AuthHandler env = AuthHandler {
-- _signin :: UserName -> Password -> RIO env User
-- _signup :: UserName -> Password -> RIO env User
-- }
-- makeLenses ''AuthHandler
-- @
--
-- The @authHandlerImpl@ depends on another interface @UserRepo@,
-- which accesses a database to store user information.
--
-- @
-- data UserRepo env = UserRepo {
-- _createUser :: UserName -> RIO env UserId,
-- _setPassword :: UserId -> Password -> RIO env ()
-- }
-- makeLenses ''UserRepo
--
-- userRepoImpl :: (Has ConnectionPool env) => UserRepo env
-- userRepoImpl = ...
--
-- authHandlerImpl :: ('Has1' UserRepo env) => AuthHandler env
-- authHandlerImpl = AuthHandler signinImpl signupImpl
--
-- signupImpl :: (Has1 UserRepo env) => UserName -> Password -> RIO env User
-- signupImpl usernm passwd = ...
--
-- signinImpl :: (Has1 UserRepo env) => UserName -> Password -> RIO env User
-- signinImpl usernm passwd = ...
-- @
--
--
-- Assume that @UserRepo@ is a private interface and should not be exported.
-- Let's refactor @authHandlerImpl@ by using 'mapBaseRIO'.
--
-- @
-- data AuthHandler env = AuthHandler {
-- _signin :: UserName -> Password -> RIO env User
-- _signup :: UserName -> Password -> RIO env User
-- } deriving(Generic)
--
-- instance 'Interface' AuthHandler where
-- type 'IBase' AuthHandler = RIO
--
-- data AuthEnv env = AuthEnv !(UserRepo (AutheEnv env)) !(Extends env)
-- 'deriveEnv' ''AuthEnv
--
-- authHandlerImpl :: ('Has' ConnectionPool env) => AuthHandler env
-- authHandlerImpl = 'mapBaseRIO' (AuthEnv userRepoImpl . Extends) handler
-- where
-- handler = AuthHandler signinImpl signupImpl
-- @
--
-- Now, the dependency to @UserRepo@ is resolved in the module
-- and hidden from the signature of @authHandlerImpl@