{-|
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 reading
    -- 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 c
            ++ " on port "
            ++ show $ port c
            ++ ". Debug mode is "
            ++ if debug c then "on" else "off"
            ++ "."
@
-}

        envAsBool
      , envAsInt
      , envAsInteger
      , envAsString ) where

import Control.Exception ( try )
import System.Environment ( getEnv )
import Text.Read ( readMaybe )
import Data.Maybe ( fromMaybe )
import Data.Function ( (&) )
import qualified Data.Char as Char


{-| Get an environment variable as a string, with a default fallback value -}
envAsString :: String
            -- ^Name of environment varaiable
            -> String
            -- ^Fallback value
            -> IO String
            -- ^Result
envAsString name defaultValue =
    (try $ getEnv name :: IO (Either IOError String)) >>= choice
    where
        choice (Left _)    = return defaultValue
        choice (Right val) = return val


{-| Get an environment variable as an int, with a default fallback value -}
envAsInt :: String
         -- ^Name of environment variable
         -> Int
         -- ^Fallback value
         -> IO Int
         -- ^Result
envAsInt name defaultValue =
    envAsString name "" >>= choice
    where
        choice v
          | v == ""   = return defaultValue
          | otherwise = (readMaybe v :: Maybe Int) & fromMaybe defaultValue & return


{-| Get an environment variable as an integer, with a default fallback value -}
envAsInteger :: String
             -- ^Name of environment variable
             -> Integer
             -- ^Fallback value
             -> IO Integer
             -- ^Result
envAsInteger name defaultValue =
    envAsString name "" >>= choice
    where
        choice v | v == ""   = return defaultValue
                 | otherwise = (readMaybe v :: Maybe Integer) & fromMaybe defaultValue & return


{-| Get an environment variable as a boolean, with a default fallback value -}
envAsBool :: String
          -- ^Name of environment variable
          -> Bool
          -- ^Fallback value
          -> IO Bool
          -- ^Result
envAsBool name defaultValue =
    envAsString name "" >>= \val ->
        if val == ""
           then return defaultValue
           else (readMaybe $ capitalize val :: Maybe Bool)
                & fromMaybe defaultValue
                & return


{-| 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 [] = []
capitalize (head':tail') =
    Char.toUpper head' : map Char.toLower tail'