system environment-variables generics Haskell
Need help with envy?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.
dmjio

Description

:angry: Environmentally friendly environment variables

126 Stars 15 Forks Other 133 Commits 3 Opened issues

Services available

Need anything else?

envy

Hackage Hackage Dependencies Haskell Programming Language BSD3 License Build Status

Let's face it, dealing with environment variables in Haskell isn't that satisfying.

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 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 "PGHOST" <*> env "PGPORT" <> env "PG_USER" <> env "PGPASS" <*> env "PGDB" ``

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.
{-# 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

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.
{-# 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 _ 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.

defConfig :: PGConfig
defConfig = PGConfig "localhost" 5432

main :: IO () main = _ 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.

{-# 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 = _

It's also possible to avoid typeclasses altogether using

runEnv
with
gFromEnvCustom
.
{-# LANGUAGE DeriveGeneric #-}
module Main where

import System.Envy import GHC.Generics

data PGConfig = PGConfig { pgHost :: String -- "PG_HOST" , pgPort :: 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 }

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.