mmzk-env-0.4.0.0: src/Data/Env/TypeParser.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE CPP #-}
-- |
-- Module: Data.Env.TypeParser
-- Description: Type class that provides parsers for types.
--
-- This module provides a type class 'TypeParser' that provides parsers for
-- different types. The parsers are used to parse environment variables from
-- their string representation.
module Data.Env.TypeParser (
TypeParser (..),
) where
import Data.Int (Int8, Int16, Int32, Int64)
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Tuple ( Solo(..) )
import Data.Word (Word8, Word16, Word32, Word64)
import GHC.Generics
import Text.Gigaparsec qualified as P
import Text.Gigaparsec.Char qualified as P
import Text.Gigaparsec.Combinator qualified as P
import Text.Gigaparsec.Errors.ErrorGen qualified as P
import Text.Gigaparsec.Errors.Combinator qualified as P
import Text.Gigaparsec.Token.Descriptions qualified as L
import Text.Gigaparsec.Token.Lexer qualified as L
-- | Type class for parsers associated with types.
class TypeParser a where
-- | Parse a value by its string representation.
parseType :: String -> Either String a
default parseType
:: (Generic a, GTypeParser (Rep a)) => String -> Either String a
parseType s = to <$> gTypeParser s
{-# INLINE parseType #-}
-- | Result to use when the environment variable is absent (empty string).
--
-- The default signals that the variable is required. Override this for
-- types that have a meaningful absence value, e.g. 'Maybe' returns
-- 'Right' 'Nothing'.
parseMissing :: Either String a
parseMissing = Left "missing required environment variable"
{-# INLINE parseMissing #-}
-- | Parse a value, converting 'Either' to 'Maybe' and dropping any error messages.
parseType' :: String -> Maybe a
parseType' str = case parseType str of
Right val -> Just val
Left _ -> Nothing
{-# INLINE parseType' #-}
-- | Required (non-empty) String field.
--
-- In POSIX systems, an empty env variable is equivalent to an undefined env
-- variable. To ensure consistency across platforms, we require that all
-- environment variables are non-empty.
instance TypeParser String where
parseType :: String -> Either String String
parseType = parse (P.label (Set.singleton "a non-empty string") (P.some P.item))
{-# INLINE parseType #-}
-- | Required @Integer@ field (parsed from String).
instance TypeParser Integer where
parseType :: String -> Either String Integer
parseType = parse (P.label (Set.singleton "an integer") (L.decimal integerParser))
{-# INLINE parseType #-}
-- | Required @Int@ field (parsed from String).
instance TypeParser Int where
parseType :: String -> Either String Int
parseType = (fromInteger <$>) . parse do
P.filterSWith (simpleErrorGen boundsMsg) validateInt
$ P.label (Set.singleton "an integer") (L.decimal integerParser)
where
validateInt n = n >= fromIntegral @Int minBound
&& n <= fromIntegral @Int maxBound
boundsMsg = "integer out of range for Int (valid: "
++ show (minBound :: Int) ++ " to " ++ show (maxBound :: Int) ++ ")"
{-# INLINE parseType #-}
-- | Required @Word@ field (parsed from String).
instance TypeParser Word where
parseType :: String -> Either String Word
parseType = (fromInteger <$>) . parse do
P.filterSWith (simpleErrorGen boundsMsg) validateWord
$ P.label (Set.singleton "a natural number") (L.decimal naturalParser)
where
validateWord n = n <= fromIntegral @Word maxBound
boundsMsg = "natural number out of range for Word (valid: 0 to "
++ show (maxBound :: Word) ++ ")"
{-# INLINE parseType #-}
-- | Required @Bool@ field (parsed from String).
--
-- Accepts only @True@ or @False@ (case-sensitive). For a more lenient
-- boolean parser (accepting @true@, @1@, @t@, etc.) use the @DefaultBool@
-- witness.
instance TypeParser Bool where
parseType :: String -> Either String Bool
parseType = parse $ P.label (Set.fromList ["True", "False"])
$ P.choice [P.string "True" P.$> True, P.string "False" P.$> False]
{-# INLINE parseType #-}
-- | Required @Int8@ field (parsed from String).
instance TypeParser Int8 where
parseType :: String -> Either String Int8
parseType = parse (P.label (Set.singleton "an integer") (L.decimal8 integerParser))
{-# INLINE parseType #-}
-- | Required @Int16@ field (parsed from String).
instance TypeParser Int16 where
parseType :: String -> Either String Int16
parseType = parse (P.label (Set.singleton "an integer") (L.decimal16 integerParser))
{-# INLINE parseType #-}
-- | Required @Int32@ field (parsed from String).
instance TypeParser Int32 where
parseType :: String -> Either String Int32
parseType = parse (P.label (Set.singleton "an integer") (L.decimal32 integerParser))
{-# INLINE parseType #-}
-- | Required @Int64@ field (parsed from String).
instance TypeParser Int64 where
parseType :: String -> Either String Int64
parseType = parse (P.label (Set.singleton "an integer") (L.decimal64 integerParser))
{-# INLINE parseType #-}
-- | Required @Word8@ field (parsed from String).
instance TypeParser Word8 where
parseType :: String -> Either String Word8
parseType = parse (P.label (Set.singleton "a natural number") (L.decimal8 naturalParser))
{-# INLINE parseType #-}
-- | Required @Word16@ field (parsed from String).
instance TypeParser Word16 where
parseType :: String -> Either String Word16
parseType = parse (P.label (Set.singleton "a natural number") (L.decimal16 naturalParser))
{-# INLINE parseType #-}
-- | Required @Word32@ field (parsed from String).
instance TypeParser Word32 where
parseType :: String -> Either String Word32
parseType = parse (P.label (Set.singleton "a natural number") (L.decimal32 naturalParser))
{-# INLINE parseType #-}
-- | Required @Word64@ field (parsed from String).
instance TypeParser Word64 where
parseType :: String -> Either String Word64
parseType = parse (P.label (Set.singleton "a natural number") (L.decimal64 naturalParser))
{-# INLINE parseType #-}
-- | Required strict @Text@ field (parsed from String).
instance TypeParser T.Text where
parseType :: String -> Either String T.Text
parseType = fmap T.pack . parseType
{-# INLINE parseType #-}
-- | Required lazy @Text@ field (parsed from String).
instance TypeParser TL.Text where
parseType :: String -> Either String TL.Text
parseType = fmap TL.pack . parseType
{-# INLINE parseType #-}
-- | Required @()@ field (parsed from String).
instance TypeParser () where
parseType :: String -> Either String ()
parseType = parse (P.label (Set.singleton "\"()\"") (P.string "()" P.$> ()))
{-# INLINE parseType #-}
-- | Solo fields isomorphic to the original (@Solo a@).
instance TypeParser a => TypeParser (Solo a) where
parseType :: String -> Either String (Solo a)
#if MIN_VERSION_base(4,18,0)
parseType s = MkSolo <$> parseType s
#else
parseType s = Solo <$> parseType s
#endif
{-# INLINE parseType #-}
-- | Optional fields (@Maybe a@).
--
-- An absent (empty) value maps to 'Nothing'; a non-empty value is parsed
-- by the inner 'TypeParser'.
instance TypeParser a => TypeParser (Maybe a) where
-- | An absent optional field is 'Nothing', not an error.
parseMissing :: Either String (Maybe a)
parseMissing = Right Nothing
parseType :: String -> Either String (Maybe a)
parseType "" = Right Nothing
parseType s = Just <$> parseType s
{-# INLINE parseType #-}
--------------------------------------------------------------------------------
-- Generic instances
--------------------------------------------------------------------------------
-- | Generic validation class.
class GTypeParser f where
gTypeParser :: String -> Either String (f p)
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
simpleLexeme :: L.Lexeme
simpleLexeme = L.nonlexeme (L.mkLexer L.plain)
{-# INLINE simpleLexeme #-}
integerParser :: L.IntegerParsers L.CanHoldSigned
integerParser = L.integer simpleLexeme
{-# INLINE integerParser #-}
naturalParser :: L.IntegerParsers L.CanHoldUnsigned
naturalParser = L.natural simpleLexeme
{-# INLINE naturalParser #-}
parseResultToEither :: P.Result String a -> Either String a
parseResultToEither (P.Failure e) = Left e
parseResultToEither (P.Success a) = Right a
{-# INLINE parseResultToEither #-}
parse :: P.Parsec a -> String -> Either String a
parse parser = parseResultToEither . P.parse (parser >>= (P.eof P.$>))
{-# INLINE parse #-}
simpleErrorGen :: String -> P.ErrorGen a
simpleErrorGen msg = case P.vanillaGen of
P.VanillaGen {..} -> P.VanillaGen { reason = const (Just msg), .. }
impossible -> impossible
{-# INLINE simpleErrorGen #-}