{"id":13687659,"url":"https://github.com/dmjio/envy","last_synced_at":"2025-08-26T03:08:19.265Z","repository":{"id":34221904,"uuid":"38085752","full_name":"dmjio/envy","owner":"dmjio","description":":angry: Environmentally friendly environment variables","archived":false,"fork":false,"pushed_at":"2024-12-10T03:29:06.000Z","size":122,"stargazers_count":151,"open_issues_count":6,"forks_count":26,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-28T11:11:09.842Z","etag":null,"topics":["environment-variables","envy","generics","hackage","haskell","system","typeclass"],"latest_commit_sha":null,"homepage":"http://hackage.haskell.org/package/envy","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmjio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-06-26T02:17:19.000Z","updated_at":"2024-12-20T03:36:36.000Z","dependencies_parsed_at":"2025-01-15T05:13:27.539Z","dependency_job_id":"9287ef86-f219-4867-be49-baab6da25e9f","html_url":"https://github.com/dmjio/envy","commit_stats":{"total_commits":119,"total_committers":21,"mean_commits":5.666666666666667,"dds":0.2941176470588235,"last_synced_commit":"80284f8a166aa5101f23984725edcc69d88c1e7a"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmjio%2Fenvy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmjio%2Fenvy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmjio%2Fenvy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmjio%2Fenvy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmjio","download_url":"https://codeload.github.com/dmjio/envy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247174453,"owners_count":20896078,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["environment-variables","envy","generics","hackage","haskell","system","typeclass"],"created_at":"2024-08-02T15:00:58.198Z","updated_at":"2025-04-04T12:09:14.614Z","avatar_url":"https://github.com/dmjio.png","language":"Haskell","funding_links":[],"categories":["Haskell"],"sub_categories":[],"readme":"envy\n===================\n[![Hackage](https://img.shields.io/hackage/v/envy.svg)](https://hackage.haskell.org/package/envy)\n[![Hackage Dependencies](https://img.shields.io/hackage-deps/v/envy.svg)](https://packdeps.haskellers.com/feed?needle=envy)\n![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)\n![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)\n[![Build Status](https://travis-ci.org/dmjio/envy.svg?branch=master)](https://travis-ci.org/dmjio/envy)\n\nLet's face it, dealing with environment variables in Haskell isn't that satisfying.\n\n```haskell\nimport System.Environment\nimport Data.Text (pack)\nimport Text.Read (readMaybe)\n\ndata ConnectInfo = ConnectInfo {\n  pgPort :: Int\n  pgURL  :: Text\n} deriving (Show, Eq)\n\ngetPGPort :: IO ConnectInfo\ngetPGPort = do\n  portResult \u003c- lookupEnv \"PG_PORT\"\n  urlResult  \u003c- lookupEnv \"PG_URL\"\n  case (portResult, urlResult) of\n    (Just port, Just url) -\u003e\n      case readMaybe port :: Maybe Int of\n\tNothing -\u003e error \"PG_PORT isn't a number\"\n\tJust portNum -\u003e return $ ConnectInfo portNum (pack url)\n    (Nothing, _) -\u003e error \"Couldn't find PG_PORT\"\n    (_, Nothing) -\u003e error \"Couldn't find PG_URL\"\n    -- Pretty gross right...\n```\n\nAnother attempt to remedy the lookup madness is with a `MaybeT IO a`. See below.\n```haskell\n{-# LANGUAGE GeneralizedNewtypeDeriving #-}\n\nimport Control.Applicative\nimport Control.Monad.Trans.Maybe\nimport Control.Monad.IO.Class\nimport System.Environment\n\nnewtype Env a = Env { unEnv :: MaybeT IO a }\n    deriving (Functor, Applicative, Monad, MonadIO, Alternative, MonadPlus)\n\ngetEnv :: Env a -\u003e IO (Maybe a)\ngetEnv env = runMaybeT (unEnv env)\n\nenv :: String -\u003e Env a\nenv key = Env (MaybeT (lookupEnv key))\n\nconnectInfo :: Env ConnectInfo\nconnectInfo = ConnectInfo\n   \u003c$\u003e env \"PG_HOST\"\n   \u003c*\u003e env \"PG_PORT\"\n   \u003c*\u003e env \"PG_USER\"\n   \u003c*\u003e env \"PG_PASS\"\n   \u003c*\u003e env \"PG_DB\"\n```\nThis abstraction falls short in two areas:\n  - Lookups don't return any information when a variable doesn't exist (just a `Nothing`)\n  - Lookups don't attempt to parse the returned type into something meaningful (everything is returned as a `String` because `lookupEnv :: String -\u003e IO (Maybe String)`)\n\nWhat if we could apply aeson's `FromJSON` / `ToJSON` pattern to give us variable lookups that provide both key-lookup and parse failure information?\nArmed with the `GeneralizedNewTypeDeriving` extension we can derive instances of `Var` that will parse to and from an environment variable. The `Var` typeclass is simply:\n```haskell\nclass Var a where\n  toVar   :: a -\u003e String\n  fromVar :: String -\u003e Maybe a\n```\nWith 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.\n\n```haskell\n{-# LANGUAGE ScopedTypeVariables        #-}\n{-# LANGUAGE RecordWildCards            #-}\n{-# LANGUAGE GeneralizedNewtypeDeriving #-}\n{-# LANGUAGE OverloadedStrings          #-}\n{-# LANGUAGE DeriveDataTypeable         #-}\n------------------------------------------------------------------------------\nmodule Main ( main ) where\n------------------------------------------------------------------------------\nimport           Control.Applicative\nimport           Control.Exception\nimport           Control.Monad\nimport           Data.Either\nimport           Data.Word\nimport           System.Environment\nimport           System.Envy\n------------------------------------------------------------------------------\ndata ConnectInfo = ConnectInfo {\n      pgHost :: String\n    , pgPort :: Word16\n    , pgUser :: String\n    , pgPass :: String\n    , pgDB   :: String\n  } deriving (Show)\n\n------------------------------------------------------------------------------\n-- | FromEnv instances support popular aeson combinators *and* IO\n-- for dealing with connection pool initialization. `env` is equivalent to (.:) in `aeson`\n-- and `envMaybe` is equivalent to (.:?), except here the lookups are impure.\ninstance FromEnv ConnectInfo where\n  fromEnv _ =\n    ConnectInfo \u003c$\u003e envMaybe \"PG_HOST\" .!= \"localhost\"\n\t\t\u003c*\u003e env \"PG_PORT\"\n\t\t\u003c*\u003e env \"PG_USER\"\n\t\t\u003c*\u003e env \"PG_PASS\"\n\t\t\u003c*\u003e env \"PG_DB\"\n\n------------------------------------------------------------------------------\n-- | To Environment Instances\n-- (.=) is a smart constructor for producing types of `EnvVar` (which ensures\n-- that Strings are set properly in an environment so they can be parsed properly\ninstance ToEnv ConnectInfo where\n  toEnv ConnectInfo {..} = makeEnv\n       [ \"PG_HOST\" .= pgHost\n       , \"PG_PORT\" .= pgPort\n       , \"PG_USER\" .= pgUser\n       , \"PG_PASS\" .= pgPass\n       , \"PG_DB\"   .= pgDB\n       ]\n\n------------------------------------------------------------------------------\n-- | Example\nmain :: IO ()\nmain = do\n   setEnvironment (toEnv :: EnvList ConnectInfo)\n   print =\u003c\u003c do decodeEnv :: IO (Either String ConnectInfo)\n   -- unsetEnvironment (toEnv :: EnvList ConnectInfo)  -- remove when done\n```\n\nOur parser might also make use a set of an optional default values provided by the user,\nfor dealing with errors when reading from the environment\n\n```haskell\ninstance FromEnv ConnectInfo where\n  fromEnv Nothing =\n    ConnectInfo \u003c$\u003e envMaybe \"PG_HOST\" .!= \"localhost\"\n\t\t\u003c*\u003e env \"PG_PORT\"\n\t\t\u003c*\u003e env \"PG_USER\"\n\t\t\u003c*\u003e env \"PG_PASS\"\n\t\t\u003c*\u003e env \"PG_DB\"\n\n  fromEnv (Just def) =\n    ConnectInfo \u003c$\u003e envMaybe \"PG_HOST\" .!= (pgHost def)\n\t\t\u003c*\u003e envMaybe \"PG_PORT\" .!= (pgPort def)\n\t\t\u003c*\u003e env \"PG_USER\" .!= (pgUser def)\n\t\t\u003c*\u003e env \"PG_PASS\" .!= (pgPass def)\n\t\t\u003c*\u003e env \"PG_DB\" .!= (pgDB def)\n```\n\n\n*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\npurely to the end user.\n\nGenerics\n===================\n\nAs of version `1.0`, all `FromEnv` instance boilerplate can be completely removed thanks to `GHC.Generics`! Below is an example.\n\n```haskell\n{-# LANGUAGE DeriveGeneric #-}\nmodule Main where\n\nimport System.Envy\nimport GHC.Generics\nimport System.Environment.Blank\n\n-- This record corresponds to our environment, where the field names become the variable names, and the values the environment variable value\ndata PGConfig = PGConfig {\n    pgHost :: String -- \"PG_HOST\"\n  , pgPort :: Int    -- \"PG_PORT\"\n  } deriving (Generic, Show)\n\ninstance FromEnv PGConfig\n-- Generically creates instance for retrieving environment variables (PG_HOST, PG_PORT)\n\nmain :: IO ()\nmain = do\n  _ \u003c- setEnv \"PG_HOST\" \"valueFromEnv\" True\n  _ \u003c- setEnv \"PG_PORT\"  \"66354651\" True\n  print =\u003c\u003c do decodeEnv :: IO (Either String PGConfig)\n -- \u003e PGConfig { pgHost = \"valueFromEnv\", pgPort = 66354651 }\n```\n\nIf the variables are not found in the environment, the parser will currently fail with an error about the first missing field.\n\nThe user can decide to provide a default value, whose fields will be used by the generic instance, if retrieving them from the environment fails.\n\n```haskell\ndefConfig :: PGConfig\ndefConfig = PGConfig \"localhost\" 5432\n\nmain :: IO ()\nmain = do\n  _ \u003c- setEnv \"PG_HOST\" \"customURL\" True\n  print =\u003c\u003c decodeWithDefaults defConfig\n -- \u003e PGConfig { pgHost = \"customURL\", pgPort = 5432 }\n```\n\nSuppose 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.\n\n```haskell\n{-# LANGUAGE DeriveGeneric #-}\nmodule Main where\n\nimport System.Envy\nimport GHC.Generics\n\ndata PGConfig = PGConfig {\n    connectHost :: String -- \"PG_HOST\"\n  , connectPort :: Int    -- \"PG_PORT\"\n  } deriving (Generic, Show)\n\ninstance DefConfig PGConfig where\n  defConfig = PGConfig \"localhost\" 5432\n\n-- All fields will be converted to uppercase\ninstance FromEnv PGConfig where\n  fromEnv = gFromEnvCustom Option {\n                    dropPrefixCount = 7\n                  , customPrefix = \"CUSTOM\"\n\t\t  }\n\nmain :: IO ()\nmain =\n  _ \u003c- setEnv \"CUSTOM_HOST\" \"customUrl\" True\n  print =\u003c\u003c do decodeEnv :: IO (Either String PGConfig)\n -- PGConfig { pgHost = \"customUrl\", pgPort = 5432 }\n```\n\nIt's also possible to avoid typeclasses altogether using `runEnv` with `gFromEnvCustom`.\n\n```haskell\n{-# LANGUAGE DeriveGeneric #-}\nmodule Main where\n\nimport System.Envy\nimport GHC.Generics\n\ndata PGConfig = PGConfig {\n    pgHost :: String -- \"PG_HOST\"\n  , pgPort :: Int    -- \"PG_PORT\"\n  } deriving (Generic, Show)\n\n-- All fields will be converted to uppercase\ngetPGEnv :: IO (Either String PGConfig)\ngetPGEnv = runEnv $ gFromEnvCustom defOption\n                                   (Just (PGConfig \"localhost\" 5432))\n\nmain :: IO ()\nmain = print =\u003c\u003c getPGEnv\n -- PGConfig { pgHost = \"localhost\", pgPort = 5432 }\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmjio%2Fenvy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmjio%2Fenvy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmjio%2Fenvy/lists"}