env-parser 0.0.1.1 → 0.0.2
raw patch · 4 files changed
+139/−43 lines, 4 files
Files
- env-parser.cabal +12/−15
- src/System/Environment/Parser.hs +120/−27
- src/System/Environment/Parser/Class.hs +5/−1
- src/System/Environment/Parser/Database.hs +2/−0
env-parser.cabal view
@@ -1,22 +1,19 @@ name: env-parser-version: 0.0.1.1+version: 0.0.2 synopsis: Pull configuration information from the ENV description: - @env-parser@ is a small library for configuring programs based on information- from the environment.- .- Command-line oriented programs tend to pull much of their runtime- configuration from command-line arguments and libraries such as @cmdargs@ are- useful for encoding this configuration in your program. For programs which- draw configuration from the environment, however, this must usually be done- manually.- .- @env-parser@ focuses on making ENV-configured programs both easy to build,- declarative, self-documenting, and easy to test.++ @env-parser is a small library for configuring programs based on information+ from the environment. It's goals and design are similar to that of @cmdargs@+ or @optparse-applicative@ but aimed at automatically managed programs such as+ those that might be run via Heroku or Runit/daemontools. .- A particular use case of this parser is to make it easy to build applications- which launch under Heroku-style infrastructure. We provide explicit parsers- for some of Heroku's conventions for this purpose.+ @env-parser@ intentionally sacrifices power for comprehensibility---the+ primary interface, @Parser@, implements only @Applicative@. This provides+ better runtime error messages and automatically generated static help using+ parser annotations. It also expresses a principle of simplicity in+ configuration: arbitrary uses of @Monad@ or even @Alternative@ can lead to+ opaque failures prior to a program even beginning to run. homepage: http://github.com/tel/env-parser license: MIT
src/System/Environment/Parser.hs view
@@ -12,25 +12,64 @@ -- Portability : non-portable -- -- Functions for building generic environment parsers which provide--- automatic documentation and easy testing.+-- automatic documentation and easy testing. --+-- This module is intended to be imported qualified, for example, we might+-- parse out a Heroku database URL and a base-64 encoded encryption key in+-- the following manner.+--+-- > import qualified System.Environment.Parser as Env+-- > import qualified System.Environment.Parser.Database as Env+-- > import qualified System.Environment.Parser.Encoded as Env+-- >+-- > data Config = Config { db :: Env.DBConnection, key :: Env.Base64 }+-- > +-- > configP :: Parser Config+-- > configP = Config <$> Env.get "DATABASE_URL"+-- > <*> Env.get "ENCRYPTION_KEY"+--+-- We can then use that 'Parser' value to attempt to compute a @Config@ in+-- 'IO' and print out the missing and needed variables on failure+--+-- > do cp <- Env.run configP :: IO (Either Errors Config)+-- > case cp of+-- > Left errs -> do+-- > putStrLn "Missing the following variables: "+-- > mapM_ putStrLn (Env.missing errs)+-- > putStrLn "Needs the following variables: "+-- > mapM_ putStrLn (Env.references (Env.analyze configP))+-- > Right config -> do+-- > ...+-- module System.Environment.Parser ( -- * Basic interface - run -- :: Parser a -> IO (Either Errors a)- , test -- :: Parser a -> Map.Map String String -> Either Errors a- , help -- :: Parser a -> Dep+ -- ** Building a ''Parser'++ Parser+ , get -- :: FromEnv a => String -> Parser a , getParse -- :: FE.FromEnv a => (a -> Either String b) -> String -> Parser b , json -- :: FromJSON a => String -> Parser a - -- * Interface types+ -- *** Annotating a 'Parser'+ , def -- :: Show a => a -> Parser a -> Parser a+ , def' -- :: a -> Parser a -> Parser a+ , doc -- :: String -> Parser a -> Parser a - , Parser- , Errors (..), Err (..)- , Dep (..), references+ -- * Interpreting a 'Parser'+ + , run -- :: Parser a -> IO (Either Errors a)+ , test -- :: Parser a -> Map.Map String String -> Either Errors a+ , analyze -- :: Parser a -> Analysis + , Errors (..), missing+ , Err (..)++ -- ** Analysis and documentation+ , Analysis (..), references+ ) where import Control.Applicative@@ -38,6 +77,7 @@ import qualified Data.Aeson.Types as Ae import Data.Functor.Compose import qualified Data.Map as Map+import Data.Maybe (mapMaybe) import Data.Monoid import Data.Foldable (toList, foldMap) import Data.Sequence ((<|), (|>))@@ -49,12 +89,23 @@ -- ----------------------------------------------------------------------------- -- Error types +-- | We consider two broad classes of failure: either an environment+-- variable was expected to exist and didn't (it was 'Wanted') or the+-- value failed to validate during parsing and we've 'Joined' an+-- error message from that failed parse. data Err = Wanted String | Joined String deriving ( Eq, Ord, Show ) newtype Errors = Errors { getErrors :: Seq.Seq Err } deriving ( Eq, Ord, Show, Monoid ) +-- | Determine all of the variables which were missing in the environment+-- yet required.+missing :: Errors -> [String]+missing = mapMaybe go . toList . getErrors where+ go (Wanted s) = Just s+ go (Joined _) = Nothing+ instance Cls.Satisfiable Errors where wants = Errors . Seq.singleton . Wanted errors = Errors . Seq.singleton . Joined@@ -62,22 +113,31 @@ -- ----------------------------------------------------------------------------- -- Analysis types -data Dep = Succeeding- | Needing String- | Branching (Seq.Seq Dep)- | Joining Dep- | Defaulting String Dep+-- | The 'Analysis' type is a result of running the parser+-- statically. It provides some information about the kind of parse+-- that would be attempted and is thus useful for error messages or+-- manual documentation.+data Analysis+ = Succeeding+ | Wanting String+ | Branching (Seq.Seq Analysis)+ | Joining Analysis+ | Defaulting String Analysis+ | Documenting String Analysis deriving ( Eq, Show ) -references :: Dep -> [String]-references = toList . foldDep where- foldDep Succeeding = Seq.empty- foldDep (Needing key) = Seq.singleton key- foldDep (Branching ds) = foldMap foldDep ds- foldDep (Joining d) = foldDep d- foldDep (Defaulting _ d) = foldDep d+-- | Get each environment varaible the parser wants. This will include+-- ones that may be optional due to default values.+references :: Analysis -> [String]+references = toList . foldAnalysis where+ foldAnalysis Succeeding = Seq.empty+ foldAnalysis (Wanting key) = Seq.singleton key+ foldAnalysis (Branching ds) = foldMap foldAnalysis ds+ foldAnalysis (Joining d) = foldAnalysis d+ foldAnalysis (Defaulting _ d) = foldAnalysis d+ foldAnalysis (Documenting doc d) = foldAnalysis d -data Df a = Df { runDf :: Dep } deriving Functor+data Df a = Df { runDf :: Analysis } deriving Functor instance Applicative Df where pure _ = Df Succeeding@@ -87,7 +147,7 @@ Df df <*> Df dx = Df (Branching $ Seq.fromList [df, dx]) instance Cls.HasEnv Df where- getEnv key = Df (Needing key)+ getEnv key = Df (Wanting key) instance Cls.Env Df where joinFailure (Df dep) = Df (Joining dep)@@ -96,10 +156,15 @@ -- ----------------------------------------------------------------------------- -- Parser types +-- | The generic environment 'Parser'. This type is used to specify+-- the structure of a configuration which can be read from the+-- environment. Later that structure can either be examined using+-- 'analyze', tested using 'test', or performed on the actual+-- environment using 'run'. data Parser a = Parser- { run' :: Compose IO (Miss Errors) a- , test' :: Compose ((->) (Map.Map String String)) (Miss Errors) a- , help' :: Df a+ { run' :: Compose IO (Miss Errors) a+ , test' :: Compose ((->) (Map.Map String String)) (Miss Errors) a+ , analyze' :: Df a } deriving ( Functor ) @@ -117,20 +182,48 @@ def a sho (Parser i1 i2 i3) = Parser (Cls.def a sho i1) (Cls.def a sho i2) (Cls.def a sho i3) +-- | Run a 'Parser' in 'IO' using the actual environment. run :: Parser a -> IO (Either Errors a) run = fmap toEither . getCompose . run' +-- | Run a 'Parser' purely using a 'Map.Map' to simulate the+-- environment. test :: Parser a -> Map.Map String String -> Either Errors a test = fmap toEither . getCompose . test' -help :: Parser a -> Dep-help = runDf . help'+-- | Run a completely pure, static analysis of the 'Parser' that can+-- be used to generate helpful documentation.+analyze :: Parser a -> Analysis+analyze = runDf . analyze' +-- | Pull a value from the environment. get :: FE.FromEnv a => String -> Parser a get = FE.fromEnv . Cls.getEnv +-- | Assign a default value to a 'Parser'. If the parser should fail at+-- runtime the default value will be returned instead. The value must+-- be showable in order to provide documentation.+def :: Show a => a -> Parser a -> Parser a+def a = Cls.def a show++-- | Assign a default value to a 'Parser'. This is identical to 'def'+-- but does not require the default value has a 'Show'+-- instance---instead a constant descriptive string should be passed+-- directly.+def' :: a -> String -> Parser a -> Parser a+def' a str = Cls.def a (const $ "{" ++ str ++ "}")++-- | Assign documentation to a branch of the 'Parser'. This can be+-- later retrieved in the 'Analysis' type.+doc :: String -> Parser a -> Parser a+doc = Cls.doc++-- | Pull a string from the environment and interpret it as a+-- JSON-serializable type. json :: Ae.FromJSON a => String -> Parser a json = getParse (Ae.parseEither Ae.parseJSON) +-- | Pull a value from the environment and attempt to parse it into+-- some other type. Failures will be 'Joined' into the result. getParse :: FE.FromEnv a => (a -> Either String b) -> String -> Parser b getParse parse key = Cls.joinFailure $ fmap parse $ get key
src/System/Environment/Parser/Class.hs view
@@ -60,11 +60,15 @@ -- | Replace a result with a default if necessary def :: a -> (a -> String) -> r a -> r a + -- | Provide documentation on a particular value+ doc :: String -> r a -> r a+ doc = flip const+ instance Satisfiable e => HasEnv (Miss e) where getEnv key = Miss (wants key) instance Satisfiable e => Env (Miss e) where- joinFailure (Miss er) = Miss er+ joinFailure (Miss er) = Miss er joinFailure (Got (Left er)) = Miss (errors er) joinFailure (Got (Right a)) = Got a
src/System/Environment/Parser/Database.hs view
@@ -38,6 +38,7 @@ | AMQP S.ByteString | HTTP S.ByteString | Other S.ByteString+ deriving ( Eq, Ord, Show, Read ) providerString :: Provider -> S.ByteString providerString p = case p of@@ -70,6 +71,7 @@ , location :: S.ByteString , params :: Map.Map S.ByteString S.ByteString }+ deriving ( Eq, Ord, Show, Read ) instance FromEnv DBConnection where parseEnv = tryParse