envy 0.3.0.1 → 0.3.0.2
raw patch · 2 files changed
+145/−1 lines, 2 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- README.md +139/−0
- envy.cabal +6/−1
+ README.md view
@@ -0,0 +1,139 @@+envy +===================+++++++Let's face it, dealing with environment variables in Haskell isn't that satisfying.++```haskell+import System.Environment+import Data.Text (pack)+import Text.Read (readMaybe)++data PGConfig = PGConfig {+ pgPort :: Int+ pgURL :: Text+} deriving (Show, Eq)++getPGPort :: IO PGConfig+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" + -- Pretty gross right...+```++Another attempt to remedy the lookup madness is with a `MaybeT IO a`. See below.+```haskell+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++import Control.Applicative+import Control.Monad.Trans.Maybe+import Control.Monad.IO.Class+import System.Environment++newtype Env a = Env { unEnv :: MaybeT IO a }+ deriving (Functor, Applicative, Monad, MonadIO, Alternative, MonadPlus)++getEnv :: Env a -> IO (Maybe a)+getEnv env = runMaybeT (unEnv env)++env :: String -> Env a+env key = Env (MaybeT (lookupEnv key))++connectInfo :: Env ConnectInfo+connectInfo = ConnectInfo+ <$> env "PG_HOST"+ <*> env "PG_PORT"+ <*> env "PG_USER"+ <*> env "PG_PASS"+ <*> env "PG_DB"+```+This abstraction falls short in two areas:+ - Lookups don't return any information when a variable doesn't exist (just a `Nothing`)+ - Lookups don't attempt to parse the returned type into something meaningful (everything is returned as a `String`)++What if we could apply aeson's `FromJSON` / `ToJSON` pattern to give us variable lookups that provide both key-lookup and parse failure information?+Armed with the `GeneralizedNewTypeDeriving` extension we can derive instances of `Var` that will parse to and from an environment. The `Var` typeclass is simply:+```haskell+class (Read a, Show a) => Var a where+ 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.++```haskell+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveDataTypeable #-}+------------------------------------------------------------------------------+module Main ( main ) where+------------------------------------------------------------------------------+import Control.Applicative+import Control.Exception+import Control.Monad+import Data.Either+import Data.Word+import System.Environment+import System.Envy+------------------------------------------------------------------------------+data ConnectInfo = ConnectInfo {+ pgHost :: String+ , pgPort :: Word16+ , pgUser :: String+ , pgPass :: String+ , pgDB :: String+ } 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`+-- 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")++------------------------------------------------------------------------------+-- | 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 + [ "PG_HOST" .= pgHost+ , "PG_PORT" .= pgPort+ , "PG_USER" .= pgUser+ , "PG_PASS" .= pgPass+ , "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+```++*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.
envy.cabal view
@@ -1,5 +1,5 @@ name: envy-version: 0.3.0.1+version: 0.3.0.2 synopsis: An environmentally friendly way to deal with environment variables license: BSD3 license-file: LICENSE@@ -11,6 +11,10 @@ cabal-version: >=1.10 description: For package use information see the <https://github.com/dmjio/envy/blob/master/README.md README.md>++extra-source-files:+ README.md+ library exposed-modules: System.Envy ghc-options: -Wall@@ -40,6 +44,7 @@ , time == 1.5.* , transformers == 0.4.* default-language: Haskell2010+ source-repository head type: git