dmjio / envy Goto Github PK
View Code? Open in Web Editor NEW:angry: Environmentally friendly environment variables
Home Page: http://hackage.haskell.org/package/envy
License: Other
:angry: Environmentally friendly environment variables
Home Page: http://hackage.haskell.org/package/envy
License: Other
It could be nice to provide FromEnv/ToEnv instances for tuples, including the empty tuple. This would allow users the ability to assemble their environment out of discrete parts.
The example at the top of http://hackage.haskell.org/package/envy-2.1.0.0/docs/System-Envy.html and the last one in the README using DefConfig
don't work. I get "Variable not found" errors, when the docs claim it defaults.
It doesn't seem like DefConfig
is even used at all, however. Maybe remove the typeclass altogether and just support decodeEnvWithDefault
?
import GHC.Generics
instance ToEnv PGConfig
instance FromEnv PGConfig
etc.
It'd be nice to be able to quickly produce a list of generated environment variable names being used, when one makes an instance of FromEnv
for a type that has a Generics
instance. That way, the list can be included in documentation.
The following code provides a method extract
to do this:
class Extract a where
extract :: a -> [String]
default extract :: (GExtract (Rep a), Generic a) => a -> [String]
extract x = gExtract (from x) defOption
class GExtract f where
gExtract :: f a -> Option -> [String]
instance (GExtract a, GExtract b) => GExtract (a :*: b) where
gExtract (a :*: b) opts = gExtract a opts <> gExtract b opts
instance GExtract a => GExtract (C1 i a) where
gExtract (M1 x) = gExtract x
instance GExtract a => GExtract (D1 i a) where
gExtract (M1 x) = gExtract x
instance (Selector s, Var a) => GExtract (S1 s (K1 i a)) where
gExtract m@(M1 (K1 def)) opts = [toEnvName opts $ selName m]
toEnvName :: Option -> String -> String
toEnvName Option{..} xs =
let name = snake (drop dropPrefixCount xs)
in if customPrefix == mempty
then name
else map toUpper customPrefix ++ "_" ++ name
snake :: String -> String
snake = map toUpper . snakeCase
where
applyFirst :: (Char -> Char) -> String -> String
applyFirst _ [] = []
applyFirst f [x] = [f x]
applyFirst f (x:xs) = f x: xs
snakeCase :: String -> String
snakeCase = u . applyFirst toLower
where u [] = []
u (x:xs) | isUpper x = '_' : toLower x : snakeCase xs
| otherwise = x : u xs
Is this a feature that could be added to Envy?
As the author of #19 said, I prefer raising an error when Parser
fails some way.
So types without default value is not bad.
Why does the FromEnv
class force the instance to handle the default value?
If a default value is really necessary, just use fromLeft
:
import Data.Either
print =<< (fromLeft defaultConfig <$> decodeEnv :: IO (Either String PGConfig))
Thinking of the default is not the responsibility of the instances of FromEnv
.
If I have the following data type:
data MyEnv = MyEnv {
envvar1 :: Text
envvar2 :: Text
} deriving (Generic, FromEnv)
I am forced to write a DefConfig
. However, I would prefer that if MyEnv can't be loaded from the environment, I just get an error message Failed to load ENVVAR1...
.
I've tried just setting an error in the DefConfig
instance, but it always fails. I could make every field Maybe Text
, but then I would have to handle the errors manually.
Am I missing something?
Hi! I'd like to know what is the proper way to group multiple independent env vars prefixed with a component name into a Haskell component structure that represent the component as a whole? Basically I want these vars
DB_NAME=
DB_PORT=
to be mapped into a single data DbSettings ...
, so that DbSettings
itself can be used in larger env var parsers like:
data AppSettings = AppSettings
{ something :: Text
, dbSettings :: DbSettings
}
Normally, I'd first define an instance of Var a
, but fromVar
is only provided with a single variable, not a set of items. Does it mean that I should omit declaring instances for Var a
and proceed with manual parsing inside instances of FromEnv a
?
We often use indirect env variables such as
DB_PROVIDER=LOCAL_DB_PROVIDER
LOCAL_DB_PROVIDER=postgres://.....
I am trying to read the env variable using something like
env <$> env "DB_PROVIDER"
but I can't quite get it to work. Any thoughts on this ?
For more parse failure information, we should use the latest and greatest readEither
function from Text.Read.readEither
.
TL;DR: Can Envy add a Var
instance for ()
?
I want to keep all of my application configuration in a single data type. Most of the config comes from the environment, but some of it comes from other sources. (The stuff that comes from other sources is more like state than config, but it makes sense to me to keep it all together.) For example, I might read a Port
from the environment but also have a Manager
in the config.
Obviously the manager can't come from the environment. I wanted to express this in the type system, so I tried something like this:
data Config manager = Config { port :: Port, manager :: manager }
deriving Generic
type ConfigWithoutManager = Config () -- or Void
type ConfigWithManager = Config Manager
instance DefConfig ConfigWithoutManager where
defConfig = Config { port = 8080, manager = () }
instance FromEnv ConfigWithoutManager
With that setup I figured I could decodeEnv
to get a ConfigWithoutManager
, make the manager, then return a ConfigWithManager
. Unfortunately the whole thing screeches to a halt with:
No instance for
(Var ())
So that approach doesn't work. I can work around it by using Bool
instead of ()
, but that's not great. It would be nice if Envy could provide a Var
instance for ()
. Obviously unit doesn't convey any information, so it doesn't make a ton of sense to use it in an environment variable. But it can be handy in situations like this.
instance ToEnv PGConfig where
toEnv = makeEnv
[ "PG_HOST" .= ("localhost" :: String)
, "PG_PORT" .= (5432 :: Word16)
, "PG_USER" .= ("user" :: String)
, "PG_PASS" .= ("pass" :: String)
, "PG_DB" .= ("db" :: String)
]
Why wouldn't you want toEnv to use data from the specific instance (:: a -> EnvList a
), so that I could e.g. make PGConfig "https://myhost" 5432 "itsme" "secretpass" "dbname"
and then output those values rather than hardcoded values for a type at large?
There is an instance for Var
instance for Maybe
but not for the newtype Last
. Both Last
and First
are types that show up in option records. If the following could be added:
deriving instance (Var a, Typeable a) => Var (Last a)
deriving instance (Var a, Typeable a) => Var (First a)
Hi,
I tried to compile the example you provided verbatim.
{-# 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"
, pgPort :: 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 }
I get the following error:
/Users/traviswhitton/Projects/haskell/jstats/app/Main.hs:22:3: error:
• Couldn't match type ‘()’ with ‘Either String PGConfig’
Expected type: IO (Either String PGConfig)
Actual type: IO ()
• In the expression:
print =<< decodeEnv :: IO (Either String PGConfig)
In an equation for ‘main’:
main = print =<< decodeEnv :: IO (Either String PGConfig)
|
22 | print =<< decodeEnv :: IO (Either String PGConfig)
| ^^^^^^^^^^^^^^^^^^^
/Users/traviswhitton/Projects/haskell/jstats/app/Main.hs:22:3: error:
• Couldn't match type ‘Either String PGConfig’ with ‘()’
Expected type: IO ()
Actual type: IO (Either String PGConfig)
• In the expression:
print =<< decodeEnv :: IO (Either String PGConfig)
In an equation for ‘main’:
main = print =<< decodeEnv :: IO (Either String PGConfig)
|
22 | print =<< decodeEnv :: IO (Either String PGConfig)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Am I doing something wrong? Thanks.
Envy uses System.Environment.Blank
, but this was not introduced until base-4.11. This means that when building with older versions of GHC and Cabal, the constraint solver will succeed and select 2.1 as compatible, but fail to build. It would be better if instead the constraint solver would instead select an older version (assuming it was compatible with the user's needs) or simply failed to solve at all, so that the user would know the next appropriate step.
I'll put up a pull request shortly, but this also needs a revision in Hackage to work correctly.
The dependency on bytestring
(<0.11) prevents the latest release to build with GHC 9.2.4. It appears that a simple adjustment of the dependency would be enough.
(Maybe this is something that could be made quickly by just revising on Hackage, I see @endgame has made such changes in the past.)
Wouldn't it make sense to use a MonadIO m
instead of IO for the output.
Also, for the retrieval, one might want to separate the "capacity" to read/write named variable from its use. one capacity whose implementation could be provided by ... a MonadIO constrained monad :)
envMaybe indicates that it will return Nothing
if the environmental variable is not set, but it also returns Nothing
if the environmental variable is set but is not parsable into the domain data type - i.e., fromVar
returns Nothing
.
You can construct a function with the the same type but different semantics, in that it will return Nothing
if the variable is not set, but if the variable is set, it will attempt to parse it and error out if it cannot.
I am using such a function in my own project, and it looks like this (though I am sure there are more idiomatic ways to write this):
parseEnv :: Var a => String -> Parser (Maybe a)
parseEnv envName = do
let parseError = throwError $ "Unable to parse " <> envName
mStr <- optional (env envName) `catchError` (\_ -> pure Nothing)
maybe (pure Nothing) (maybe parseError (pure . Just) . fromVar) mStr
Should something like this be added to the library, or is it easy enough to build with the current functions and I did more work than necessary?
The haskell script below showcased the the following behavior:
Nothing
for the Int
field of the Environment
data type.Int
, then the parser fails.Int
, then the parser succeeds with a Just
.#!/usr/bin/env stack
-- stack --resolver lts-14.7 script
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}
import Control.Applicative
import Control.Monad.Error.Class
import GHC.Generics
import System.Envy
import Text.Read
newtype Custom = Custom { unCustom :: Int } deriving (Show, Eq)
instance Var Custom where
toVar = toVar . unCustom
fromVar s = Custom <$> readMaybe s
data Environment = Environment { envInt :: Maybe Int, envString :: Maybe String, envCustom :: Maybe Custom } deriving (Show, Eq, Generic)
instance FromEnv Environment where
fromEnv _ = Environment <$> parseEnv "ENV_INT" <*> parseEnv "ENV_STRING" <*> parseEnv "ENV_CUSTOM"
parseEnv :: Var a => String -> Parser (Maybe a)
parseEnv envName = do
let parseError = throwError $ "Unable to parse " <> envName
mStr <- optional (env envName) `catchError` (\_ -> pure Nothing)
maybe (pure Nothing) (maybe parseError (pure . Just) . fromVar) mStr
emptyEnv :: Environment
emptyEnv = Environment { envInt = Nothing, envString = Nothing, envCustom = Nothing}
main :: IO ()
main = do
env <- decodeEnv :: IO (Either String Environment)
print env
Parsing a bool variable, not only does "true"
not work as a value, but the error message is not helpful in finding out that I should specify "True"
:
Parse failure: could not parse variable "TEST_BOOL" into type [Char]
The released versions of envy-2.0.0.0
and envy-2.1.0.0
import System.Environment.Blank
, a module that does not appear in base
until base-4.11.0.0
.
I see that master
has been updated by #42 , so in my role as Hackage Trustee I have created metadata revisions for the affected versions on Hackage. This issue is just a notification per Trustee policy; no action is required.
We found a race condition error while trying to write environmental unit/property tests. Does this come up with Envy's Quickcheck tests? This is likely more of a problem underlying System.Environment
, but I'm wondering if there's a way to deal with it within Envy. Here's an example definition:
module Types where
import BasicPrelude
import System.Envy
data Conf = Conf
{ _cConfFile :: Maybe String
, _cPort :: Maybe Word32
, _cTimeout :: Maybe Word32
} deriving ( Eq, Show )
instance FromEnv Conf where
fromEnv =
Conf <$>
envMaybe "CONFFILE" <*>
envMaybe "PORT" <*>
envMaybe "TIMEOUT"
instance ToEnv Conf where
toEnv Conf{..} =
makeEnv [ "CONFFILE" .= _cConfFile
, "PORT" .= _cPort
, "TIMEOUT" .= _cTimeout
]
and an example test...
module Test
( tests
) where
import BasicPrelude
import Data.Text
import System.Environment
import System.Envy
import Test.Tasty
import Test.Tasty.HUnit
testEnv :: TestTree
testEnv =
testGroup "Environmental configuration unit test"
[ testCase "Empty configuration" $ do
unsetEnv "SKYLARK_CONFFILE"
unsetEnv "SKYLARK_PORT"
unsetEnv "SKYLARK_TIMEOUT"
c <- decodeEnv :: IO (Either String Conf)
c @?= Right Conf { _cConfFile = Nothing
, _cPort = Nothing
, _cTimeout = Nothing
}
, testCase "Port and Timeout" $ do
unsetEnv "SKYLARK_CONFFILE"
setEnv "SKYLARK_PORT" "1"
setEnv "SKYLARK_TIMEOUT" "1"
c <- decodeEnv :: IO (Either String Conf)
c @?= Right Conf { _cConfFile = Nothing
, _cPort = Just 1
, _cTimeout = Just 1
}
]
This test can sometimes pass, or it fail, with the decoded environment in one test case being not the one set in that specific test case. For example, the first case unsetting the environmental variables can instead read the values set in the second test case. This also appears with a Quickcheck not too different from the one committed with Envy (https://gist.github.com/mookerji/d6c9fe48bc35e16bd5690).
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.