module IHP.EnvVar
( env
, envOrDefault
, envOrNothing
, hasEnvVar
, EnvVarReader (..)
)
where

import Prelude
import Data.ByteString (ByteString)
import Data.Text (Text)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Maybe (isJust, fromMaybe)
import Data.String.Conversions (cs)
import IHP.HaskellSupport (textToInt)
import qualified System.Posix.Env.ByteString as Posix
import Network.Socket (PortNumber)
import IHP.Environment

-- | Returns a env variable. The raw string
-- value is parsed before returning it. So the return value type depends on what
-- you expect (e.g. can be Text, Int some custom type).
--
-- When the parameter is missing or cannot be parsed, an error is raised and
-- the app startup is aborted. Use 'envOrDefault' when you want to get a
-- default value instead of an error, or 'paramOrNothing' to get @Nothing@
-- when the env variable is missing.
--
-- You can define a custom env variable parser by defining a 'EnvVarReader' instance.
--
-- __Example:__ Accessing a env var PORT.
--
-- Let's say an env var PORT is set to 1337
--
-- > export PORT=1337
--
-- We can read @PORT@ like this:
--
-- > port <- env @Int "PORT"
--
-- __Example:__ Missing env vars
--
-- Let's say the @PORT@ env var is not defined. In that case we'll get an
-- error when trying to access it:
--
-- >>> port <- env @Int "PORT"
-- "Env var 'PORT' not set, but it's required for the app to run"
--
env :: forall result monad. (MonadIO monad) => EnvVarReader result => ByteString -> monad result
env :: forall result (monad :: * -> *).
(MonadIO monad, EnvVarReader result) =>
ByteString -> monad result
env ByteString
name = ByteString -> result -> monad result
forall (monad :: * -> *) result.
(MonadIO monad, EnvVarReader result) =>
ByteString -> result -> monad result
envOrDefault ByteString
name ([Char] -> result
forall a. HasCallStack => [Char] -> a
error ([Char]
"Env var '" [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> ByteString -> [Char]
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
name [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
"' not set, but it's required for the app to run"))
{-# INLINE env #-}

envOrDefault :: (MonadIO monad) => EnvVarReader result => ByteString -> result -> monad result
envOrDefault :: forall (monad :: * -> *) result.
(MonadIO monad, EnvVarReader result) =>
ByteString -> result -> monad result
envOrDefault ByteString
name result
defaultValue = result -> Maybe result -> result
forall a. a -> Maybe a -> a
fromMaybe result
defaultValue (Maybe result -> result) -> monad (Maybe result) -> monad result
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ByteString -> monad (Maybe result)
forall (monad :: * -> *) result.
(MonadIO monad, EnvVarReader result) =>
ByteString -> monad (Maybe result)
envOrNothing ByteString
name
{-# INLINE envOrDefault #-}

envOrNothing :: (MonadIO monad) => EnvVarReader result => ByteString -> monad (Maybe result)
envOrNothing :: forall (monad :: * -> *) result.
(MonadIO monad, EnvVarReader result) =>
ByteString -> monad (Maybe result)
envOrNothing ByteString
name = IO (Maybe result) -> monad (Maybe result)
forall a. IO a -> monad a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Maybe result) -> monad (Maybe result))
-> IO (Maybe result) -> monad (Maybe result)
forall a b. (a -> b) -> a -> b
$ (ByteString -> result) -> Maybe ByteString -> Maybe result
forall a b. (a -> b) -> Maybe a -> Maybe b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ByteString -> result
parseString (Maybe ByteString -> Maybe result)
-> IO (Maybe ByteString) -> IO (Maybe result)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ByteString -> IO (Maybe ByteString)
Posix.getEnv ByteString
name
    where
        parseString :: ByteString -> result
parseString ByteString
string = case ByteString -> Either Text result
forall valueType.
EnvVarReader valueType =>
ByteString -> Either Text valueType
envStringToValue ByteString
string of
            Left Text
errorMessage -> [Char] -> result
forall a. HasCallStack => [Char] -> a
error ([Char]
"Env var '" [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> ByteString -> [Char]
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
name [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
"' is invalid: " [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> Text -> [Char]
forall a b. ConvertibleStrings a b => a -> b
cs Text
errorMessage)
            Right result
value -> result
value
{-# INLINE envOrNothing #-}

hasEnvVar :: (MonadIO monad) => ByteString -> monad Bool
hasEnvVar :: forall (monad :: * -> *). MonadIO monad => ByteString -> monad Bool
hasEnvVar ByteString
name = IO Bool -> monad Bool
forall a. IO a -> monad a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO do
    value :: Maybe ByteString <- ByteString -> IO (Maybe ByteString)
forall (monad :: * -> *) result.
(MonadIO monad, EnvVarReader result) =>
ByteString -> monad (Maybe result)
envOrNothing ByteString
name
    pure (isJust value)
{-# INLINE hasEnvVar #-}

class EnvVarReader valueType where
    envStringToValue :: ByteString -> Either Text valueType

instance EnvVarReader Environment where
    envStringToValue :: ByteString -> Either Text Environment
envStringToValue ByteString
"Production"  = Environment -> Either Text Environment
forall a b. b -> Either a b
Right Environment
Production
    envStringToValue ByteString
"Development" = Environment -> Either Text Environment
forall a b. b -> Either a b
Right Environment
Development
    envStringToValue ByteString
otherwise     = Text -> Either Text Environment
forall a b. a -> Either a b
Left Text
"Should be set to 'Development' or 'Production'"

instance EnvVarReader Int where
    envStringToValue :: ByteString -> Either Text Int
envStringToValue ByteString
string = case Text -> Maybe Int
textToInt (ByteString -> Text
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
string) of
        Just Int
integer -> Int -> Either Text Int
forall a b. b -> Either a b
Right Int
integer
        Maybe Int
Nothing -> Text -> Either Text Int
forall a b. a -> Either a b
Left (Text
"Expected integer, got " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> ByteString -> Text
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
string)

instance EnvVarReader Text where
    envStringToValue :: ByteString -> Either Text Text
envStringToValue ByteString
string = Text -> Either Text Text
forall a b. b -> Either a b
Right (ByteString -> Text
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
string)

instance EnvVarReader String where
    envStringToValue :: ByteString -> Either Text [Char]
envStringToValue ByteString
string = [Char] -> Either Text [Char]
forall a b. b -> Either a b
Right (ByteString -> [Char]
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
string)

instance EnvVarReader ByteString where
    envStringToValue :: ByteString -> Either Text ByteString
envStringToValue ByteString
string = ByteString -> Either Text ByteString
forall a b. b -> Either a b
Right ByteString
string

instance EnvVarReader Bool where
    envStringToValue :: ByteString -> Either Text Bool
envStringToValue ByteString
"1"       = Bool -> Either Text Bool
forall a b. b -> Either a b
Right Bool
True
    envStringToValue ByteString
"0"       = Bool -> Either Text Bool
forall a b. b -> Either a b
Right Bool
False
    envStringToValue ByteString
otherwise = Text -> Either Text Bool
forall a b. a -> Either a b
Left Text
"Should be set to '1' or '0'"

-- | Allow reading the env var of an SMTP port number.
instance EnvVarReader PortNumber where
    envStringToValue :: ByteString -> Either Text PortNumber
envStringToValue ByteString
string = case Text -> Maybe Int
textToInt (ByteString -> Text
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
string) of
        Just Int
integer -> PortNumber -> Either Text PortNumber
forall a b. b -> Either a b
Right (PortNumber -> Either Text PortNumber)
-> PortNumber -> Either Text PortNumber
forall a b. (a -> b) -> a -> b
$ Int -> PortNumber
convertIntToPortNumber Int
integer
        Maybe Int
Nothing -> Text -> Either Text PortNumber
forall a b. a -> Either a b
Left (Text
"Expected integer to be used as a Port number, got " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> ByteString -> Text
forall a b. ConvertibleStrings a b => a -> b
cs ByteString
string)

convertIntToPortNumber :: Int -> PortNumber
convertIntToPortNumber :: Int -> PortNumber
convertIntToPortNumber Int
int = Int -> PortNumber
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int
int :: Int) :: PortNumber