envparse 0.3.2 → 0.3.3
raw patch · 7 files changed
+190/−69 lines, 7 files
Files
- CHANGELOG.markdown +6/−0
- envparse.cabal +4/−3
- example/Generic.hs +0/−46
- example/Generic0.hs +30/−0
- example/Generic1.hs +31/−0
- src/Env/Generic.hs +112/−15
- src/Env/Parse.hs +7/−5
CHANGELOG.markdown view
@@ -1,3 +1,9 @@+0.3.3+=====++ * Removed the unnecessary `AsEmpty` and `AsUnset` constraints on the error type+ in the `flag` and `switch` parsers.+ 0.3.2 =====
envparse.cabal view
@@ -1,5 +1,5 @@ name: envparse-version: 0.3.2+version: 0.3.3 synopsis: Parse environment variables description: Here's a simple example of a program that uses @envparse@'s parser:@@ -59,7 +59,8 @@ CHANGELOG.markdown example/Main.hs example/CustomError.hs- example/Generic.hs+ example/Generic0.hs+ example/Generic1.hs source-repository head type: git@@ -68,7 +69,7 @@ source-repository this type: git location: https://github.com/supki/envparse- tag: 0.3.2+ tag: 0.3.3 library default-language:
− example/Generic.hs
@@ -1,46 +0,0 @@-{-# LANGUAGE CPP #-}-#if __GLASGOW_HASKELL__ < 710-module Main (main) where--main :: IO ()-main =- return ()-#else-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE TypeOperators #-}--- | Greetings for $NAME------ @--- % GENERIC_NAME=a5579150 runhaskell -isrc example/Generic.hs--- Hello, a5579150!--- % GENERIC_NAME=a5579150 GENERIC_QUIET=1 runhaskell -isrc example/Generic.hs--- %--- @-module Main (main) where--import Control.Monad (unless)-import Env-import Env.Generic---data Hello = Hello- { name :: String ? "Target for the greeting"- , quiet :: Bool ? "Whether to actually print the greeting"- } deriving (Show, Eq, Generic)--instance Record Error Hello--main :: IO ()-main = do- Hello {name=Help n, quiet=Help q} <- hello- unless q $- putStrLn ("Hello, " ++ n ++ "!")--hello :: IO Hello-hello =- Env.parse (header "envparse example") (prefixed "GENERIC_" record)-#endif
+ example/Generic0.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE CPP #-}+#if __GLASGOW_HASKELL__ < 710+module Main (main) where++main :: IO ()+main =+ return ()+#else+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-}+module Main (main) where++import Env+import Env.Generic+++data Hello = Hello+ { name :: String+ , count :: Int+ , quiet :: Bool+ } deriving (Show, Eq, Generic)++instance Record Error Hello++main :: IO ()+main = do+ hello <- Env.parse (header "envparse example") record+ print (hello :: Hello)+#endif
+ example/Generic1.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE CPP #-}+#if __GLASGOW_HASKELL__ < 710+module Main (main) where++main :: IO ()+main =+ return ()+#else+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeOperators #-}+module Main (main) where++import Env+import Env.Generic+++data Hello = Hello+ { name :: String ? "Whom shoud I greet?"+ , count :: Int ? "How many times to greet them?"+ , quiet :: Bool ? "Should I be quiet instead?"+ } deriving (Show, Eq, Generic)++instance Record Error Hello++main :: IO ()+main = do+ hello <- Env.parse (header "envparse example") record+ print (hello :: Hello)+#endif
src/Env/Generic.hs view
@@ -9,6 +9,85 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-}+-- | Using the 'G.Generic' facility, this module can derive 'Env.Parser's automatically.+--+-- If you have a simple record:+--+-- @+-- {-\# LANGUAGE DeriveGeneric #-}+-- {-\# LANGUAGE MultiParamTypeClasses #-}+--+-- import Env+-- import Env.Generic+--+-- data Hello = Hello+-- { name :: String+-- , count :: Int+-- , quiet :: Bool+-- } deriving (Show, Eq, Generic)+--+-- instance Record Error Hello+--+-- main :: IO ()+-- main = do+-- hello <- Env.parse (header "envparse example") 'record'+-- print (hello :: Hello)+-- @+--+-- The generic implementation of the 'record' method translates named fields to field parsers:+--+-- @+-- % NAME=bob COUNT=3 runhaskell -isrc example/Generic0.hs+-- Hello {name = "bob", count = 3, quiet = False}+-- @+--+-- If you want to adorn the ugly default help message, augment the fields with descriptions:+--+-- @+-- {-\# LANGUAGE DataKinds #-}+-- {-\# LANGUAGE DeriveGeneric #-}+-- {-\# LANGUAGE MultiParamTypeClasses #-}+-- {-\# LANGUAGE TypeOperators #-}+--+-- import Env+-- import Env.Generic+--+-- data Hello = Hello+-- { name :: String ? __"Whom shoud I greet?"__+-- , count :: Int ? __"How many times to greet them?"__+-- , quiet :: Bool ? __"Should I be quiet instead?"__+-- } deriving (Show, Eq, Generic)+--+-- instance Record Error Hello+--+-- main :: IO ()+-- main = do+-- hello <- Env.parse (header "envparse example") record+-- print (hello :: Hello)+-- @+--+-- @+-- % runhaskell -isrc example/Generic1.hs+-- envparse example+--+-- Available environment variables:+--+-- COUNT __How many times to greet them?__+-- NAME __Whom shoud I greet?__+-- QUIET __Should I be quiet instead?__+--+-- Parsing errors:+--+-- COUNT is unset+-- NAME is unset+-- @+--+-- Note that this has an effect of wrapping the values in the 'Help' constructor:+--+-- @+-- % NAME=bob COUNT=3 QUIET='YES' runhaskell -isrc example/Generic1.hs+-- Hello {name = Help {unHelp = "bob"}, count = Help {unHelp = 3}, quiet = Help {unHelp = True}}+-- @ module Env.Generic ( Record(..) , Field(..)@@ -32,33 +111,38 @@ import qualified Env +-- | Given a @Record e a@ instance, a value of the type @a@ can be parsed from the environment.+-- If the parsing fails, a value of an error type @e@ is returned.+--+-- The 'record' method has a default implementation for any type that has a 'G.Generic' instance. If you+-- need to choose a concrete type for @e@, the default error type 'Env.Error' is a good candidate. Otherwise,+-- the features you'll use in your parsers will naturally guide GHC to compute the set of required+-- constraints on @e@. class Record e a where record :: Env.Parser e a default record :: (r ~ G.Rep a, G.Generic a, GRecord e r) => Env.Parser e a record = fmap G.to (gr State {statePrefix="", stateCon="", stateVar=""}) --- | Generic parsing state. data State = State- { statePrefix :: String -- ^ All variables' names have this prefix.- , stateCon :: String -- ^ Constructor currently being processed.- , stateVar :: String -- ^ Variable name to use for the next component.+ { statePrefix :: String -- All the variables' names have this prefix.+ , stateCon :: String -- The constructor currently being processed.+ , stateVar :: String -- The variable name to use for the next component. } deriving (Show, Eq) class GRecord e f where gr :: State -> Env.Parser e (f a) --- | We are not interested in any metadata of the type constructor definition. instance GRecord e a => GRecord e (G.D1 c a) where gr = fmap G.M1 . gr --- | Constant values are converted to 'Env.Parser's using their 'Field' instance.+-- Constant values are converted to 'Env.Parser's using their 'Field' instance. instance (Env.AsUnset e, Field e a) => GRecord e (G.K1 i a) where gr State {stateVar} = fmap G.K1 (field stateVar Nothing) --- | Constructor's name is used as a prefix to try to remove from+-- Constructor's name is used as a prefix to try to remove from -- selectors when building environment variable names. instance (G.Constructor c, GRecord e a) => GRecord e (G.C1 c a) where gr state =@@ -66,17 +150,14 @@ where con = G.conName (G.M1 Proxy :: G.M1 t c Proxy b) --- | Products are converted to products of parsers. instance (GRecord e f, GRecord e g) => GRecord e (f G.:*: g) where gr x = liftA2 (G.:*:) (gr x) (gr x) --- | Sums are converted to sums of parsers. instance (GRecord e f, GRecord e g) => GRecord e (f G.:+: g) where gr x = fmap G.L1 (gr x) <|> fmap G.R1 (gr x) --- | Record selectors' names determine suffixes of environment variables' names. instance (G.Selector c, Type c ~ 'Record, GRecord e a) => GRecord e (G.S1 c a) where gr state@State {statePrefix, stateCon} = fmap G.M1 (gr state {stateVar=statePrefix ++ suffix})@@ -88,7 +169,6 @@ y <- List.stripPrefix (map Char.toLower stateCon) sel camelTo2 y <$ guard (not (List.null y)) --- | Stolen from Aeson and adapted. camelTo2 :: String -> String camelTo2 = map Char.toUpper . go2 . go1 where@@ -100,14 +180,21 @@ go2 (l:u:xs) | Char.isLower l && Char.isUpper u = l : '_' : u : go2 xs go2 (x:xs) = x : go2 xs --- | Decide whether the constructor is a record. type family Type x :: ConType where Type G.NoSelector = 'Plain Type x = 'Record --- | Constructor can be either a plain thing or a record. data ConType = Plain | Record +-- | Given a @Field e a@ instance, a value of the type @a@ can be parsed from an environment variable.+-- If the parsing fails, a value of an error type @e@ is returned.+--+-- The 'field' method has a default implementation for any type that has a 'Read' instance. If you+-- need to choose a concrete type for @e@, the default error type 'Env.Error' is a good candidate. Otherwise,+-- the features you'll use in your parsers will naturally guide GHC to compute the set of required+-- constraints on @e@.+--+-- The annotated instances do not use the default implementation. class Field e a where field :: String -> Maybe String -> Env.Parser e a default field :: (Env.AsUnset e, Env.AsUnread e, Read a) => String -> Maybe String -> Env.Parser e a@@ -142,10 +229,12 @@ instance (Env.AsUnset e, Env.AsUnread e) => Field e Double +-- | Uses the 'String' value verbatim. instance Env.AsUnset e => Field e String where field name help = Env.var Env.str name (foldMap Env.help help) +-- | Expects a single-character 'String' value. instance (Env.AsUnset e, Env.AsUnread e) => Field e Char where field name help = Env.var reader name (foldMap Env.help help)@@ -154,14 +243,22 @@ [c] -> pure c str -> Left (Env.unread str) -instance (Env.AsUnset e, Env.AsEmpty e) => Field e Bool where+-- | Any set and non-empty value parses to a 'True'; otherwise, it's a 'False'. This parser+-- never fails.+instance Field e Bool where field name help = Env.switch name (foldMap Env.help help) --- | Variable tagged with its help message.+-- | A field annotation.+--+-- If you annotate a record field with a 'Symbol' literal (that is, a statically known type level string)+-- the derivation machinery will use the literal in the help message.+--+-- Please remember that the values of the annotated fields are wrapped in the 'Help' constructor. newtype a ? tag = Help { unHelp :: a } deriving (Show, Eq, Functor, Foldable, Traversable) +-- | Augments the underlying field parser with the help message. instance (G.KnownSymbol tag, Field e a) => Field e (a ? tag) where field name _ = fmap Help (field name (pure (G.symbolVal (Proxy :: Proxy tag))))
src/Env/Parse.hs view
@@ -107,13 +107,15 @@ -- -- /Note:/ this parser never fails. flag- :: forall e a. (Error.AsUnset e, Error.AsEmpty e)- => a -- ^ default value+ :: a -- ^ default value -> a -- ^ active value -> String -> Mod Flag a -> Parser e a flag f t n (Mod g) = Parser . liftAlt $ VarF { varfName = n- , varfReader = \name -> Right . either (const f) (const t) . (nonempty :: Reader e String) <=< lookupVar name+ , varfReader = \name env ->+ pure $ case (nonempty :: Reader Error.Error String) =<< lookupVar name env of+ Left _ -> f+ Right _ -> t , varfHelp = flagHelp , varfDef = Just f , varfHelpDef = Nothing@@ -123,8 +125,8 @@ -- | A simple boolean 'flag' ----- /Note:/ the same caveats apply.-switch :: (Error.AsUnset e, Error.AsEmpty e) => String -> Mod Flag Bool -> Parser e Bool+-- /Note:/ this parser never fails.+switch :: String -> Mod Flag Bool -> Parser e Bool switch = flag False True -- | The trivial reader