{-| The package provides the configuration loader. An configuration example: @ _vars: window: width: 800 height: 600 app: - name: message-layer properties: window: image: resources/window-base.png position: x: 0 y: ${${var:window.height} - ${ref:..size.height}} size: width: ${var:window.width} height: 150 @ == Syntax === @_vars@ You can define a new variable. Use object syntax under the @_vars@ field. The variables can be referenced in all siblings and under their siblings to the @_vars@, in the variable syntax @${var:_path_}@. === Expr In each field, you can specify an expression defined in the loader. - @${}@: enclose the expr by @${}@, to tell the parsers that the field is an expr not a plain string. - @${ref:_path_}@: specify any path to refer any other value. The path resolution is performed once, not recursively resolved. @_path_@ consists of field names splitted by a period. Use double dots @..@ for a parent. - @${var:_path_}@: specify any path to value defined at the field. @_path_@ consists of field names splitted by a period. - arithmetic operator: addition, subtraction, multiplication and division (@+,-,*,/@) can also be used in @${}@. -} module MiniLight.Loader ( module MiniLight.Loader.Internal.Types, createComponentBy, lookupByTagID, LoaderEnv (..), HasLoaderEnv (..), resolveConfig, loadAppConfig, patchAppConfig, resolve, parseAppConfig, ) where import Control.Lens import Control.Monad import qualified Control.Monad.Caster as Caster import Control.Monad.Catch import Control.Monad.Trans import Control.Monad.Trans.Maybe import Data.Aeson import qualified Data.Aeson.Diff as Diff import Data.Aeson.Patch import Data.Aeson.Pointer import Data.IORef import qualified Data.HashMap.Strict as HM import qualified Data.Registry as R import qualified Data.Text as T import qualified Data.Vector as V import Data.Yaml (decodeFileEither) import MiniLight.Light import MiniLight.Component import MiniLight.Loader.Internal.Types import MiniLight.Loader.Internal.Resolver (resolve, resolveWith, parseAppConfig, emptyContext, Context(..)) -- | The environment for config loader data LoaderEnv = LoaderEnv { registry :: R.Registry Component, tagRegistry :: IORef (HM.HashMap T.Text T.Text), appConfig :: IORef AppConfig } makeClassy_ ''LoaderEnv -- | Create a component with given resolver. createComponentBy :: (HasLoaderEnv env, HasLightEnv env, MonadIO m) => Resolver -> Maybe T.Text -> ComponentConfig -> LightT env m (Either String Component) createComponentBy resolver uid config = do uuid <- maybe newUID return uid result <- liftMiniLight $ resolver (componentType config) uuid (properties config) -- register tag case tagID config of Just tag -> do regRef <- view _tagRegistry liftIO $ modifyIORef' regRef $ \reg -> HM.insert tag uuid reg Caster.debug $ "TagID registered: " <> show tag <> " = " <> show uuid _ -> return () return $ fmap ( \c -> setHooks c ( fmap ( fmap $ \hk -> (,) (signalName hk) $ \evprops -> (\(Right r) -> r) $ resolveWith (emptyContext { values = evprops }) (parameter hk) ) $ hooks config ) ) result lookupByTagID :: (HasLightEnv env, HasLoaderEnv env, MonadIO m) => T.Text -> LightT env m (Maybe T.Text) lookupByTagID tag = do regRef <- view _tagRegistry reg <- liftIO $ readIORef regRef return $ HM.lookup tag reg -- | Load an config file and return the resolved @AppConfig@. resolveConfig :: MonadIO m => FilePath -> m (Either T.Text AppConfig) resolveConfig path = liftIO $ (parseAppConfig <=< either (Left . T.pack . show) Right) <$> decodeFileEither path -- | Load an config file and set in the environment. Calling this function at once, this overrides all values in the environment. -- This will generate an error log and skip the component if the configuration is invalid. -- This function also assign unique IDs for each component, using 'assignUID'. loadAppConfig :: (HasLightEnv env, HasLoaderEnv env, MonadIO m, MonadCatch m) => FilePath -- ^ Filepath to the yaml file. -> Resolver -- ^ Specify any resolver. -> LightT env m () loadAppConfig path mapper = fmap (maybe () id) $ runMaybeT $ do conf <- resolveConfig path >>= \case Left e -> do lift $ Caster.err e fail "" Right r -> return r confs <- lift $ fmap (V.mapMaybe id) $ V.forM (app conf) $ \conf -> do uid <- newUID result <- createComponentBy mapper (Just uid) conf case result of Left e -> do Caster.err e return Nothing Right component -> do reg <- view _registry R.register reg uid component Caster.info $ "Component loaded: {type: " <> show (componentType conf) <> ", uid: " <> show uid <> "}" return $ Just (uid, conf) ref <- view _appConfig liftIO $ writeIORef ref $ AppConfig (V.map snd confs) (V.map fst confs) -- | Load the config file again and place the newly loaded components. This is used for HCR (hot component replacement). -- Call 'loadAppConfig' first. patchAppConfig :: (HasLightEnv env, HasLoaderEnv env, MonadIO m, MonadCatch m) => FilePath -- ^ Filepath to the yaml file. -> Resolver -- ^ Specify any resolver. -> LightT env m () patchAppConfig path resolver = fmap (maybe () id) $ runMaybeT $ do cref <- view _appConfig appConfig <- liftIO $ readIORef cref conf' <- do mconf <- lift $ resolveConfig path case mconf of Left e -> do lift $ Caster.err e fail "" Right r -> return r lift $ forM_ ( Diff.patchOperations $ Diff.diff (toJSON $ app appConfig) (toJSON $ app conf') ) $ \op -> fmap (maybe () id) $ runMaybeT $ do lift $ Caster.debug $ "CMR detected: " <> show op case op of Add (Pointer [AKey n]) v -> create n v Rem (Pointer [AKey n]) -> remove n Rep (Pointer [AKey n ]) v -> modify n (Rep (Pointer []) v) Rep (Pointer (AKey n:path)) v -> modify n (Rep (Pointer path) v) Add (Pointer (AKey n:path)) v -> modify n (Add (Pointer path) v) Rem (Pointer (AKey n:path)) -> modify n (Rem (Pointer path)) _ -> lift $ Caster.warn $ "CMR does not support the operation yet: " <> show op where create n v = do cref <- view _appConfig compConf <- case fromJSON v of Success a -> return a Error err -> do lift $ Caster.err err fail "" newID <- lift newUID component <- do result <- lift $ createComponentBy resolver (Just newID) compConf case result of Left err -> do lift $ Caster.err $ "Failed to resolve: " <> err fail "" Right c -> return c reg <- view _registry lift $ R.insert reg n (getUID component) component lift $ Caster.info $ "Component registered: {type: " <> show (componentType compConf) <> ", uid: " <> show (getUID component) <> "}" liftIO $ modifyIORef' cref $ \conf -> conf { app = V.snoc (app conf) compConf , uuid = V.snoc (uuid conf) newID } remove n = do cref <- view _appConfig appConf <- liftIO $ readIORef cref let uid = uuid appConf V.! n reg <- view _registry lift $ R.delete reg uid lift $ Caster.info $ "Component deleted: {uid: " <> show uid <> "}" liftIO $ writeIORef cref $ appConf { app = V.ifilter (\i _ -> i /= n) $ app appConf , uuid = V.ifilter (\i _ -> i /= n) $ uuid appConf } modify n op = do cref <- view _appConfig appConf <- liftIO $ readIORef cref compConf <- case Diff.applyOperation op (toJSON (app appConf V.! n)) >>= fromJSON of Success a -> return a Error err -> do lift $ Caster.err err fail "" let uid = uuid appConf V.! n component <- do result <- lift $ createComponentBy resolver (Just uid) compConf case result of Left err -> do lift $ Caster.err $ "Failed to resolve: " <> err fail "" Right c -> return c reg <- view _registry lift $ R.write reg uid component lift $ Caster.info $ "Component replaced: {name: " <> show (componentType compConf) <> ", uid: " <> show (getUID component) <> "}" liftIO $ writeIORef cref $ appConf { app = app appConf V.// [(n, compConf)] }