envparse 0.3.0 → 0.3.1
raw patch · 7 files changed
+403/−8 lines, 7 filesdep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: base
API changes (from Hackage documentation)
- Env: infixr 1 >=>
- Env: infixr 6 <>
+ Env.Generic: Help :: a -> (?) a tag
+ Env.Generic: [unHelp] :: (?) a tag -> a
+ Env.Generic: class Field e a where field name help = var auto name (foldMap help help)
+ Env.Generic: class Generic a
+ Env.Generic: class Record e a where record = fmap to (gr (State {statePrefix = "", stateCon = "", stateVar = ""}))
+ Env.Generic: field :: Field e a => String -> Maybe String -> Parser e a
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsEmpty e) => Env.Generic.Field e GHC.Types.Bool
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Int.Int16
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Int.Int32
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Int.Int64
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Int.Int8
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Integer.Type.Integer
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Natural.Natural
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Types.Char
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Types.Double
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Types.Float
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Types.Int
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Types.Word
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Word.Word16
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Word.Word32
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Word.Word64
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Error.AsUnread e) => Env.Generic.Field e GHC.Word.Word8
+ Env.Generic: instance (Env.Error.AsUnset e, Env.Generic.Field e a) => Env.Generic.GRecord e (GHC.Generics.K1 i a)
+ Env.Generic: instance (Env.Generic.GRecord e f, Env.Generic.GRecord e g) => Env.Generic.GRecord e (f GHC.Generics.:*: g)
+ Env.Generic: instance (Env.Generic.GRecord e f, Env.Generic.GRecord e g) => Env.Generic.GRecord e (f GHC.Generics.:+: g)
+ Env.Generic: instance (GHC.Generics.Constructor c, Env.Generic.GRecord e a) => Env.Generic.GRecord e (GHC.Generics.C1 c a)
+ Env.Generic: instance (GHC.Generics.Selector c, Env.Generic.Type c ~ 'Env.Generic.Record, Env.Generic.GRecord e a) => Env.Generic.GRecord e (GHC.Generics.S1 c a)
+ Env.Generic: instance (GHC.TypeLits.KnownSymbol tag, Env.Generic.Field e a) => Env.Generic.Field e (a Env.Generic.? tag)
+ Env.Generic: instance Data.Foldable.Foldable ((Env.Generic.?) a)
+ Env.Generic: instance Data.Traversable.Traversable ((Env.Generic.?) a)
+ Env.Generic: instance Env.Error.AsUnset e => Env.Generic.Field e GHC.Base.String
+ Env.Generic: instance Env.Generic.GRecord e a => Env.Generic.GRecord e (GHC.Generics.D1 c a)
+ Env.Generic: instance GHC.Base.Functor ((Env.Generic.?) a)
+ Env.Generic: instance GHC.Classes.Eq Env.Generic.State
+ Env.Generic: instance GHC.Show.Show Env.Generic.State
+ Env.Generic: instance forall (k :: BOX) a (tag :: k). GHC.Classes.Eq a => GHC.Classes.Eq (a Env.Generic.? tag)
+ Env.Generic: instance forall (k :: BOX) a (tag :: k). GHC.Show.Show a => GHC.Show.Show (a Env.Generic.? tag)
+ Env.Generic: newtype (?) a tag
+ Env.Generic: record :: Record e a => Parser e a
- Env: flag :: forall e a. (AsUnset e, AsEmpty e) => a -> a -> String -> Mod Flag a -> Parser e a
+ Env: flag :: (AsUnset e, AsEmpty e) => a -> a -> String -> Mod Flag a -> Parser e a
Files
- CHANGELOG.markdown +6/−1
- envparse.cabal +13/−4
- example/Generic.hs +46/−0
- src/Env/Generic.hs +167/−0
- src/Env/Parse.hs +1/−3
- test/Env/GenericSpec.hs +57/−0
- test/EnvSpec.hs +113/−0
CHANGELOG.markdown view
@@ -1,4 +1,9 @@-0.2.2+0.3.1+=====++ * Supported `Generic`ally derived `Parser`s on GHC 7.10.++0.3.0 ===== * Supported user-defined `Reader` errors. (https://github.com/supki/envparse/pull/4)
envparse.cabal view
@@ -1,5 +1,5 @@ name: envparse-version: 0.3.0+version: 0.3.1 synopsis: Parse environment variables description: Here's a simple example of a program that uses @envparse@'s parser:@@ -59,6 +59,7 @@ CHANGELOG.markdown example/Main.hs example/CustomError.hs+ example/Generic.hs source-repository head type: git@@ -67,18 +68,22 @@ source-repository this type: git location: https://github.com/supki/envparse- tag: 0.3.0+ tag: 0.3.1 library default-language: Haskell2010 build-depends:- base >= 4.6 && < 5+ base+ >= 4.6 && < 5 , containers hs-source-dirs: src exposed-modules: Env+ if impl(ghc >= 7.10)+ exposed-modules:+ Env.Generic other-modules: Env.Error Env.Free@@ -94,7 +99,8 @@ type: exitcode-stdio-1.0 build-depends:- base >= 4.6 && < 5+ base+ >= 4.6 && < 5 , containers , hspec hs-source-dirs:@@ -102,5 +108,8 @@ test main-is: Spec.hs+ other-modules:+ EnvSpec+ Env.GenericSpec ghc-options: -Wall
+ example/Generic.hs view
@@ -0,0 +1,46 @@+{-# 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
+ src/Env/Generic.hs view
@@ -0,0 +1,167 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+module Env.Generic+ ( Record(..)+ , Field(..)+ , (?)(..)+ , G.Generic+ ) where++import Control.Applicative (liftA2, (<|>))+import Control.Monad (guard)+import qualified Data.Char as Char+import Data.Int (Int8, Int16, Int32, Int64)+import Data.Word (Word8, Word16, Word32, Word64)+import qualified Data.List as List+import Data.Maybe (fromMaybe)+import Data.Proxy (Proxy(Proxy))+import qualified GHC.Generics as G+import qualified GHC.TypeLits as G+import Numeric.Natural (Natural)+import Prelude hiding (mod)++import qualified Env+++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.+ } 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.+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+-- selectors when building environment variable names.+instance (G.Constructor c, GRecord e a) => GRecord e (G.C1 c a) where+ gr state =+ fmap G.M1 (gr state {stateCon=con})+ 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})+ where+ sel = G.selName (G.M1 Proxy :: G.M1 t c Proxy b)+ suffix = let+ x = camelTo2 sel+ in fromMaybe x $ do+ 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+ go1 "" = ""+ go1 (x:u:l:xs) | Char.isUpper u && Char.isLower l = x : '_' : u : l : go1 xs+ go1 (x:xs) = x : go1 xs++ go2 "" = ""+ 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++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+ field name help =+ Env.var Env.auto name (foldMap Env.help help)++instance (Env.AsUnset e, Env.AsUnread e) => Field e Int++instance (Env.AsUnset e, Env.AsUnread e) => Field e Int8++instance (Env.AsUnset e, Env.AsUnread e) => Field e Int16++instance (Env.AsUnset e, Env.AsUnread e) => Field e Int32++instance (Env.AsUnset e, Env.AsUnread e) => Field e Int64++instance (Env.AsUnset e, Env.AsUnread e) => Field e Integer++instance (Env.AsUnset e, Env.AsUnread e) => Field e Word++instance (Env.AsUnset e, Env.AsUnread e) => Field e Word8++instance (Env.AsUnset e, Env.AsUnread e) => Field e Word16++instance (Env.AsUnset e, Env.AsUnread e) => Field e Word32++instance (Env.AsUnset e, Env.AsUnread e) => Field e Word64++instance (Env.AsUnset e, Env.AsUnread e) => Field e Natural++instance (Env.AsUnset e, Env.AsUnread e) => Field e Float++instance (Env.AsUnset e, Env.AsUnread e) => Field e Double++instance Env.AsUnset e => Field e String where+ field name help =+ Env.var Env.str name (foldMap Env.help help)++instance (Env.AsUnset e, Env.AsUnread e) => Field e Char where+ field name help =+ Env.var reader name (foldMap Env.help help)+ where+ reader = \case+ [c] -> pure c+ str -> Left (Env.unread str)++instance (Env.AsUnset e, Env.AsEmpty e) => Field e Bool where+ field name help =+ Env.switch name (foldMap Env.help help)++-- | Variable tagged with its help message.+newtype a ? tag = Help { unHelp :: a }+ deriving (Show, Eq, Functor, Foldable, Traversable)++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
@@ -151,8 +151,6 @@ mempty = Mod id mappend (Mod f) (Mod g) = Mod (g . f) -- -- | Environment variable metadata data Var a = Var { varHelp :: Maybe String@@ -175,7 +173,7 @@ -- | Flag metadata data Flag a = Flag- { flagHelp :: Maybe String+ { flagHelp :: Maybe String } defaultFlag :: Flag a
+ test/Env/GenericSpec.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+module Env.GenericSpec (spec) where++import Test.Hspec+#if __GLASGOW_HASKELL__ < 710+++spec :: Spec+spec =+ return ()+#else++import Env+import Env.Generic+++spec :: Spec+spec = do+ fooBarSpec+ quxSpec++data FooBar = FooBar+ { foo :: String+ , bar :: Int+ } deriving (Show, Eq, Generic)++instance Record Error FooBar++fooBarSpec :: Spec+fooBarSpec =+ describe "FooBar" $+ it "can be parsed using the Generic parser" $+ p [("FOO", "blah"), ("BAR", "4")] `shouldBe`+ pure FooBar {foo="blah", bar=4}++data Qux = Qux+ { quxXyz :: Bool+ , quxXyzZy :: Int+ } deriving (Show, Eq, Generic)++instance Record Error Qux++quxSpec :: Spec+quxSpec =+ describe "Qux" $+ it "can be parsed using the Generic parser" $+ p [("XYZ", "1"), ("XYZ_ZY", "7")] `shouldBe`+ pure Qux {quxXyz=True, quxXyzZy=7}++p :: Record Error a => [(String, String)] -> Either [(String, Error)] a+p =+ parsePure record+#endif
+ test/EnvSpec.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ExtendedDefaultRules #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}+module EnvSpec (spec) where++import Control.Applicative+import Control.Monad+#if __GLASGOW_HASKELL__ < 710+import Data.Monoid (mempty)+#endif+import Prelude hiding (pi)+import Test.Hspec+import Text.Read (readMaybe)++import Env++default (Integer, Double, String)+++spec :: Spec+spec =+ describe "parsing" $ do+ it "parsing the environment with the noop parser always succeeds" $+ p (pure ()) `shouldBe` Just ()++ it "parsing the environment with the failing parser always fails" $+ p Control.Applicative.empty `shouldBe` Nothing++ it "looking for the non-existing env var fails" $+ p (var str "xyzzy" mempty) `shouldBe` Nothing++ it "looking for the existing env var is a success" $+ p (var str "foo" mempty) `shouldBe` Just "bar"++ it "looking for many existing env vars is a success" $ do+ let x = (,) <$> var str "foo" mempty <*> var str "qux" mempty+ p x `shouldBe` Just ("bar", "quux")++ it "looking for the existing env var selects the active flag value" $+ p (flag 4 7 "foo" mempty) `shouldBe` Just 7++ it "looking for the existing but empty env var selects the default flag value" $+ p (flag 4 7 "empty" mempty) `shouldBe` Just 4++ it "looking for the non-existing env var selects the default flag value" $+ p (flag 4 7 "xyzzy" mempty) `shouldBe` Just 4++ context "readers" $ do+ it "can use a reader to parse the env var value" $+ p (var auto "num" mempty) `shouldBe` Just 4++ it "can use a custom reader" $ do+ p (var greaterThan5 "num" mempty) `shouldBe` Nothing+ p (var greaterThan5 "num2" mempty) `shouldBe` Just 7++ it "'nonempty' weeds out variables set to the empty string" $+ p (var (str <=< nonempty) "empty" mempty) `shouldBe` Nothing++ context "alternatives" $ do+ it "can look through a list of alternatives" $+ p (asum+ [ var (\_ -> pure 1) "nope" mempty+ , var (\_ -> pure 2) "still-nope" mempty+ , var (\_ -> pure 3) "yep" mempty+ ]) `shouldBe` Just 3++ it "variables can have default values" $+ p (asum+ [ var (\_ -> pure 1) "nope" mempty+ , var (\_ -> pure 2) "still-nope" (def 4)+ , var (\_ -> pure 3) "yep" mempty+ ]) `shouldBe` Just 4++ context "modifiers" $ do+ it "the latter modifier overwrites the former" $+ p (var (\_ -> Left (unread "nope")) "never" (def 4 <> def 7)) `shouldBe` Just 7++ it "‘prefixed’ modifier changes the names of the variables" $+ p (prefixed "spec_" (var str "foo" mempty)) `shouldBe` Just "totally-not-bar"++ it "‘prefixed’ modifier can be nested" $+ p (prefixed "pre" (prefixed "pro" (var str "morphism" mempty)))+ `shouldBe`+ Just "zygohistomorphic"+++greaterThan5 :: AsUnread e => Reader e Int+greaterThan5 s =+ note (unread "fail") (do v <- readMaybe s; guard (v > 5); return v)++p :: Parser Error a -> Maybe a+p x = hush (parsePure x fancyEnv)++fancyEnv :: [(String, String)]+fancyEnv =+ [ "foo" ~> "bar"+ , "spec_foo" ~> "totally-not-bar"+ , "prepromorphism"+ ~> "zygohistomorphic"+ , "qux" ~> "quux"+ , "num" ~> "4"+ , "num2" ~> "7"+ , "yep" ~> "!"+ , "empty" ~> ""+ ]+ where+ (~>) = (,)++note :: a -> Maybe b -> Either a b+note a = maybe (Left a) Right++hush :: Either a b -> Maybe b+hush = either (const Nothing) Just