packages feed

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 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