dotenv 0.5.1.1 → 0.5.2.0
raw patch · 13 files changed
+526/−44 lines, 13 filesdep +directorydep +yamlPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: directory, yaml
API changes (from Hackage documentation)
+ Configuration.Dotenv.Scheme: checkConfig :: [(String, String)] -> [Env] -> IO ()
+ Configuration.Dotenv.Scheme: loadSafeFile :: MonadIO m => FilePath -> Config -> m [(String, String)]
+ Configuration.Dotenv.Scheme: runSchemaChecker :: FilePath -> Config -> IO ()
+ Configuration.Dotenv.Scheme.Helpers: joinEnvs :: [Env] -> [(String, String)] -> [(Env, (String, String))]
+ Configuration.Dotenv.Scheme.Helpers: matchValueAndType :: [(Env, (String, String))] -> [(String, EnvType)]
+ Configuration.Dotenv.Scheme.Helpers: missingDotenvs :: [Env] -> [(Env, (String, String))] -> [Env]
+ Configuration.Dotenv.Scheme.Helpers: missingSchemeEnvs :: [(String, String)] -> [(Env, (String, String))] -> [(String, String)]
+ Configuration.Dotenv.Scheme.Helpers: sepWithCommas :: [String] -> String
+ Configuration.Dotenv.Scheme.Helpers: showMissingDotenvs :: [Env] -> String
+ Configuration.Dotenv.Scheme.Helpers: showMissingSchemeEnvs :: [(String, String)] -> String
+ Configuration.Dotenv.Scheme.Parser: parseEnvAs :: String -> EnvType -> Either (ParseError Char Void) ()
+ Configuration.Dotenv.Scheme.Parser: parseEnvsWithScheme :: [(String, EnvType)] -> Either [ParseError Char Void] ()
+ Configuration.Dotenv.Scheme.Parser: showType :: EnvType -> String
+ Configuration.Dotenv.Scheme.Types: Env :: String -> EnvType -> Bool -> Env
+ Configuration.Dotenv.Scheme.Types: EnvBool :: EnvType
+ Configuration.Dotenv.Scheme.Types: EnvInteger :: EnvType
+ Configuration.Dotenv.Scheme.Types: EnvText :: EnvType
+ Configuration.Dotenv.Scheme.Types: [envName] :: Env -> String
+ Configuration.Dotenv.Scheme.Types: [envType] :: Env -> EnvType
+ Configuration.Dotenv.Scheme.Types: [required] :: Env -> Bool
+ Configuration.Dotenv.Scheme.Types: data Env
+ Configuration.Dotenv.Scheme.Types: data EnvType
+ Configuration.Dotenv.Scheme.Types: instance Data.Aeson.Types.FromJSON.FromJSON Configuration.Dotenv.Scheme.Types.Env
+ Configuration.Dotenv.Scheme.Types: instance Data.Aeson.Types.FromJSON.FromJSON Configuration.Dotenv.Scheme.Types.EnvType
+ Configuration.Dotenv.Scheme.Types: instance GHC.Classes.Eq Configuration.Dotenv.Scheme.Types.Env
+ Configuration.Dotenv.Scheme.Types: instance GHC.Classes.Eq Configuration.Dotenv.Scheme.Types.EnvType
+ Configuration.Dotenv.Scheme.Types: instance GHC.Show.Show Configuration.Dotenv.Scheme.Types.Env
+ Configuration.Dotenv.Scheme.Types: instance GHC.Show.Show Configuration.Dotenv.Scheme.Types.EnvType
- Configuration.Dotenv: loadFile :: MonadIO m => Config -> m ()
+ Configuration.Dotenv: loadFile :: MonadIO m => Config -> m [(String, String)]
Files
- CHANGELOG.md +8/−0
- README.md +80/−5
- app/Main.hs +33/−19
- dotenv.cabal +17/−1
- spec/Configuration/Dotenv/Scheme/ParseSpec.hs +63/−0
- spec/Configuration/Dotenv/SchemeSpec.hs +60/−0
- spec/Configuration/DotenvSpec.hs +10/−8
- spec/fixtures/.scheme.yml +14/−0
- src/Configuration/Dotenv.hs +10/−11
- src/Configuration/Dotenv/Scheme.hs +70/−0
- src/Configuration/Dotenv/Scheme/Helpers.hs +76/−0
- src/Configuration/Dotenv/Scheme/Parser.hs +47/−0
- src/Configuration/Dotenv/Scheme/Types.hs +38/−0
CHANGELOG.md view
@@ -1,4 +1,12 @@ ## MASTER+## Dotenv 0.5.2.0++* Add `loadSafeFile` to typecheck the envs.+* Add `(--schema|-s) FILE` flag to the `dotenv` CLI tool to enable safe mode.+* Add `(--no-schema)` flag to the `dotenv` CLI tool to disable safe mode.+* Turn safe mode on automatically when the `.schema.yml` file is present.+* Make `required` optional in the `.schema.yml`.+ ## Dotenv 0.5.1.1 * Allow `.env` empty files
README.md view
@@ -36,7 +36,7 @@ ```haskell import qualified Configuration.Dotenv as Dotenv-Dotenv.loadFile False "/path/to/your/file"+Dotenv.loadFile defaultConfig ``` After calling `Dotenv.load`, you are able to read the values set in your@@ -67,7 +67,7 @@ ``` DATABASE=postgres://${USER}@localhost/database ```- + Running it on the CLI: ```@@ -83,7 +83,7 @@ ``` DATABASE=postgres://$(whoami)@localhost/database ```- + Running it on the CLI: ```@@ -91,13 +91,65 @@ postgres://myusername@localhost/database ``` +### Type checking envs+Env variables are simple strings. However, they can represent other types like+integers, booleans, IP addresses, emails, URIs, and so on. We provide an interface+that performs type checking after loading the envs and before running your application.+If the type-check succeeded the application is executed, otherwise you will get an+error with the types that mismatch.++In order to use this functionality you can use the `loadSafeFile` which takes the same+configuration value as the `loadFile` function. Also, you need to have a `.schema.yml`+in your current directory. This file must have the following structure:++```yaml+- name: DOTENV+ type: bool+ required: true+- name: OTHERENV+ type: bool+- name: PORT+ type: integer+ required: true+- name: TOKEN+ type: text+ required: false+```++It is a list of type and envs. So, in this example, `DOTENV` must have a value+of `true` or `false` otherwise it won't be parsed as a boolean value. And envs+like `PORT` must be any integer. Currently, we are supporting the following types:++- `bool` - Accepts values `false` or `true`+- `integer` - Accepts values of possitive integers+- `text` - Any text++**require** specifies if the env var is obligatory or not. In case you set it to true+but do not provide it, you wil get an exception. When **required** is omited, the default+value is `false`.++**NOTE:** All the variables which are **required** in the `schema.yml` must be defined+in the dotenvs.+ ## Configuration -The first argument to `loadFile` specifies whether you want to-override system settings. `False` means Dotenv will respect+The first argument to `loadFile` specifies the configuration. You cans use+`defaultConfig` which parses the `.env` file in your current directory and+doesn't override your envs. You can also define your own configuration with+the `Config` type.++`False` in `configOverride` means Dotenv will respect already-defined variables, and `True` means Dotenv will overwrite already-defined variables. +In the `configPath` you can write a list of all the dotenv files where are+envs defined (e.g `[".env", ".tokens", ".public_keys"]`).++In the `configExamplePath` you can write a list of all the dotenv example files+where you can specify which envs **must be defined** until running a program+(e.g `[".env.example", ".tokens.example", ".public_keys.example"]`). If you don't+need this functionality you can set `configExamplePath` to an empty list.+ ## Advanced Dotenv File Syntax You can add comments to your Dotenv file, on separate lines or after@@ -170,6 +222,29 @@ current environment settings. By invoking the program `env` in place of `myprogram` above you can see what the environment will look like after evaluating multiple Dotenv files.++The `--schema FILE` will get the envs configuration from the `FILE`. For instance:++```shell+$ cat .env+PORT=123a+$ cat .schema.yml+- name: PORT+ required: true+ type: integer+```++running `dotenv` will throw:++```shell+$ dotenv -s .schema.yml "echo $PORT"+dotenv: 1:4:+unexpected 'a'+expecting digit or end of input+```++**NOTE:** The flag can be omited when the `.schema.yml` is in the current working+directory. To disable type checking add the flag `--no-schema`. ## Author
app/Main.hs view
@@ -11,10 +11,11 @@ import Options.Applicative import Paths_dotenv (version) -import Control.Monad (void)+import Control.Monad (void, unless) import Configuration.Dotenv (loadFile) import Configuration.Dotenv.Types (Config(..), defaultConfig)+import Configuration.Dotenv.Scheme ( runSchemaChecker ) import System.Process (system) import System.Exit (exitWith)@@ -25,29 +26,36 @@ , override :: Bool , program :: String , args :: [String]+ , schemaFile :: FilePath+ , disableSchema :: Bool } deriving (Show) main :: IO () main = do Options{..} <- execParser opts- void $ loadFile Config- { configExamplePath = dotenvExampleFiles- , configOverride = override- , configPath =- if null dotenvFiles- then configPath defaultConfig- else dotenvFiles- }- system (program ++ concatMap (" " ++) args) >>= exitWith- where- opts = info (helper <*> versionOption <*> config)- ( fullDesc- <> progDesc "Runs PROGRAM after loading options from FILE"- <> header "dotenv - loads options from dotenv files" )- versionOption =- infoOption- (showVersion version)- (long "version" <> short 'v' <> help "Show version of the program")+ let configDotenv =+ Config+ { configExamplePath = dotenvExampleFiles+ , configOverride = override+ , configPath =+ if null dotenvFiles+ then configPath defaultConfig+ else dotenvFiles+ }+ in do+ void $ loadFile configDotenv+ unless disableSchema+ (runSchemaChecker schemaFile configDotenv)+ system (program ++ concatMap (" " ++) args) >>= exitWith+ where+ opts = info (helper <*> versionOption <*> config)+ ( fullDesc+ <> progDesc "Runs PROGRAM after loading options from FILE"+ <> header "dotenv - loads options from dotenv files" )+ versionOption =+ infoOption+ (showVersion version)+ (long "version" <> short 'v' <> help "Show version of the program") config :: Parser Options config = Options@@ -70,3 +78,9 @@ <*> many (argument str (metavar "ARG")) + <*> strOption ( long "schema"+ <> short 's'+ <> help "Set the file path for the schema.yml file"+ <> value ".schema.yml" )++ <*> switch ( long "no-schema" <> help "Omit type checking" )
dotenv.cabal view
@@ -1,5 +1,5 @@ name: dotenv-version: 0.5.1.1+version: 0.5.2.0 synopsis: Loads environment variables from dotenv files homepage: https://github.com/stackbuilders/dotenv-hs description:@@ -48,6 +48,7 @@ data-files: .dotenv .dotenv.example+ .scheme.yml flag dev description: Turn on development settings.@@ -64,6 +65,7 @@ , process >= 1.4.3.0 , text , transformers >=0.4 && < 0.6+ , yaml other-modules: Paths_dotenv hs-source-dirs: app@@ -79,14 +81,20 @@ , Configuration.Dotenv.ParsedVariable , Configuration.Dotenv.Text , Configuration.Dotenv.Types+ , Configuration.Dotenv.Scheme+ , Configuration.Dotenv.Scheme.Helpers+ , Configuration.Dotenv.Scheme.Parser+ , Configuration.Dotenv.Scheme.Types build-depends: base >=4.7 && <5.0 , base-compat >= 0.4+ , directory , megaparsec >= 6.0 && < 7.0 , process >= 1.4.3.0 , text , transformers >=0.4 && < 0.6 , exceptions >= 0.8 && < 0.9+ , yaml if !impl(ghc >= 7.10) build-depends: void == 0.7.*@@ -111,10 +119,17 @@ , Configuration.Dotenv.Types , Configuration.Dotenv.Parse , Configuration.Dotenv.ParsedVariable+ , Configuration.Dotenv.SchemeSpec+ , Configuration.Dotenv.Scheme.ParseSpec+ , Configuration.Dotenv.Scheme+ , Configuration.Dotenv.Scheme.Helpers+ , Configuration.Dotenv.Scheme.Parser+ , Configuration.Dotenv.Scheme.Types build-depends: base >=4.7 && <5.0 , base-compat >= 0.4 , dotenv+ , directory , megaparsec >= 6.0 && < 7.0 , hspec , process >= 1.4.3.0@@ -122,6 +137,7 @@ , transformers >=0.4 && < 0.6 , exceptions >= 0.8 && < 0.9 , hspec-megaparsec >= 1.0 && < 2.0+ , yaml if !impl(ghc >= 7.10) build-depends: void == 0.7.*
+ spec/Configuration/Dotenv/Scheme/ParseSpec.hs view
@@ -0,0 +1,63 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Configuration.Dotenv.Scheme.ParseSpec (spec) where++import Data.Either (isLeft, isRight)+import Data.Yaml (decodeFileEither)+import Test.Hspec++import Configuration.Dotenv.Scheme.Types+import Configuration.Dotenv.Scheme.Parser++fromRight :: (Show a, Show b) => Either a b -> b+fromRight (Right v) = v+fromRight (Left v) = error ("Left " ++ show v ++ " constructor instead" )++spec :: Spec+spec = do+ specInstance+ specParse++specInstance :: Spec+specInstance = describe "parse a config env file" $+ it "parses the env config values from a file" $+ let expected :: [Env]+ expected =+ [ Env "DOTENV" EnvBool True+ , Env "OTHERENV" EnvBool False+ , Env "PORT" EnvInteger True+ , Env "TOKEN" EnvInteger False+ , Env "URL" EnvText True+ ]+ in do+ actual <- decodeFileEither "spec/fixtures/.scheme.yml"+ fromRight actual `shouldBe` expected++specParse :: Spec+specParse =+ describe "parseEnvAs" $ do+ context "given an integer" $ do+ context "when the integer can be parsed" $+ it "should return Right ()" $+ let varContent = "123"+ integer = EnvInteger+ in varContent `parseEnvAs` integer `shouldSatisfy` isRight++ context "when the integer can't be parsed" $+ it "should return Left (ParseError Char Void)" $+ let varContent = "123x"+ integer = EnvInteger+ in varContent `parseEnvAs` integer `shouldSatisfy` isLeft++ context "given a bool" $ do+ context "when the bool can be parsed" $+ it "should return Right ()" $+ let varContent = "true"+ boolean = EnvBool+ in varContent `parseEnvAs` boolean `shouldSatisfy` isRight++ context "when the bool can't be parsed" $+ it "should return False" $+ let varContent = "truex"+ boolean = EnvBool+ in varContent `parseEnvAs` boolean `shouldSatisfy` isLeft
+ spec/Configuration/Dotenv/SchemeSpec.hs view
@@ -0,0 +1,60 @@+module Configuration.Dotenv.SchemeSpec (spec) where++import Configuration.Dotenv.Scheme+import Configuration.Dotenv.Scheme.Types++import Test.Hspec++spec :: Spec+spec =+ describe "checkConfig" $+ context "when the envs have the correct type" $ do+ context "when the envs in the dotenvs are defined in the scheme" $ do+ context "when the required envs are defined" $+ it "should succeed the type check" $+ let schemeEnvs =+ [ Env "FOO" EnvBool True+ , Env "BAR" EnvInteger False+ ]+ dotenvs = [("FOO","true"), ("BAR","123")]+ in checkConfig dotenvs schemeEnvs `shouldReturn` ()++ context "when the not required envs are missing" $+ it "should succeed the type check" $+ let schemeEnvs =+ [ Env "FOO" EnvBool True+ , Env "BAR" EnvInteger False+ ]+ dotenvs = [("FOO","true")]+ in checkConfig dotenvs schemeEnvs `shouldReturn` ()++ context "when the required envs are missing" $+ it "should fail before the type check" $+ let schemeEnvs =+ [ Env "FOO" EnvBool True+ , Env "BAR" EnvInteger False+ ]+ dotenvs = [("BAR","123")]+ msg = "The following envs: FOO must be in the dotenvs"+ in checkConfig dotenvs schemeEnvs `shouldThrow` errorCall msg++ context "when there are missing dotenvs in the scheme" $+ it "should fail before type checking" $+ let schemeEnvs =+ [ Env "FOO" EnvBool True+ , Env "BAR" EnvInteger False+ ]+ dotenvs = [("FOO","true"), ("BAR","123"), ("BAZ","text")]+ msg = "The following envs: BAZ must be in your scheme.yml"+ in checkConfig dotenvs schemeEnvs `shouldThrow` errorCall msg++ context "when there are missing scheme envs in the dotenv vars" $+ it "should fail before type checking" $+ let schemeEnvs =+ [ Env "FOO" EnvBool True+ , Env "BAZ" EnvText True+ , Env "BAR" EnvInteger False+ ]+ dotenvs = [("FOO","true"), ("BAR","123")]+ msg = "The following envs: BAZ must be in the dotenvs"+ in checkConfig dotenvs schemeEnvs `shouldThrow` errorCall msg
spec/Configuration/DotenvSpec.hs view
@@ -8,8 +8,8 @@ import Test.Hspec import System.Process (readCreateProcess, shell)-import System.Environment (lookupEnv)-import Control.Monad (liftM)+import System.Environment (lookupEnv, getEnv)+import Control.Monad (liftM, void) import Data.Maybe (fromMaybe) #if !MIN_VERSION_base(4,8,0) import Data.Functor ((<$>))@@ -58,21 +58,21 @@ it "loads the configuration options to the environment from a file" $ do lookupEnv "DOTENV" `shouldReturn` Nothing - loadFile $ Config ["spec/fixtures/.dotenv"] [] False+ void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] False lookupEnv "DOTENV" `shouldReturn` Just "true" it "respects predefined settings when overload is false" $ do setEnv "DOTENV" "preset" - loadFile $ Config ["spec/fixtures/.dotenv"] [] False+ void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] False lookupEnv "DOTENV" `shouldReturn` Just "preset" it "overrides predefined settings when overload is true" $ do setEnv "DOTENV" "preset" - loadFile $ Config ["spec/fixtures/.dotenv"] [] True+ void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] True lookupEnv "DOTENV" `shouldReturn` Just "true" @@ -81,12 +81,14 @@ context "when the needed env vars are missing" $ it "should fail with an error call" $- loadFile config `shouldThrow` anyErrorCall+ void $ loadFile config `shouldThrow` anyErrorCall context "when the needed env vars are not missing" $ it "should succeed when loading all of the needed env vars" $ do setEnv "ANOTHER_ENV" "hello"- loadFile config `shouldReturn` ()+ me <- getEnv "USER"+ home <- getEnv "HOME"+ loadFile config `shouldReturn` [("DOTENV","true"),("UNICODE_TEST","Manab\237"),("ENVIRONMENT", home),("PREVIOUS","true"),("ME", me),("ANOTHER_ENV","")] lookupEnv "DOTENV" `shouldReturn` Just "true" lookupEnv "UNICODE_TEST" `shouldReturn` Just "Manabí" lookupEnv "ANOTHER_ENV" `shouldReturn` Just "hello"@@ -121,7 +123,7 @@ describe "onMissingFile" $ after_ (unsetEnv "DOTENV") $ do context "when target file is present" $ it "loading works as usual" $ do- onMissingFile (loadFile $ Config ["spec/fixtures/.dotenv"] [] True) (return ())+ void $ onMissingFile (loadFile $ Config ["spec/fixtures/.dotenv"] [] True) (return []) lookupEnv "DOTENV" `shouldReturn` Just "true" context "when target file is missing" $
+ spec/fixtures/.scheme.yml view
@@ -0,0 +1,14 @@+- name: DOTENV+ type: bool+ required: true+- name: OTHERENV+ type: bool+ required: false+- name: PORT+ type: integer+ required: true+- name: TOKEN+ type: integer+- name: URL+ type: text+ required: true
src/Configuration/Dotenv.hs view
@@ -52,7 +52,7 @@ loadFile :: MonadIO m => Config- -> m ()+ -> m [(String, String)] loadFile Config{..} = do environment <- liftIO getEnvironment readedVars <- concat `liftM` mapM parseFile configPath@@ -68,7 +68,7 @@ then readedVars `unionEnvs` neededVars else error $ "Missing env vars! Please, check (this/these) var(s) (is/are) set:" ++ concatMap ((++) " " . fst) neededVars else readedVars- mapM_ (applySetting configOverride) vars+ mapM (applySetting configOverride) vars -- | Parses the given dotenv file and returns values /without/ adding them to -- the environment.@@ -83,17 +83,16 @@ Left e -> error $ parseErrorPretty e Right options -> liftIO $ interpolateParsedVariables options -applySetting :: MonadIO m => Bool -> (String, String) -> m ()+applySetting :: MonadIO m => Bool -> (String, String) -> m (String, String) applySetting override (key, value) =- if override then- liftIO $ setEnv key value-- else do- res <- liftIO $ lookupEnv key+ if override+ then liftIO (setEnv key value) >> return (key, value)+ else do+ res <- liftIO $ lookupEnv key - case res of- Nothing -> liftIO $ setEnv key value- Just _ -> return ()+ case res of+ Nothing -> liftIO $ setEnv key value >> return (key, value)+ Just _ -> return (key, value) -- | The helper allows to avoid exceptions in the case of missing files and -- perform some action instead.
+ src/Configuration/Dotenv/Scheme.hs view
@@ -0,0 +1,70 @@+module Configuration.Dotenv.Scheme+ ( checkConfig+ , loadSafeFile+ , runSchemaChecker+ )+ where++import Control.Monad+import Control.Monad.IO.Class (MonadIO(..))++import Data.Yaml (decodeFileEither, prettyPrintParseException)+import Text.Megaparsec+import System.Directory (doesFileExist)++import Configuration.Dotenv (loadFile)+import Configuration.Dotenv.Types (Config(..))+import Configuration.Dotenv.Scheme.Helpers+import Configuration.Dotenv.Scheme.Parser+import Configuration.Dotenv.Scheme.Types++-- | @loadSafeFile@ parses the /.scheme.yml/ file and will perform the type checking+-- of the environment variables in the /.env/ file.+loadSafeFile+ :: MonadIO m+ => FilePath+ -> Config+ -> m [(String, String)]+loadSafeFile schemaFile config = do+ envs <- loadFile config+ liftIO (readScheme schemaFile >>= checkConfig envs)+ return envs++readScheme :: FilePath -> IO [Env]+readScheme schemeFile = do+ eitherEnvConf <- decodeFileEither schemeFile+ case eitherEnvConf of+ Right envConfs -> return envConfs+ Left errorYaml -> error (prettyPrintParseException errorYaml)++checkConfig+ :: [(String, String)]+ -> [Env]+ -> IO ()+checkConfig envvars envsWithType =+ let prettyParsedErrors = unlines . fmap parseErrorTextPretty+ envsTypeAndValue = joinEnvs envsWithType envvars+ valuesAndTypes = matchValueAndType envsTypeAndValue+ dotenvsMissing = filter required (missingDotenvs envsWithType envsTypeAndValue)+ schemeEnvsMissing = missingSchemeEnvs envvars envsTypeAndValue+ in do+ unless (null dotenvsMissing)+ (error $ "The following envs: "+ ++ showMissingDotenvs dotenvsMissing+ ++ " must be in the dotenvs")+ unless (null schemeEnvsMissing)+ (error $ "The following envs: "+ ++ showMissingSchemeEnvs schemeEnvsMissing+ ++ " must be in your scheme.yml")+ case parseEnvsWithScheme valuesAndTypes of+ Left errors -> error (prettyParsedErrors errors)+ _ -> return ()++runSchemaChecker+ :: FilePath+ -> Config+ -> IO ()+runSchemaChecker schemeFile config = do+ exists <- doesFileExist schemeFile+ when exists+ (void $ loadSafeFile schemeFile config)
+ src/Configuration/Dotenv/Scheme/Helpers.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE RecordWildCards #-}++module Configuration.Dotenv.Scheme.Helpers+ ( joinEnvs+ , matchValueAndType+ , missingDotenvs+ , missingSchemeEnvs+ , sepWithCommas+ , showMissingDotenvs+ , showMissingSchemeEnvs+ )+ where++#if !MIN_VERSION_base(4,8,0)+import Data.Functor ((<$>))+import Control.Applicative ((<*>))+#endif+import Data.List++import Configuration.Dotenv.Scheme.Types++matchValueAndType+ :: [(Env, (String, String))]+ -> [(String, EnvType)]+matchValueAndType =+ let getValueAndType (Env{..}, (_, value)) = (value, envType)+ in map getValueAndType++joinEnvs+ :: [Env]+ -> [(String, String)]+ -> [(Env, (String, String))]+joinEnvs =+ let sameName (Env{..}, (name,_)) = envName == name+ in joinBy sameName++joinBy :: ((a,b) -> Bool) -> [a] -> [b] -> [(a,b)]+joinBy p xs ys =+ let cartesianProduct = (,) <$> xs <*> ys+ in filter p cartesianProduct++missingDotenvs+ :: [Env]+ -> [(Env, (String, String))]+ -> [Env]+missingDotenvs =+ let sameName envOne envTwo = envName envOne == envName envTwo+ in missingLeft sameName++missingLeft :: (a -> a -> Bool) -> [a] -> [(a,b)] -> [a]+missingLeft p xs xys =+ let getAllLeft = map fst xys+ in deleteFirstsBy p xs getAllLeft++missingSchemeEnvs+ :: [(String, String)]+ -> [(Env, (String, String))]+ -> [(String, String)]+missingSchemeEnvs =+ let sameName (nameOne,_) (nameTwo,_) = nameOne == nameTwo+ in missingRight sameName++missingRight :: (b -> b -> Bool) -> [b] -> [(a,b)] -> [b]+missingRight p ys xys =+ let getAllRight = map snd xys+ in deleteFirstsBy p ys getAllRight++sepWithCommas :: [String] -> String+sepWithCommas = intercalate ", "++showMissingDotenvs :: [Env] -> String+showMissingDotenvs = sepWithCommas . map envName++showMissingSchemeEnvs :: [(String, String)] -> String+showMissingSchemeEnvs = sepWithCommas . map fst
+ src/Configuration/Dotenv/Scheme/Parser.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++module Configuration.Dotenv.Scheme.Parser where++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((*>), pure)+import Data.Functor ((<$>))+#endif++import Data.Either+import Data.Void+import Text.Megaparsec+import Text.Megaparsec.Char++import Configuration.Dotenv.Scheme.Types++parseEnvsWithScheme+ :: [(String, EnvType)]+ -> Either [ParseError Char Void] ()+parseEnvsWithScheme valuesAndTypes =+ let parsedEithers = parseEnvs <$> valuesAndTypes+ parseEnvs (val, type') = val `parseEnvAs` type'+ in if all isRight parsedEithers+ then Right ()+ else Left (lefts parsedEithers)++parseEnvAs+ :: String -- ^ Value of the env variable+ -> EnvType -- ^ Type that the env variable should have+ -> Either (ParseError Char Void) ()+parseEnvAs envVal envTypeNeeded =+ parse dispatch "" envVal+ where+ errorMsg = "Couldn't parse " ++ envVal ++ " as " ++ showType envTypeNeeded+ evalParse parser = parser *> eof *> pure () <|> fail errorMsg+ dispatch :: Parsec Void String ()+ dispatch =+ case envTypeNeeded of+ EnvInteger -> evalParse (many digitChar)+ EnvBool -> evalParse (string "true" <|> string "false")+ EnvText -> pure ()++showType :: EnvType -> String+showType EnvInteger = "integer"+showType EnvBool = "boolean"+showType _ = "text"
+ src/Configuration/Dotenv/Scheme/Types.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++module Configuration.Dotenv.Scheme.Types where++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((<*>), pure)+import Data.Functor ((<$>))+#endif++import Data.Yaml++data EnvType =+ EnvInteger+ | EnvBool+ | EnvText deriving (Show, Eq)++instance FromJSON EnvType where+ parseJSON (String "integer") = pure EnvInteger+ parseJSON (String "bool") = pure EnvBool+ parseJSON (String "text") = pure EnvText+ parseJSON (String x) = fail ("Don't know how to parse that kind of type: " ++ show x)+ parseJSON x = fail ("Not an object: " ++ show x)++data Env =+ Env+ { envName :: String+ , envType :: EnvType+ , required :: Bool+ } deriving (Show, Eq)++instance FromJSON Env where+ parseJSON (Object m) =+ Env+ <$> m .: "name"+ <*> m .: "type"+ <*> m .:? "required" .!= False+ parseJSON x = fail ("Not an object: " ++ show x)