{-# LANGUAGE ScopedTypeVariables #-}
{-|
Module      : System.Environment.MrEnv
Description : Read environment variables, with default fallbacks
Copyright   : 2020 Christian Rocha
License     : MIT
Maintainer  : christian@rocha.is
Stability   : experimental
Portability : POSIX

A simple way to read environment variables.
-}

module System.Environment.MrEnv (
{-|
Read environment variables with fallback values.

A simple example with @do@ notation:

@
import System.Environment.MrEnv ( envAsBool, envAsInt, envAsInteger, envAsString )

main :: IO ()
main = do

    -- Get a string, with a fallback value if nothing is set.
    host <- envAsString \"HOST\" "localhost"

    -- Get an int. If you need an integer instead you could also use envAsInteger.
    port <- envAsInt \"PORT\" 8000

    -- Get a boolean. Here we're expecting the environment variable to read
    -- something along the lines of "true", \"TRUE\", \"True\", "truE" and so on.
    debug <- envAsBool \"DEBUG\" False

    putStrLn $
        "Let's connect to "
        ++ host
        ++ " on port "
        ++ show port
        ++ ". Debug mode is "
        ++ if debug then "on" else "off"
        ++ "."
@

You can also read into a record:

@
import System.Environment.MrEnv ( envAsBool, envAsInt, envAsInteger, envAsString )

data Config =
    Config { host  :: String
           , port  :: Int
           , debug :: Bool
           }

getConfig :: IO Config
getConfig = Config
    \<$\> envAsString \"HOST\" "localhost"
    \<*\> envAsInt \"PORT\" 8000
    \<*\> envAsBool \"DEBUG\" False

main :: IO ()
main =
    getConfig >>= \conf ->
        putStrLn $
            "Let's connect to "
            ++ host conf
            ++ " on port "
            ++ show $ port conf
            ++ ". Debug mode is "
            ++ if debug conf then "on" else "off"
            ++ "."
@
-}

        envAsBool
      , envAsInt
      , envAsInteger
      , envAsString ) where

import Control.Exception ( catch )
import Data.Char ( toLower, toUpper )
import Data.Maybe ( fromMaybe )
import System.Environment ( getEnv )
import Text.Read ( readMaybe )



{-| Get an environment variable, with a fallback value and the ability to
   preprocess the raw string before @read@ing it. -}
envAs' :: forall a. Read a
       => String              -- ^Name of environment variable
       -> (String -> Maybe a) -- ^Preprocessing function
       -> a                   -- ^Fallback value
       -> IO a                -- ^Result
envAs' :: forall a. Read a => String -> (String -> Maybe a) -> a -> IO a
envAs' String
name String -> Maybe a
prep a
defaultValue =
    forall e a. Exception e => IO a -> (e -> IO a) -> IO a
catch (forall a. a -> Maybe a -> a
fromMaybe a
defaultValue forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Maybe a
prep forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> IO String
getEnv String
name)
          ((forall a b. a -> b -> a
const forall a b. (a -> b) -> a -> b
$ forall (f :: * -> *) a. Applicative f => a -> f a
pure a
defaultValue) :: IOError -> IO a)


{-| Get an environment variable, with a fallback value. -}
envAs :: forall a. Read a
      => String -- ^Name of environment variable
      -> a      -- ^Fallback value
      -> IO a   -- ^Result
envAs :: forall a. Read a => String -> a -> IO a
envAs String
name =
    forall a. Read a => String -> (String -> Maybe a) -> a -> IO a
envAs' String
name forall a. Read a => String -> Maybe a
readMaybe

{-| Get an environment variable as a @'String'@, with a fallback value.

    Internally we use this instead of @'envAs\' String@, because 'readMaybe'
    fails unless 'String's are doubly-quoted (i.e. '"\"value\""'. -}
envAsString :: String    -- ^Name of environment variable
            -> String    -- ^Fallback value
            -> IO String -- ^Result
envAsString :: String -> String -> IO String
envAsString String
name =
    forall a. Read a => String -> (String -> Maybe a) -> a -> IO a
envAs' String
name (forall a. Read a => String -> Maybe a
readMaybe forall b c a. (b -> c) -> (a -> b) -> a -> c
. (\String
v -> String
"\"" forall a. [a] -> [a] -> [a]
++ String
v forall a. [a] -> [a] -> [a]
++ String
"\""))


{-| Get an environment variable as an @'Int'@, with a fallback value. -}
envAsInt :: String -- ^Name of environment variable
         -> Int    -- ^Fallback value
         -> IO Int -- ^Result
envAsInt :: String -> Int -> IO Int
envAsInt =
    forall a. Read a => String -> a -> IO a
envAs


{-| Get an environment variable as an @'Integer'@, with a fallback value. -}
envAsInteger :: String     -- ^Name of environment variable
             -> Integer    -- ^Fallback value
             -> IO Integer -- ^Result
envAsInteger :: String -> Integer -> IO Integer
envAsInteger =
    forall a. Read a => String -> a -> IO a
envAs


{-| Get an environment variable as a boolean, with a fallback value.

    Internally we use this instead of @'envAs\' Bool@, as it handles
    nonstandard capitalization. -}
envAsBool :: String    -- ^Name of environment variable
          -> Bool    -- ^Fallback value
          -> IO Bool -- ^Result
envAsBool :: String -> Bool -> IO Bool
envAsBool String
name =
    forall a. Read a => String -> (String -> Maybe a) -> a -> IO a
envAs' String
name (forall a. Read a => String -> Maybe a
readMaybe forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> String
capitalize)


{-| Capitalize the first character in a string and make all other characters
    lowercase. In our case we're doing this so values like like TRUE, true,
    True, and truE all become "True," which can then be coerced to a boolean. -}
capitalize :: String -> String
capitalize :: String -> String
capitalize [] = []
capitalize (Char
head':String
tail') = Char -> Char
toUpper Char
head' forall a. a -> [a] -> [a]
: forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toLower String
tail'