packages feed

nri-redis-0.4.1.1: src/Redis/Settings.hs

module Redis.Settings
  ( Settings (..),
    ClusterMode (..),
    DefaultExpiry (..),
    QueryTimeout (..),
    MaxKeySize (..),
    decoder,
    decoderWithEnvVarPrefix,
    decoderWithCustomConnectionString,
  )
where

import qualified Data.List.NonEmpty as NonEmpty
import Data.Text.Encoding (encodeUtf8)
import Database.Redis hiding (Ok)
import qualified Environment
import qualified Text
import qualified Text.URI as URI
import Prelude (Either (Left, Right), foldr, fromIntegral, id, pure)

data ClusterMode = Cluster | NotCluster
  deriving (Show)

-- | Settings required to initiate a redis connection.
data Settings = Settings
  { -- | Full redis connection string.
    --
    -- Default env var name is REDIS_CONNECTION_STRING
    -- default is "redis://localhost:6379"
    connectionInfo :: ConnectInfo,
    -- | Set to 1 for cluster, everything else is not.
    --
    -- Default env var name is REDIS_CLUSTER
    -- Default is 0
    clusterMode :: ClusterMode,
    -- | Set a default amount of seconds after which all keys touched by this
    -- handler will expire. The expire time of a key is reset every time it is
    -- read or written. A value of 0 means no default expiry.
    --
    -- Default env var name is REDIS_DEFAULT_EXPIRY_SECONDS
    -- default is 0
    defaultExpiry :: DefaultExpiry,
    -- | 0 means no timeout, every other value is a timeout in milliseconds.
    --
    -- Default env var name is REDIS_QUERY_TIMEOUT_MILLISECONDS
    -- default is 1000
    queryTimeout :: QueryTimeout,
    maxKeySize :: MaxKeySize
  }
  deriving (Show)

data MaxKeySize = NoMaxKeySize | MaxKeySize Int
  deriving (Show)

data DefaultExpiry = NoDefaultExpiry | ExpireKeysAfterSeconds Int
  deriving (Show)

data QueryTimeout = NoQueryTimeout | TimeoutQueryAfterMilliseconds Int
  deriving (Show)

-- | decodes Settings from environmental variables
decoder :: Environment.Decoder Settings
decoder =
  decoderWithEnvVarPrefix ""

-- | decodes Settings from environmental variables with custom connection string
decoderWithCustomConnectionString :: Text -> Environment.Decoder Settings
decoderWithCustomConnectionString connectionStringEnvVar =
  map5
    Settings
    (decoderConnectInfo connectionStringEnvVar)
    (decoderClusterMode "")
    (decoderDefaultExpiry "")
    (decoderQueryTimeout "")
    (decoderMaxKeySize "")

-- | decodes Settings from environmental variables prefixed with a Text
-- >>> decoderWithEnvVarPrefix "WORKER_"
decoderWithEnvVarPrefix :: Text -> Environment.Decoder Settings
decoderWithEnvVarPrefix prefix =
  map5
    Settings
    (decoderConnectInfo (prefix ++ "REDIS_CONNECTION_STRING"))
    (decoderClusterMode prefix)
    (decoderDefaultExpiry prefix)
    (decoderQueryTimeout prefix)
    (decoderMaxKeySize prefix)

decoderClusterMode :: Text -> Environment.Decoder ClusterMode
decoderClusterMode prefix =
  Environment.variable
    Environment.Variable
      { Environment.name = prefix ++ "REDIS_CLUSTER",
        Environment.description = "Set to 1 for cluster, everything else is not",
        Environment.defaultValue = "0"
      }
    ( Environment.custom
        Environment.text
        ( \str ->
            if Text.trim str == "1"
              then Ok Cluster
              else Ok NotCluster
        )
    )

decoderConnectInfo :: Text -> Environment.Decoder ConnectInfo
decoderConnectInfo envVarName =
  Environment.variable
    Environment.Variable
      { Environment.name = envVarName,
        Environment.description = "Full redis connection string",
        Environment.defaultValue = "redis://localhost:6379"
      }
    ( Environment.custom
        Environment.uri
        ( \uri ->
            case map URI.unRText (URI.uriScheme uri) of
              Just "redis" -> parseRedisSchemeURI uri
              Just "redis+unix" -> parseRedisSocketSchemeURI uri
              Just unrecognizedScheme -> Err ("Invalid URI scheme for connection string: " ++ unrecognizedScheme)
              Nothing -> Err "URI scheme missing from connection string"
        )
    )

parseRedisSchemeURI :: URI.URI -> Result Text ConnectInfo
parseRedisSchemeURI uri =
  case parseConnectInfo (URI.renderStr uri) of
    Left parseError -> Err ("Invalid Redis connection string: " ++ Text.fromList parseError)
    Right info' -> Ok info'

parseRedisSocketSchemeURI :: URI.URI -> Result Text ConnectInfo
parseRedisSocketSchemeURI uri =
  let uriPathTextFromURI =
        case URI.uriPath uri of
          Nothing -> Err "URI path missing from connection string"
          Just (_, uriPathSegments) ->
            uriPathSegments
              |> map URI.unRText
              |> NonEmpty.intersperse "/"
              |> foldr (++) ""
              |> (if URI.isPathAbsolute uri then ("/" ++) else id)
              |> Ok

      dbNumFromParams queryParams =
        case queryParams of
          [] -> Ok 0
          URI.QueryParam key value : rest ->
            if URI.unRText key == "db"
              then case Text.toInt (URI.unRText value) of
                Nothing -> Err "Expected an integer for db in connection string"
                Just dbNum -> Ok (fromIntegral dbNum)
              else dbNumFromParams rest
          _ : rest -> dbNumFromParams rest

      maybePasswordFromURI =
        case URI.uriAuthority uri of
          Right (URI.Authority (Just (URI.UserInfo _ (Just passwordRText))) _ _) ->
            Just (encodeUtf8 <| URI.unRText passwordRText)
          _ -> Nothing
   in do
        uriPathText <- uriPathTextFromURI
        dbNum <- dbNumFromParams (URI.uriQuery uri)
        pure <|
          defaultConnectInfo
            { connectPort = UnixSocket (Text.toList uriPathText),
              connectDatabase = dbNum,
              connectAuth = maybePasswordFromURI
            }

decoderDefaultExpiry :: Text -> Environment.Decoder DefaultExpiry
decoderDefaultExpiry prefix =
  Environment.variable
    Environment.Variable
      { Environment.name = prefix ++ "REDIS_DEFAULT_EXPIRY_SECONDS",
        Environment.description = "Set a default amount of seconds after which all keys touched by this handler will expire. The expire time of a key is reset every time it is read or written. A value of 0 means no default expiry.",
        Environment.defaultValue = "0"
      }
    Environment.int
    |> map
      ( \secs ->
          if secs == 0
            then NoDefaultExpiry
            else ExpireKeysAfterSeconds secs
      )

decoderQueryTimeout :: Text -> Environment.Decoder QueryTimeout
decoderQueryTimeout prefix =
  Environment.variable
    Environment.Variable
      { Environment.name = prefix ++ "REDIS_QUERY_TIMEOUT_MILLISECONDS",
        Environment.description = "0 means no timeout, every other value is a timeout in milliseconds.",
        Environment.defaultValue = "1000"
      }
    ( Environment.custom
        Environment.int
        ( \milliseconds ->
            if milliseconds <= 0
              then Ok NoQueryTimeout
              else Ok (TimeoutQueryAfterMilliseconds milliseconds)
        )
    )

decoderMaxKeySize :: Text -> Environment.Decoder MaxKeySize
decoderMaxKeySize prefix =
  Environment.variable
    Environment.Variable
      { Environment.name = prefix ++ "REDIS_MAX_KEY_SIZE",
        Environment.description = "0 means no max key size, every other value is a max key size.",
        Environment.defaultValue = "0"
      }
    ( Environment.custom
        Environment.int
        ( \maxKeySize ->
            if maxKeySize <= 0
              then Ok NoMaxKeySize
              else Ok (MaxKeySize maxKeySize)
        )
    )