packages feed

mmzk-env-0.2.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.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 #-}

  -- | Parse a value, converting 'Either' to 'Maybe' and dropping any error messages.
  --
  -- This is a convenience function that calls 'parseType' and converts the result
  -- from 'Either String a' to 'Maybe a', discarding the error message on failure.
  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.some P.item)
  {-# INLINE parseType #-}

-- | Required @Integer@ field (parsed from String).
instance TypeParser Integer where
  parseType :: String -> Either String Integer
  parseType = parse (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 "Int out of bound")
                    validateInt
                    (L.decimal integerParser)
    where
      validateInt n = n >= fromIntegral @Int minBound
                   && n <= fromIntegral @Int maxBound
  {-# INLINE parseType #-}

-- | Required @Word@ field (parsed from String).
instance TypeParser Word where
  parseType :: String -> Either String Word
  parseType = (fromInteger <$>) . parse do
      P.filterSWith (simpleErrorGen "Word out of bound")
                    validateWord
                    (L.decimal naturalParser)
    where
      validateWord n = n <= fromIntegral @Word maxBound
  {-# INLINE parseType #-}

-- | Required @Bool@ field (parsed from String).
instance TypeParser Bool where
  parseType :: String -> Either String Bool
  parseType = parse do
    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 (L.decimal8 integerParser)
  {-# INLINE parseType #-}

-- | Required @Int16@ field (parsed from String).
instance TypeParser Int16 where
  parseType :: String -> Either String Int16
  parseType = parse (L.decimal16 integerParser)
  {-# INLINE parseType #-}

-- | Required @Int32@ field (parsed from String).
instance TypeParser Int32 where
  parseType :: String -> Either String Int32
  parseType = parse (L.decimal32 integerParser)
  {-# INLINE parseType #-}

-- | Required @Int64@ field (parsed from String).
instance TypeParser Int64 where
  parseType :: String -> Either String Int64
  parseType = parse (L.decimal64 integerParser)
  {-# INLINE parseType #-}

-- | Required @Word8@ field (parsed from String).
instance TypeParser Word8 where
  parseType :: String -> Either String Word8
  parseType = parse (L.decimal8 naturalParser)
  {-# INLINE parseType #-}

-- | Required @Word16@ field (parsed from String).
instance TypeParser Word16 where
  parseType :: String -> Either String Word16
  parseType = parse (L.decimal16 naturalParser)
  {-# INLINE parseType #-}

-- | Required @Word32@ field (parsed from String).
instance TypeParser Word32 where
  parseType :: String -> Either String Word32
  parseType = parse (L.decimal32 naturalParser)
  {-# INLINE parseType #-}

-- | Required @Word64@ field (parsed from String).
instance TypeParser Word64 where
  parseType :: String -> Either String Word64
  parseType = parse (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.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@).
instance TypeParser a => TypeParser (Maybe a) where
  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 (show 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 #-}