diff --git a/.env b/.env index 9644bf0..5e33e87 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # This supports the example; see README.md. LIBRARY_NAME=dotenv -TEST_VAR="Greetings from ${LIBRARY_NAME}" +TEST_VAR="Greetings from ${LIBRARY_NAME}, $(whoami)!" diff --git a/.gitignore b/.gitignore index 30efe19..44852cc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /.purs* /.psa* /.spago +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md index 6df5a37..5754f27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +*0.4.0* +* Added support for command substitution. + *0.3.0* * Updated library to support new PureScript compiler version 0.13. * Migrated to Spago for dev/CI. diff --git a/bower.json b/bower.json index 369467f..d6214d7 100644 --- a/bower.json +++ b/bower.json @@ -25,12 +25,14 @@ "dependencies": { "purescript-console": "^4.2.0", "purescript-effect": "^2.0.1", + "purescript-node-fs-aff": "^6.0.0", "purescript-node-process": "^7.0.0", "purescript-parsing": "^5.0.3", - "purescript-node-fs-aff": "^6.0.0" + "purescript-run": "^3.0.1", + "purescript-sunde": "^2.0.0" }, "devDependencies": { "purescript-psci-support": "^4.0.0", - "purescript-spec": "^4.0.0-rc.1" + "purescript-spec": "^4.0.0" } } diff --git a/packages.dhall b/packages.dhall index 4626bd1..11ca5c9 100644 --- a/packages.dhall +++ b/packages.dhall @@ -1,8 +1,8 @@ let mkPackage = - https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.0-20190607/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57 + https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.0-20190626/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57 let upstream = - https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.0-20190607/src/packages.dhall sha256:96b28e434b8a62caea5f10376b4f7dc1736a668592cabe914f117ecf5673c2ff + https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.0-20190626/src/packages.dhall sha256:9905f07c9c3bd62fb3205e2108515811a89d55cff24f4341652f61ddacfcf148 let overrides = {=} diff --git a/spago.dhall b/spago.dhall index 8cb230b..30c6d9a 100644 --- a/spago.dhall +++ b/spago.dhall @@ -1,7 +1,16 @@ { name = "dotenv" , dependencies = - [ "console", "effect", "node-fs-aff", "node-process", "parsing", "psci-support", "spec" ] + [ "console" + , "effect" + , "node-fs-aff" + , "node-process" + , "parsing" + , "psci-support" + , "run" + , "spec" + , "sunde" + ] , packages = ./packages.dhall } diff --git a/src/Dotenv.purs b/src/Dotenv.purs index 1b86947..3601a18 100644 --- a/src/Dotenv.purs +++ b/src/Dotenv.purs @@ -1,54 +1,55 @@ -- | This is the base module for the Dotenv library. -module Dotenv (module Dotenv.Types, loadFile) where +module Dotenv (Name, Setting, Settings, Value, loadFile) where import Prelude import Control.Monad.Error.Class (class MonadThrow, catchError, throwError) import Data.Either (either) -import Data.Maybe (Maybe(..)) -import Data.Traversable (traverse) -import Data.Tuple (Tuple(..)) +import Data.Maybe (Maybe) +import Data.Tuple (Tuple) +import Dotenv.Internal.Apply (applySettings) +import Dotenv.Internal.ChildProcess (_childProcess, handleChildProcess) +import Dotenv.Internal.Environment (_environment, handleEnvironment) import Dotenv.Internal.Parse (settings) as Parse -import Dotenv.Internal.Resolve (values) as Resolve -import Dotenv.Internal.Types (Settings) as Internal -import Dotenv.Types (Setting, Settings) +import Dotenv.Internal.Resolve (resolveValues) +import Dotenv.Internal.Types (Setting) as IT +import Dotenv.Internal.Types (UnresolvedValue) import Effect.Aff.Class (class MonadAff, liftAff) -import Effect.Class (liftEffect) import Effect.Exception (Error, error) import Node.Encoding (Encoding(UTF8)) import Node.FS.Aff (readTextFile) -import Node.Process (getEnv, lookupEnv, setEnv) +import Run (case_, interpret, on) import Text.Parsing.Parser (parseErrorMessage, runParser) +-- The type of a setting name +type Name = String + +-- The type of a (resolved) value +type Value = Maybe String + +-- The type of a setting +type Setting = Tuple Name Value + +-- The type of settings +type Settings = Array Setting + -- | Loads the `.env` file into the environment. loadFile :: forall m. MonadAff m => MonadThrow Error m => m Settings -loadFile = (Resolve.values <$> liftEffect getEnv <*> (readSettings >>= parseSettings)) >>= applySettings +loadFile = readDotenv + >>= (flip runParser Parse.settings >>> either (parseErrorMessage >>> error >>> throwError) pure) + >>= processSettings -- | Reads the `.env` file. -readSettings :: forall m. MonadAff m => m String -readSettings = liftAff $ readTextFile UTF8 ".env" - # flip catchError (const $ pure "") - --- | Parses the contents of a `.env` file. -parseSettings :: forall m. MonadThrow Error m => String -> m Internal.Settings -parseSettings settings = runParser settings Parse.settings - # either (throwError <<< error <<< append "Invalid .env file: " <<< parseErrorMessage) pure - --- | Applies the specified settings to the environment. -applySettings :: forall m. MonadAff m => Settings -> m Settings -applySettings = traverse applySetting - --- | Applies the specified setting to the environment. -applySetting :: forall m. MonadAff m => Setting -> m Setting -applySetting setting@(Tuple key settingValue) = do - envValue <- liftEffect $ lookupEnv key - case envValue of - Just value -> - pure $ Tuple key $ Just value - Nothing -> - case settingValue of - Just value -> do - liftEffect $ setEnv key value - pure setting - Nothing -> - pure setting +readDotenv :: forall m. MonadAff m => m String +readDotenv = liftAff $ readTextFile UTF8 ".env" + # flip catchError (const $ pure "") + +-- | Processes settings by resolving their values and then applying them to the environment. +processSettings :: forall m. MonadAff m => Array (IT.Setting UnresolvedValue) -> m Settings +processSettings = (resolveValues >=> applySettings) + >>> interpret + ( case_ + # on _childProcess handleChildProcess + # on _environment handleEnvironment + ) + >>> liftAff diff --git a/src/Dotenv/Internal/Apply.purs b/src/Dotenv/Internal/Apply.purs new file mode 100644 index 0000000..d6e3ede --- /dev/null +++ b/src/Dotenv/Internal/Apply.purs @@ -0,0 +1,24 @@ +-- | This module encapsulates the logic for applying settings to the environment. + +module Dotenv.Internal.Apply (applySettings) where + +import Prelude +import Data.Maybe (fromMaybe, isJust) +import Data.Traversable (traverse) +import Data.Tuple (Tuple(..)) +import Dotenv.Internal.Environment (ENVIRONMENT, lookupEnv, setEnv) +import Dotenv.Internal.Types (ResolvedValue, Setting) +import Run (Run) + +-- | Applies the specified settings to the environment. +applySettings + :: forall r + . Array (Setting ResolvedValue) + -> Run (environment :: ENVIRONMENT | r) (Array (Setting ResolvedValue)) +applySettings = traverse \(Tuple name resolvedValue) -> do + currentValue <- lookupEnv name + if isJust currentValue + then pure $ Tuple name currentValue + else do + when (isJust resolvedValue) (setEnv name $ fromMaybe "" resolvedValue) + pure $ Tuple name resolvedValue diff --git a/src/Dotenv/Internal/ChildProcess.purs b/src/Dotenv/Internal/ChildProcess.purs new file mode 100644 index 0000000..6b049fc --- /dev/null +++ b/src/Dotenv/Internal/ChildProcess.purs @@ -0,0 +1,40 @@ +-- | This module encapsulates the logic for running a child process. + +module Dotenv.Internal.ChildProcess (CHILD_PROCESS, ChildProcessF(..), _childProcess, handleChildProcess, spawn) where + +import Prelude +import Control.Monad.Error.Class (throwError) +import Data.Maybe (Maybe(Nothing)) +import Data.Symbol (SProxy(..)) +import Effect.Aff (Aff) +import Effect.Exception (error) +import Node.ChildProcess (Exit(..), defaultSpawnOptions) +import Run (FProxy, Run, lift) +import Sunde (spawn) as Sunde + +-- | A data type representing the supported operations +data ChildProcessF a = Spawn String (Array String) (String -> a) + +derive instance functorChildProcessF :: Functor ChildProcessF + +-- | The effect label used for a child process +_childProcess = SProxy :: SProxy "childProcess" + +-- | The effect type used for a child process +type CHILD_PROCESS = FProxy (ChildProcessF) + +-- | The default interpreter for handling a child process +handleChildProcess :: ChildProcessF ~> Aff +handleChildProcess (Spawn cmd args callback) = do + { stderr, stdout, exit } <- Sunde.spawn { cmd, args, stdin: Nothing } defaultSpawnOptions + case exit of + Normally 0 -> + pure $ callback stdout + Normally code -> + throwError (error $ "Exited with code " <> show code <> ": " <> stderr) + BySignal signal -> + throwError (error $ "Exited: " <> show signal) + +-- | Constructs the value used to spawn a child process. +spawn :: forall r. String -> Array String -> Run (childProcess :: CHILD_PROCESS | r) String +spawn cmd args = lift _childProcess (Spawn cmd args identity) diff --git a/src/Dotenv/Internal/Environment.purs b/src/Dotenv/Internal/Environment.purs new file mode 100644 index 0000000..28a8f67 --- /dev/null +++ b/src/Dotenv/Internal/Environment.purs @@ -0,0 +1,49 @@ +-- | This module encapsulates the logic for reading or modifying the environment. +module Dotenv.Internal.Environment + ( ENVIRONMENT + , EnvironmentF(..) + , _environment + , handleEnvironment + , lookupEnv + , setEnv + ) where + +import Prelude +import Data.Maybe (Maybe) +import Data.Symbol (SProxy(..)) +import Effect.Aff (Aff) +import Effect.Class (liftEffect) +import Node.Process (lookupEnv, setEnv) as P +import Run (FProxy, Run, lift) + +-- | A data type representing the supported operations. +data EnvironmentF a + = LookupEnv String (Maybe String -> a) + | SetEnv String String a + +derive instance functorEnvironmentF :: Functor EnvironmentF + +-- The effect label used for reading or modifying the environment. +_environment = SProxy :: SProxy "environment" + +-- | The effect type used for reading or modifying the environment +type ENVIRONMENT = FProxy (EnvironmentF) + +-- | The default interpreter used for reading or modifying the environment +handleEnvironment :: EnvironmentF ~> Aff +handleEnvironment op = liftEffect $ + case op of + LookupEnv name callback -> do + value <- P.lookupEnv name + pure $ callback value + SetEnv name value next -> do + P.setEnv name value + pure next + +-- | Constructs the value used to look up an environment variable. +lookupEnv :: forall r. String -> Run (environment :: ENVIRONMENT | r) (Maybe String) +lookupEnv name = lift _environment (LookupEnv name identity) + +-- | Constructs the value used to set an environment variable. +setEnv :: forall r. String -> String -> Run (environment :: ENVIRONMENT | r) Unit +setEnv name value = lift _environment (SetEnv name value unit) diff --git a/src/Dotenv/Internal/Parse.purs b/src/Dotenv/Internal/Parse.purs index b19fb76..562c12d 100644 --- a/src/Dotenv/Internal/Parse.purs +++ b/src/Dotenv/Internal/Parse.purs @@ -1,14 +1,14 @@ -- | This module encapsulates the parsing logic for a `.env` file. -module Dotenv.Internal.Parse (settings) where +module Dotenv.Internal.Parse where import Prelude hiding (between) import Control.Alt ((<|>)) -import Data.Array (fromFoldable, head, length, many, some) +import Data.Array ((:), fromFoldable, head, length, many, some) import Data.Maybe (fromMaybe) import Data.String.CodeUnits (fromCharArray) import Data.Tuple (Tuple(..)) -import Dotenv.Internal.Types (Name, Setting, Settings, Value(..)) +import Dotenv.Internal.Types (Name, Setting, UnresolvedValue(..)) import Text.Parsing.Parser (Parser) import Text.Parsing.Parser.Combinators ((), lookAhead, notFollowedBy, skipMany, sepEndBy, try) import Text.Parsing.Parser.String (char, noneOf, oneOf, string, whiteSpace) @@ -23,7 +23,7 @@ whitespaceChars :: Array Char whitespaceChars = [' ', '\t'] -- | Parses `.env` settings. -settings :: Parser String Settings +settings :: Parser String (Array (Setting UnresolvedValue)) settings = fromFoldable <$> do skipMany notSetting (setting <* many (noneOf newlineChars)) `sepEndBy` skipMany notSetting @@ -39,37 +39,51 @@ name :: Parser String Name name = fromCharArray <$> many (alphaNum <|> char '_') <* char '=' -- | Parses a variable substitution, i.e. `${VARIABLE_NAME}`. -variableSubstitution :: Parser String Value +variableSubstitution :: Parser String UnresolvedValue variableSubstitution = string "${" *> (VariableSubstitution <<< fromCharArray <$> some (alphaNum <|> char '_')) <* char '}' +-- | Parses a command substitution, i.e. `$(whoami)`. +commandSubstitution :: Parser String UnresolvedValue +commandSubstitution = do + _ <- string "$(" + command <- fromCharArray <$> (some $ noneOf (')' : whitespaceChars)) + arguments <- many $ whiteSpace *> (fromCharArray <$> (some $ noneOf (')' : whitespaceChars))) + _ <- whiteSpace *> char ')' + pure $ CommandSubstitution command arguments + -- | Parses a quoted value, enclosed in the specified type of quotation mark. -quotedValue :: Char -> Parser String Value -quotedValue q = valueFromValues <$> (char q *> (some $ variableSubstitution <|> literal) <* char q) - where - literal = LiteralValue <<< fromCharArray <$> some (noneOf ['$', q] <|> try (char '$' <* notFollowedBy (char '{'))) +quotedValue :: Char -> Parser String UnresolvedValue +quotedValue q = + let + literal = + LiteralValue <<< fromCharArray <$> some (noneOf ['$', q] <|> try (char '$' <* notFollowedBy (oneOf ['{', '(']))) + in + valueFromValues <$> (char q *> (some $ variableSubstitution <|> commandSubstitution <|> literal) <* char q) -- | Parses an unquoted value. -unquotedValue :: Parser String Value -unquotedValue = valueFromValues <$> (whiteSpace *> (some $ variableSubstitution <|> literal)) - where +unquotedValue :: Parser String UnresolvedValue +unquotedValue = + let literal = map - ( LiteralValue <<< fromCharArray) + ( LiteralValue <<< fromCharArray ) $ some $ try (noneOf (['$', '#'] <> whitespaceChars <> newlineChars)) - <|> try (char '$' <* notFollowedBy (char '{')) + <|> try (char '$' <* notFollowedBy (oneOf ['{', '('])) <|> try (oneOf whitespaceChars <* lookAhead (noneOf $ ['#'] <> whitespaceChars <> newlineChars)) + in + valueFromValues <$> (whiteSpace *> (some $ variableSubstitution <|> commandSubstitution <|> literal)) -- | Assembles a single value from a series of values. -valueFromValues :: Array Value -> Value +valueFromValues :: Array UnresolvedValue -> UnresolvedValue valueFromValues v | length v == 1 = fromMaybe (ValueExpression []) (head v) | otherwise = ValueExpression v -- | Parses a setting value. -value :: Parser String Value +value :: Parser String UnresolvedValue value = (quotedValue '"' <|> quotedValue '\'' <|> unquotedValue) "variable value" -- | Parses a setting in the form of `NAME=value`. -setting :: Parser String Setting +setting :: Parser String (Setting UnresolvedValue) setting = Tuple <$> name <*> value diff --git a/src/Dotenv/Internal/Resolve.purs b/src/Dotenv/Internal/Resolve.purs index 90c7c4f..e1d4cec 100644 --- a/src/Dotenv/Internal/Resolve.purs +++ b/src/Dotenv/Internal/Resolve.purs @@ -1,34 +1,49 @@ --- | This module contains the logic for resolving `.env` values. +-- | This module encapsulates the logic for resolving `.env` values. -module Dotenv.Internal.Resolve (values) where +module Dotenv.Internal.Resolve (resolveValues) where import Prelude -import Control.Alt ((<|>)) -import Data.Bifunctor (rmap) +import Data.Array (unzip, zip) import Data.Foldable (find) -import Data.Maybe (Maybe) -import Data.String (joinWith) -import Data.Traversable (sequence) -import Data.Tuple (fst, snd) -import Dotenv.Internal.Types (Environment, Settings, Value(..)) -import Dotenv.Types (Settings) as Public -import Foreign.Object (lookup) +import Data.Maybe (Maybe(..)) +import Data.String (joinWith, trim) +import Data.Traversable (sequence, traverse) +import Data.Tuple (Tuple(..), fst, snd) +import Dotenv.Internal.ChildProcess (CHILD_PROCESS, spawn) +import Dotenv.Internal.Environment (ENVIRONMENT, lookupEnv) +import Dotenv.Internal.Types (ResolvedValue, Setting, UnresolvedValue(..)) +import Run (Run) --- | Given the environment and an array of `.env` settings, resolves the specified value. -value - :: Environment - -> Settings - -> Value - -> Maybe String -value env settings val = +-- | A row that tracks the effects involved in value resolution +type Resolution r = (childProcess :: CHILD_PROCESS, environment :: ENVIRONMENT | r) + +-- | Resolves a value according to its expression. +resolveValue :: forall r. Array (Setting UnresolvedValue) -> UnresolvedValue -> Run (Resolution r) ResolvedValue +resolveValue settings = case _ of + LiteralValue value -> + pure $ Just value + CommandSubstitution cmd args -> do + value <- spawn cmd args + pure $ Just (trim value) + VariableSubstitution var -> do + envValueMaybe <- lookupEnv var + case envValueMaybe of + Just value -> + pure $ Just value + Nothing -> do + case (snd <$> find (eq var <<< fst) settings) of + Just unresolvedValue -> + resolveValue settings unresolvedValue + Nothing -> + pure Nothing + ValueExpression unresolvedValues -> do + resolvedValues <- traverse (resolveValue settings) unresolvedValues + pure $ joinWith "" <$> sequence resolvedValues + +-- | Resolves the values within an array of settings. +resolveValues :: forall r. Array (Setting UnresolvedValue) -> Run (Resolution r) (Array (Setting ResolvedValue)) +resolveValues settings = let - value' = value env settings + (Tuple names unresolvedValues) = unzip settings in - case val of - LiteralValue v -> pure v - ValueExpression vs -> joinWith "" <$> (sequence $ value' <$> vs) - VariableSubstitution name -> lookup name env <|> (value' =<< snd <$> find (eq name <<< fst) settings) - --- | Given the environment and an array of `.env` settings, resolves the value of each setting. -values :: Environment -> Settings -> Public.Settings -values env settings = rmap (value env settings) <$> settings + zip names <$> traverse (resolveValue settings) unresolvedValues diff --git a/src/Dotenv/Internal/Types.purs b/src/Dotenv/Internal/Types.purs index 03d5c0d..59ab939 100644 --- a/src/Dotenv/Internal/Types.purs +++ b/src/Dotenv/Internal/Types.purs @@ -1,29 +1,31 @@ --- | This module contains data types representing parsed `.env` content and the unmodified environment. +-- | This module contains data types representing `.env` settings. -module Dotenv.Internal.Types (Environment, Name, Setting, Settings, Value(..)) where +module Dotenv.Internal.Types (Name, ResolvedValue, Setting, UnresolvedValue(..)) where import Prelude +import Data.Maybe (Maybe) import Data.Tuple (Tuple) -import Foreign.Object (Object) -- | The name of a setting type Name = String --- | The value of a setting -data Value = LiteralValue String | VariableSubstitution String | ValueExpression (Array Value) +-- | The expressed value of a setting, which has not been resolved yet +data UnresolvedValue + = LiteralValue String + | VariableSubstitution String + | CommandSubstitution String (Array String) + | ValueExpression (Array UnresolvedValue) -derive instance eqValue :: Eq Value +derive instance eqUnresolvedValue :: Eq UnresolvedValue -instance showValue :: Show Value where +instance showUnresolvedValue :: Show UnresolvedValue where show (LiteralValue v) = "(LiteralValue \"" <> v <> "\")" show (VariableSubstitution v) = "(VariableSubstitution \"" <> v <> "\")" + show (CommandSubstitution c a) = "(CommandSubstitution \"" <> c <> " " <> show a <> "\")" show (ValueExpression vs) = "(ValueExpression " <> show vs <> ")" --- | The conjunction of a setting name and the corresponding value -type Setting = Tuple Name Value +-- | The type of a resolved value +type ResolvedValue = Maybe String --- | A collection of settings -type Settings = Array (Tuple Name Value) - --- | Environment variables -type Environment = Object String +-- | The product of a setting name and the corresponding value +type Setting v = Tuple Name v diff --git a/src/Dotenv/Types.purs b/src/Dotenv/Types.purs deleted file mode 100644 index 3bdf2e0..0000000 --- a/src/Dotenv/Types.purs +++ /dev/null @@ -1,18 +0,0 @@ --- | This module defines type aliases representing `.env` settings. - -module Dotenv.Types (Name, Setting, Settings, Value) where - -import Data.Maybe (Maybe) -import Data.Tuple (Tuple) - --- The type of a setting name -type Name = String - --- The type of a (resolved) value -type Value = Maybe String - --- The type of a setting -type Setting = Tuple Name Value - --- The type of settings -type Settings = Array Setting diff --git a/test/Apply.purs b/test/Apply.purs new file mode 100644 index 0000000..7cbea5b --- /dev/null +++ b/test/Apply.purs @@ -0,0 +1,51 @@ +module Test.Apply (tests) where + +import Prelude +import Data.Foldable (find) +import Data.Map (Map, insert, lookup, singleton) +import Data.Maybe (Maybe(..)) +import Data.Tuple (Tuple(..), fst, snd) +import Dotenv.Internal.Apply (applySettings) +import Dotenv.Internal.Environment (EnvironmentF(..), _environment) +import Dotenv.Internal.Types (ResolvedValue, Setting) +import Run (Run, case_, extract, interpret, on) +import Run.Writer (WRITER, runWriter, tell) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual, shouldNotContain) + +settings :: Array (Setting ResolvedValue) +settings = [ Tuple "VAR_ONE" $ Just "one", Tuple "VAR_TWO" $ Just "two" ] + +predefinedVariables :: Map String String +predefinedVariables = singleton "VAR_TWO" "2" # insert "VAR_THREE" "3" + +handleEnvironment :: forall r. EnvironmentF ~> Run (writer :: WRITER (Array (Tuple String String)) | r) +handleEnvironment = + case _ of + LookupEnv name callback -> + pure $ callback (lookup name predefinedVariables) + SetEnv name value next -> do + tell [Tuple name value] + pure next + +applySettingsResult :: Tuple (Array (Tuple String String)) (Array (Setting ResolvedValue)) +applySettingsResult = + extract $ runWriter $ interpret (case_ # on _environment handleEnvironment) $ applySettings settings + +appliedSettings :: Array (Tuple String String) +appliedSettings = fst applySettingsResult + +returnedSettings :: Array (Setting ResolvedValue) +returnedSettings = snd applySettingsResult + +tests :: Spec Unit +tests = describe "applySettings" do + + it "should apply settings where the environment variable is not already defined" $ + (snd <$> find (eq "VAR_ONE" <<< fst) appliedSettings) `shouldEqual` Just "one" + + it "should not apply settings where the environment variable is already defined" $ + (fst <$> appliedSettings) `shouldNotContain` "VAR_TWO" + + it "should return the specified settings with the values defined in the environment as a result" $ + returnedSettings `shouldEqual` [Tuple "VAR_ONE" $ Just "one", Tuple "VAR_TWO" $ Just "2"] diff --git a/test/Load.purs b/test/Load.purs deleted file mode 100644 index f7bdd48..0000000 --- a/test/Load.purs +++ /dev/null @@ -1,46 +0,0 @@ -module Test.Load (tests) where - -import Prelude -import Data.Maybe (Maybe(Just)) -import Dotenv as Dotenv -import Effect.Aff (Aff, finally) -import Effect.Class (liftEffect) -import Node.Buffer (fromString) as Buffer -import Node.Encoding (Encoding(UTF8)) -import Node.FS.Aff (writeFile) -import Node.FS.Sync (rename) -import Node.Process (lookupEnv, setEnv) -import Test.Spec (Spec, describe, it) -import Test.Spec.Assertions (shouldEqual) - -setup :: Aff Unit -setup = liftEffect $ rename ".env" ".env.bak" - -teardown :: Aff Unit -teardown = liftEffect $ rename ".env.bak" ".env" - -writeConfig :: String -> Aff Unit -writeConfig config = writeFile ".env" <=< liftEffect $ Buffer.fromString config UTF8 - -tests :: Spec Unit -tests = describe "loadFile" do - - it "applies settings from .env" $ do - setup - writeConfig "TEST_ONE=hello" - _ <- Dotenv.loadFile - testOne <- liftEffect $ lookupEnv "TEST_ONE" - testOne `shouldEqual` (Just "hello") - # finally teardown - - it "does not replace existing environment variables" $ do - setup - writeConfig "TEST_TWO=hi2" - liftEffect $ setEnv "TEST_TWO" "hi" - _ <- Dotenv.loadFile - two <- liftEffect $ lookupEnv "TEST_TWO" - two `shouldEqual` Just "hi" - # finally teardown - - it "does not throw an error when the .env file does not exist" $ - setup *> Dotenv.loadFile *> teardown diff --git a/test/Main.purs b/test/Main.purs index 4e54d43..11fedf5 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -5,12 +5,12 @@ import Effect (Effect) import Effect.Aff (launchAff_) import Test.Spec.Reporter.Console (consoleReporter) import Test.Spec.Runner (runSpec) -import Test.Load as Load +import Test.Apply as Apply import Test.Parse as Parse import Test.Resolve as Resolve main :: Effect Unit main = launchAff_ $ runSpec [ consoleReporter ] do - Load.tests + Apply.tests Parse.tests Resolve.tests diff --git a/test/Parse.purs b/test/Parse.purs index 97c5b08..3fbd571 100644 --- a/test/Parse.purs +++ b/test/Parse.purs @@ -4,7 +4,7 @@ import Prelude import Data.Either (Either(..)) import Data.Tuple (Tuple(..)) import Dotenv.Internal.Parse (settings) -import Dotenv.Internal.Types (Value(..)) +import Dotenv.Internal.Types (UnresolvedValue(..)) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) import Text.Parsing.Parser (runParser) @@ -82,10 +82,42 @@ tests = describe "settings parser" do in actual `shouldEqual` expected - it "parses variable substitutions" $ + it "parses variable substitutions within unquoted values" $ let expected = Right [ Tuple "A" $ ValueExpression [ LiteralValue "Hi, ", VariableSubstitution "USER", LiteralValue "!" ] ] actual = "A=Hi, ${USER}!" `runParser` settings in actual `shouldEqual` expected + + it "parses variable substitutions within quoted values" $ + let + expected = + Right [ Tuple "A" $ ValueExpression [ LiteralValue "Hi, ", VariableSubstitution "USER", LiteralValue "!" ] ] + actual = "A=\"Hi, ${USER}!\"" `runParser` settings + in + actual `shouldEqual` expected + + it "parses command substitutions within unquoted values" $ + let + expected = + Right + [ Tuple "A" $ ValueExpression + [ LiteralValue "Hello, ", CommandSubstitution "head" ["-n", "1", "user.txt"], LiteralValue "!" + ] + ] + actual = "A=Hello, $(head -n 1 user.txt)!" `runParser` settings + in + actual `shouldEqual` expected + + it "parses command substitutions within quoted values" $ + let + expected = + Right + [ Tuple "A" $ ValueExpression + [ LiteralValue "Hello, ", CommandSubstitution "head" ["-n", "1", "user.txt"], LiteralValue "!" + ] + ] + actual = "A=\"Hello, $(head -n 1 user.txt)!\"" `runParser` settings + in + actual `shouldEqual` expected diff --git a/test/Resolve.purs b/test/Resolve.purs index 1c99988..d175b8b 100644 --- a/test/Resolve.purs +++ b/test/Resolve.purs @@ -1,21 +1,27 @@ module Test.Resolve (tests) where import Prelude +import Control.Monad.Error.Class (throwError) import Data.Foldable (find) +import Data.Map (Map, lookup, singleton) import Data.Maybe (Maybe(..)) +import Data.String.Common (joinWith) import Data.Tuple (Tuple(..), fst, snd) -import Dotenv.Internal.Resolve (values) as Resolve -import Dotenv.Internal.Types (Name, Value(..)) -import Dotenv.Types (Settings) -import Foreign.Object (singleton) -import Test.Spec (Spec, describe, it) +import Dotenv.Internal.ChildProcess (ChildProcessF(..), _childProcess) +import Dotenv.Internal.Environment (EnvironmentF(..), _environment) +import Dotenv.Internal.Resolve (resolveValues) +import Dotenv.Internal.Types (ResolvedValue, Setting, UnresolvedValue(..)) +import Effect.Aff (Aff) +import Effect.Exception (error) +import Run (case_, interpret, on) +import Test.Spec (Spec, before, describe, it) import Test.Spec.Assertions (shouldEqual) -settings :: Settings -settings = Resolve.values (singleton "DB_PASSWORD" "asdf") $ +configuration :: Array (Setting UnresolvedValue) +configuration = [ Tuple "DB_HOSTNAME" $ LiteralValue "localhost" , Tuple "DB_HOST" $ VariableSubstitution "DB_HOSTNAME" - , Tuple "DB_USER" $ LiteralValue "nick" + , Tuple "DB_USER" $ CommandSubstitution "whoami" [] , Tuple "DB_PASS" $ VariableSubstitution "DB_PASSWORD" , Tuple "DB_NAME" $ LiteralValue "development" , Tuple "DB_CRED" $ @@ -35,23 +41,58 @@ settings = Resolve.values (singleton "DB_PASSWORD" "asdf") $ ] ] -settingValue :: Name -> Maybe String -settingValue name = join $ snd <$> find (eq name <<< fst) settings +commands :: Map (Tuple String (Array String)) String +commands = singleton (Tuple "whoami" []) "user\n" + +variables :: Map String String +variables = singleton "DB_PASSWORD" "p4s5w0rD!" + +handleChildProcess :: ChildProcessF ~> Aff +handleChildProcess (Spawn cmd args callback) = + case (lookup (Tuple cmd args) commands) of + Just result -> + pure $ callback result + Nothing -> + throwError $ error ("Unrecognized command: " <> cmd <> " " <> joinWith " " args) + +handleEnvironment :: EnvironmentF ~> Aff +handleEnvironment op = + case op of + LookupEnv name callback -> + pure $ callback (lookup name variables) + SetEnv _ _ _ -> + throwError $ error "The environment was modified while resolving values." + +resolve :: Array (Setting UnresolvedValue) -> Aff (Array (Setting ResolvedValue)) +resolve = resolveValues + >>> interpret + ( case_ + # on _childProcess handleChildProcess + # on _environment handleEnvironment + ) + +lookupSetting :: String -> Array (Setting ResolvedValue) -> ResolvedValue +lookupSetting name = join <<< map snd <<< find (eq name <<< fst) tests :: Spec Unit tests = describe "value resolver" do - it "resolves literal values" $ - settingValue "DB_HOST" `shouldEqual` Just "localhost" + before (flip lookupSetting <$> resolve configuration) do + + it "resolves literal values" \setting -> do + setting "DB_HOST" `shouldEqual` Just "localhost" + + it "resolves variable substitutions from the environment" \setting -> do + setting "DB_PASS" `shouldEqual` Just "p4s5w0rD!" - it "resolves variable substitutions from the environment" $ - settingValue "DB_PASS" `shouldEqual` Just "asdf" + it "resolves variable substitutions from the settings" \setting -> do + setting "DB_HOST" `shouldEqual` Just "localhost" - it "resolves variable substitutions from the settings" $ - settingValue "DB_HOST" `shouldEqual` Just "localhost" + it "resolves command substitutions" \setting -> do + setting "DB_USER" `shouldEqual` Just "user" - it "resolves value expressions" $ - settingValue "DB_CRED" `shouldEqual` Just "nick:asdf" + it "resolves value expressions" \setting -> do + setting "DB_CRED" `shouldEqual` Just "user:p4s5w0rD!" - it "resolves value expressions recursively" $ - settingValue "DB_CONNECTION_STRING" `shouldEqual` Just "db://nick:asdf@localhost/development" + it "resolves value expressions recursively" \setting -> do + setting "DB_CONNECTION_STRING" `shouldEqual` Just "db://user:p4s5w0rD!@localhost/development"