packages feed

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