Skip to content

Commit 4d7aab2

Browse files
authored
Fix unexpected error with preset ENV and .env.example (#156)
* Fix building on Stackage LTS - Bump LTS, remove extra-deps - Relax lower bound on mtl * Reproduce bug * Fix `union`/`unionBy` bug When the ambient environment is combined with the read values, this code used `union` which compares key _and value_. This ends up duplicating the variable in the list that's intersected (correctly on key only there) and the difference in length results in the missing-variable error unnecessarily. We already have a `unionEnvs` here that does the right thing, we just have to use it. * Don't union in neededVars I could be missing something, but this didn't make sense to me: If we're in this branch, then the `ambient+read` variables have the same keys as needed, this means `read+needed` would only add those keys in needed that are already in the `ambient`, which also means they need to be set. Using only `read` here is functionally equivalent. I'd also argue that actually _using_ anything from the `.env.example` file (beyond validating the `.env`), if that were somehow happening despite my analysis above, would be surprising to end users anyway. * Refactor .env.example check, improve error message I found all of the `*By` and `cmpEnv` stuff really hard to follow and unnecessarily complex. I think it led to the recently-fixed bug. I find this version more straight forward. Hopefully it's objectively less complicated too; it at least reduced two conditionals to one, which is one quantitative improvement. This version also made it easier to identify specifically the missing keys for the error message. The code before would report *all* keys from the example file(s) and leave it up to the user to sort out what was present or not. Now we can report specific keys directly. I made some minor quality-of-life improvements to the message too. Before: Missing env vars! Please, check (this/these) var(s) (is/are) set: THIS_ENV, THAT_ENV, MISSING_ENV, THE_OTHER_ENV, .... After (one `.env`, one `.env.example`): The following variables are present in .env.example, but not set in the current environment or .env: MISSING_ENV After (many `.env`s, many `.env.example`s): The following variables are present in one of .env.example.1, .env.example.2, but not set in the current environment, or any of .env.1, .env.2: MISSING_ENV This should be much less confusing to end-users. * Model unexpected empty list case in the types
1 parent fad5561 commit 4d7aab2

File tree

5 files changed

+47
-43
lines changed

5 files changed

+47
-43
lines changed

dotenv.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ library
9696
, shellwords >= 0.1.3.0
9797
, text
9898
, exceptions >= 0.8 && < 0.11
99-
, mtl >= 2.3 && < 2.4
99+
, mtl >= 2.2.2 && < 2.4
100100

101101
hs-source-dirs: src
102102
ghc-options: -Wall

spec/Configuration/DotenvSpec.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ spec = do
7272
unsetEnv "ANOTHER_ENV"
7373
loadFile config `shouldThrow` anyErrorCall
7474

75-
context "when the needed env vars are not missing" $
75+
context "when the needed env vars are not missing" $ do
7676
it "should succeed when loading all of the needed env vars" $ do
7777
-- Load extra information
7878
me <- init <$> readCreateProcess (shell "whoami") ""
@@ -88,6 +88,13 @@ spec = do
8888
lookupEnv "UNICODE_TEST" `shouldReturn` Just "Manabí"
8989
lookupEnv "ANOTHER_ENV" `shouldReturn` Just "hello"
9090

91+
it "succeeds when variable is preset" $ do
92+
setEnv "DOTENV" "preset"
93+
94+
loadFile config
95+
96+
lookupEnv "DOTENV" `shouldReturn` Just "preset"
97+
9198
describe "parseFile" $ after_ clearEnvs $ do
9299
it "returns variables from a file without changing the environment" $ do
93100
lookupEnv "DOTENV" `shouldReturn` Nothing

src/Configuration/Dotenv.hs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ import Control.Exception (throw)
3131
import Control.Monad (unless, when)
3232
import Control.Monad.Catch
3333
import Control.Monad.IO.Class (MonadIO (..))
34-
import Data.List (intersectBy, union,
35-
unionBy)
34+
import Data.Function (on)
35+
import Data.List.NonEmpty (NonEmpty(..))
36+
import qualified Data.List.NonEmpty as NE
37+
import Data.List ((\\), intercalate, union)
3638
import System.IO.Error (isDoesNotExistError)
3739
import Text.Megaparsec (errorBundlePretty, parse)
3840

@@ -59,22 +61,36 @@ loadFile ::
5961
-> m ()
6062
loadFile config@Config {..} = do
6163
environment <- liftIO getEnvironment
62-
readVars <- fmap concat (mapM parseFile configPath)
63-
neededVars <- fmap concat (mapM parseFile configExamplePath)
64-
let coincidences = (environment `union` readVars) `intersectEnvs` neededVars
65-
cmpEnvs env1 env2 = fst env1 == fst env2
66-
intersectEnvs = intersectBy cmpEnvs
67-
unionEnvs = unionBy cmpEnvs
68-
vars =
69-
if (not . null) neededVars
70-
then if length neededVars == length coincidences
71-
then readVars `unionEnvs` neededVars
72-
else error $
73-
"Missing env vars! Please, check (this/these) var(s) (is/are) set:" ++
74-
concatMap ((++) " " . fst) neededVars
75-
else readVars
64+
65+
vars <- case (NE.nonEmpty configPath, NE.nonEmpty configExamplePath) of
66+
(Nothing, _) -> pure []
67+
(Just envs, Nothing) -> concat <$> mapM parseFile envs
68+
(Just envs, Just envExamples) -> do
69+
readVars <- concat <$> mapM parseFile envs
70+
neededKeys <- map fst . concat <$> mapM parseFile envExamples
71+
72+
let
73+
presentKeys = (union `on` map fst) environment readVars
74+
missingKeys = neededKeys \\ presentKeys
75+
76+
pure $
77+
if null missingKeys
78+
then readVars
79+
else error $ concat
80+
[ "The following variables are present in "
81+
, showPaths "one of " envExamples
82+
, ", but not set in the current environment, or"
83+
, showPaths "any of " envs
84+
, ": "
85+
, intercalate ", " missingKeys
86+
]
87+
7688
unless allowDuplicates $ (lookUpDuplicates . map fst) vars
7789
runReaderT (mapM_ applySetting vars) config
90+
where
91+
showPaths :: String -> NonEmpty FilePath -> String
92+
showPaths _ (p:|[]) = p
93+
showPaths prefix ps = prefix <> intercalate ", " (NE.toList ps)
7894

7995
-- | Parses the given dotenv file and returns values /without/ adding them to
8096
-- the environment.

stack.yaml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
resolver: lts-19.0
2-
packages:
3-
- '.'
4-
extra-deps:
5-
- optparse-applicative-0.17.0.0
6-
- shellwords-0.1.3.0
1+
resolver: lts-20.11

stack.yaml.lock

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,10 @@
33
# For more information, please see the documentation at:
44
# https://docs.haskellstack.org/en/stable/lock_files
55

6-
packages:
7-
- completed:
8-
hackage: optparse-applicative-0.17.0.0@sha256:0713e54cbb341e5cae979e2ac441eb3a5ff42e303001f432bd58c19e5638bdda,4967
9-
pantry-tree:
10-
size: 3124
11-
sha256: 3b4398845ee6718fe2303870a5ab75bd190eb712fed2f22caaae30f790c490fd
12-
original:
13-
hackage: optparse-applicative-0.17.0.0
14-
- completed:
15-
hackage: shellwords-0.1.3.0@sha256:baf03b1e80dbc5dffbe6ba5451ee7c389ac39e3e2f9c24030c26a639522a1032,2226
16-
pantry-tree:
17-
size: 496
18-
sha256: ce076c4ec323107f8e84d25147b4f7c865e8cd27d804f384b9d1e2d5706ba0c3
19-
original:
20-
hackage: shellwords-0.1.3.0
6+
packages: []
217
snapshots:
228
- completed:
23-
size: 616897
24-
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/0.yaml
25-
sha256: bbf2be02f17940bac1f87cb462d4fb0c3355de6dcfc53d84f4f9ad3ee2164f65
26-
original: lts-19.0
9+
sha256: adbc602422dde10cc330175da7de8609e70afc41449a7e2d6e8b1827aa0e5008
10+
size: 649342
11+
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/11.yaml
12+
original: lts-20.11

0 commit comments

Comments
 (0)