{-# 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@