packages feed

env-parser 0.0.1.1 → 0.0.2

raw patch · 4 files changed

+139/−43 lines, 4 files

Files

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