packages feed

envy-2.0.0.0: README.md

envy
===================
[![Hackage](https://img.shields.io/hackage/v/envy.svg)](https://hackage.haskell.org/package/envy)
[![Hackage Dependencies](https://img.shields.io/hackage-deps/v/envy.svg)](https://packdeps.haskellers.com/feed?needle=envy)
![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)
![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)
[![Build Status](https://travis-ci.org/dmjio/envy.svg?branch=master)](https://travis-ci.org/dmjio/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 ConnectInfo = ConnectInfo {
  pgPort :: Int
  pgURL  :: Text
} deriving (Show, Eq)

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 $ ConnectInfo 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` because `lookupEnv :: String -> IO (Maybe 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 variable. The `Var` typeclass is simply:
```haskell
class Var a where
  toVar   :: a -> String
  fromVar :: String -> Maybe a
```
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        #-}
{-# 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)

------------------------------------------------------------------------------
-- | FromEnv instances support popular aeson combinators *and* IO
-- 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 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 ConnectInfo where
  toEnv ConnectInfo {..} = makeEnv
       [ "PG_HOST" .= pgHost
       , "PG_PORT" .= pgPort
       , "PG_USER" .= pgUser
       , "PG_PASS" .= pgPass
       , "PG_DB"   .= pgDB
       ]

------------------------------------------------------------------------------
-- | Example
main :: IO ()
main = do
   setEnvironment (toEnv :: EnvList ConnectInfo)
   print =<< do decodeEnv :: IO (Either String ConnectInfo)
   -- unsetEnvironment (toEnv :: EnvList ConnectInfo)  -- remove when done
```

Our parser might also make use a set of an optional default values provided by the user, 
for dealing with errors when reading from the environment

```haskell
instance FromEnv ConnectInfo where
  fromEnv Nothing = 
    ConnectInfo <$> envMaybe "PG_HOST" .!= "localhost"
		<*> env "PG_PORT"
		<*> env "PG_USER"
		<*> env "PG_PASS"
		<*> env "PG_DB"
        
  fromEnv (Just def) = 
    ConnectInfo <$> envMaybe "PG_HOST" .!= (pgHost def)
		<*> envMaybe "PG_PORT" .!= (pgPort def)
		<*> env "PG_USER" .!= (pgUser def)
		<*> env "PG_PASS" .!= (pgPass def) 
		<*> env "PG_DB" .!= (pgDB def)
```


*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
import System.Environment.Blank

-- 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"
  , pgPort :: Int    -- "PG_PORT"
  } deriving (Generic, Show)

instance FromEnv PGConfig
-- Generically creates instance for retrieving environment variables (PG_HOST, PG_PORT)

main :: IO ()
main = do
  _ <- setEnv "PG_HOST" "valueFromEnv" True
  _ <- setEnv "PG_PORT"  "66354651" True
  print =<< do decodeEnv :: IO (Either String PGConfig)
 -- > PGConfig { pgHost = "valueFromEnv", pgPort = 66354651 }
```

If the variables are not found in the environment, the parser will currently fail with an error about the first missing field.

The user can decide to provide a default value, whose fields will be used by the generic instance, if retrieving them from the environment fails.

```haskell
defConfig :: PGConfig
defConfig = PGConfig "localhost" 5432

main :: IO ()
main =
  _ <- setEnv "PG_HOST" "customURL" True
  print =<< decodeWithDefaults defConfig :: 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 = gFromEnvCustom Option {
                    dropPrefixCount = 7
                  , customPrefix = "CUSTOM"
		  }

main :: IO ()
main =
  _ <- setEnv "CUSTOM_HOST" "customUrl" True
  print =<< do decodeEnv :: IO (Either String PGConfig)
 -- PGConfig { pgHost = "customUrl", pgPort = 5432 }
```

It's also possible to avoid typeclasses altogether using `runEnv` with `gFromEnvCustom`.

```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)

-- All fields will be converted to uppercase
getPGEnv :: IO (Either String PGConfig)
getPGEnv = runEnv $ gFromEnvCustom defOption
                                   (Just (PGConfig "localhost" 5432))

main :: IO ()
main = print =<< getPGEnv
 -- PGConfig { pgHost = "localhost", pgPort = 5432 }
```