envy 0.3.1.2 → 1.0.0.0
raw patch · 4 files changed
+249/−126 lines, 4 filesdep ~basedep ~envyPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base, envy
API changes (from Hackage documentation)
+ System.Envy: Option :: Int -> String -> Option
+ System.Envy: [customPrefix] :: Option -> String
+ System.Envy: [dropPrefixCount] :: Option -> Int
+ System.Envy: class DefConfig a
+ System.Envy: data Option
+ System.Envy: defConfig :: DefConfig a => a
+ System.Envy: fromEnvCustom :: (FromEnv a, DefConfig a, Generic a, GFromEnv (Rep a)) => Option -> Parser a
+ System.Envy: instance (GHC.Generics.Selector s, Data.Typeable.Internal.Typeable a, System.Envy.Var a) => System.Envy.GFromEnv (GHC.Generics.S1 s (GHC.Generics.K1 i a))
+ System.Envy: instance (System.Envy.GFromEnv a, System.Envy.GFromEnv b) => System.Envy.GFromEnv (a GHC.Generics.:*: b)
+ System.Envy: instance GHC.Show.Show System.Envy.Option
+ System.Envy: instance System.Envy.GFromEnv a => System.Envy.GFromEnv (GHC.Generics.C1 i a)
+ System.Envy: instance System.Envy.GFromEnv a => System.Envy.GFromEnv (GHC.Generics.D1 i a)
- System.Envy: class FromEnv a
+ System.Envy: class FromEnv a where fromEnvCustom opts = to <$> gFromEnv (from (defConfig :: a)) opts fromEnv = fromEnvCustom defOption
- System.Envy: class Show a => ToEnv a
+ System.Envy: class ToEnv a
- System.Envy: unsetEnvironment :: EnvList a -> IO (Either String ())
+ System.Envy: unsetEnvironment :: ToEnv a => EnvList a -> IO (Either String ())
Files
- README.md +83/−27
- envy.cabal +4/−3
- examples/Test.hs +22/−0
- src/System/Envy.hs +140/−96
README.md view
@@ -1,4 +1,4 @@-envy +envy ===================  @@ -13,22 +13,22 @@ import Data.Text (pack) import Text.Read (readMaybe) -data PGConfig = PGConfig {+data ConnectInfo = ConnectInfo { pgPort :: Int pgURL :: Text } deriving (Show, Eq) -getPGPort :: IO PGConfig+getPGPort :: IO ConnectInfo getPGPort = do portResult <- lookupEnv "PG_PORT" urlResult <- lookupEnv "PG_URL" case (portResult, urlResult) of (Just port, Just url) -> case readMaybe port :: Maybe Int of- Nothing -> error "PG_PORT isn't a number"- Just portNum -> return $ PGConfig portNum (pack url)- (Nothing, _) -> error "Couldn't find PG_PORT" - (_, Nothing) -> error "Couldn't find PG_URL" + Nothing -> error "PG_PORT isn't a number"+ Just portNum -> return $ ConnectInfo portNum (pack url)+ (Nothing, _) -> error "Couldn't find PG_PORT"+ (_, Nothing) -> error "Couldn't find PG_URL" -- Pretty gross right... ``` @@ -69,7 +69,7 @@ toVar :: a -> String fromVar :: String -> Maybe a ```-With instances for most primitive types supported (`Word8` - `Word64`, `Int`, `Integer`, `String`, `Text`, etc.) the `Var` class is easily deriveable. The `FromEnv` typeclass provides a parser type that is an instance of `MonadError String` and `MonadIO`. This allows for connection pool initialization inside of our environment parser and custom error handling. The `ToEnv` class allows us to create an environment configuration given any `a`. See below for an example.+With instances for most concrete and primitive types supported (`Word8` - `Word64`, `Int`, `Integer`, `String`, `Text`, etc.) the `Var` class is easily deriveable. The `FromEnv` typeclass provides a parser type that is an instance of `MonadError String` and `MonadIO`. This allows for connection pool initialization inside of our environment parser and custom error handling. The `ToEnv` class allows us to create an environment configuration given any `a`. See below for an example. ```haskell {-# LANGUAGE ScopedTypeVariables #-}@@ -97,43 +97,99 @@ } deriving (Show) --------------------------------------------------------------------------------- | Posgtres config-data PGConfig = PGConfig {- pgConnectInfo :: ConnectInfo -- ^ Connnection Info- } deriving Show-------------------------------------------------------------------------------- -- | FromEnv instances support popular aeson combinators *and* IO--- for dealing with connection pools. `env` is equivalent to (.:) in `aeson`+-- for dealing with connection pool initialization. `env` is equivalent to (.:) in `aeson` -- and `envMaybe` is equivalent to (.:?), except here the lookups are impure.-instance FromEnv PGConfig where- fromEnv = PGConfig <$> (ConnectInfo <$> envMaybe "PG_HOST" .!= "localhost"- <*> env "PG_PORT"- <*> env "PG_USER" - <*> env "PG_PASS" - <*> env "PG_DB")+instance FromEnv ConnectInfo where+ fromEnv =+ ConnectInfo <$> envMaybe "PG_HOST" .!= "localhost"+ <*> env "PG_PORT"+ <*> env "PG_USER"+ <*> env "PG_PASS"+ <*> env "PG_DB") ------------------------------------------------------------------------------ -- | To Environment Instances -- (.=) is a smart constructor for producing types of `EnvVar` (which ensures -- that Strings are set properly in an environment so they can be parsed properly-instance ToEnv PGConfig where- toEnv PGConfig {..} = makeEnv +instance ToEnv ConnectInfo where+ toEnv ConnectInfo {..} = makeEnv [ "PG_HOST" .= pgHost , "PG_PORT" .= pgPort , "PG_USER" .= pgUser , "PG_PASS" .= pgPass- , "PG_DB" .= pgDB + , "PG_DB" .= pgDB ] ------------------------------------------------------------------------------ -- | Example main :: IO () main = do- setEnvironment (toEnv :: EnvList PGConfig)- print =<< do decodeEnv :: IO (Either String PGConfig)- -- unsetEnvironment (toEnv :: EnvList PGConfig) -- remove when done+ setEnvironment (toEnv :: EnvList ConnectInfo)+ print =<< do decodeEnv :: IO (Either String ConnectInfo)+ -- unsetEnvironment (toEnv :: EnvList ConnectInfo) -- remove when done ``` *Note*: As of base 4.7 `setEnv` and `getEnv` throw an `IOException` if a `=` is present in an environment. `envy` catches these synchronous exceptions and delivers them purely to the end user.++Generics+===================++As of version `1.0`, all `FromEnv` instance boilerplate can be completely removed thanks to `GHC.Generics`! Below is an example.++```haskell+{-# LANGUAGE DeriveGeneric #-}+module Main where++import System.Envy+import GHC.Generics++-- This record corresponds to our environment, where the field names become the variable names, and the values the environment variable value+data PGConfig = PGConfig {+ pgHost :: String -- "PG_HOST"+ , pgHost :: Int -- "PG_PORT"+ } deriving (Generic, Show)++-- Default configuration will be used for fields that could not be retrieved from the environment+instance DefConfig PGConfig where+ defConfig = PGConfig "localhost" 5432++instance FromEnv PGConfig +-- Generically creates instance for retrieving environment variables (PG_HOST, PG_PORT)++main :: IO ()+main =+ print =<< decodeEnv :: IO (Either String PGConfig)+ -- > PGConfig { pgHost = "customURL", pgPort = 5432 }+```++Suppose you'd like to customize the field name (i.e. add your own prefix, or drop the existing record prefix). This too is possible. See below.++```haskell+{-# LANGUAGE DeriveGeneric #-}+module Main where++import System.Envy+import GHC.Generics++data PGConfig = PGConfig {+ connectHost :: String -- "PG_HOST"+ , connectPort :: Int -- "PG_PORT"+ } deriving (Generic, Show)++instance DefConfig PGConfig where+ defConfig = PGConfig "localhost" 5432++-- All fields will be converted to uppercase+instance FromEnv PGConfig where+ fromEnv = fromEnvCustom Option {+ dropPrefixCount = 7+ , customPrefix = "PG"+ }++main :: IO ()+main =+ print =<< decodeEnv :: IO (Either String PGConfig)+ -- PGConfig { pgHost = "customUrl", pgPort = 5432 }+```
envy.cabal view
@@ -1,5 +1,5 @@ name: envy-version: 0.3.1.2+version: 1.0.0.0 synopsis: An environmentally friendly way to deal with environment variables license: BSD3 license-file: LICENSE@@ -10,10 +10,11 @@ build-type: Simple cabal-version: >=1.10 description:- For package use information see the <https://github.com/dmjio/envy/blob/master/README.md README.md>+ For package use information see the <https://github.com/dmjio/envy/blob/master/README.md README.md> extra-source-files: README.md+ examples/Test.hs library exposed-modules: System.Envy@@ -35,7 +36,7 @@ main-is: Main.hs build-depends: base >= 4.7 && < 5 , bytestring == 0.10.*- , envy == 0.3.*+ , envy == 1.0.* , hspec == 2.2.* , mtl == 2.2.* , quickcheck-instances == 0.3.*
+ examples/Test.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE DeriveGeneric #-}+module Main where++import System.Envy+import GHC.Generics++data PGConfig = PGConfig {+ connectHost :: String -- "PG_HOST"+ , connectPort :: Int -- "PG_PORT"+ } deriving (Generic, Show)++instance DefConfig PGConfig where+ defConfig = PGConfig "localhost" 5432++instance FromEnv PGConfig where+ fromEnv = fromEnvCustom (Option 7 "PG")+ -- Generically creates instance for retrieving environment variables (PG_HOST, PG_PORT)++main :: IO ()+main =+ print =<< decodeEnv :: IO (Either String PGConfig)+ -- PGConfig { pgHost = "foobah", pgPort = 5432 }
src/System/Envy.hs view
@@ -1,8 +1,12 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-} ------------------------------------------------------------------------------ -- | -- Module : System.Envy@@ -10,8 +14,33 @@ -- Maintainer : djohnson.m@ngmail.com -- Stability : experimental -- Portability : POSIX--- -------------------------------------------------------------------------------+--+-- > {-# LANGUAGE DeriveGeneric #-}+-- >+-- > module Main ( main ) where+-- >+-- > import System.Envy+-- > import GHC.Generics+-- >+-- > data PGConfig = PGConfig {+-- > pgHost :: String -- "PG_HOST"+-- > , pgPort :: Int -- "PG_PORT"+-- > } deriving (Generic, Show)+-- >+-- > -- Default instance used if environment variable doesn't exist+-- > instance DefConfig PGConfig where+-- > defConfig = PGConfig "localhost" 5432+-- >+-- > instance FromEnv PGConfig+-- > -- Generically produces the following body (no implementation needed if using Generics):+-- > -- fromEnv = PGConfig <$> envMaybe "PG_HOST" .!= "localhost"+-- > -- <*> envMaybe "PG_PORT" .!= 5432+-- >+-- > main :: IO ()+-- > main =+-- > print =<< do decodeEnv :: IO (Either String PGConfig)+-- > -- PGConfig { pgHost = "custom-pg-url", pgPort = 5432 }+-- module System.Envy ( -- * Classes FromEnv (..)@@ -25,18 +54,23 @@ , setEnvironment , setEnvironment' , unsetEnvironment- , makeEnv + , makeEnv , env , envMaybe , (.=) , (.!=)+ -- * Generics+ , DefConfig (..)+ , Option (..) ) where ------------------------------------------------------------------------------ import Control.Applicative import Control.Monad.Except import Control.Exception import Data.Maybe+import Data.Char import Data.Time+import GHC.Generics import Data.Typeable import System.Environment import Text.Read (readMaybe)@@ -48,22 +82,23 @@ import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy.Char8 as BL8 --------------------------------------------------------------------------------- | Parser+-- | Parser Monad for environment variable retrieval newtype Parser a = Parser { runParser :: ExceptT String IO a } deriving ( Functor, Monad, Applicative, MonadError String , MonadIO, Alternative, MonadPlus )+ --------------------------------------------------------------------------------- | Variable type, smart constructor for handling Env. Variables+-- | Variable type, smart constructor for handling environment variables data EnvVar = EnvVar { getEnvVar :: (String, String) } deriving (Show, Eq) --------------------------------------------------------------------------------- | Execute Parser+-- | Executes `Parser` evalParser :: FromEnv a => Parser a -> IO (Either String a) evalParser = runExceptT . runParser --------------------------------------------------------------------------------- | Infix environment variable getter+-- | Environment variable getter getE :: forall a . (Typeable a, Var a) => String@@ -79,14 +114,14 @@ Just x -> return x --------------------------------------------------------------------------------- | Environment variable getter +-- | Environment variable getter env :: forall a. (Typeable a, Var a) => String -> Parser a env = getE --------------------------------------------------------------------------------- | Infix environment variable getter+-- | Environment variable getter returning `Maybe` getEMaybe :: forall a . (Typeable a, Var a) => String@@ -98,7 +133,7 @@ Just x -> fromVar x --------------------------------------------------------------------------------- | Maybe parser+-- | Environment variable getter returning `Maybe` envMaybe :: forall a. (Typeable a, Var a) => String -> Parser (Maybe a)@@ -109,123 +144,132 @@ (.!=) :: forall a. (Typeable a, Var a) => Parser (Maybe a) -> a- -> Parser a + -> Parser a (.!=) p x = fmap (fromMaybe x) p --------------------------------------------------------------------------------- | Infix environment variable setter --- this is a smart constructor for producing types of `EnvVar`+-- | Infix environment variable setter+-- Smart constructor for producing types of `EnvVar` (.=) :: Var a => String -> a- -> EnvVar + -> EnvVar (.=) x y = EnvVar (x, toVar y) --------------------------------------------------------------------------------- | FromEnv Typeclass+-- | `FromEnv` Typeclass w/ Generic default implementation class FromEnv a where fromEnv :: Parser a+ fromEnvCustom :: (DefConfig a, Generic a, GFromEnv (Rep a)) => Option -> Parser a+ fromEnvCustom opts = to <$> gFromEnv (from (defConfig :: a)) opts+ default fromEnv :: (DefConfig a, Generic a, GFromEnv (Rep a)) => Parser a+ fromEnv = fromEnvCustom defOption --------------------------------------------------------------------------------- | ToEnv Typeclass-class Show a => ToEnv a where- toEnv :: a -> EnvList a+-- | `Generic` FromEnv+class GFromEnv f where+ gFromEnv :: f a -> Option -> Parser (f a) --------------------------------------------------------------------------------- | EnvList type w/ phanton-data EnvList a = EnvList [EnvVar] deriving (Show)+-- | Default Config+class DefConfig a where defConfig :: a --------------------------------------------------------------------------------- | smart constructor, Environment creation helper-makeEnv :: ToEnv a => [EnvVar] -> EnvList a-makeEnv = EnvList+-- | For customizing environment variable generation+data Option = Option {+ dropPrefixCount :: Int -- ^ Applied first+ , customPrefix :: String -- ^ Converted toUpper+ } deriving Show --------------------------------------------------------------------------------- | Class for converting to / from an environment variable-class Var a where- toVar :: a -> String- fromVar :: String -> Maybe a--instance Var Text where- toVar = T.unpack- fromVar = Just . T.pack--instance Var TL.Text where- toVar = TL.unpack- fromVar = Just . TL.pack --instance Var BL8.ByteString where- toVar = BL8.unpack- fromVar = Just . BL8.pack--instance Var B8.ByteString where- toVar = B8.unpack- fromVar = Just . B8.pack--instance Var Int where- toVar = show- fromVar = readMaybe --instance Var Int8 where- toVar = show- fromVar = readMaybe --instance Var Int16 where- toVar = show- fromVar = readMaybe +-- | Default `Option` for field modification+defOption :: Option+defOption = Option 0 mempty -instance Var Int32 where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | Products+instance (GFromEnv a, GFromEnv b) => GFromEnv (a :*: b) where+ gFromEnv (a :*: b) opts = liftA2 (:*:) (gFromEnv a opts) (gFromEnv b opts) -instance Var Int64 where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | Don't absorb meta data+instance GFromEnv a => GFromEnv (C1 i a) where gFromEnv (M1 x) opts = M1 <$> gFromEnv x opts -instance Var Integer where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | Don't absorb meta data+instance GFromEnv a => GFromEnv (D1 i a) where gFromEnv (M1 x) opts = M1 <$> gFromEnv x opts -instance Var UTCTime where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | Construct a `Parser` from a `selName` and `DefConfig` record field+instance (Selector s, Typeable a, Var a) => GFromEnv (S1 s (K1 i a)) where+ gFromEnv m@(M1 (K1 def)) opts =+ M1 . K1 <$> envMaybe (toEnvName opts $ selName m) .!= def+ where+ toEnvName :: Option -> String -> String+ toEnvName Option{..} xs =+ let name = snake (drop dropPrefixCount xs)+ in if customPrefix == mempty+ then name+ else map toUpper customPrefix ++ "_" ++ name -instance Var Day where- toVar = show- fromVar = readMaybe + applyFirst :: (Char -> Char) -> String -> String+ applyFirst _ [] = []+ applyFirst f [x] = [f x]+ applyFirst f (x:xs) = f x: xs -instance Var Word8 where- toVar = show- fromVar = readMaybe + snakeCase :: String -> String+ snakeCase = u . applyFirst toLower+ where u [] = []+ u (x:xs) | isUpper x = '_' : toLower x : snakeCase xs+ | otherwise = x : u xs -instance Var Bool where- toVar = show- fromVar = readMaybe + snake :: String -> String+ snake = map toUpper . snakeCase -instance Var Double where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | ToEnv Typeclass+class ToEnv a where toEnv :: a -> EnvList a -instance Var Word16 where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | EnvList type w/ phanton+data EnvList a = EnvList [EnvVar] deriving (Show) -instance Var Word32 where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | smart constructor, Environment creation helper+makeEnv :: ToEnv a => [EnvVar] -> EnvList a+makeEnv = EnvList -instance Var Word64 where- toVar = show- fromVar = readMaybe +------------------------------------------------------------------------------+-- | Class for converting to / from an environment variable+class Var a where+ toVar :: a -> String+ fromVar :: String -> Maybe a -instance Var String where- toVar = id- fromVar = Just +------------------------------------------------------------------------------+instance Var Text where toVar = T.unpack; fromVar = Just . T.pack+instance Var TL.Text where toVar = TL.unpack; fromVar = Just . TL.pack+instance Var BL8.ByteString where toVar = BL8.unpack; fromVar = Just . BL8.pack+instance Var B8.ByteString where toVar = B8.unpack; fromVar = Just . B8.pack+instance Var Int where toVar = show; fromVar = readMaybe+instance Var Int8 where toVar = show; fromVar = readMaybe+instance Var Int16 where toVar = show; fromVar = readMaybe+instance Var Int32 where toVar = show; fromVar = readMaybe+instance Var Int64 where toVar = show; fromVar = readMaybe+instance Var Integer where toVar = show; fromVar = readMaybe+instance Var UTCTime where toVar = show; fromVar = readMaybe+instance Var Day where toVar = show; fromVar = readMaybe+instance Var Word8 where toVar = show; fromVar = readMaybe+instance Var Bool where toVar = show; fromVar = readMaybe+instance Var Double where toVar = show; fromVar = readMaybe+instance Var Word16 where toVar = show; fromVar = readMaybe+instance Var Word32 where toVar = show; fromVar = readMaybe+instance Var Word64 where toVar = show; fromVar = readMaybe+instance Var String where toVar = id; fromVar = Just ------------------------------------------------------------------------------ -- | Environment retrieval with failure info decodeEnv :: FromEnv a => IO (Either String a)-decodeEnv = evalParser fromEnv +decodeEnv = evalParser fromEnv ------------------------------------------------------------------------------ -- | Environment retrieval (with no failure info)@@ -250,8 +294,8 @@ setEnvironment' = setEnvironment . toEnv --------------------------------------------------------------------------------- | Unset Environment from a ToEnv constrained type-unsetEnvironment :: EnvList a -> IO (Either String ())+-- | Unset Environment from a `ToEnv` constrained type+unsetEnvironment :: ToEnv a => EnvList a -> IO (Either String ()) unsetEnvironment (EnvList xs) = do result <- try $ mapM_ (unsetEnv . fst . getEnvVar) xs return $ case result of@@ -259,6 +303,6 @@ Right () -> Right () --------------------------------------------------------------------------------- | Env helper+-- | Display all environment variables, for convenience showEnv :: IO () showEnv = mapM_ print =<< getEnvironment