-- | Tiny library for handling environment variables stored in @.env@ files.
--
-- == File format
--
-- @.env@ files are plain text UTF-8 files with rows of the form:
--
-- @
-- KEY=VALUE
-- @
--
-- The rows are separated by newline and the @KEY@s must not contain any equal sign.
--
-- The @VALUE@ strings on the other hand can contain equal signs (the string is ingested up to the newline).
--
-- NB: Currently this library does /not/ support variables.
--
-- == Important
--
-- Add the paths of your @.env@ files to the @.gitignore@ file or equivalent, so that they are not checked into source control.
module DotEnv.Micro (loadDotEnv) where

import Control.Monad.IO.Class (MonadIO(..))
import Data.Foldable (traverse_)
import Data.Functor (void)
import Data.List (sortOn)
import Data.Maybe (listToMaybe, fromMaybe)
import Data.Ord (Down(..))
import System.Environment (lookupEnv, setEnv)
import qualified Text.ParserCombinators.ReadP as P (ReadP, readP_to_S, char, munch, sepBy1)

-- directory
import System.Directory (doesFileExist)

-- | Load, parse and apply a @.env@ file
--
-- NB : does not overwrite any preexisting env vars.
--
-- NB2 : if the given @.env@ file is not found or cannot be parsed the program crashes with @fail@.
loadDotEnv :: MonadIO m =>
              Maybe FilePath -- ^ defaults to @.env@ in the Cabal project base directory if Nothing.
           -> m ()
loadDotEnv :: forall (m :: * -> *). MonadIO m => Maybe FilePath -> m ()
loadDotEnv Maybe FilePath
mfp = forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ do
  let
    fpath :: FilePath
fpath = forall a. a -> Maybe a -> a
fromMaybe FilePath
".env" Maybe FilePath
mfp
  Bool
ok <- FilePath -> IO Bool
doesFileExist FilePath
fpath
  if Bool
ok
    then
    do
      Maybe [(FilePath, FilePath)]
mp <- FilePath -> Maybe [(FilePath, FilePath)]
parseDotEnv forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> IO FilePath
readFile FilePath
fpath
      case Maybe [(FilePath, FilePath)]
mp of
        Just [(FilePath, FilePath)]
es -> forall (m :: * -> *). MonadIO m => [(FilePath, FilePath)] -> m ()
setEnvs [(FilePath, FilePath)]
es
        Maybe [(FilePath, FilePath)]
Nothing -> forall (m :: * -> *) a. MonadFail m => FilePath -> m a
fail forall a b. (a -> b) -> a -> b
$ [FilePath] -> FilePath
unwords [FilePath
"dotenv: cannot parse", FilePath
fpath]
    else
    do
      forall (m :: * -> *) a. MonadFail m => FilePath -> m a
fail forall a b. (a -> b) -> a -> b
$ [FilePath] -> FilePath
unwords [FilePath
"dotenv:", FilePath
fpath, FilePath
"file not found"]

setEnvs :: MonadIO m => [(String, String)] -> m ()
setEnvs :: forall (m :: * -> *). MonadIO m => [(FilePath, FilePath)] -> m ()
setEnvs = forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ forall {m :: * -> *}. MonadIO m => (FilePath, FilePath) -> m ()
insf
  where
    insf :: (FilePath, FilePath) -> m ()
insf (FilePath
k, FilePath
v) = forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ do
      Maybe FilePath
me <- FilePath -> IO (Maybe FilePath)
lookupEnv FilePath
k
      case Maybe FilePath
me of
        Just FilePath
_ -> do
          FilePath -> IO ()
putStrLn forall a b. (a -> b) -> a -> b
$ [FilePath] -> FilePath
unwords [FilePath
"Variable", FilePath
k, FilePath
"already set in environment"]
        Maybe FilePath
Nothing -> do
          FilePath -> FilePath -> IO ()
setEnv FilePath
k FilePath
v
          FilePath -> IO ()
putStrLn forall a b. (a -> b) -> a -> b
$ [FilePath] -> FilePath
unwords [FilePath
"dotenv:", FilePath
k, FilePath
"set"] -- DEBUG

parseDotEnv :: String -- ^ contents of the @.env@ file
            -> Maybe [(String, String)]
parseDotEnv :: FilePath -> Maybe [(FilePath, FilePath)]
parseDotEnv = forall (t :: * -> *) a.
Foldable t =>
ReadP (t a) -> FilePath -> Maybe (t a)
parse1 ReadP [(FilePath, FilePath)]
keyValues

keyValues :: P.ReadP [(String, String)]
keyValues :: ReadP [(FilePath, FilePath)]
keyValues = forall a sep. ReadP a -> ReadP sep -> ReadP [a]
P.sepBy1 ReadP (FilePath, FilePath)
keyValue (Char -> ReadP Char
P.char Char
'\n') forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* Char -> ReadP Char
P.char Char
'\n'

keyValue :: P.ReadP (String, String)
keyValue :: ReadP (FilePath, FilePath)
keyValue = do
  FilePath
k <- ReadP FilePath
keyP
  forall (f :: * -> *) a. Functor f => f a -> f ()
void forall a b. (a -> b) -> a -> b
$ Char -> ReadP Char
P.char Char
'='
  FilePath
v <- ReadP FilePath
valueP
  forall (f :: * -> *) a. Applicative f => a -> f a
pure (FilePath
k, FilePath
v)

keyP, valueP :: P.ReadP String
keyP :: ReadP FilePath
keyP = (Char -> Bool) -> ReadP FilePath
P.munch (forall a. Eq a => a -> a -> Bool
/= Char
'=')
valueP :: ReadP FilePath
valueP = (Char -> Bool) -> ReadP FilePath
P.munch (forall a. Eq a => a -> a -> Bool
/= Char
'\n')

-- parse :: P.ReadP b -> String -> Maybe b
-- parse p str = fst <$> (listToMaybe $ P.readP_to_S p str)

parse1 :: Foldable t => P.ReadP (t a) -> String -> Maybe (t a)
parse1 :: forall (t :: * -> *) a.
Foldable t =>
ReadP (t a) -> FilePath -> Maybe (t a)
parse1 ReadP (t a)
p FilePath
str = forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap forall a b. (a, b) -> a
fst forall a b. (a -> b) -> a -> b
$ forall a. [a] -> Maybe a
listToMaybe forall a b. (a -> b) -> a -> b
$ forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn (forall a. a -> Down a
Down forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall (t :: * -> *) a. Foldable t => t a -> Int
length forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a, b) -> a
fst) (forall a. ReadP a -> ReadS a
P.readP_to_S ReadP (t a)
p FilePath
str)