envparse 0.3.3 → 0.4
raw patch · 16 files changed
+733/−555 lines, 16 filesdep +textdep ~base
Dependencies added: text
Dependency ranges changed: base
Files
- CHANGELOG.markdown +14/−0
- envparse.cabal +10/−9
- src/Env.hs +22/−9
- src/Env/Error.hs +0/−64
- src/Env/Free.hs +0/−89
- src/Env/Generic.hs +19/−7
- src/Env/Help.hs +0/−134
- src/Env/Internal/Error.hs +64/−0
- src/Env/Internal/Free.hs +91/−0
- src/Env/Internal/Help.hs +137/−0
- src/Env/Internal/Parser.hs +271/−0
- src/Env/Internal/Val.hs +42/−0
- src/Env/Parse.hs +0/−203
- src/Env/Val.hs +0/−40
- test/Env/Internal/ParserSpec.hs +35/−0
- test/EnvSpec.hs +28/−0
CHANGELOG.markdown view
@@ -1,3 +1,17 @@+0.4+===++ * Supported GHC 8.0.1.++ * On GHC 7.8 and newer, as a secutiry measure, all declared variables are unset by the end of+ a successful parse. If you want to keep the variable in the environment, use the `keep` modifier.+ (https://github.com/supki/envparse/pull/7)++0.3.4+=====++ * Added `splitOn`.+ 0.3.3 =====
envparse.cabal view
@@ -1,5 +1,5 @@ name: envparse-version: 0.3.3+version: 0.4 synopsis: Parse environment variables description: Here's a simple example of a program that uses @envparse@'s parser:@@ -53,7 +53,7 @@ category: System build-type: Simple cabal-version: >= 1.10-tested-with: GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.3+tested-with: GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.3, GHC == 8.0.1 extra-source-files: README.markdown CHANGELOG.markdown@@ -69,7 +69,7 @@ source-repository this type: git location: https://github.com/supki/envparse- tag: 0.3.3+ tag: 0.4 library default-language:@@ -82,15 +82,14 @@ src exposed-modules: Env+ Env.Internal.Error+ Env.Internal.Free+ Env.Internal.Help+ Env.Internal.Val+ Env.Internal.Parser if impl(ghc >= 7.10) exposed-modules: Env.Generic- other-modules:- Env.Error- Env.Free- Env.Help- Env.Parse- Env.Val ghc-options: -Wall @@ -104,6 +103,7 @@ >= 4.6 && < 5 , containers , hspec+ , text hs-source-dirs: src test@@ -112,5 +112,6 @@ other-modules: EnvSpec Env.GenericSpec+ Env.Internal.ParserSpec ghc-options: -Wall
src/Env.hs view
@@ -60,6 +60,7 @@ , Reader , str , nonempty+ , splitOn , auto , def , helpDef@@ -68,6 +69,8 @@ , Flag , HasHelp , help+ , HasKeep+ , keep , Help.helpDoc , Error(..) , Error.AsUnset(..)@@ -83,20 +86,23 @@ import Control.Applicative import Control.Monad ((>=>), (<=<))-import Data.Foldable (asum)+import Data.Foldable (asum, for_) #if __GLASGOW_HASKELL__ < 710 import Data.Monoid (Monoid(..), (<>)) #else import Data.Monoid ((<>)) #endif import System.Environment (getEnvironment)+#if __GLASGOW_HASKELL__ >= 708+import System.Environment (unsetEnv)+#endif import System.Exit (exitFailure) import qualified System.IO as IO -import qualified Env.Help as Help-import Env.Parse-import Env.Error (Error)-import qualified Env.Error as Error+import qualified Env.Internal.Help as Help+import Env.Internal.Parser+import Env.Internal.Error (Error)+import qualified Env.Internal.Error as Error -- $re-exports -- External functions that may be useful to the consumer of the library@@ -120,11 +126,18 @@ -- -- Use this if simply dying on failure (the behavior of 'parse') is inadequate for your needs. parseOr :: (String -> IO a) -> (Help.Info Error -> Help.Info e) -> Parser e b -> IO (Either a b)-parseOr f g p =- traverseLeft (f . Help.helpInfo (g Help.defaultInfo) p) . parsePure p =<< getEnvironment+parseOr onFailure helpMod parser = do+ b <- fmap (parsePure parser) getEnvironment+#if __GLASGOW_HASKELL__ >= 708+ for_ b $ \_ ->+ eachUnsetVar parser unsetEnv+#endif+ traverseLeft (onFailure . Help.helpInfo (helpMod Help.defaultInfo) parser) b die :: String -> IO a-die m = do IO.hPutStrLn IO.stderr m; exitFailure+die m =+ do IO.hPutStrLn IO.stderr m; exitFailure traverseLeft :: Applicative f => (a -> f b) -> Either a t -> f (Either b t)-traverseLeft f = either (fmap Left . f) (pure . Right)+traverseLeft f =+ either (fmap Left . f) (pure . Right)
− src/Env/Error.hs
@@ -1,64 +0,0 @@--- | This module contains an extensible error infrastructure.------ Each kind of errors gets a separate type class which encodes--- a 'Prism' (roughly a getter and a constructor). The 'Reader's, then,--- have the constraints for precisely the set of errors they can return.-module Env.Error- ( Error(..)- , AsUnset(..)- , AsEmpty(..)- , AsUnread(..)- ) where----- | The type of errors returned by @envparse@'s 'Reader's. These fall into 3--- categories:------ * Variables that are unset in the environment.--- * Variables whose value is empty.--- * Variables whose value cannot be parsed using the 'Read' instance.-data Error- = UnsetError- | EmptyError- | UnreadError String- deriving (Show, Eq)---- | The class of types that contain and can be constructed from--- the error returned from parsing unset variables.-class AsUnset e where- unset :: e- tryUnset :: e -> Maybe ()--instance AsUnset Error where- unset = UnsetError- tryUnset err =- case err of- UnsetError -> Just ()- _ -> Nothing---- | The class of types that contain and can be constructed from--- the error returned from parsing variables whose value is empty.-class AsEmpty e where- empty :: e- tryEmpty :: e -> Maybe ()--instance AsEmpty Error where- empty = EmptyError- tryEmpty err =- case err of- EmptyError -> Just ()- _ -> Nothing---- | The class of types that contain and can be constructed from--- the error returned from parsing variable whose value cannot--- be parsed using the 'Read' instance.-class AsUnread e where- unread :: String -> e- tryUnread :: e -> Maybe String--instance AsUnread Error where- unread = UnreadError- tryUnread err =- case err of- UnreadError msg -> Just msg- _ -> Nothing
− src/Env/Free.hs
@@ -1,89 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}--- | @Alt F@ is the free 'Alternative' functor on @F@-module Env.Free- ( Alt(..)- , liftAlt- , runAlt- , foldAlt- , hoistAlt- -- * Debug- , inspect- ) where--#if __GLASGOW_HASKELL__ < 710-import Control.Applicative (Applicative(..))-#endif-import Control.Applicative (Alternative(..))-#if __GLASGOW_HASKELL__ < 710-import Data.Monoid (Monoid(..))-#endif---data Alt f a where- Nope :: Alt f a- Pure :: a -> Alt f a- Ap :: Alt f (a -> b) -> Alt f a -> Alt f b- Alt :: Alt f a -> Alt f a -> Alt f a- Lift :: f a -> Alt f a---- | Print the free structure-inspect :: Alt f a -> String-inspect Nope = "Nope"-inspect (Pure _) = "Pure _"-inspect (Ap f x) = concat ["(", inspect f, ") <*> (", inspect x, ")"]-inspect (Alt x y) = concat ["(", inspect x, ") <|> (", inspect y, ")"]-inspect (Lift _) = "Lift _"--instance Functor f => Functor (Alt f) where- fmap _ Nope = Nope- fmap f (Pure a) = Pure (f a)- fmap f (Ap a v) = Ap (fmap (f .) a) v- fmap f (Alt a b) = Alt (fmap f a) (fmap f b)- fmap f (Lift a) = Lift (fmap f a)--instance Functor f => Applicative (Alt f) where- pure = Pure- (<*>) = Ap--instance Functor f => Alternative (Alt f) where- empty = Nope- (<|>) = Alt---liftAlt :: f a -> Alt f a-liftAlt = Lift--runAlt :: forall f g a. Alternative g => (forall x. f x -> g x) -> Alt f a -> g a-runAlt u = go where- go :: Alt f b -> g b- go Nope = empty- go (Pure a) = pure a- go (Ap f x) = go f <*> go x- go (Alt s t) = go s <|> go t- go (Lift x) = u x--foldAlt :: Monoid p => (forall a. f a -> p) -> Alt f b -> p-foldAlt f = unMon . runAlt (Mon . f)--hoistAlt :: forall f g b. Functor g => (forall a. f a -> g a) -> Alt f b -> Alt g b-hoistAlt nat = runAlt (Lift . nat)----- | The 'Alternative' functor induced by the 'Monoid'-newtype Mon m a = Mon- { unMon :: m- } deriving (Show, Eq)--instance Functor (Mon m) where- fmap _ (Mon a) = Mon a--instance Monoid m => Applicative (Mon m) where- pure _ = Mon mempty- Mon x <*> Mon y = Mon (mappend x y)--instance Monoid m => Alternative (Mon m) where- empty = Mon mempty- Mon x <|> Mon y = Mon (mappend x y)
src/Env/Generic.hs view
@@ -1,6 +1,10 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE DeriveTraversable #-}+#if __GLASGOW_HASKELL__ < 800+{-# LANGUAGE ExplicitNamespaces #-}+#endif {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE LambdaCase #-}@@ -91,7 +95,11 @@ module Env.Generic ( Record(..) , Field(..)+#if __GLASGOW_HASKELL__ < 800 , (?)(..)+#else+ , type (?)(..)+#endif , G.Generic ) where @@ -138,7 +146,7 @@ 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+instance Field e a => GRecord e (G.K1 i a) where gr State {stateVar} = fmap G.K1 (field stateVar Nothing) @@ -158,7 +166,17 @@ gr x = fmap G.L1 (gr x) <|> fmap G.R1 (gr x) +#if __GLASGOW_HASKELL__ < 800+type family Type x :: ConType where+ Type G.NoSelector = 'Plain+ Type x = 'Record++data ConType = Plain | Record+ instance (G.Selector c, Type c ~ 'Record, GRecord e a) => GRecord e (G.S1 c a) where+#else+instance (G.Selector c, c ~ 'G.MetaSel ('Just x1) x2 x3 x4, GRecord e a) => GRecord e (G.S1 c a) where+#endif gr state@State {statePrefix, stateCon} = fmap G.M1 (gr state {stateVar=statePrefix ++ suffix}) where@@ -179,12 +197,6 @@ go2 "" = "" go2 (l:u:xs) | Char.isLower l && Char.isUpper u = l : '_' : u : go2 xs go2 (x:xs) = x : go2 xs--type family Type x :: ConType where- Type G.NoSelector = 'Plain- Type x = '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.
− src/Env/Help.hs
@@ -1,134 +0,0 @@-{-# LANGUAGE NamedFieldPuns #-}-module Env.Help- ( helpInfo- , helpDoc- , Info- , ErrorHandler- , defaultInfo- , defaultErrorHandler- , header- , desc- , footer- , handleError- ) where--import Data.Foldable (asum)-import qualified Data.List as List-import qualified Data.Map as Map-import Data.Maybe (catMaybes, mapMaybe)-import Data.Ord (comparing)--import Env.Error (Error)-import qualified Env.Error as Error-import Env.Free-import Env.Parse hiding (Mod)---helpInfo :: Info e -> Parser e b -> [(String, e)] -> String-helpInfo Info {infoHeader, infoDesc, infoFooter, infoHandleError} p errors =- List.intercalate "\n\n" $ catMaybes- [ infoHeader- , fmap (List.intercalate "\n" . splitWords 50) infoDesc- , Just (helpDoc p)- , fmap (List.intercalate "\n" . splitWords 50) infoFooter- ] ++ helpErrors infoHandleError errors---- | A pretty-printed list of recognized environment variables suitable for usage messages-helpDoc :: Parser e a -> String-helpDoc p =- List.intercalate "\n" ("Available environment variables:\n" : helpParserDoc p)--helpParserDoc :: Parser e a -> [String]-helpParserDoc = concat . Map.elems . foldAlt (\v -> Map.singleton (varfName v) (helpVarfDoc v)) . unParser--helpVarfDoc :: VarF e a -> [String]-helpVarfDoc VarF { varfName, varfHelp, varfHelpDef } =- case varfHelp of- Nothing -> [indent 2 varfName]- Just h- | k > 15 -> indent 2 varfName : map (indent 25) (splitWords 30 t)- | otherwise ->- case zipWith indent (23 - k : repeat 25) (splitWords 30 t) of- (x : xs) -> (indent 2 varfName ++ x) : xs- [] -> [indent 2 varfName]- where k = length varfName- t = maybe h (\s -> h ++ " (default: " ++ s ++")") varfHelpDef--splitWords :: Int -> String -> [String]-splitWords n = go [] 0 . words- where- go acc _ [] = prep acc- go acc k (w : ws)- | k + z < n = go (w : acc) (k + z) ws- | z > n = prep acc ++ case splitAt n w of (w', w'') -> w' : go [] 0 (w'' : ws)- | otherwise = prep acc ++ go [w] z ws- where- z = length w-- prep [] = []- prep acc = [unwords (reverse acc)]--indent :: Int -> String -> String-indent n s = replicate n ' ' ++ s--helpErrors :: ErrorHandler e -> [(String, e)] -> [String]-helpErrors _ [] = []-helpErrors handler fs =- [ "Parsing errors:"- , List.intercalate "\n" (mapMaybe (uncurry handler) (List.sortBy (comparing varName) fs))- ]---- | Parser's metadata-data Info e = Info- { infoHeader :: Maybe String- , infoDesc :: Maybe String- , infoFooter :: Maybe String- , infoHandleError :: ErrorHandler e- }---- | Given a variable name and an error value, try to produce a useful error message-type ErrorHandler e = String -> e -> Maybe String--defaultInfo :: Info Error-defaultInfo = Info- { infoHeader = Nothing- , infoDesc = Nothing- , infoFooter = Nothing- , infoHandleError = defaultErrorHandler- }---- | Set the help text header (it usually includes the application's name and version)-header :: String -> Info e -> Info e-header h i = i {infoHeader=Just h}---- | Set the short description-desc :: String -> Info e -> Info e-desc h i = i {infoDesc=Just h}---- | Set the help text footer (it usually includes examples)-footer :: String -> Info e -> Info e-footer h i = i {infoFooter=Just h}---- | An error handler-handleError :: ErrorHandler e -> Info x -> Info e-handleError handler i = i {infoHandleError=handler}---- | The default error handler-defaultErrorHandler :: (Error.AsUnset e, Error.AsEmpty e, Error.AsUnread e) => ErrorHandler e-defaultErrorHandler name err =- asum [handleUnsetError name err, handleEmptyError name err, handleUnreadError name err]--handleUnsetError :: Error.AsUnset e => ErrorHandler e-handleUnsetError name =- fmap (\() -> indent 2 (name ++ " is unset")) . Error.tryUnset--handleEmptyError :: Error.AsEmpty e => ErrorHandler e-handleEmptyError name =- fmap (\() -> indent 2 (name ++ " is empty")) . Error.tryEmpty--handleUnreadError :: Error.AsUnread e => ErrorHandler e-handleUnreadError name =- fmap (\val -> indent 2 (name ++ " has value " ++ val ++ " that cannot be parsed")) . Error.tryUnread--varName :: (String, e) -> String-varName (n, _) = n
+ src/Env/Internal/Error.hs view
@@ -0,0 +1,64 @@+-- | This module contains an extensible error infrastructure.+--+-- Each kind of errors gets a separate type class which encodes+-- a 'Prism' (roughly a getter and a constructor). The 'Reader's, then,+-- have the constraints for precisely the set of errors they can return.+module Env.Internal.Error+ ( Error(..)+ , AsUnset(..)+ , AsEmpty(..)+ , AsUnread(..)+ ) where+++-- | The type of errors returned by @envparse@'s 'Reader's. These fall into 3+-- categories:+--+-- * Variables that are unset in the environment.+-- * Variables whose value is empty.+-- * Variables whose value cannot be parsed using the 'Read' instance.+data Error+ = UnsetError+ | EmptyError+ | UnreadError String+ deriving (Show, Eq)++-- | The class of types that contain and can be constructed from+-- the error returned from parsing unset variables.+class AsUnset e where+ unset :: e+ tryUnset :: e -> Maybe ()++instance AsUnset Error where+ unset = UnsetError+ tryUnset err =+ case err of+ UnsetError -> Just ()+ _ -> Nothing++-- | The class of types that contain and can be constructed from+-- the error returned from parsing variables whose value is empty.+class AsEmpty e where+ empty :: e+ tryEmpty :: e -> Maybe ()++instance AsEmpty Error where+ empty = EmptyError+ tryEmpty err =+ case err of+ EmptyError -> Just ()+ _ -> Nothing++-- | The class of types that contain and can be constructed from+-- the error returned from parsing variable whose value cannot+-- be parsed using the 'Read' instance.+class AsUnread e where+ unread :: String -> e+ tryUnread :: e -> Maybe String++instance AsUnread Error where+ unread = UnreadError+ tryUnread err =+ case err of+ UnreadError msg -> Just msg+ _ -> Nothing
+ src/Env/Internal/Free.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | @Alt F@ is the free 'Alternative' functor on @F@+module Env.Internal.Free+ ( Alt(..)+ , liftAlt+ , runAlt+ , foldAlt+ , hoistAlt+ -- * Debug+ , inspect+ ) where++#if __GLASGOW_HASKELL__ < 710+import Control.Applicative (Applicative(..))+#endif+import Control.Applicative (Alternative(..))+#if __GLASGOW_HASKELL__ < 710+import Data.Monoid (Monoid(..))+#endif+++data Alt f a where+ Nope :: Alt f a+ Pure :: a -> Alt f a+ Ap :: Alt f (a -> b) -> Alt f a -> Alt f b+ Alt :: Alt f a -> Alt f a -> Alt f a+ Lift :: f a -> Alt f a++-- | Print the free structure+inspect :: Alt f a -> String+inspect Nope = "Nope"+inspect (Pure _) = "Pure _"+inspect (Ap f x) = concat ["(", inspect f, ") <*> (", inspect x, ")"]+inspect (Alt x y) = concat ["(", inspect x, ") <|> (", inspect y, ")"]+inspect (Lift _) = "Lift _"++instance Functor f => Functor (Alt f) where+ fmap _ Nope = Nope+ fmap f (Pure a) = Pure (f a)+ fmap f (Ap a v) = Ap (fmap (f .) a) v+ fmap f (Alt a b) = Alt (fmap f a) (fmap f b)+ fmap f (Lift a) = Lift (fmap f a)++instance Functor f => Applicative (Alt f) where+ pure = Pure+ (<*>) = Ap++instance Functor f => Alternative (Alt f) where+ empty = Nope+ (<|>) = Alt+++liftAlt :: f a -> Alt f a+liftAlt = Lift++runAlt :: forall f g a. Alternative g => (forall x. f x -> g x) -> Alt f a -> g a+runAlt u = go where+ go :: Alt f b -> g b+ go Nope = empty+ go (Pure a) = pure a+ go (Ap f x) = go f <*> go x+ go (Alt s t) = go s <|> go t+ go (Lift x) = u x++foldAlt :: Monoid p => (forall a. f a -> p) -> Alt f b -> p+foldAlt f =+ unMon . runAlt (Mon . f)++hoistAlt :: forall f g b. Functor g => (forall a. f a -> g a) -> Alt f b -> Alt g b+hoistAlt nat =+ runAlt (Lift . nat)+++-- | The 'Alternative' functor induced by the 'Monoid'+newtype Mon m a = Mon+ { unMon :: m+ } deriving (Show, Eq)++instance Functor (Mon m) where+ fmap _ (Mon a) = Mon a++instance Monoid m => Applicative (Mon m) where+ pure _ = Mon mempty+ Mon x <*> Mon y = Mon (mappend x y)++instance Monoid m => Alternative (Mon m) where+ empty = Mon mempty+ Mon x <|> Mon y = Mon (mappend x y)
+ src/Env/Internal/Help.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE NamedFieldPuns #-}+module Env.Internal.Help+ ( helpInfo+ , helpDoc+ , Info+ , ErrorHandler+ , defaultInfo+ , defaultErrorHandler+ , header+ , desc+ , footer+ , handleError+ ) where++import Data.Foldable (asum)+import qualified Data.List as List+import qualified Data.Map as Map+import Data.Maybe (catMaybes, mapMaybe)+import Data.Ord (comparing)++import Env.Internal.Error (Error)+import qualified Env.Internal.Error as Error+import Env.Internal.Free+import Env.Internal.Parser hiding (Mod)+++helpInfo :: Info e -> Parser e b -> [(String, e)] -> String+helpInfo Info {infoHeader, infoDesc, infoFooter, infoHandleError} p errors =+ List.intercalate "\n\n" $ catMaybes+ [ infoHeader+ , fmap (List.intercalate "\n" . splitWords 50) infoDesc+ , Just (helpDoc p)+ , fmap (List.intercalate "\n" . splitWords 50) infoFooter+ ] ++ helpErrors infoHandleError errors++-- | A pretty-printed list of recognized environment variables suitable for usage messages+helpDoc :: Parser e a -> String+helpDoc p =+ List.intercalate "\n" ("Available environment variables:\n" : helpParserDoc p)++helpParserDoc :: Parser e a -> [String]+helpParserDoc =+ concat . Map.elems . foldAlt (\v -> Map.singleton (varfName v) (helpVarfDoc v)) . unParser++helpVarfDoc :: VarF e a -> [String]+helpVarfDoc VarF {varfName, varfHelp, varfHelpDef} =+ case varfHelp of+ Nothing -> [indent 2 varfName]+ Just h+ | k > 15 -> indent 2 varfName : map (indent 25) (splitWords 30 t)+ | otherwise ->+ case zipWith indent (23 - k : repeat 25) (splitWords 30 t) of+ (x : xs) -> (indent 2 varfName ++ x) : xs+ [] -> [indent 2 varfName]+ where k = length varfName+ t = maybe h (\s -> h ++ " (default: " ++ s ++")") varfHelpDef++splitWords :: Int -> String -> [String]+splitWords n =+ go [] 0 . words+ where+ go acc _ [] = prep acc+ go acc k (w : ws)+ | k + z < n = go (w : acc) (k + z) ws+ | z > n = prep acc ++ case splitAt n w of (w', w'') -> w' : go [] 0 (w'' : ws)+ | otherwise = prep acc ++ go [w] z ws+ where+ z = length w++ prep [] = []+ prep acc = [unwords (reverse acc)]++indent :: Int -> String -> String+indent n s =+ replicate n ' ' ++ s++helpErrors :: ErrorHandler e -> [(String, e)] -> [String]+helpErrors _ [] = []+helpErrors handler fs =+ [ "Parsing errors:"+ , List.intercalate "\n" (mapMaybe (uncurry handler) (List.sortBy (comparing varName) fs))+ ]++-- | Parser's metadata+data Info e = Info+ { infoHeader :: Maybe String+ , infoDesc :: Maybe String+ , infoFooter :: Maybe String+ , infoHandleError :: ErrorHandler e+ }++-- | Given a variable name and an error value, try to produce a useful error message+type ErrorHandler e = String -> e -> Maybe String++defaultInfo :: Info Error+defaultInfo = Info+ { infoHeader = Nothing+ , infoDesc = Nothing+ , infoFooter = Nothing+ , infoHandleError = defaultErrorHandler+ }++-- | Set the help text header (it usually includes the application's name and version)+header :: String -> Info e -> Info e+header h i = i {infoHeader=Just h}++-- | Set the short description+desc :: String -> Info e -> Info e+desc h i = i {infoDesc=Just h}++-- | Set the help text footer (it usually includes examples)+footer :: String -> Info e -> Info e+footer h i = i {infoFooter=Just h}++-- | An error handler+handleError :: ErrorHandler e -> Info x -> Info e+handleError handler i = i {infoHandleError=handler}++-- | The default error handler+defaultErrorHandler :: (Error.AsUnset e, Error.AsEmpty e, Error.AsUnread e) => ErrorHandler e+defaultErrorHandler name err =+ asum [handleUnsetError name err, handleEmptyError name err, handleUnreadError name err]++handleUnsetError :: Error.AsUnset e => ErrorHandler e+handleUnsetError name =+ fmap (\() -> indent 2 (name ++ " is unset")) . Error.tryUnset++handleEmptyError :: Error.AsEmpty e => ErrorHandler e+handleEmptyError name =+ fmap (\() -> indent 2 (name ++ " is empty")) . Error.tryEmpty++handleUnreadError :: Error.AsUnread e => ErrorHandler e+handleUnreadError name =+ fmap (\val -> indent 2 (name ++ " has value " ++ val ++ " that cannot be parsed")) . Error.tryUnread++varName :: (String, e) -> String+varName (n, _) = n
+ src/Env/Internal/Parser.hs view
@@ -0,0 +1,271 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE ViewPatterns #-}+module Env.Internal.Parser+ ( Parser(..)+ , VarF(..)+ , parsePure+ , eachUnsetVar+ , Mod(..)+ , prefixed+ , var+ , Var(..)+ , defaultVar+ , Reader+ , str+ , nonempty+ , splitOn+ , auto+ , def+ , helpDef+ , showDef+ , flag+ , switch+ , Flag+ , HasHelp+ , help+ , HasKeep+ , keep+ ) where++import Control.Applicative+import Control.Arrow (left)+import Control.Monad ((<=<))+import Data.Foldable (for_)+import Data.Map (Map)+import qualified Data.Map as Map+import qualified Data.Set as Set+#if __GLASGOW_HASKELL__ < 710+import Data.Monoid (Monoid(..))+#endif+import Data.String (IsString(..))++import Env.Internal.Free+import qualified Env.Internal.Error as Error+import Env.Internal.Val+++-- | Try to parse a pure environment+parsePure :: Parser e a -> [(String, String)] -> Either [(String, e)] a+parsePure (Parser p) (Map.fromList -> env) =+ toEither (runAlt go p)+ where+ go v = maybe id (\d x -> x <|> pure d) (varfDef v) (fromEither (readVar v env))++eachUnsetVar :: Applicative m => Parser e a -> (String -> m b) -> m ()+eachUnsetVar Parser {unParser} =+ for_ (foldAlt (\VarF {varfKeep, varfName} -> if varfKeep then Set.empty else Set.singleton varfName) unParser)++readVar :: VarF e a -> Map String String -> Either [(String, e)] a+readVar VarF {varfName, varfReader} =+ left (pure . (\err -> (varfName, err))) . varfReader varfName+++-- | An environment parser+newtype Parser e a = Parser { unParser :: Alt (VarF e) a }+ deriving (Functor)++instance Applicative (Parser e) where+ pure =+ Parser . pure+ Parser f <*> Parser x =+ Parser (f <*> x)++instance Alternative (Parser e) where+ empty =+ Parser empty+ Parser f <|> Parser x =+ Parser (f <|> x)++-- | The string to prepend to the name of every declared environment variable+prefixed :: String -> Parser e a -> Parser e a+prefixed pre =+ Parser . hoistAlt (\v -> v {varfName=pre ++ varfName v}) . unParser+++data VarF e a = VarF+ { varfName :: String+ , varfReader :: String -> Map String String -> Either e a+ , varfHelp :: Maybe String+ , varfDef :: Maybe a+ , varfHelpDef :: Maybe String+ , varfKeep :: Bool+ } deriving (Functor)++liftVarF :: VarF e a -> Parser e a+liftVarF =+ Parser . liftAlt++-- | An environment variable's value parser. Use @(<=<)@ and @(>=>)@ to combine these+type Reader e a = String -> Either e a++lookupVar :: Error.AsUnset e => String -> Map String String -> Either e String+lookupVar name =+ maybe (Left Error.unset) Right . Map.lookup name++-- | Parse a particular variable from the environment+--+-- @+-- >>> var 'str' \"EDITOR\" ('def' \"vim\" <> 'helpDef' show)+-- @+var :: Error.AsUnset e => Reader e a -> String -> Mod Var a -> Parser e a+var r n (Mod f) =+ liftVarF $ VarF+ { varfName = n+ , varfReader = \name -> r <=< lookupVar name+ , varfHelp = varHelp+ , varfDef = varDef+ , varfHelpDef = varHelpDef <*> varDef+ , varfKeep = varKeep+ }+ where+ Var {varHelp, varDef, varHelpDef, varKeep} = f defaultVar++-- | A flag that takes the active value if the environment variable+-- is set and non-empty and the default value otherwise+--+-- /Note:/ this parser never fails.+flag+ :: a -- ^ default value+ -> a -- ^ active value+ -> String -> Mod Flag a -> Parser e a+flag f t n (Mod g) =+ liftVarF $ VarF+ { varfName = n+ , 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+ , varfKeep = flagKeep+ }+ where+ Flag {flagHelp, flagKeep} = g defaultFlag++-- | A simple boolean 'flag'+--+-- /Note:/ this parser never fails.+switch :: String -> Mod Flag Bool -> Parser e Bool+switch =+ flag False True++-- | The trivial reader+str :: IsString s => Reader e s+str =+ Right . fromString++-- | The reader that accepts only non-empty strings+nonempty :: (Error.AsEmpty e, IsString s) => Reader e s+nonempty =+ fmap fromString . go where go [] = Left Error.empty; go xs = Right xs++-- | The reader that uses the 'Read' instance of the type+auto :: (Error.AsUnread e, Read a) => Reader e a+auto s =+ case reads s of [(v, "")] -> Right v; _ -> Left (Error.unread (show s))++-- | The reader that splits a string into a list of strings consuming the separator.+splitOn :: Char -> Reader e [String]+splitOn sep = Right . go+ where+ go [] = []+ go xs = go' xs++ go' xs =+ case break (== sep) xs of+ (ys, []) ->+ ys : []+ (ys, _ : zs) ->+ ys : go' zs+++-- | This represents a modification of the properties of a particular 'Parser'.+-- Combine them using the 'Monoid' instance.+newtype Mod t a = Mod (t a -> t a)++instance Monoid (Mod t a) where+ mempty = Mod id+ mappend (Mod f) (Mod g) = Mod (g . f)++-- | Environment variable metadata+data Var a = Var+ { varHelp :: Maybe String+ , varHelpDef :: Maybe (a -> String)+ , varDef :: Maybe a+ , varKeep :: Bool+ }++defaultVar :: Var a+defaultVar = Var+ { varHelp = Nothing+ , varDef = Nothing+ , varHelpDef = Nothing+ , varKeep = defaultKeep+ }++defaultKeep :: Bool+defaultKeep = False++-- | The default value of the variable+--+-- /Note:/ specifying it means the parser won't ever fail.+def :: a -> Mod Var a+def d =+ Mod (\v -> v {varDef=Just d})++-- | Flag metadata+data Flag a = Flag+ { flagHelp :: Maybe String+ , flagKeep :: Bool+ }++defaultFlag :: Flag a+defaultFlag = Flag+ { flagHelp = Nothing+ , flagKeep = defaultKeep+ }++-- | Show the default value of the variable in help.+helpDef :: (a -> String) -> Mod Var a+helpDef d =+ Mod (\v -> v {varHelpDef=Just d})++-- | Use the 'Show' instance to show the default value of the variable in help.+showDef :: Show a => Mod Var a+showDef =+ helpDef show+++-- | A class of things that can have a help message attached to them+class HasHelp t where+ setHelp :: String -> t a -> t a++instance HasHelp Var where+ setHelp h v = v {varHelp=Just h}++instance HasHelp Flag where+ setHelp h v = v {flagHelp=Just h}++-- | Attach help text to the variable+help :: HasHelp t => String -> Mod t a+help =+ Mod . setHelp++-- | A class of things that can be still kept in an environment when the+-- parsing has been completed.+class HasKeep t where+ setKeep :: t a -> t a++instance HasKeep Var where+ setKeep v = v {varKeep=True}++instance HasKeep Flag where+ setKeep v = v {flagKeep=True}++-- | Keep a variable.+keep :: HasKeep t => Mod t a+keep =+ Mod setKeep
+ src/Env/Internal/Val.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveFunctor #-}+module Env.Internal.Val+ ( Val(..)+ , fromEither+ , toEither+ ) where++import Control.Applicative+#if __GLASGOW_HASKELL__ < 710+import Data.Monoid (Monoid(..))+#endif+import Data.Monoid ((<>))+++-- | A type isomorphic to 'Either' with the accumulating 'Applicative' instance.+data Val e a+ = Err e+ | Ok a+ deriving (Functor, Show, Eq)++instance Monoid e => Applicative (Val e) where+ pure = Ok++ Err e <*> Err e' = Err (e <> e')+ Err e <*> _ = Err e+ _ <*> Err e' = Err e'+ Ok f <*> Ok a = Ok (f a)++instance Monoid e => Alternative (Val e) where+ empty = Err mempty++ Err _ <|> Ok x = Ok x+ x <|> _ = x++fromEither :: Either e a -> Val e a+fromEither =+ either Err Ok++toEither :: Val e a -> Either e a+toEither x =+ case x of Err e -> Left e; Ok a -> Right a
− src/Env/Parse.hs
@@ -1,203 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE ViewPatterns #-}-module Env.Parse- ( Parser(..)- , VarF(..)- , parsePure- , Mod(..)- , prefixed- , var- , Var(..)- , defaultVar- , Reader- , str- , nonempty- , auto- , def- , helpDef- , showDef- , flag- , switch- , Flag- , HasHelp- , help- ) where--import Control.Applicative-import Control.Arrow (left)-import Control.Monad ((<=<))-import Data.Map (Map)-import qualified Data.Map as Map-#if __GLASGOW_HASKELL__ < 710-import Data.Monoid (Monoid(..))-#endif-import Data.String (IsString(..))--import Env.Free-import qualified Env.Error as Error-import Env.Val----- | Try to parse a pure environment-parsePure :: Parser e b -> [(String, String)] -> Either [(String, e)] b-parsePure (Parser p) (Map.fromList -> env) =- toEither (runAlt go p)- where- go v = maybe id (\d x -> x <|> pure d) (varfDef v) (fromEither (readVar v env))--readVar :: VarF e a -> Map String String -> Either [(String, e)] a-readVar VarF {varfName, varfReader} =- left (pure . (\err -> (varfName, err))) . varfReader varfName----- | An environment parser-newtype Parser e a = Parser { unParser :: Alt (VarF e) a }- deriving (Functor)--instance Applicative (Parser e) where- pure = Parser . pure- Parser f <*> Parser x = Parser (f <*> x)--instance Alternative (Parser e) where- empty = Parser empty- Parser f <|> Parser x = Parser (f <|> x)---- | The string to prepend to the name of every declared environment variable-prefixed :: String -> Parser e a -> Parser e a-prefixed pre =- Parser . hoistAlt (\v -> v { varfName = pre ++ varfName v }) . unParser---data VarF e a = VarF- { varfName :: String- , varfReader :: String -> Map String String -> Either e a- , varfHelp :: Maybe String- , varfDef :: Maybe a- , varfHelpDef :: Maybe String- } deriving (Functor)---- | An environment variable's value parser. Use @(<=<)@ and @(>=>)@ to combine these-type Reader e a = String -> Either e a--lookupVar :: Error.AsUnset e => String -> Map String String -> Either e String-lookupVar name =- maybe (Left Error.unset) Right . Map.lookup name---- | Parse a particular variable from the environment------ @--- >>> var 'str' \"EDITOR\" ('def' \"vim\" <> 'helpDef' show)--- @-var :: Error.AsUnset e => Reader e a -> String -> Mod Var a -> Parser e a-var r n (Mod f) = Parser . liftAlt $ VarF- { varfName = n- , varfReader = \name -> r <=< lookupVar name- , varfHelp = varHelp- , varfDef = varDef- , varfHelpDef = varHelpDef <*> varDef- }- where- Var { varHelp, varDef, varHelpDef } = f defaultVar---- | A flag that takes the active value if the environment variable--- is set and non-empty and the default value otherwise------ /Note:/ this parser never fails.-flag- :: 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 env ->- pure $ case (nonempty :: Reader Error.Error String) =<< lookupVar name env of- Left _ -> f- Right _ -> t- , varfHelp = flagHelp- , varfDef = Just f- , varfHelpDef = Nothing- }- where- Flag { flagHelp } = g defaultFlag---- | A simple boolean 'flag'------ /Note:/ this parser never fails.-switch :: String -> Mod Flag Bool -> Parser e Bool-switch = flag False True---- | The trivial reader-str :: IsString s => Reader e s-str = Right . fromString---- | The reader that accepts only non-empty strings-nonempty :: (Error.AsEmpty e, IsString s) => Reader e s-nonempty = fmap fromString . go where go [] = Left Error.empty; go xs = Right xs---- | The reader that uses the 'Read' instance of the type-auto :: (Error.AsUnread e, Read a) => Reader e a-auto s = case reads s of [(v, "")] -> Right v; _ -> Left (Error.unread (show s))----- | This represents a modification of the properties of a particular 'Parser'.--- Combine them using the 'Monoid' instance.-newtype Mod t a = Mod (t a -> t a)--instance Monoid (Mod t a) where- mempty = Mod id- mappend (Mod f) (Mod g) = Mod (g . f)---- | Environment variable metadata-data Var a = Var- { varHelp :: Maybe String- , varHelpDef :: Maybe (a -> String)- , varDef :: Maybe a- }--defaultVar :: Var a-defaultVar = Var- { varHelp = Nothing- , varDef = Nothing- , varHelpDef = Nothing- }---- | The default value of the variable------ /Note:/ specifying it means the parser won't ever fail.-def :: a -> Mod Var a-def d = Mod (\v -> v { varDef = Just d })---- | Flag metadata-data Flag a = Flag- { flagHelp :: Maybe String- }--defaultFlag :: Flag a-defaultFlag = Flag { flagHelp = Nothing }---- | Show the default value of the variable in help.-helpDef :: (a -> String) -> Mod Var a-helpDef d = Mod (\v -> v { varHelpDef = Just d })---- | Use the 'Show' instance to show the default value of the variable in help.-showDef :: Show a => Mod Var a-showDef =- helpDef show----- | A class of things that can have a help message attached to them-class HasHelp t where- setHelp :: String -> t a -> t a--instance HasHelp Var where- setHelp h v = v { varHelp = Just h }--instance HasHelp Flag where- setHelp h v = v { flagHelp = Just h }---- | Attach help text to the variable-help :: HasHelp t => String -> Mod t a-help = Mod . setHelp
− src/Env/Val.hs
@@ -1,40 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveFunctor #-}-module Env.Val- ( Val(..)- , fromEither- , toEither- ) where--import Control.Applicative-#if __GLASGOW_HASKELL__ < 710-import Data.Monoid (Monoid(..))-#endif-import Data.Monoid ((<>))----- | A type isomorphic to 'Either' with the accumulating 'Applicative' instance.-data Val e a- = Err e- | Ok a- deriving (Functor, Show, Eq)--instance Monoid e => Applicative (Val e) where- pure = Ok-- Err e <*> Err e' = Err (e <> e')- Err e <*> _ = Err e- _ <*> Err e' = Err e'- Ok f <*> Ok a = Ok (f a)--instance Monoid e => Alternative (Val e) where- empty = Err mempty-- Err _ <|> Ok x = Ok x- x <|> _ = x--fromEither :: Either e a -> Val e a-fromEither = either Err Ok--toEither :: Val e a -> Either e a-toEither x = case x of Err e -> Left e; Ok a -> Right a
+ test/Env/Internal/ParserSpec.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}+module Env.Internal.ParserSpec (spec) where++import Control.Monad ((<=<))+import Data.Text (Text)+import Test.Hspec++import Env.Internal.Error (Error)+import Env.Internal.Parser+++spec :: Spec+spec = do+ describe "str" $ do+ it "works with String" $+ str "foo" `shouldBe` ok ("foo" :: String)++ it "works with Text" $+ str "foo" `shouldBe` ok ("foo" :: Text)++ describe "splitOn" $ do+ it "splits the string on a separator character" $+ splitOn ',' "1,2,3" `shouldBe` ok ["1", "2", "3"]++ it "splits the empty string into the empty list" $+ splitOn ',' "" `shouldBe` ok []++ it "creates an extra element for leading and trailing separators" $+ splitOn ',' ",2," `shouldBe` ok ["", "2", ""]++ it "is nicely composable " $+ (mapM auto <=< splitOn ',') "1,2,3" `shouldBe` ok ([1, 2, 3] :: [Int])++ok :: a -> Either Error a+ok = Right
test/EnvSpec.hs view
@@ -9,6 +9,9 @@ import Data.Monoid (mempty) #endif import Prelude hiding (pi)+#if __GLASGOW_HASKELL__ >= 708+import System.Environment (lookupEnv, setEnv)+#endif import Test.Hspec import Text.Read (readMaybe) @@ -82,6 +85,31 @@ p (prefixed "pre" (prefixed "pro" (var str "morphism" mempty))) `shouldBe` Just "zygohistomorphic"++#if __GLASGOW_HASKELL__ >= 708+ it "unsets parsed variables" $ do+ setEnv "FOO" "4"+ setEnv "BAR" "7"+ parse (header "hi") (liftA2 (+) (var auto "FOO" (help "a")) (var auto "BAR" (help "b"))) `shouldReturn` (11 :: Int)+ lookupEnv "FOO" `shouldReturn` Nothing+ lookupEnv "BAR" `shouldReturn` Nothing++ context "some variables are marked as kept" $+ it "does not unset them" $ do+ setEnv "FOO" "4"+ setEnv "BAR" "7"+ parse (header "hi") (liftA2 (+) (var auto "FOO" (help "a" <> keep)) (var auto "BAR" (help "b"))) `shouldReturn` (11 :: Int)+ lookupEnv "FOO" `shouldReturn` Just "4"+ lookupEnv "BAR" `shouldReturn` Nothing++ context "parsing fails" $+ it "does not unset any variables" $ do+ setEnv "FOO" "4"+ setEnv "BAR" "bar"+ parse (header "hi") (liftA2 (+) (var auto "FOO" (help "a" <> keep)) (var auto "BAR" (help "b"))) `shouldThrow` anyException+ lookupEnv "FOO" `shouldReturn` Just "4"+ lookupEnv "BAR" `shouldReturn` Just "bar"+#endif greaterThan5 :: AsUnread e => Reader e Int