{-| An 'Environment' contains textual key value pairs, relavant for string template substitution. The variables are passed to the B9 build either via command line, OS environment variables or configuration file. @since 0.5.62 -} module B9.Environment ( Environment() , fromStringPairs , addStringBinding , insertPositionalArguments , EnvironmentReaderT , MonadEnvironment , runEnvironmentReaderT , askEnvironment , localEnvironment , lookupOrThrow , lookupEither , KeyNotFound(..) , DuplicateKey(..) ) where import Control.Arrow ((***)) import Control.Exception (Exception) import Control.Monad.Catch (MonadThrow, throwM) import Control.Monad.Reader import Control.Parallel.Strategies import Data.Data import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.Maybe (maybe) import Data.Text.Lazy (Text) import qualified Data.Text.Lazy as Text import GHC.Generics (Generic) -- | A map of textual keys to textual values. -- -- @since 0.5.62 data Environment = MkEnvironment { nextPosition :: Int , fromEnvironment :: HashMap Text Text } deriving (Show, Typeable, Data, Eq, Generic) instance NFData Environment instance Semigroup Environment where e1 <> e2 = MkEnvironment { nextPosition = case (nextPosition e1, nextPosition e2) of (0, 0) -> 0 (0, p2) -> p2 (p1, 0) -> p1 _ -> error ("Overlapping positional arguments (<>): (" ++ show e1 ++ ") <> (" ++ show e2 ++ ")") , fromEnvironment = let i = HashMap.intersection (fromEnvironment e1) (fromEnvironment e2) in if HashMap.null i then fromEnvironment e1 <> fromEnvironment e2 else error ("Overlapping entries (<>): (" ++ show e1 ++ ") <> (" ++ show e2 ++ "): (" ++ show i ++ ")") } instance Monoid Environment where mempty = MkEnvironment 0 HashMap.empty -- | If environment variables @arg_1 .. arg_n@ are bound -- and a list of @k@ additional values are passed to this function, -- store them with keys @arg_(n+1) .. arg_(n+k)@. -- -- Note that the Environment contains an index of the next position. -- -- @since 0.5.62 insertPositionalArguments :: [Text] -> Environment -> Environment insertPositionalArguments = flip (foldr (\arg (MkEnvironment i e) -> MkEnvironment (i + 1) (HashMap.insert (Text.pack ("arg_" ++ show i)) arg e))) -- | Create an 'Environment' from a list of pairs ('String's) -- -- @since 0.5.62 fromStringPairs :: [(String, String)] -> Environment fromStringPairs = MkEnvironment 0 . HashMap.fromList . fmap (Text.pack *** Text.pack) -- | Insert a value into an 'Environment'. -- -- @since 0.5.62 addStringBinding :: MonadThrow m => (String, String) -> Environment -> m Environment addStringBinding (k, vNew) env = case HashMap.lookup (Text.pack k) (fromEnvironment env) of Just vOld -> throwM (MkDuplicateKey (Text.pack k) vOld (Text.pack vNew)) Nothing -> pure (MkEnvironment (nextPosition env) (HashMap.insert (Text.pack k) (Text.pack vNew) (fromEnvironment env))) -- | A monad transformer providing a 'MonadReader' instance for 'Environment' -- -- @since 0.5.62 type EnvironmentReaderT m a = ReaderT Environment m a -- | A constraint on a monad @m@ ensuring (read-only) access to an 'Environment' -- -- @since 0.5.62 type MonadEnvironment m = MonadReader Environment m -- | Run a 'ReaderT' of 'Environment'. -- -- @since 0.5.62 runEnvironmentReaderT :: Environment -> EnvironmentReaderT m a -> m a runEnvironmentReaderT = flip runReaderT -- | Get the current 'Environment' -- -- @since 0.5.62 askEnvironment :: MonadEnvironment m => m Environment askEnvironment = ask -- | Run a computation with a modified 'Environment' -- -- @since 0.5.62 localEnvironment :: MonadEnvironment m => (Environment -> Environment) -> m a -> m a localEnvironment = local -- | Lookup a key for a value. -- -- 'throwM' a 'KeyNotFound' 'Exception' if no value with the given key exists -- in the 'Environment'. -- -- @Since 0.5.62 lookupOrThrow :: (MonadThrow m, MonadEnvironment m) => Text -> m Text lookupOrThrow key = do env <- askEnvironment maybe (throwM (MkKeyNotFound key env)) return (HashMap.lookup key (fromEnvironment env)) -- | Lookup a key for a value. -- -- Return 'Either' 'Left' 'KeyNotFound', if no value with the given key exists -- in the 'Environment', or 'Right' the value. -- -- @Since 0.5.62 lookupEither :: MonadEnvironment m => Text -> m (Either KeyNotFound Text) lookupEither key = do env <- askEnvironment (return . maybe (Left (MkKeyNotFound key env)) Right) (HashMap.lookup key (fromEnvironment env)) -- | An 'Exception' thrown by 'addBinding' indicating that a key already exists. -- -- @Since 0.5.62 data DuplicateKey = MkDuplicateKey { duplicateKey :: Text , duplicateKeyOldValue :: Text , duplicateKeyNewValue :: Text } deriving (Typeable, Show) instance Exception DuplicateKey -- | An 'Exception' thrown by 'lookupOrThrow' indicating that a key does not exist. -- -- @Since 0.5.62 data KeyNotFound = MkKeyNotFound Text Environment deriving (Typeable) instance Exception KeyNotFound instance Show KeyNotFound where showsPrec _ (MkKeyNotFound key env) = let keys = unlines (Text.unpack <$> HashMap.keys (fromEnvironment env)) in showString "Invalid template parameter: \"" . showString (Text.unpack key) . showString "\".\nValid variables:\n" . showString keys