packages feed

configifier 0.0.3 → 0.0.4

raw patch · 4 files changed

+318/−114 lines, 4 filesdep ~aesondep ~basePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: aeson, base

API changes (from Hackage documentation)

- Data.Configifier: ConfigFileYaml :: SBS -> Source
- Data.Configifier: InvalidYaml :: SBS -> String -> Error
- Data.Configifier: configify' :: (tm ~ TaggedM cfg, Show tm, Monoid tm, Freeze cfg, FromJSON tm, HasParseShellEnv cfg, HasParseCommandLine cfg, CanonicalizePartial cfg) => tm -> [Source] -> Result cfg
- Data.Configifier: instance [overlap ok] (cfg ~ 'Label p cfg', t ~ ToConfig cfg Id, Sel cfg' ps, KnownSymbol p) => Sel ('Label p cfg') (p : ps)
- Data.Configifier: instance [overlap ok] Eq Error
- Data.Configifier: instance [overlap ok] Ord Error
- Data.Configifier: type Result cfg = Either Error (Tagged cfg)
+ Data.Configifier: InvalidYamlFile :: FilePath -> ParseException -> Error
+ Data.Configifier: InvalidYamlString :: SBS -> ParseException -> Error
+ Data.Configifier: YamlFile :: FilePath -> Source
+ Data.Configifier: YamlString :: SBS -> Source
+ Data.Configifier: configifyWithDefault :: (tm ~ TaggedM cfg, Show tm, Monoid tm, Freeze cfg, FromJSON tm, HasParseShellEnv cfg, HasParseCommandLine cfg, CanonicalizePartial cfg) => tm -> [Source] -> IO (Tagged cfg)
+ Data.Configifier: defaultSources :: [FilePath] -> IO [Source]
+ Data.Configifier: instance [overlap ok] (ToConfig ('Descr a s) Maybe ~ ToConfig a Maybe, ToConfig a Maybe ~ Maybe a', Monoid (TaggedM a)) => Monoid (TaggedM ('Descr a s))
+ Data.Configifier: instance [overlap ok] (ToConfig ('Descr cfg s) Maybe ~ ToConfig cfg Maybe, FromJSON (TaggedM cfg)) => FromJSON (TaggedM ('Descr cfg s))
+ Data.Configifier: instance [overlap ok] (ToConfig ('Descr cfg s) Maybe ~ ToConfig cfg Maybe, HasParseShellEnv cfg) => HasParseShellEnv ('Descr cfg s)
+ Data.Configifier: instance [overlap ok] (ToConfig ('Descr cfg s) Maybe ~ ToConfig cfg Maybe, ToJSON (TaggedM cfg)) => ToJSON (TaggedM ('Descr cfg s))
+ Data.Configifier: instance [overlap ok] (ToConfig ('Descr t s) Maybe ~ ToConfig t Maybe, ToConfig ('Descr t s) Id ~ ToConfig t Id, Freeze t) => Freeze ('Descr t s)
+ Data.Configifier: instance [overlap ok] (cfg ~ 'Descr cfg' s, CanonicalizePartial cfg') => CanonicalizePartial ('Descr cfg' s)
+ Data.Configifier: instance [overlap ok] (cfg ~ 'Descr cfg' s, Sel cfg' ps, ToConfig ('Descr cfg' s) Id ~ ToConfig cfg' Id) => Sel ('Descr cfg' s) ps
+ Data.Configifier: instance [overlap ok] (cfg ~ 'Label p cfg', Sel cfg' ps, KnownSymbol p) => Sel ('Label p cfg') (p : ps)
+ Data.Configifier: invalidYamlFile :: Error -> FilePath
+ Data.Configifier: parseConfigFileWithIncludes :: FromJSON (TaggedM cfg) => FilePath -> IO (Either Error (TaggedM cfg))
+ Data.Configifier: popArg :: Args -> Either String ((String, String), Args)
+ Data.Configifier: readUserConfigFiles :: [Source] -> [Source]
+ Data.Configifier: withShellEnvPrefix :: Env -> IO Env
+ Data.Configifier: withShellEnvPrefix' :: String -> Env -> Env
- Data.Configifier: configify :: (tm ~ TaggedM cfg, Show tm, Monoid tm, Freeze cfg, FromJSON tm, HasParseShellEnv cfg, HasParseCommandLine cfg, CanonicalizePartial cfg) => [Source] -> Result cfg
+ Data.Configifier: configify :: (tm ~ TaggedM cfg, Show tm, Monoid tm, Freeze cfg, FromJSON tm, HasParseShellEnv cfg, HasParseCommandLine cfg, CanonicalizePartial cfg) => [Source] -> IO (Tagged cfg)
- Data.Configifier: invalidYamlMsg :: Error -> String
+ Data.Configifier: invalidYamlMsg :: Error -> ParseException
- Data.Configifier: parseArgsWithEqSign :: String -> Either String Env
+ Data.Configifier: parseArgsWithEqSign :: String -> Either String (String, String)
- Data.Configifier: parseArgsWithSpace :: String -> String -> Either String Env
+ Data.Configifier: parseArgsWithSpace :: String -> String -> Either String (String, String)

Files

configifier.cabal view
@@ -1,15 +1,7 @@ name:                configifier-version:             0.0.3-synopsis:-    parser for config files, shell variables, command line args.-description:--    WARNING NOT READY FOR USE YET.  JUST PUTTING IT OUT THERE TO HAVE A DISCUSSION.-    .-    Read runtime configuration from files, command line, and shell-    environment in a uniform, canonical, and flexible way.  Inspired-    by, among others, Configurator, cmdargs, optparse-applicative.-+version:             0.0.4+synopsis:            parser for config files, shell variables, command line args.+description:         See <https://github.com/zerobuzz/configifier/blob/master/README.md README> license:             AGPL-3 license-file:        LICENSE author:              Matthias Fischmann <mf@zerobuzz.net>, Andres Löh <andres@well-typed.com>@@ -26,6 +18,9 @@ flag profiling   default: False +flag with-example+  default: False+ library   hs-source-dirs:       src@@ -36,9 +31,9 @@   exposed-modules:       Data.Configifier   build-depends:-      base >=4.7 && <4.8+      base >=4.7 && <5 -    , aeson >=0.8 && <0.9+    , bytestring >=0.10 && <0.11     , case-insensitive >=1.2 && <1.3     , containers >=0.5 && <0.6     , either >=4.3 && <4.4@@ -51,6 +46,11 @@     , yaml >=0.8 && <0.9  executable configifier-example+  if flag(with-example)+    Buildable: True+  else+    Buildable: False+   default-language:       Haskell2010   hs-source-dirs:
examples/Main.hs view
@@ -17,7 +17,7 @@ where  import Control.Applicative ((<$>))-import Data.String.Conversions (ST, cs)+import Data.String.Conversions (ST, cs, (<>)) import Data.Typeable (Proxy(Proxy)) import System.Environment (getEnvironment, getArgs) import Text.Show.Pretty (ppShow)@@ -30,20 +30,20 @@  -- * an interesting example -type Cfg = NoDesc CfgDesc-type CfgDesc = ToConfigCode Cfg'-+type Cfg = ToConfigCode Cfg' type Cfg' =-             "frontend"      :> ServerCfg :>: "descr"-  :*> Maybe ("backend"       :> ServerCfg)-  :*>        "default_users" :> [UserCfg] :>: "list of users that are created on start if database is empty"+             "frontend"      :> ServerCfg' :>: "descr"+  :*> Maybe ("backend"       :> ServerCfg')+  :*>        "default_users" :> [UserCfg'] :>: "list of users that are created on start if database is empty" -type ServerCfg =+type ServerCfg = ToConfigCode ServerCfg'+type ServerCfg' =              "bind_port"   :> Int   :*>        "bind_host"   :> ST   :*> Maybe ("expose_host" :> ST) -type UserCfg =+type UserCfg = ToConfigCode UserCfg'+type UserCfg' =              "name"     :> ST :>: "user name (must be unique)"   :*>        "email"    :> ST :>: "email address (must also be unique)"   :*>        "password" :> ST :>: "password (not encrypted)"@@ -62,12 +62,12 @@ main :: IO () main = do     sources <- sequence-        [ ConfigFileYaml <$> SBS.readFile "examples/config.yaml"-        , ShellEnv       <$> getEnvironment-        , CommandLine    <$> getArgs+        [ YamlString  <$> SBS.readFile "examples/config.yaml"+        , ShellEnv    <$> getEnvironment+        , CommandLine <$> getArgs         ] -    ST.putStrLn $ docs (Proxy :: Proxy CfgDesc)+    ST.putStrLn $ docs (Proxy :: Proxy Cfg) <> "\n\n"      let dump cfg = do             putStrLn $ ppShow cfg@@ -75,11 +75,56 @@      dump defaultCfg -    case configify sources :: Result Cfg of-        Left e -> print e-        Right cfg -> do-            dump cfg+    cfg :: Tagged Cfg <- configify sources+    dump cfg -            putStrLn "accessing config values:"-            print $ cfg >>. (Proxy :: Proxy '["frontend"])-            print $ cfg >>. (Proxy :: Proxy '["frontend", "expose_host"])+    putStrLn "accessing config values:"+    print $ (Tagged (cfg >>. (Proxy :: Proxy '["frontend"])) :: Tagged ServerCfg)+    print $ cfg >>. (Proxy :: Proxy '["frontend", "expose_host"])+    print $ cfg >>. (Proxy :: Proxy '["frontend", "bind_port"])+++{-++Example session:++$ configifier-example+[...]+accessing config values:+(Tagged Id 8001 :*> (Id "localhost" :*> JustO (Id "fost")))+Just "fost"+8001++$ FRONTEND_BIND_PORT=31 configifier-example+[...]+accessing config values:+(Tagged Id 31 :*> (Id "localhost" :*> JustO (Id "fost")))+Just "fost"+31++$ FRONTEND_BIND_PORT=31 configifier-example --frontend-bind-port 15+[...]+accessing config values:+(Tagged Id 15 :*> (Id "localhost" :*> JustO (Id "fost")))+Just "fost"+15++$ configifier-example --frontend-expose-host "false"+[...]+configifier-example: CommandLinePrimitiveOtherError (ShellEnvNoParse {shellEnvNoParseType = "Text", shellEnvNoParseValue = "false", shellEnvNoParseMsg = "when expecting a Text, encountered Boolean instead"})++$ configifier-example --frontend-expose-host "\"false\""+[...]+accessing config values:+(Tagged Id 8001 :*> (Id "localhost" :*> JustO (Id "false")))+Just "false"+8001++$ configifier-example --config examples/config2.yaml+[...]+backend:+  bind_host: arrr+  bind_port: 281+[...]++-}
src/Data/Configifier.hs view
@@ -20,22 +20,23 @@ module Data.Configifier where -import Control.Applicative ((<$>), (<*>), (<|>))-import Control.Exception (Exception)-import Data.Aeson (ToJSON, FromJSON, Value(Object, Null), object, toJSON, (.=))+import Control.Applicative ((<$>), (<|>))+import Control.Exception (Exception, throwIO) import Data.CaseInsensitive (mk) import Data.Char (toUpper) import Data.Either.Combinators (mapLeft) import Data.Function (on)-import Data.List (nubBy, intercalate)+import Data.List (nubBy, intercalate, isPrefixOf) import Data.Maybe (catMaybes) import Data.Monoid (Monoid, (<>), mempty, mappend, mconcat) import Data.String.Conversions (ST, SBS, cs) import Data.Typeable (Typeable, Proxy(Proxy), typeOf)+import Data.Yaml.Include (decodeFileEither)+import Data.Yaml (ToJSON, FromJSON, Value(Object, Array, Null), object, toJSON, parseJSON, (.=)) import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)+import System.Environment (getEnvironment, getArgs, getProgName) -import qualified Data.Aeson as Aeson-import qualified Data.Aeson.Types as Aeson+import qualified Data.ByteString as SBS import qualified Data.HashMap.Strict as HashMap import qualified Data.Vector as Vector import qualified Data.Yaml as Yaml@@ -80,19 +81,15 @@     ToConfigCode (Maybe a) = Option (ToConfigCode a)     ToConfigCode a         = Type a --- | Filter 'Descr' constructors from 'ConfigCode'.+{-# DEPRECATED NoDesc "use of NoDesc is redundant and can be dropped without replacement." #-} type family NoDesc (a :: ConfigCode *) :: ConfigCode * where-    NoDesc (Record a b) = Record (NoDesc a) (NoDesc b)-    NoDesc (Label s a)  = Label s (NoDesc a)-    NoDesc (Descr a s)  = NoDesc a-    NoDesc (List a)     = List (NoDesc a)-    NoDesc (Option a)   = Option (NoDesc a)-    NoDesc (Type a)     = Type a+    NoDesc a = a  -- | Map 'ConfgCode' types to the types of config values. type family ToConfig (a :: ConfigCode *) (f :: * -> *) :: * where     ToConfig (Record a b) f = ToConfig a f :*> ToConfig b f     ToConfig (Label s a)  f = f (ToConfig a f)+    ToConfig (Descr a s)  f = ToConfig a f     ToConfig (List a)     f = [ToConfig a f]     ToConfig (Option a)   f = MaybeO (ToConfig a f)     ToConfig (Type a)     f = a@@ -111,7 +108,8 @@ -- * sources  data Source =-      ConfigFileYaml SBS+      YamlString SBS+    | YamlFile FilePath     | ShellEnv [(String, String)]     | CommandLine [String]   deriving (Eq, Ord, Show, Typeable)@@ -146,13 +144,15 @@  -- * results and errors -type Result cfg = Either Error (Tagged cfg)- data Error =-      InvalidYaml+      InvalidYamlString         { invalidYamlInput :: SBS-        , invalidYamlMsg :: String+        , invalidYamlMsg :: Yaml.ParseException         }+    | InvalidYamlFile+        { invalidYamlFile :: FilePath+        , invalidYamlMsg :: Yaml.ParseException+        }     | ShellEnvNil     | ShellEnvNoParse         { shellEnvNoParseType  :: String@@ -164,7 +164,7 @@     | FreezeIncomplete         { freezeIncompleteAtPath :: [String]         }-  deriving (Eq, Ord, Show, Typeable)+  deriving (Show, Typeable)  instance Exception Error @@ -180,10 +180,10 @@       , HasParseShellEnv cfg       , HasParseCommandLine cfg       , CanonicalizePartial cfg-      ) => [Source] -> Result cfg-configify = configify' (mempty :: tm)+      ) => [Source] -> IO (Tagged cfg)+configify = configifyWithDefault (mempty :: tm) -configify' :: forall cfg tm .+configifyWithDefault :: forall cfg tm .       ( tm ~ TaggedM cfg       , Show tm       , Monoid tm@@ -192,20 +192,90 @@       , HasParseShellEnv cfg       , HasParseCommandLine cfg       , CanonicalizePartial cfg-      ) => tm -> [Source] -> Result cfg-configify' def sources = sequence (get <$> sources) >>= merge . (def:)+      ) => tm -> [Source] -> IO (Tagged cfg)+configifyWithDefault def sources = sequence (get <$> readUserConfigFiles sources) >>= run . merge . (def:)   where-    get :: Source -> Either Error tm-    get (ConfigFileYaml sbs) = parseConfigFile sbs-    get (ShellEnv env)       = parseShellEnv env-    get (CommandLine args)   = parseCommandLine args+    run :: Either Error a -> IO a+    run = either throwIO return +    get :: Source -> IO tm+    get (YamlString sbs)     = run $ parseConfigFile sbs+    get (YamlFile fpath)     = parseConfigFileWithIncludes fpath >>= run+    get (ShellEnv env)       = run $ parseShellEnv env+    get (CommandLine args)   = run $ parseCommandLine args ++-- * IO++-- | From a list of config file paths, construct a source list that+-- (1) reads those files allowing for recursive includes; then (2)+-- processes shell environment variables (with `getProgName` as+-- prefix), and finally (3) processes command line args, turning+-- @--config@ arguments into further recursive config file loads.+defaultSources :: [FilePath] -> IO [Source]+defaultSources filePaths = do+    let files = YamlFile <$> filePaths+    env  <- ShellEnv     <$> (getEnvironment >>= withShellEnvPrefix)+    args <- CommandLine  <$> getArgs+    return $ files ++ [env] ++ readUserConfigFiles [args]++-- | Require that all shell env variables start with executable name.+-- (This is just a call to 'requireShellEnvPrefix'' with the result of+-- 'progName'.)+withShellEnvPrefix :: Env -> IO Env+withShellEnvPrefix env = (`withShellEnvPrefix'` env) <$> getProgName+++-- * corner cases++-- FUTURE WORK: the functions in this section operate on sources, not+-- parsers.  This has the unintended effect of leaving the+-- documentation functionality oblivious of what happens here, so the+-- user of the apliation that uses configifier has no way of knowing+-- about '--config' or the program name prefix of shell env variables.+-- there should be a better way.++-- | Handle `--config=<FILE>`, `--config <FILE>`: split up+-- 'CommandLine' source on each of these, and inject a+-- 'YamlFile' source with the resp. file name.+readUserConfigFiles :: [Source] -> [Source]+readUserConfigFiles = mconcat . map f+  where+    f :: Source -> [Source]+    f s@(YamlString _)   = [s]+    f s@(YamlFile _)     = [s]+    f s@(ShellEnv _)     = [s]+    f (CommandLine args) = filter (not . (== CommandLine [])) $ g [] args++    g :: [String] -> [String] -> [Source]+    g acc [] = [CommandLine (reverse acc)]+    g acc fresh@(freshHead:freshTail) = case popArg fresh of+        Right (("config", v), fresh') -> CommandLine (reverse acc) : YamlFile v : g [] fresh'+        _ -> g (freshHead : acc) freshTail++-- | Require prefix for shell env variables.  This function will chop+-- off the given prefix of all env entries, and filter all entries+-- that do not have this prefix.+withShellEnvPrefix' :: String -> Env -> Env+withShellEnvPrefix' prefix = catMaybes . map f+  where+    f (k, v) = if prefix `isPrefixOf` k+        then Just (take (length prefix) k, v)+        else Nothing++ -- * yaml / json  parseConfigFile :: (FromJSON (TaggedM cfg)) => SBS -> Either Error (TaggedM cfg)-parseConfigFile sbs = mapLeft (InvalidYaml sbs) $ Yaml.decodeEither sbs+parseConfigFile sbs = mapLeft (InvalidYamlString (trunc sbs)) $ Yaml.decodeEither' sbs+  where+    l = 21+    trunc s = if SBS.length s > l then SBS.take l s <> "..." else s +-- | See "Data.Yaml.Include".+parseConfigFileWithIncludes :: (FromJSON (TaggedM cfg)) => FilePath -> IO (Either Error (TaggedM cfg))+parseConfigFileWithIncludes fpath = mapLeft (InvalidYamlFile fpath) <$> decodeFileEither fpath+ renderConfigFile :: (Freeze cfg, t ~ Tagged cfg, ToJSON (TaggedM cfg)) => t -> SBS renderConfigFile = Yaml.encode . thaw @@ -229,11 +299,16 @@          , KnownSymbol s          )         => ToJSON (TaggedM (Label s cfg)) where-    toJSON (TaggedM Nothing) = Aeson.Null+    toJSON (TaggedM Nothing) = Null     toJSON (TaggedM (Just v)) = case toJSON (TaggedM v :: TaggedM cfg) of-        Aeson.Null -> Aeson.Null+        Null -> Null         val        -> object [key .= val]  where key = cs $ symbolVal (Proxy :: Proxy s) +instance ( ToConfig (Descr cfg s) Maybe ~ ToConfig cfg Maybe+         , ToJSON (TaggedM cfg)+         ) => ToJSON (TaggedM (Descr cfg s)) where+    toJSON (TaggedM v) = toJSON (TaggedM v :: TaggedM cfg)+ -- | @instance ToJSON List@ instance ( t ~ ToConfig cfg Maybe          , ToJSON (TaggedM cfg)@@ -247,7 +322,7 @@          , ToJSON (TaggedM cfg)          ) => ToJSON (TaggedM (Option cfg)) where     toJSON (TaggedM (JustO v)) = toJSON $ (TaggedM v :: TaggedM cfg)-    toJSON (TaggedM NothingO)  = Aeson.Null+    toJSON (TaggedM NothingO)  = Null  -- | @instance ToJSON Type@ instance (ToJSON a) => ToJSON (TaggedM (Type a)) where@@ -259,36 +334,46 @@ -- | @instance FromJSON Record@ instance (FromJSON (TaggedM cfg1), FromJSON (TaggedM cfg2)) => FromJSON (TaggedM (Record cfg1 cfg2)) where     parseJSON json = do-        TaggedM o1 :: TaggedM cfg1 <- Aeson.parseJSON json-        TaggedM o2 :: TaggedM cfg2 <- Aeson.parseJSON json+        TaggedM o1 :: TaggedM cfg1 <- parseJSON json+        TaggedM o2 :: TaggedM cfg2 <- parseJSON json         return . TaggedM $ o1 :*> o2  -- | @instance FromJSON Label@ (tolerates unknown fields in json object.) instance (FromJSON (TaggedM cfg), KnownSymbol s) => FromJSON (TaggedM (Label s cfg)) where-    parseJSON = Aeson.withObject "configifier object" $ \ m ->-          case HashMap.lookup key m of+    parseJSON (Object hashmap) =+          case HashMap.lookup key hashmap of             (Just json) -> TaggedM . Just . fromTaggedM <$> parseJSON' json             Nothing     -> return $ TaggedM Nothing         where           key = cs $ symbolVal (Proxy :: Proxy s)-          parseJSON' :: Aeson.Value -> Aeson.Parser (TaggedM cfg) = Aeson.parseJSON+          parseJSON' :: Value -> Yaml.Parser (TaggedM cfg) = parseJSON+    parseJSON bad = fail $ "when expecting 'Label', encountered this instead: " ++ show bad +instance ( ToConfig (Descr cfg s) Maybe ~ ToConfig cfg Maybe+         , FromJSON (TaggedM cfg)+         ) => FromJSON (TaggedM (Descr cfg s)) where+    parseJSON v = cast <$> parseJSON v+      where+        cast :: TaggedM cfg -> TaggedM (Descr cfg s)+        cast = TaggedM . fromTaggedM+ -- | @instance ParseJSON List@ instance (FromJSON (TaggedM cfg)) => FromJSON (TaggedM (List cfg)) where-    parseJSON = Aeson.withArray "configifier list" $ \ vector -> do-        vector' :: [TaggedM cfg] <- sequence $ Aeson.parseJSON <$> Vector.toList vector+    parseJSON (Array vector) = do+        vector' :: [TaggedM cfg] <- sequence $ parseJSON <$> Vector.toList vector         return . TaggedM . (fromTaggedM <$>) $ vector'+    parseJSON bad = fail $ "when expecting 'List', encountered this instead: " ++ show bad  -- | @instance ParseJSON Option@ instance (FromJSON (TaggedM cfg)) => FromJSON (TaggedM (Option cfg)) where     parseJSON Null = return (TaggedM NothingO :: TaggedM (Option cfg))     parseJSON v = do-        TaggedM js :: TaggedM cfg <- Aeson.parseJSON v+        TaggedM js :: TaggedM cfg <- parseJSON v         return $ (TaggedM (JustO js) :: TaggedM (Option cfg))  -- | @instance FromJSON Type@ instance (FromJSON a) => FromJSON (TaggedM (Type a)) where-    parseJSON = (TaggedM <$>) . Aeson.parseJSON+    parseJSON = (TaggedM <$>) . parseJSON   -- * shell env@@ -326,6 +411,14 @@             (key', '_':s@(_:_)) | mk key == mk key' -> Just (s, v)             _                                       -> Nothing +instance ( ToConfig (Descr cfg s) Maybe ~ ToConfig cfg Maybe+         , HasParseShellEnv cfg+         ) => HasParseShellEnv (Descr cfg s) where+    parseShellEnv env = cast <$> parseShellEnv env+      where+        cast :: TaggedM cfg -> TaggedM (Descr cfg s)+        cast = TaggedM . fromTaggedM+ -- | You can provide a list value via the shell environment by -- providing a single element.  This element will be put into a list -- implicitly.@@ -369,7 +462,7 @@     parseCommandLine = primitiveParseCommandLine  --- | Very basic fist approach: read @/--(key)(=|\s+)(value)/@;+-- | Very basic first approach: read @/--(key)(=|\s+)(value)/@; -- construct shell env from keys and names, and use 'parseShellEnv' on -- the command line.  If it doesn't like the syntax used in the -- command line, it will crash.  I hope for this to get much fancier@@ -383,18 +476,22 @@  parseArgs :: Args -> Either String Env parseArgs [] = Right []-parseArgs (h:[]) = parseArgsWithEqSign h-parseArgs (h:h':t) = ((++) <$> parseArgsWithEqSign h   <*> parseArgs (h':t))-                 <|> ((++) <$> parseArgsWithSpace h h' <*> parseArgs t)+parseArgs args = popArg args >>= \ ((k, v), args') -> ((parseArgName k, v):) <$> parseArgs args' -parseArgsWithEqSign :: String -> Either String Env+popArg :: Args -> Either String ((String, String), Args)+popArg []       = Left "empty argument list."+popArg (h:[])   = (, []) <$> parseArgsWithEqSign h+popArg (h:h':t) = ((, h':t) <$> parseArgsWithEqSign h)+              <|> ((,    t) <$> parseArgsWithSpace h h')++parseArgsWithEqSign :: String -> Either String (String, String) parseArgsWithEqSign s = case cs s Regex.=~- "^--([^=]+)=(.*)$" of-    [_, k, v] -> Right [(parseArgName $ cs k, cs v)]+    [_, k, v] -> Right (cs k, cs v)     bad -> Left $ "could not parse last arg: " ++ show (s, bad) -parseArgsWithSpace :: String -> String -> Either String Env+parseArgsWithSpace :: String -> String -> Either String (String, String) parseArgsWithSpace s v = case cs s Regex.=~- "^--([^=]+)$" of-    [_, k] -> Right [(parseArgName $ cs k, cs v)]+    [_, k] -> Right (cs k, cs v)     bad -> Left $ "could not parse long-arg with value: " ++ show (s, v, bad)  parseArgName :: String -> String@@ -422,6 +519,7 @@     ToVal (Record a b) '[]       = Just (ToConfig (Record a b) Id)     ToVal (Record a b) ps        = OrElse (ToVal a ps) (ToVal b ps)     ToVal (Label p a)  (p ': ps) = ToVal a ps+    ToVal (Descr a s)  ps        = ToVal a ps     ToVal (Option a)   ps        = ToValueMaybe (ToVal a ps)     ToVal a            '[]       = Just (ToConfig a Id)     ToVal a            (p ': ps) = Nothing@@ -483,20 +581,26 @@     sel (Tagged record) Proxy = CJust record  instance ( cfg ~ Record cfg' cfg''-         -- @ToVal cfg ps ~ Just t@ or @ToVal cfg ps ~ Nothing@          , Sel cfg' (p ': ps)          , Sel cfg'' (p ': ps)          ) => Sel (Record cfg' cfg'') (p ': ps) where     sel (Tagged (a :*> b)) path = orElse (sel (Tagged a :: Tagged cfg') path) (sel (Tagged b :: Tagged cfg'') path)  instance ( cfg ~ Label p cfg'-         -- @ToVal cfg ps ~ Just t@ or @ToVal cfg ps ~ Nothing@-         , t ~ ToConfig cfg Id          , Sel cfg' ps          , KnownSymbol p          ) => Sel (Label p cfg') (p ': ps) where     sel (Tagged (Id a)) Proxy = sel (Tagged a :: Tagged cfg') (Proxy :: Proxy ps) +instance ( cfg ~ Descr cfg' s+         , Sel cfg' ps+         , ToConfig (Descr cfg' s) Id ~ ToConfig cfg' Id+         ) => Sel (Descr cfg' s) ps where+    sel = sel . cast+      where+        cast :: Tagged (Descr cfg' s) -> Tagged cfg'+        cast = Tagged . fromTagged+ instance ( cfg ~ Option cfg'          , NothingValue (ToVal cfg' ps)          , Sel cfg' ps@@ -586,6 +690,23 @@         tagA :: ToConfig a Maybe -> TaggedM a         tagA = TaggedM +instance ( ToConfig (Descr a s) Maybe ~ ToConfig a Maybe+         , ToConfig a Maybe ~ Maybe a'+         , Monoid (TaggedM a)+         ) => Monoid (TaggedM (Descr a s)) where+    mempty = cast $ TaggedM Nothing+      where+        cast :: TaggedM a -> TaggedM (Descr a s)+        cast = TaggedM . fromTaggedM++    mappend x x' = cast $ cast' x <> cast' x'+      where+        cast :: TaggedM a -> TaggedM (Descr a s)+        cast' :: TaggedM (Descr a s) -> TaggedM a++        cast = TaggedM . fromTaggedM+        cast' = TaggedM . fromTaggedM+ -- | There is no @instance Monoid (TaggedM (Type a))@, since there -- is no reasonable 'mempty'.  Therefore, we offer a specialized -- instance for labels that map to 'Type'.@@ -636,6 +757,13 @@      thw Proxy (Id x) = Just $ thw (Proxy :: Proxy t) x +instance ( ToConfig (Descr t s) Maybe ~ ToConfig t Maybe+         , ToConfig (Descr t s) Id ~ ToConfig t Id+         , Freeze t+         ) => Freeze (Descr t s) where+    frz Proxy path x = frz (Proxy :: Proxy t) path x+    thw Proxy = thw (Proxy :: Proxy t)+ instance (Freeze c) => Freeze (List c) where     frz Proxy path xs = sequence $ frz (Proxy :: Proxy c) path <$> xs     thw Proxy xs = thw (Proxy :: Proxy c) <$> xs@@ -669,6 +797,9 @@     thw Proxy x = x  +-- | Partials are constructed with every 'Nothing' spelled out,+-- resulting in deep skeletons of 'Nothing's.  'CanonicalizePartial'+-- replaces those with single 'Nothing's at their tops. class CanonicalizePartial a where     canonicalizePartial :: TaggedM a -> TaggedM a     emptyPartial :: TaggedM a -> Bool@@ -695,6 +826,21 @@      emptyPartial (TaggedM Nothing) = True     emptyPartial (TaggedM (Just a)) = emptyPartial (TaggedM a :: TaggedM cfg')++instance (cfg ~ Descr cfg' s, CanonicalizePartial cfg')+        => CanonicalizePartial (Descr cfg' s) where+    canonicalizePartial = cast . canonicalizePartial . cast'+      where+        cast :: TaggedM a -> TaggedM (Descr a s)+        cast' :: TaggedM (Descr a s) -> TaggedM a++        cast = TaggedM . fromTaggedM+        cast' = TaggedM . fromTaggedM++    emptyPartial = emptyPartial . cast+      where+        cast :: TaggedM (Descr a s) -> TaggedM a+        cast = TaggedM . fromTaggedM  instance (cfg ~ List cfg', CanonicalizePartial cfg')         => CanonicalizePartial (List cfg') where
tests/Data/ConfigifierSpec.hs view
@@ -31,6 +31,7 @@     selectSpec     mergeSpec     sourcesSpec+    readUserConfigFilesSpec  miscSpec :: Spec miscSpec = do@@ -47,8 +48,7 @@     it "descriptions" $         let text :: SBS             want :: ( c ~ ToConfigCode ("bla" :> Int :>: "describe stuff!")-                    , c' ~ NoDesc c-                    ) => Tagged c'+                    ) => Tagged c              text = "bla: 3"             want = Tagged $ Id 3@@ -65,13 +65,13 @@          in run text want      it "option, no sources" $-        let have :: (c ~ ToConfigCode (Maybe ("bla" :> Int))) => Configifier.Result c+        let have :: (c ~ ToConfigCode (Maybe ("bla" :> Int))) => IO (Tagged c)             want :: (c ~ ToConfigCode (Maybe ("bla" :> Int))) => Tagged c              have = configify []             want = Tagged NothingO -         in have `shouldBe` Right want+         in have >>= (`shouldBe` want)      it "yaml cannot parse empty cfg files (even if all config data is optional!)" $         let text1, text2, text3 :: SBS@@ -127,8 +127,7 @@     it "lists" $         let text :: SBS             want :: ( c ~ ToConfigCode ("bla" :> [Bool])-                    , c' ~ NoDesc c-                    ) => Tagged c'+                    ) => Tagged c              text = "bla: [yes, no]"             want = Tagged $ Id [True, False]@@ -149,11 +148,11 @@       , CanonicalizePartial cfg       ) => SBS -> ti -> IO () run text parsedWant = do-    let f :: SBS -> Either Error ti-        f sbs = configify [ConfigFileYaml sbs]+    let f :: SBS -> IO ti+        f sbs = configify [YamlString sbs] -    f text `shouldBe` Right parsedWant-    f (renderConfigFile parsedWant) `shouldBe` Right parsedWant+    f text                          >>= (`shouldBe` parsedWant)+    f (renderConfigFile parsedWant) >>= (`shouldBe` parsedWant)   selectSpec :: Spec@@ -234,22 +233,22 @@      it "partial select paths and non-leaf sub-configs" $         let t :: forall config config' ponfig ponfig' .-                    ( config ~ Tagged (NoDesc (ToConfigCode config'))+                    ( config ~ Tagged (ToConfigCode config')                     , config' ~ (Maybe ("a" :> ST) :*> ("b" :> ST)) -                    , ponfig ~ Tagged (NoDesc (ToConfigCode ponfig'))+                    , ponfig ~ Tagged (ToConfigCode ponfig')                     , ponfig' ~ ("c" :> config')                      ) => IO ()             t = do-                  let Right (cfg1 :: ponfig) = configify [ConfigFileYaml . cs . unlines $+                  cfg1 :: ponfig <- configify [YamlString . cs . unlines $                           "c:" :                           "  a: goih" :                           "  b: c38" :                           "..." :                           []] -                      Right (Tagged cfg2 :: config) = configify [ConfigFileYaml . cs . unlines $+                  Tagged cfg2 :: config <- configify [YamlString . cs . unlines $                           "a: goih" :                           "b: c38" :                           "..." :@@ -304,10 +303,10 @@     let f :: ( c  ~ ToConfigCode c'              , c' ~ ("frontend" :> sc :*> Maybe ("backend" :> sc))              , sc ~ ("bind_port" :> Int :*> Maybe ("expose_host" :> ST))-             ) => [Source] -> Result c+             ) => [Source] -> IO (Tagged c)         f = configify -        configFile1 :: Source = ConfigFileYaml . cs . unlines $+        configFile1 :: Source = YamlString . cs . unlines $               "frontend:" :               "  bind_port: 3" :               "  expose_host: host" :@@ -315,7 +314,7 @@               "  bind_port: 4" :               "  expose_host: hist" :               []-        configFile2 :: Source = ConfigFileYaml . cs . unlines $+        configFile2 :: Source = YamlString . cs . unlines $               "frontend:" :               "  bind_port: 3" :               []@@ -331,21 +330,35 @@             parseArgs (["--arg=31", "--flob", "gluh"] :: Args) `shouldBe` Right ([("ARG", "31"), ("FLOB", "gluh")] :: Env)          it "1" $-            f [configFile1] `shouldBe`-                (Right . Tagged $ Id (Id 3 :*> JustO (Id "host"))-                               :*> JustO (Id (Id 4 :*> JustO (Id "hist"))))+            f [configFile1] >>= (`shouldBe`+                (Tagged $ Id (Id 3 :*> JustO (Id "host"))+                      :*> JustO (Id (Id 4 :*> JustO (Id "hist")))))          it "2" $-            f [configFile1, shellEnv1, shellEnv2] `shouldBe`-                (Right . Tagged $ Id (Id 18 :*> JustO (Id "host"))-                               :*> JustO (Id (Id 4 :*> JustO (Id "bom"))))+            f [configFile1, shellEnv1, shellEnv2] >>= (`shouldBe`+                (Tagged $ Id (Id 18 :*> JustO (Id "host"))+                      :*> JustO (Id (Id 4 :*> JustO (Id "bom")))))          it "3" $-            f [configFile1, shellEnv1, shellEnv2, commandLine1] `shouldBe`-                (Right . Tagged $ Id (Id 31 :*> JustO (Id "host"))-                               :*> JustO (Id (Id 4 :*> JustO (Id "bom"))))+            f [configFile1, shellEnv1, shellEnv2, commandLine1] >>= (`shouldBe`+                (Tagged $ Id (Id 31 :*> JustO (Id "host"))+                      :*> JustO (Id (Id 4 :*> JustO (Id "bom")))))          it "4" $-            f [configFile2, commandLine2] `shouldBe`-                (Right . Tagged $ Id (Id 3 :*> NothingO)-                               :*> JustO (Id (Id 8710 :*> JustO (Id "mab"))))+            f [configFile2, commandLine2] >>= (`shouldBe`+                (Tagged $ Id (Id 3 :*> NothingO)+                      :*> JustO (Id (Id 8710 :*> JustO (Id "mab")))))+++readUserConfigFilesSpec :: Spec+readUserConfigFilesSpec = describe "readUserConfigFiles" $ do+    it "finds --config args" $+        readUserConfigFiles [CommandLine ["--config=FILE"]] `shouldBe` [YamlFile "FILE"]++    it "keeps surrounding args intact" $ do+        readUserConfigFiles [CommandLine ["1", "2", "--config=FILE", "3", "4"]] `shouldBe`+            [CommandLine ["1", "2"], YamlFile "FILE", CommandLine ["3", "4"]]+        readUserConfigFiles [CommandLine ["--config=FILE", "2"]] `shouldBe`+            [YamlFile "FILE", CommandLine ["2"]]+        readUserConfigFiles [CommandLine ["1", "--config=FILE"]] `shouldBe`+            [CommandLine ["1"], YamlFile "FILE"]