-- | -- Module: Salak -- Copyright: 2019 Daniel YU -- License: MIT -- Maintainer: leptonyu@gmail.com -- Stability: experimental -- Portability: portable -- -- Configuration (re)Loader and Parser. -- module Salak( -- * How to use this library -- $use -- * Salak Main Functions runSalak , runSalakWith , loadAndRunSalak , loadAndRunSalak' , PropConfig(..) -- * Parsing Properties Function , MonadSalak(..) , RunSalakT , RunSalak -- ** Operators , PropOp(..) , FromProp(..) , Prop , readPrimitive , readEnum , SourcePack , Salak , SalakException(..) , module Salak.Internal.Writable -- * Load Functions -- ** Monad for Loader , LoadSalakT , LoadSalak -- ** Basic loaders , loadCommandLine , ParseCommandLine , defaultParseCommandLine , loadEnv , loadMock , loadSalak , loadSalakWith -- ** File Loaders , ExtLoad , loadByExt , HasLoad(..) , (:|:)(..) -- ** Reload Functions , ReloadResult(..) -- * Reexport , MonadCatch , MonadThrow , MonadIO ) where import Control.Monad.Catch import Control.Monad.IO.Class (MonadIO) import Control.Monad.IO.Unlift import Control.Monad.Reader import Data.Default import Data.Maybe import Data.Text (Text) import Salak.Internal import Salak.Internal.Prop import Salak.Internal.Source import Salak.Internal.Writable import System.Directory import System.FilePath ((</>)) -- | Type synonyms of 'SourcePack' type Salak = SourcePack -- | Prop load configuration data PropConfig = PropConfig { configKey :: !Text -- ^ Specify config key, default is @application@. , configName :: !String -- ^ Specify config name, default is @application@. , searchCurrent :: !Bool -- ^ Search current directory, default true. , searchHome :: !Bool -- ^ Search home directory, default false. , commandLine :: !ParseCommandLine -- ^ How to parse commandline. , loggerF :: !LFunc , loadExt :: FilePath -> LoadSalak () } instance Default PropConfig where def = PropConfig "application" "application" True False defaultParseCommandLine (\_ _ -> return ()) (\_ -> return ()) data FileConfig = FileConfig { configNm :: Maybe String , configDir :: Maybe FilePath } instance FromProp m FileConfig where {-# INLINE fromProp #-} fromProp = FileConfig <$> "name" .?= Nothing <*> "dir" .?= Nothing -- | Load file by extension type ExtLoad = (String, FilePath -> LoadSalak ()) class HasLoad a where loaders :: a -> [ExtLoad] data a :|: b = a :|: b infixr 3 :|: instance (HasLoad a, HasLoad b) => HasLoad (a :|: b) where loaders (a :|: b) = loaders a ++ loaders b -- | Load files with specified format, yaml or toml, etc. loadByExt :: HasLoad a => a -> FilePath -> LoadSalak () loadByExt xs f = mapM_ go (loaders xs) where {-# INLINE go #-} go (ext, ly) = tryLoadFile ly $ f ++ "." ++ ext -- | Default load salak. -- All these configuration sources has orders, from highest priority to lowest priority: -- -- > 1. loadCommandLine -- > 2. loadEnvironment -- > 3. loadConfFiles -- > 4. load file from folder `salak.conf.dir` if defined -- > 5. load file from current folder if enabled -- > 6. load file from home folder if enabled -- > 7. file extension matching, support yaml or toml or any other loader. -- loadSalak :: (MonadThrow m, MonadIO m) => PropConfig -> LoadSalakT m () loadSalak PropConfig{..} = do setLogF loggerF loadCommandLine commandLine loadEnv FileConfig{..} <- require configKey forM_ [ return configDir , ifS searchCurrent getCurrentDirectory , ifS searchHome getHomeDirectory ] (loadConf $ fromMaybe configName configNm) where {-# INLINE ifS #-} ifS True gxd = Just <$> liftIO gxd ifS _ _ = return Nothing {-# INLINE loadConf #-} loadConf n mf = lift mf >>= mapM_ (liftNT . loadExt . (</> n)) loadSalakWith :: (MonadThrow m, MonadIO m, HasLoad file) => file -> String -> LoadSalakT m () loadSalakWith file name = loadSalak def { configName = name, loadExt = loadByExt file } -- | Run salak, load strategy refer to `loadSalak` runSalak :: (MonadCatch m, MonadIO m) => PropConfig -> RunSalakT m a -> m a runSalak c = loadAndRunSalak (loadSalak c) -- | Run salak, load strategy refer to `loadSalakWith` runSalakWith :: (MonadCatch m, MonadIO m, HasLoad file) => String -> file -> RunSalakT m a -> m a runSalakWith name file = loadAndRunSalak (loadSalakWith file name) -- $use -- -- | This library defines a universal procedure to load configurations and parse properties, also supports reload configurations. -- -- -- We can load configurations from command lines, environment, configuration files such as yaml or toml etc., -- and we may want to have our own strategies to load configurations from multiply sources and overwrite properties by orders of these sources. -- -- `PropConfig` defines a common loading strategy: -- -- > 1. loadCommandLine -- > 2. loadEnvironment -- > 3. loadConfFiles -- > 4. load file from folder `application.dir` if defined -- > 5. load file from current folder if enabled -- > 6. load file from home folder if enabled -- > 7. file extension matching, support yaml or toml or any other loader. -- -- Load earlier has higher priority. Priorities cannot be changed. -- -- After loading configurations, we can use `require` to parse properties. For example: -- -- > a :: Bool <- require "bool.key" -- > b :: Maybe Int <- require "int.optional.key" -- > c :: Either String Int <- require "int.error.key" -- > d :: IO Int <- require "int.reloadable.key" -- -- Salak supports parse `IO` values, which actually wrap a 'Control.Concurrent.MVar.MVar' variable and can be reseted by reloading configurations. -- Normal value will not be affected by reloading configurations. -- -- GHCi play -- -- >>> :set -XFlexibleInstances -XMultiParamTypeClasses -XOverloadedStrings -- >>> import Salak -- >>> import Data.Default -- >>> import Data.Text(Text) -- >>> data Config = Config { name :: Text, dir :: Maybe Text, ext :: Int} deriving (Eq, Show) -- >>> instance FromProp m Config where fromProp = Config <$> "user" <*> "dir" <*> "ext" .?= 1 -- >>> runSalak def (require "") :: IO Config -- Config {name = "daniel", dir = Nothing, ext = 1} --