mmzk-env (empty) → 0.1.0.0
raw patch · 9 files changed
+690/−0 lines, 9 filesdep +basedep +containersdep +gigaparsec
Dependencies added: base, containers, gigaparsec, hspec
Files
- CHANGELOG.md +6/−0
- LICENSE +21/−0
- README.md +36/−0
- mmzk-env.cabal +76/−0
- src/Data/Env.hs +28/−0
- src/Data/Env/ExtractFields.hs +113/−0
- src/Data/Env/RecordParser.hs +62/−0
- src/Data/Env/TypeParser.hs +183/−0
- test/Spec.hs +165/−0
+ CHANGELOG.md view
@@ -0,0 +1,6 @@+# Revision history for mmzk-env+++## 0.1.0.0 -- 2025-08-03++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2025 Yitang Chen++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,36 @@+# mmzk-env++mmzk-env is a library for reading environment variables into a user-defined data+type. It provides a type-safe way to parse and validate environment variables,+ensuring that they conform to the expected types.++## Quick Start++```Haskell+module Data.Env where++import Control.Monad.IO.Class+import Data.Env.ExtractFields+import Data.Env.RecordParser+import GHC.Generics++-- | Example: Define an environment schema+data Config = Config+ { port :: Int+ , name :: String+ , mainHost :: String+ , debug :: Maybe Bool }+ deriving (Show, Generic, EnvSchema)++-- | Run the validation+main :: IO ()+main = do+ errOrEnv <- validateEnv @Config+ case errOrEnv of+ Left err -> putStrLn $ "Validation failed: " ++ err+ Right cfg -> putStrLn $ "Config loaded successfully: " ++ show cfg+```++With this setup, it requires the environment variables `PORT`, `NAME`, `MAIN_HOST`, and `DEBUG` to be set according to the types defined in the `Config` data type. The library will automatically parse these variables and validate them against the schema.++If any variable is missing or has an incorrect type, the validation will fail, and an error message will be printed.
+ mmzk-env.cabal view
@@ -0,0 +1,76 @@+cabal-version: 2.4+name: mmzk-env+version: 0.1.0.0++synopsis: Read environment variables into a user-defined data type+description:+ mmzk-env is a Haskell library that provides functionality to read environment+ variables into user-defined data types, allowing for flexible and type-safe+ configuration management.++homepage: +bug-reports: +license: MIT+author: Yitang Chen <mmzk1526@outlook.com>+maintainer: Yitang Chen <mmzk1526@outlook.com>+category: Data, Environment+extra-source-files:+ CHANGELOG.md+ LICENSE+ README.md+++common settings+ ghc-options: -Wall+ default-extensions:+ BlockArguments+ DeriveAnyClass+ DeriveGeneric+ FlexibleContexts+ FlexibleInstances+ InstanceSigs+ LambdaCase+ MultiWayIf+ NoFieldSelectors+ OverloadedRecordDot+ OverloadedStrings+ PolyKinds+ ScopedTypeVariables+ TupleSections+ TypeApplications+ TypeFamilies+ TypeOperators+ default-language: Haskell2010+++library+ import: settings+ exposed-modules:+ Data.Env+ Data.Env.ExtractFields+ Data.Env.RecordParser+ Data.Env.TypeParser+ build-depends:+ base >=4.16 && <5,+ containers >= 0.6.7 && < 0.7,+ gigaparsec ^>=0.3.1,+ hs-source-dirs: src+++test-suite test+ import: settings+ main-is: Spec.hs+ type: exitcode-stdio-1.0+ other-modules:+ Data.Env+ Data.Env.ExtractFields+ Data.Env.RecordParser+ Data.Env.TypeParser+ build-depends:+ base >=4.16 && <5,+ containers,+ gigaparsec,+ hspec ^>=2.11,+ hs-source-dirs:+ src+ test
+ src/Data/Env.hs view
@@ -0,0 +1,28 @@+-- |+-- Module : Data.Env+-- Description : Environment schema validation+--+-- This module provides functionality to validate environment variables against+-- a schema (a type that implements the `EnvSchema` class).+module Data.Env (EnvSchema(..)) where++import Control.Monad.IO.Class+import Data.Env.ExtractFields+import Data.Env.RecordParser++-- | Type class for validating environment schemas.+class (ExtractFields a, RecordParser a) => EnvSchema a where+ -- | Validate the environment variables against the schema, transforming field+ -- names from camelCase to UPPER_SNAKE_CASE.+ validateEnv :: MonadIO m => m (Either String a)+ validateEnv = do+ envRaw <- getEnvRawCamelCaseToUpperSnake @a+ return $ parseRecord envRaw++ -- | Validate the environment variables against the schema, allowing for a+ -- cusutom transformation function to be applied to the field names to match+ -- with the environment variable names.+ validateEnvWith :: MonadIO m => (String -> String) -> m (Either String a)+ validateEnvWith transform = do+ envRaw <- getEnvRaw @a transform+ return $ parseRecord envRaw
+ src/Data/Env/ExtractFields.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module: Data.Env.ExtractFields+-- Description: Type class for extracting fields from a record type.+--+-- This module provides a type class 'ExtractFields' that extracts field names+-- from a record type. It also provides functions to retrieve environment+-- variables based on these field names, with options for different naming+-- conventions.+module Data.Env.ExtractFields (+ ExtractFields,+ extractFields,+ getEnvRaw,+ getEnvRawLowerToUpperSnake,+ getEnvRawCamelCaseToUpperSnake,+) where++import Control.Monad+import Control.Monad.IO.Class+import Data.Char++import Data.Maybe+import Data.Map (Map)+import Data.Proxy+import GHC.Generics+import qualified Data.Map as M+import System.Environment++-- | Type class for extracting field names from a record type.+class ExtractFields a where+ extractFields' :: Proxy a -> [String]++-- | Extract field names from a record type.+extractFields :: forall a. ExtractFields a => [String]+extractFields = extractFields' (Proxy :: Proxy a)+{-# INLINE extractFields #-}++-- | Retrieve environment variables based on field names, applying a mapping+-- function to the field names.+getEnvRaw :: forall a m. (MonadIO m, ExtractFields a)+ => (String -> String)+ -> m (Map String String)+getEnvRaw mapper = liftIO $ M.fromList <$> forM (extractFields @a) \field -> do+ value <- lookupEnv (mapper field)+ return (field, fromMaybe "" value)+{-# INLINE getEnvRaw #-}++-- | Retrieve environment variables based on field names, converting them to+-- upper case.+getEnvRawLowerToUpperSnake :: forall a m. (MonadIO m, ExtractFields a)+ => m (Map String String)+getEnvRawLowerToUpperSnake = getEnvRaw @a (map toUpper)+{-# INLINE getEnvRawLowerToUpperSnake #-}+++--------------------------------------------------------------------------------+-- Generic instances+--------------------------------------------------------------------------------++-- | Retrieve environment variables based on field names, converting them from+-- camel case (record field naming) to upper snake case (environment variable+-- naming).+getEnvRawCamelCaseToUpperSnake :: forall a m. (MonadIO m, ExtractFields a)+ => m (Map String String)+getEnvRawCamelCaseToUpperSnake = getEnvRaw @a camelToUpperSnake+{-# INLINE getEnvRawCamelCaseToUpperSnake #-}++class GExtractFields f where+ gExtractFields :: Proxy f -> [String]++instance GExtractFields f => GExtractFields (M1 D c f) where+ gExtractFields :: Proxy (M1 D c f) -> [String]+ gExtractFields _ = gExtractFields (Proxy :: Proxy f)+ {-# INLINE gExtractFields #-}++instance GExtractFields f => GExtractFields (M1 C c f) where+ gExtractFields :: Proxy (M1 C c f) -> [String]+ gExtractFields _ = gExtractFields (Proxy :: Proxy f)+ {-# INLINE gExtractFields #-}++instance (GExtractFields f, GExtractFields g) => GExtractFields (f :*: g) where+ gExtractFields :: Proxy (f :*: g) -> [String]+ gExtractFields _ = gExtractFields (Proxy :: Proxy f)+ ++ gExtractFields (Proxy :: Proxy g)+ {-# INLINE gExtractFields #-}++instance (Selector s) => GExtractFields (M1 S s (K1 i a)) where+ gExtractFields :: Proxy (M1 S s (K1 i a)) -> [String]+ gExtractFields _ = [selName (undefined :: M1 S s (K1 i a) p)]+ {-# INLINE gExtractFields #-}++instance (Generic a, GExtractFields (Rep a)) => ExtractFields a where+ extractFields' :: Proxy a -> [String]+ extractFields' _ = gExtractFields (Proxy :: Proxy (Rep a))+ {-# INLINE extractFields' #-}+++--------------------------------------------------------------------------------+-- Helpers+--------------------------------------------------------------------------------++camelToUpperSnake :: String -> String+camelToUpperSnake = go False+ where+ go _ [] = []+ go prevUpper (x:xs)+ | x == '_' = '_' : go True xs+ | isUpper x =+ let rest = go True xs+ in if prevUpper then toUpper x : rest else '_' : toUpper x : rest+ | otherwise = toUpper x : go False xs
+ src/Data/Env/RecordParser.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module: Data.Env.RecordParser+-- Description: Type class that provides parsers for records.+--+-- This module provides a type class 'RecordParser' that provides parsers for+-- records. The parsers are used to parse environment variables into records+-- based on their string representation.+module Data.Env.RecordParser (+ RecordParser (..),+) where++import Data.Env.TypeParser+import Data.Map (Map)+import GHC.Generics+import qualified Data.Map as M+import Data.Maybe++-- | Type class for validating environment schemas.+class RecordParser a where+ parseRecord :: Map String String -> Either String a++instance (Generic a, GRecordParser (Rep a)) => RecordParser a where++ parseRecord :: (Generic a, GRecordParser (Rep a))+ => Map String String -> Either String a+ parseRecord env = to <$> gParseRecord env+++--------------------------------------------------------------------------------+-- Generic instances+--------------------------------------------------------------------------------++-- | Generic validation class.+class GRecordParser f where+ gParseRecord :: Map String String -> Either String (f p)++-- | Handle metadata (wrapping fields in `M1`)+instance GRecordParser f => GRecordParser (M1 D c f) where+ gParseRecord :: Map String String -> Either String (M1 D c f p)+ gParseRecord env = M1 <$> gParseRecord env++-- | Handle metadata (wrapping fields in `M1`)+instance GRecordParser f => GRecordParser (M1 C c f) where+ gParseRecord :: Map String String -> Either String (M1 C c f p)+ gParseRecord env = M1 <$> gParseRecord env++-- | Handle multiple fields in a record+instance (GRecordParser f, GRecordParser g) => GRecordParser (f :*: g) where+ gParseRecord :: Map String String -> Either String ((f :*: g) p)+ gParseRecord env = (:*:) <$> gParseRecord env <*> gParseRecord env++-- | Handle individual fields+instance (TypeParser a, Selector s) => GRecordParser (M1 S s (K1 i a)) where+ gParseRecord :: Map String String -> Either String (M1 S s (K1 i a) p)+ gParseRecord env =+ let key = selName (undefined :: M1 S s (K1 i a) p)+ in M1 . K1 <$> case parseType (fromMaybe "" $ M.lookup key env) of+ Left err -> Left $ "Field " ++ show key ++ " parsing error:\n" ++ err+ Right val -> Right val
+ src/Data/Env/TypeParser.hs view
@@ -0,0 +1,183 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- 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.Word (Word8, Word16, Word32, Word64)+import GHC.Generics+import qualified Text.Gigaparsec as P+import qualified Text.Gigaparsec.Char as P+import qualified Text.Gigaparsec.Combinator as P+import qualified Text.Gigaparsec.Errors.ErrorGen as P+import qualified Text.Gigaparsec.Errors.Combinator as P+import qualified Text.Gigaparsec.Token.Descriptions as L+import qualified Text.Gigaparsec.Token.Lexer 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 #-}++-- | 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 @()@ field (parsed from String).+instance TypeParser () where+ parseType :: String -> Either String ()+ parseType = parse (P.string "()" P.$> ())+ {-# 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 #-}
+ test/Spec.hs view
@@ -0,0 +1,165 @@+import Data.Either (isLeft)+import Data.Env.RecordParser+import Data.Env.TypeParser+import Data.Int (Int8, Int16, Int32, Int64)+import qualified Data.Map as M+import Data.Word (Word8, Word16, Word32, Word64)+import GHC.Generics+import Test.Hspec++main :: IO ()+main = do+ hspec typeParserSpecSpec+ hspec recordParserSpec++typeParserSpecSpec :: Spec+typeParserSpecSpec = describe "parseType" do+ describe "parse String" do+ it "parses non-empty string" do+ parseType @String "hello" `shouldBe` Right "hello"+ it "does not parse empty string" do+ parseType @String "" `shouldSatisfy` isLeft+ describe "parse Bool" do+ it "parses True" do+ parseType @Bool "True" `shouldBe` Right True+ it "parses False" do+ parseType @Bool "False" `shouldBe` Right False+ it "fails to parse other values" do+ parseType @Bool "1" `shouldSatisfy` isLeft+ parseType @Bool "true" `shouldSatisfy` isLeft+ parseType @Bool "false" `shouldSatisfy` isLeft+ describe "parse ()" do+ it "parses ()" do+ parseType @() "()" `shouldBe` Right ()+ it "fails to parse other values" do+ parseType @() "" `shouldSatisfy` isLeft+ parseType @() "1" `shouldSatisfy` isLeft+ describe "parse Int8" do+ it "parses 0" do+ parseType @Int8 "0" `shouldBe` Right 0+ it "parses -128" do+ parseType @Int8 "-128" `shouldBe` Right (-128)+ it "parses 127" do+ parseType @Int8 "127" `shouldBe` Right 127+ it "fails to parse other values" do+ parseType @Int8 "128" `shouldSatisfy` isLeft+ parseType @Int8 "-129" `shouldSatisfy` isLeft+ parseType @Int8 "1.0" `shouldSatisfy` isLeft+ describe "parse Int16" do+ it "parses 0" do+ parseType @Int16 "0" `shouldBe` Right 0+ it "parses -32768" do+ parseType @Int16 "-32768" `shouldBe` Right (-32768)+ it "parses 32767" do+ parseType @Int16 "32767" `shouldBe` Right 32767+ it "fails to parse other values" do+ parseType @Int16 "32768" `shouldSatisfy` isLeft+ parseType @Int16 "-32769" `shouldSatisfy` isLeft+ parseType @Int16 "1.0" `shouldSatisfy` isLeft+ describe "parse Int32" do+ it "parses 0" do+ parseType @Int32 "0" `shouldBe` Right 0+ it "parses -2147483648" do+ parseType @Int32 "-2147483648" `shouldBe` Right (-2147483648)+ it "parses 2147483647" do+ parseType @Int32 "2147483647" `shouldBe` Right 2147483647+ it "fails to parse other values" do+ parseType @Int32 "2147483648" `shouldSatisfy` isLeft+ parseType @Int32 "-2147483649" `shouldSatisfy` isLeft+ parseType @Int32 "1.0" `shouldSatisfy` isLeft+ describe "parse Int64" do+ it "parses 0" do+ parseType @Int64 "0" `shouldBe` Right 0+ it "parses -9223372036854775808" do+ parseType @Int64 "-9223372036854775808" `shouldBe` Right (-9223372036854775808)+ it "parses 9223372036854775807" do+ parseType @Int64 "9223372036854775807" `shouldBe` Right 9223372036854775807+ it "fails to parse other values" do+ parseType @Int64 "9223372036854775808" `shouldSatisfy` isLeft+ parseType @Int64 "-9223372036854775809" `shouldSatisfy` isLeft+ parseType @Int64 "1.0" `shouldSatisfy` isLeft+ describe "parse Int" do+ it "parses 0" do+ parseType @Int "0" `shouldBe` Right 0+ it "parses min Int" do+ parseType @Int (show (minBound :: Int)) `shouldBe` Right (minBound :: Int)+ it "parses max Int" do+ parseType @Int (show (maxBound :: Int)) `shouldBe` Right (maxBound :: Int)+ it "fails to parse other values" do+ parseType @Int (show ((fromIntegral (maxBound :: Int) :: Integer) + 1)) `shouldSatisfy` isLeft+ parseType @Int (show ((fromIntegral (minBound :: Int) :: Integer) - 1)) `shouldSatisfy` isLeft+ parseType @Int "1.0" `shouldSatisfy` isLeft+ describe "parse Word8" do+ it "parses 0" do+ parseType @Word8 "0" `shouldBe` Right 0+ it "parses 255" do+ parseType @Word8 "255" `shouldBe` Right 255+ it "fails to parse other values" do+ parseType @Word8 "256" `shouldSatisfy` isLeft+ parseType @Word8 "-1" `shouldSatisfy` isLeft+ parseType @Word8 "1.0" `shouldSatisfy` isLeft+ describe "parse Word16" do+ it "parses 0" do+ parseType @Word16 "0" `shouldBe` Right 0+ it "parses 65535" do+ parseType @Word16 "65535" `shouldBe` Right 65535+ it "fails to parse other values" do+ parseType @Word16 "65536" `shouldSatisfy` isLeft+ parseType @Word16 "-1" `shouldSatisfy` isLeft+ parseType @Word16 "1.0" `shouldSatisfy` isLeft+ describe "parse Word32" do+ it "parses 0" do+ parseType @Word32 "0" `shouldBe` Right 0+ it "parses 4294967295" do+ parseType @Word32 "4294967295" `shouldBe` Right 4294967295+ it "fails to parse other values" do+ parseType @Word32 "4294967296" `shouldSatisfy` isLeft+ parseType @Word32 "-1" `shouldSatisfy` isLeft+ parseType @Word32 "1.0" `shouldSatisfy` isLeft+ describe "parse Word64" do+ it "parses 0" do+ parseType @Word64 "0" `shouldBe` Right 0+ it "parses 18446744073709551615" do+ parseType @Word64 "18446744073709551615" `shouldBe` Right 18446744073709551615+ it "fails to parse other values" do+ parseType @Word64 "18446744073709551616" `shouldSatisfy` isLeft+ parseType @Word64 "-1" `shouldSatisfy` isLeft+ parseType @Word64 "1.0" `shouldSatisfy` isLeft+ describe "parse Word" do+ it "parses 0" do+ parseType @Word "0" `shouldBe` Right 0+ it "parses max Word" do+ parseType @Word (show (maxBound :: Word)) `shouldBe` Right (maxBound :: Word)+ it "fails to parse other values" do+ parseType @Word (show ((fromIntegral (maxBound :: Word) :: Integer) + 1)) `shouldSatisfy` isLeft+ parseType @Word "-1" `shouldSatisfy` isLeft+ parseType @Word "1.0" `shouldSatisfy` isLeft+ describe "parse Maybe" do+ it "should success" do+ parseType @(Maybe Int) "1" `shouldBe` Right (Just 1)+ parseType @(Maybe Int) "" `shouldBe` Right Nothing+ parseType @(Maybe String) "hello" `shouldBe` Right (Just "hello")+ parseType @(Maybe String) "" `shouldBe` Right Nothing+ it "should fail" do+ parseType @(Maybe Int) "hello" `shouldSatisfy` isLeft+ parseType @(Maybe Bool) "fALSE" `shouldSatisfy` isLeft++data Person = Person+ { name :: String+ , age :: Int+ } deriving (Show, Eq, Generic)++recordParserSpec :: Spec+recordParserSpec = describe "parseRecord" do+ it "parses a valid record" do+ let env = M.fromList [("name", "Alice"), ("age", "30")]+ parseRecord @Person env `shouldBe` Right (Person "Alice" 30)+ it "allows extra fields" do+ let env = M.fromList [("name", "Alice"), ("age", "30"), ("extra", "value")]+ parseRecord @Person env `shouldBe` Right (Person "Alice" 30)+ it "fails to parse an invalid record" do+ let env = M.fromList [("name", "Alice"), ("age", "thirty")]+ parseRecord @Person env `shouldSatisfy` isLeft+ it "fails to parse a missing field" do+ let env = M.fromList [("name", "Alice")]+ parseRecord @Person env `shouldSatisfy` isLeft