{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NoOverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module Salak.Internal( loadAndRunSalak' , loadTrie , loadList , LoadSalakT , LoadSalak , RunSalakT , RunSalak , runRunSalak , HasSalak(..) , MonadSalak(..) , loadMock , loadEnv , loadCommandLine , ParseCommandLine , defaultParseCommandLine , tryLoadFile , Source , TraceSource , Keys(..) , Key(..) , simpleKeys , ToKeys(..) , setVal , Val(..) , Value(..) , ToValue(..) , liftNT , SourcePack(..) , MonadIO ) where import Control.Concurrent.MVar import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class (MonadIO, liftIO) import qualified Control.Monad.IO.Unlift as IU import Control.Monad.Reader import qualified Control.Monad.State as MS import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM import Data.Maybe import Data.Text (Text, pack) import qualified Data.Text as TT import Salak.Internal.Key import Salak.Internal.Prop import Salak.Internal.Source import Salak.Internal.Val import qualified Salak.Trie as T import System.Directory import System.Environment data UpdateSource = UpdateSource { ref :: MVar Source , refNo :: Int , refMap :: HashMap Int String , qfunc :: MVar QFunc , update :: MVar (IO ( TraceSource -- Updated Tries , IO ())) -- Confirm action } -- | Configuration Loader Monad, used for load properties from sources. Custom loaders using `loadTrie` newtype LoadSalakT m a = LoadSalakT { unLoad :: MS.StateT UpdateSource m a } deriving (Functor, Applicative, Monad, MS.MonadTrans) -- | Simple IO Monad type LoadSalak = LoadSalakT IO liftNT :: MonadIO m => LoadSalak () -> LoadSalakT m () liftNT (LoadSalakT a) = do ud <- MS.get MS.liftIO $ MS.evalStateT a ud instance MonadIO m => MonadSalak (LoadSalakT m) where askSalak = MS.get >>= toSourcePack instance MonadThrow m => MonadThrow (LoadSalakT m) where throwM = LoadSalakT . throwM instance MonadCatch m => MonadCatch (LoadSalakT m) where catch m f = do us <- MS.get lift $ MS.evalStateT (unLoad m) us `catch` (\e -> MS.evalStateT (unLoad $ f e) us) instance Monad m => MS.MonadState UpdateSource (LoadSalakT m) where state f = LoadSalakT $ MS.state f instance MonadIO m => MonadIO (LoadSalakT m) where liftIO = LoadSalakT . liftIO instance IU.MonadUnliftIO m => IU.MonadUnliftIO (LoadSalakT m) where askUnliftIO = LoadSalakT $ do ut <- MS.get f <- MS.lift IU.askUnliftIO return $ IU.UnliftIO $ IU.unliftIO f . (`MS.evalStateT` ut) . unLoad -- | Standard `HasSalak` instance. newtype RunSalakT m a = RunSalakT { unRun :: ReaderT SourcePack m a } deriving (Functor, Applicative, Monad, MonadTrans) -- | Simple IO Monad type RunSalak = RunSalakT IO instance Monad m => MonadSalak (RunSalakT m) where askSalak = RunSalakT ask instance Monad m => MonadReader SourcePack (RunSalakT m) where ask = RunSalakT ask local f m = RunSalakT $ local f $ unRun m instance MonadThrow m => MonadThrow (RunSalakT m) where throwM = RunSalakT . throwM instance MonadCatch m => MonadCatch (RunSalakT m) where catch m f = do us <- ask lift $ runReaderT (unRun m) us `catch` (\e -> runReaderT (unRun $ f e) us) instance MonadIO m => MonadIO (RunSalakT m) where liftIO = RunSalakT . liftIO instance IU.MonadUnliftIO m => IU.MonadUnliftIO (RunSalakT m) where askUnliftIO = RunSalakT $ do ut <- ask f <- lift IU.askUnliftIO return $ IU.UnliftIO $ IU.unliftIO f . (`runReaderT` ut) . unRun instance {-# OVERLAPPABLE #-} (m' ~ t (RunSalakT m), MonadTrans t, Monad m, Monad m') => MonadSalak m' where askSalak = lift askSalak runRunSalak :: SourcePack -> RunSalakT m a -> m a runRunSalak sp (RunSalakT m) = runReaderT m sp -- | Basic loader loadTrie :: MonadIO m => Bool -> String -> (Int -> IO TraceSource) -> LoadSalakT m () loadTrie canReload name f = do UpdateSource{..} <- MS.get v <- liftIO $ readMVar ref ts <- liftIO $ loadSource f refNo (fmap ([],) v) let (t,_,es) = extract v ts if null es then do liftIO $ modifyMVar_ update $ \u -> go ts u refNo let nut = UpdateSource ref (refNo + 1) (HM.insert refNo name refMap) qfunc update _ <- liftIO $ swapMVar ref t MS.put nut else error $ show es where go ts ud n = return $ do (c,d) <- ud c1 <- loadSource (if canReload then f else (\_ -> return ts)) n c return (c1,d) -- | Simple loader loadList :: (MonadIO m, Foldable f, ToKeys k, ToValue v) => Bool -> String -> IO (f (k,v)) -> LoadSalakT m () loadList canReload name iof = loadTrie canReload name (\i -> gen i <$> iof) -- | Standard salak functions, by load and with a `SourcePack` instance. -- Users should use `SourcePack` to create custom `MonadSalak` instances, then you get will an instance of `HasSalak`. loadAndRunSalak' :: (MonadThrow m, MonadIO m) => LoadSalakT m () -> (SourcePack -> m a) -> m a loadAndRunSalak' lstm f = load lstm >>= f load :: MonadIO m => LoadSalakT m () -> m SourcePack load (LoadSalakT lm) = do r <- liftIO $ newMVar T.empty q <- liftIO $ newMVar $ \s -> Right $ void $ swapMVar r s u <- liftIO $ newMVar $ return (T.empty, return ()) MS.execStateT lm (UpdateSource r 0 HM.empty q u) >>= toSourcePack toSourcePack :: MonadIO m => UpdateSource -> m SourcePack toSourcePack UpdateSource{..} = liftIO (readMVar ref) >>= \s -> return $ SourcePack s [] qfunc go where go = do t <- readMVar ref (ts, ac) <- join $ readMVar update let (s,cs,es) = extract t ts f <- readMVar qfunc if null es then case f s of Left e -> return (ReloadResult True $ lines e) Right a -> ac >> a >> return (ReloadResult False $ lines $ show cs) else return (ReloadResult True es) -- | Load mock variables into `Source` loadMock :: MonadIO m => [(Text, Text)] -> LoadSalakT m () loadMock fa = loadList False "mock" (return fa) -- | Load environment variables into `Source` loadEnv :: MonadIO m => LoadSalakT m () loadEnv = loadList False "environment" go where go = concatMap split2 . filter ((/= '_') . head . fst) <$> getEnvironment split2 (k,v) = [(TT.pack k,v),(convert k,v)] convert = TT.toLower . TT.pack . map (\c -> if c == '_' then '.' else c) -- | Convert arguments to properties type ParseCommandLine = [String] -> IO [(Text, Text)] -- | Default way to parse command line arguments defaultParseCommandLine :: ParseCommandLine defaultParseCommandLine = return . mapMaybe go where go ('-':'-':as) = case break (=='=') as of (a,'=':b) -> Just (pack a, pack b) _ -> Nothing go _ = Nothing -- | Default way to parse command line arguments loadCommandLine :: MonadIO m => ParseCommandLine -> LoadSalakT m () loadCommandLine pcl = loadList False "commandLine" (getArgs >>= pcl) -- | Try load file, if file does not exist then do nothing. tryLoadFile :: MonadIO m => (FilePath -> LoadSalakT m ()) -> FilePath -> LoadSalakT m () tryLoadFile f file = do b <- liftIO $ doesFileExist file when b $ do liftIO $ putStrLn $ "Load " ++ file f file