packages feed

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 view
@@ -1,4 +1,4 @@-envy +envy =================== ![Hackage](https://img.shields.io/hackage/v/envy.svg) ![Hackage Dependencies](https://img.shields.io/hackage-deps/v/envy.svg)@@ -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