registry-options-0.1.0.0: src/Data/Registry/Options/Sources.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- | This module provides way to get option values from different sources:
--
-- - the command line
-- - the system environment variables
-- - a YAML configuration file
--
-- A registry is used to bring some extensibility:
--
-- - change the configuration file name
-- - change the mapping between option names and environment variable names
-- - change the mapping between options names and yaml names
--
-- Here is an example:
--
-- getLexemesWith (
-- -- restrict the env / config file search to the options of a given parser
-- setOptionNames (getOptionNames parser) .
-- -- change the config file path
-- setConfigFilePath "~/.config" .
-- -- change the config for retrieving environment variables based on option names
-- setEnvironmentNames env1 .
-- -- change the config for retrieving yaml values based on option names
-- setYamlNames yaml1 .
-- -- set command line arguments instead of taking them from getArgs
-- setArguments args .
-- -- set the priorities for the option values
-- setPriorities [commandLineSource, yamlSource])
module Data.Registry.Options.Sources where
import Data.ByteString qualified as BS
import Data.List qualified as L
import Data.Map qualified as M
import Data.Registry
import Data.Registry.Options.Lexemes hiding (getArguments)
import Data.Registry.Options.Text
import Data.Text qualified as T
import Data.YAML
import Protolude
import System.Environment (getEnvironment, lookupEnv)
-- | Get lexemes
getLexemes :: MonadIO m => m Lexemes
getLexemes = getLexemesWith (const sources)
-- | Get lexemes with a modified registry
getLexemesWith :: MonadIO m => (Registry _ _ -> Registry _ _) -> m Lexemes
getLexemesWith f = liftIO $ make @(IO Lexemes) (f sources)
-- | Set option names on the registry
setOptionNames :: [Text] -> Registry _ _ -> Registry _ _
setOptionNames names r = valTo @IO (OptionNames names) +: r
-- | Set the config file path
setConfigFilePath :: Text -> Registry _ _ -> Registry _ _
setConfigFilePath path r = valTo @IO (Just $ YamlPath path) +: r
-- | Set the configuration for environment names
setEnvironmentNames :: EnvironmentNames -> Registry _ _ -> Registry _ _
setEnvironmentNames names r = funTo @IO names +: r
-- | Set the configuration for yaml names
setYamlNames :: YamlNames -> Registry _ _ -> Registry _ _
setYamlNames names r = funTo @IO names +: r
-- | Set arguments as if they were read from the command line
setArguments :: [Text] -> Registry _ _ -> Registry _ _
setArguments args r = valTo @IO (Arguments args) +: r
-- | Set source priorities
setPriorities :: [Source] -> Registry _ _ -> Registry _ _
setPriorities ps r = valTo @IO (Priorities ps) +: r
-- | Registry allowing the retrieval of Lexemes from various sources: command line, environment variable, configuration file
sources :: Registry _ _
sources =
funTo @IO selectValues
<: funTo @IO getValuesFromEnvironment
<: funTo @IO getValuesFromYaml
<: funTo @IO getValuesFromCommandLine
<: funTo @IO defaultPriorities
<: fun getCommandlineArguments
<: funTo @IO defaultEnvironmentNames
<: funTo @IO defaultYamlNames
<: funTo @IO readYamlFile
<: valTo @IO (mempty :: OptionNames)
<: valTo @IO defaultYamlPath
-- | List of option names defined in a parser
-- It is used to restrict the names parsed in environment variables or in a configuration file
newtype OptionNames = OptionNames {_optionNames :: [Text]} deriving (Eq, Show, Semigroup, Monoid)
-- | Select lexed option names / values according to user defined priorities
selectValues :: Priorities -> Tag "environment" Lexemes -> Tag "yaml" Lexemes -> Tag "commandline" Lexemes -> Lexemes
selectValues priorities env yaml cl = do
let bySource = [(environmentSource, unTag env), (yamlSource, unTag yaml), (commandLineSource, unTag cl)]
foldr override mempty (reverse $ sortBySource priorities bySource)
-- * Command line
-- | Lex the arguments coming from the command line
getValuesFromCommandLine :: Arguments -> Tag "commandline" Lexemes
getValuesFromCommandLine = tag . lexArgs . _arguments
-- | By default arguments are retrieved from the base 'getArgs' function
getCommandlineArguments :: IO Arguments
getCommandlineArguments = Arguments . fmap toS <$> getArgs
-- | List of strings retrieved from the command line
newtype Arguments = Arguments {_arguments :: [Text]} deriving (Eq, Show, Semigroup, Monoid)
-- * Environment
-- | Get values from the environment
getValuesFromEnvironment :: OptionNames -> EnvironmentNames -> IO (Tag "environment" Lexemes)
getValuesFromEnvironment (OptionNames []) ens = do
lexemes <- fmap (\(n, v) -> optionLexemes (fromEnvironmentName ens $ toS n) (toS v)) <$> getEnvironment
pure . tag $ fold lexemes
getValuesFromEnvironment (OptionNames os) ens = do
lexemes <- for os $ \o -> maybe mempty (optionLexemes o . toS) <$> lookupEnv (toS $ toEnvironmentName ens o)
pure . tag $ fold lexemes
-- | Configuration for transforming an environment name into an option name
-- and for transforming an option name into an environment name
data EnvironmentNames = EnvironmentNames
{ fromEnvironmentName :: Text -> Text,
toEnvironmentName :: Text -> Text
}
-- | Default conversion functions for environment variables names to option names
-- @fromEnvironmentName "OPTION_NAME" == "optionName"@
-- @toEnvironmentName "optionName" == "OPTION_NAME"@
defaultEnvironmentNames :: EnvironmentNames
defaultEnvironmentNames =
EnvironmentNames
{ fromEnvironmentName = underscoreToHyphenated . T.toLower,
toEnvironmentName = T.toUpper . hyphenatedToUnderscore
}
-- * Yaml
-- | Values can be retrieved from a Yaml file
getValuesFromYaml :: YamlNames -> OptionNames -> Maybe YamlByteString -> IO (Tag "yaml" Lexemes)
getValuesFromYaml _ _ Nothing = pure (tag (mempty :: Lexemes))
getValuesFromYaml yns optionNames (Just (YamlByteString bs)) = do
case decodeNode (BS.fromStrict bs) of
Left e -> throwIO ("cannot decode the YAML document: " <> show e :: Text)
Right docs -> do
let yamlOptions = collectYamlOptions . docRoot =<< docs
let yos = case optionNames of
OptionNames [] -> yamlOptions
OptionNames os -> do
let osNames = toYamlName yns <$> os
filter (\(name, _) -> name `elem` osNames) yamlOptions
pure . tag . fold $ (\(name, vs) -> optionsLexemes (fromYamlName yns name) vs) <$> yos
-- | Text needs to have an Exception instance in order to use throwIO
instance Exception Text
-- | Path for a YAML document
newtype YamlPath = YamlPath {yamlPath :: Text} deriving (Eq, Show, Semigroup, Monoid)
-- | ByteString representing a YAML document
newtype YamlByteString = YamlByteString {yamlByteString :: ByteString} deriving (Eq, Show, Semigroup, Monoid)
-- | Default path for a configuration file
-- By default we don't read from a configuration file
defaultYamlPath :: Maybe YamlPath
defaultYamlPath = Nothing
-- | Read yaml as a ByteString from a configuration file
readYamlFile :: Maybe YamlPath -> IO (Maybe YamlByteString)
readYamlFile Nothing = pure Nothing
readYamlFile (Just (YamlPath path)) = Just . YamlByteString <$> BS.readFile (toS path)
-- | Collect what looks like options in a YAML document i.e. any list of strings leading to a scalar
collectYamlOptions :: Node Pos -> [(YamlName, [Text])]
collectYamlOptions (Scalar _ (SStr t)) = [(YamlName [], [t])]
collectYamlOptions (Scalar _ (SBool b)) = [(YamlName [], [show b])]
collectYamlOptions (Scalar _ (SInt i)) = [(YamlName [], [show i])]
collectYamlOptions (Scalar _ _) = []
collectYamlOptions (Mapping _ _ m) =
concat $ uncurry toKeysValue <$> M.assocs m
where
toKeysValue :: Node Pos -> Node Pos -> [(YamlName, [Text])]
toKeysValue (Scalar _ (SStr k)) n =
(\(YamlName ks, vs) -> (YamlName (k : ks), vs)) <$> collectYamlOptions n
toKeysValue _ _ = []
collectYamlOptions (Sequence _ _ ns) = concat $ collectYamlOptions <$> ns
collectYamlOptions (Anchor _ _ n) = collectYamlOptions n
-- | A YAML name is represented by a list of keys
newtype YamlName = YamlName {yamlName :: [Text]} deriving (Eq, Show)
-- | Configuration for transforming a YAML name into an option name
-- and for transforming an option name into a YAML name
-- We consider that a YAML name is a sequence of string keys in a nested YAML document
data YamlNames = YamlNames
{ fromYamlName :: YamlName -> Text,
toYamlName :: Text -> YamlName
}
-- | Default conversion functions for YAML variables names to option names
-- We only keep in names on the leaves of the YAML tree
--
-- @fromYamlName ["section", "option_name"] == "option-name"@
-- @toYamlName "option-name" == ["option_name"]@
defaultYamlNames :: YamlNames
defaultYamlNames =
YamlNames
{ fromYamlName = \(YamlName ts) ->
case reverse ts of
[] -> ""
t : _ -> underscoreToHyphenated t,
toYamlName = YamlName . pure . hyphenatedToUnderscore
}
-- * Sources and priorities
-- | List of sources sorted by the highest priority to the lowest
newtype Priorities = Priorities [Source] deriving (Eq, Show)
-- | By default we take environment values, then command line values, then values coming from a configuration file
defaultPriorities :: Priorities
defaultPriorities = Priorities [environmentSource, commandLineSource, yamlSource]
-- | Sort a list of values associated with a source, using Priorities to determine the order
sortBySource :: Priorities -> [(Source, a)] -> [a]
sortBySource (Priorities ps) ss = do
let compareSource source1 source2 = compare (L.elemIndex source1 ps) (L.elemIndex source2 ps)
snd <$> sortBy (\(s1, _) (s2, _) -> compareSource s1 s2) ss
-- | Source of an option value
-- This is modelled as a simple newtype on Text in order to enable
-- the creation of new sources
newtype Source = Source Text deriving (Eq, Show)
-- | Source of options values coming from the environment
environmentSource :: Source
environmentSource = Source "environment"
-- | Source of options values coming from the command line
commandLineSource :: Source
commandLineSource = Source "commandline"
-- | Source of options values coming from a YAML configuration file
yamlSource :: Source
yamlSource = Source "yaml"