mmzk-env 0.1.2.0 → 0.2.0.0
raw patch · 19 files changed
+887/−238 lines, 19 filesdep +mmzk-envdep ~containersdep ~gigaparsecnew-component:exe:enum-examplenew-component:exe:newtype-examplenew-component:exe:quickstart-examplenew-component:exe:witness-examplePVP ok
version bump matches the API change (PVP)
Dependencies added: mmzk-env
Dependency ranges changed: containers, gigaparsec
API changes (from Hackage documentation)
+ Data.Env.ExtractFields: extractFields' :: ExtractFields a => Proxy a -> [String]
+ Data.Env.RecordParser: parseRecord' :: RecordParser a => Map String String -> Maybe a
+ Data.Env.RecordParserW: -- | The result type after parsing, which removes witness types.
+ Data.Env.RecordParserW: Dec :: ColumnType
+ Data.Env.RecordParserW: Res :: ColumnType
+ Data.Env.RecordParserW: class RecordParserW (a :: k) where {
+ Data.Env.RecordParserW: data ColumnType
+ Data.Env.RecordParserW: instance (Data.Env.TypeParserW.TypeParserW p a, GHC.Internal.Generics.Selector s) => Data.Env.RecordParserW.GRecordParserW (GHC.Internal.Generics.M1 GHC.Internal.Generics.S s (GHC.Internal.Generics.K1 i (p, a)))
+ Data.Env.RecordParserW: instance (GHC.Internal.Generics.Generic (a 'Data.Env.RecordParserW.Dec), Data.Env.RecordParserW.GRecordParserW (GHC.Internal.Generics.Rep (a 'Data.Env.RecordParserW.Dec)), GHC.Internal.Generics.Generic (a 'Data.Env.RecordParserW.Res), Data.Env.RecordParserW.GRecordParsedType (GHC.Internal.Generics.Rep (a 'Data.Env.RecordParserW.Dec)) () GHC.Types.~ GHC.Internal.Generics.Rep (a 'Data.Env.RecordParserW.Res) ()) => Data.Env.RecordParserW.RecordParserW (a 'Data.Env.RecordParserW.Dec)
+ Data.Env.RecordParserW: instance GHC.Classes.Eq Data.Env.RecordParserW.ColumnType
+ Data.Env.RecordParserW: instance GHC.Internal.Show.Show Data.Env.RecordParserW.ColumnType
+ Data.Env.RecordParserW: instance forall k (f :: k -> *) (c :: GHC.Internal.Generics.Meta). Data.Env.RecordParserW.GRecordParserW f => Data.Env.RecordParserW.GRecordParserW (GHC.Internal.Generics.M1 GHC.Internal.Generics.C c f)
+ Data.Env.RecordParserW: instance forall k (f :: k -> *) (c :: GHC.Internal.Generics.Meta). Data.Env.RecordParserW.GRecordParserW f => Data.Env.RecordParserW.GRecordParserW (GHC.Internal.Generics.M1 GHC.Internal.Generics.D c f)
+ Data.Env.RecordParserW: instance forall k (f :: k -> *) (g :: k -> *). (Data.Env.RecordParserW.GRecordParserW f, Data.Env.RecordParserW.GRecordParserW g) => Data.Env.RecordParserW.GRecordParserW (f GHC.Internal.Generics.:*: g)
+ Data.Env.RecordParserW: parseRecordW :: RecordParserW a => Map String String -> Either String (RecordParsedType a)
+ Data.Env.RecordParserW: parseRecordW' :: RecordParserW a => Map String String -> Maybe (RecordParsedType a)
+ Data.Env.RecordParserW: type Di (f :: Type -> Type) (c :: ColumnType) a = Column c f a a
+ Data.Env.RecordParserW: type RecordParsedType (a :: k);
+ Data.Env.RecordParserW: type family Column (t :: ColumnType) p a
+ Data.Env.RecordParserW: }
+ Data.Env.TypeParser: instance Data.Env.TypeParser.TypeParser a => Data.Env.TypeParser.TypeParser (GHC.Tuple.Solo a)
+ Data.Env.TypeParser: parseType' :: TypeParser a => String -> Maybe a
+ Data.Env.TypeParserW: class TypeParserW (p :: k) a | p -> a
+ Data.Env.TypeParserW: data Solo a
+ Data.Env.TypeParserW: instance (Data.Env.TypeParserW.TypeParserW p1 GHC.Internal.Base.String, Data.Env.TypeParserW.TypeParserW p2 GHC.Internal.Base.String) => Data.Env.TypeParserW.TypeParserW (p1, p2) GHC.Internal.Base.String
+ Data.Env.TypeParserW: instance Data.Env.TypeParser.TypeParser a => Data.Env.TypeParserW.TypeParserW (GHC.Tuple.Solo a) a
+ Data.Env.TypeParserW: parseTypeW :: TypeParserW p a => Proxy p -> String -> Either String a
+ Data.Env.TypeParserW: parseTypeW' :: TypeParserW p a => Proxy p -> String -> Maybe a
+ Data.Env.Witness.DefaultNum: data DefaultNum (n :: Nat) (a :: k)
+ Data.Env.Witness.DefaultNum: instance (Data.Env.TypeParser.TypeParser a, GHC.Internal.Num.Num a, GHC.Internal.TypeNats.KnownNat n) => Data.Env.TypeParserW.TypeParserW (Data.Env.Witness.DefaultNum.DefaultNum n a) a
Files
- CHANGELOG.md +21/−1
- README.md +141/−5
- app/EnumExample.hs +16/−0
- app/NewtypeExample.hs +37/−0
- app/QuickstartExample.hs +24/−0
- app/WitnessExample.hs +37/−0
- mmzk-env.cabal +51/−11
- src/Data/Env.hs +4/−4
- src/Data/Env/EnumParser.hs +2/−1
- src/Data/Env/ExtractFields.hs +10/−11
- src/Data/Env/RecordParser.hs +14/−5
- src/Data/Env/RecordParserW.hs +121/−0
- src/Data/Env/TypeParser.hs +34/−13
- src/Data/Env/TypeParserW.hs +51/−0
- src/Data/Env/Witness/DefaultNum.hs +74/−0
- test/RecordParserSpec.hs +50/−0
- test/RecordParserWSpec.hs +48/−0
- test/Spec.hs +1/−187
- test/TypeParserSpec.hs +151/−0
CHANGELOG.md view
@@ -1,9 +1,29 @@ # Revision history for mmzk-env +## 0.2.0.0 -- 2025-11-29++* Witnessed record parsing (witness types carry type-level information that determines parsing behaviour):+ * Add `RecordParserW` for witnessed record parsing.+ * Add `TypeParserW` for witnessed type parsing.+ * Add `DefaultNum` witness type for providing default numeric values.+ +* Add convenient `Maybe` result variants for parsers.++* Add runnable example executables in `app/`:+ * `quickstart-example`: Basic environment validation example.+ * `enum-example`: Enumerated type parsing example.+ * `newtype-example`: Custom parser with newtype wrapper example.+ * `witness-example`: Witness types with default values example.++* Add more tests.++* Heavy refactoring.++ ## 0.1.2.0 -- 2025-11-16 -* Add spport for parsing `Text`.+* Add support for parsing `Text`. ## 0.1.1.1 -- 2025-09-29
README.md view
@@ -4,15 +4,29 @@ type. It provides a type-safe way to parse and validate environment variables, ensuring that they conform to the expected types. +## Contents++- [mmzk-env](#mmzk-env)+ - [Contents](#contents)+ - [Quick Start](#quick-start)+ - [Enum Support](#enum-support)+ - [Witness Types: Avoiding Newtype Boilerplate](#witness-types-avoiding-newtype-boilerplate)+ - [The Problem: Newtype Boilerplate](#the-problem-newtype-boilerplate)+ - [The Solution: Witnesses](#the-solution-witnesses)+ - [Key Benefits](#key-benefits)+ - [Available Witnesses](#available-witnesses)+ ## Quick Start +**[Full example →](app/QuickstartExample.hs)**+ ```Haskell-module Data.Env where+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-} -import Control.Monad.IO.Class-import Data.Env.ExtractFields-import Data.Env.RecordParser-import GHC.Generics+import Data.Env+import GHC.Generics -- | Example: Define an environment schema data Config = Config@@ -37,12 +51,15 @@ ## Enum Support +**[Full example →](app/EnumExample.hs)**+ The library also supports automatic parsing of enumerated types. You can define an enum and derive the `TypeParser` instance using the helper type `EnumParser`. The extension `DerivingVia` is required for this feature. ```Haskell {-# LANGUAGE DerivingVia #-}+{-# LANGUAGE TypeApplications #-} data Gender = Male | Female deriving (Show, Eq, Enum, Bounded)@@ -51,3 +68,122 @@ print $ parseEnum @Gender "Male" -- Right Male print $ parseEnum @Gender "Female" -- Right Female ```++## Witness Types: Avoiding Newtype Boilerplate++The library provides a "witness" pattern that allows you to enhance parsing behaviour without wrapping values in newtypes. This is useful when you need features like default values, validation, or transformation but want to keep your final data types simple.++### The Problem: Newtype Boilerplate++**[Full example →](app/NewtypeExample.hs)**++Let's say you want to parse a PostgreSQL port that defaults to 5432. Without witnesses, you might create a newtype wrapper:++```Haskell+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}++import Data.Env+import Data.Env.TypeParser+import Data.Word+import GHC.Generics++-- Define a newtype wrapper for the port+newtype PsqlPort = PsqlPort Word16+ deriving (Show, Eq)++-- Implement custom parsing with default value+instance TypeParser PsqlPort where+ parseType "" = Right (PsqlPort 5432) -- Default to 5432+ parseType str = case parseType str of+ Right port -> Right (PsqlPort port)+ Left err -> Left err++data Config = Config+ { psqlPort :: PsqlPort+ , dbName :: String }+ deriving (Show, Generic, EnvSchema)+```++Now when you use your config, you have to constantly unwrap the value:++```Haskell+unpackPort :: PsqlPort -> Word16+unpackPort (PsqlPort port) = port++connectToDatabase :: Config -> IO Connection+connectToDatabase cfg = connect $ defaultConnectInfo+ { connectPort = unpackPort (psqlPort cfg) -- Annoying unpacking!+ , connectDatabase = dbName cfg }+ where+ unpackPort (PsqlPort port) = port+```++### The Solution: Witnesses++**[Full example →](app/WitnessExample.hs)**++With witness types, you can specify parsing behaviour at the type level while keeping the final value unwrapped:++```Haskell+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}++import Data.Env.RecordParserW+import Data.Env.Witness.DefaultNum+import Data.Word+import GHC.Generics+import Data.Env.TypeParserW++data Config c = Config+ { psqlPort :: Column c (DefaultNum 5432 Word16) Word16 -- Defaults to 5432+ , dbName :: Column c (Solo String) String }+ deriving (Generic)++deriving stock instance Show (Config 'Res) -- For printing the result++-- Parse with defaults+main :: IO ()+main = do+ errOrConfig <- parseRecordW @(Config 'Dec) mempty+ case errOrConfig of+ Left err -> putStrLn $ "Parse failed: " ++ err+ Right cfg -> connectToDatabase cfg -- cfg :: Config 'Res+```++The magic happens with the `Column` type family and the `ColumnType` phantom type:++- **`Config 'Dec`** (Declaration): The type used for parsing, where each field is `(witness, value)`+ - This works under the hood for the generic instances and users typically don't interact with it directly+- **`Config 'Res`** (Result): The type you work with, where each field is just `value`+- **`Column c witness a`**: Expands to `(witness a, a)` when `c = 'Dec`, or just `a` when `c = 'Res`++Now your final config has no wrappers:++```Haskell+connectToDatabase :: Config 'Res -> IO Connection+connectToDatabase cfg = connect $ defaultConnectInfo+ { connectPort = psqlPort cfg -- Direct access to Word16!+ , connectDatabase = dbName cfg }+```++### Key Benefits++1. **No Unpacking**: Your final data type contains raw values (Word16, String, etc.), not newtypes+2. **Type-Level Defaults**: Default values are specified in the type signature using type-level naturals+3. **Flexible Parsing**: Different witness types provide different parsing strategies (defaults, validation, transformation)++### Available Witnesses++- **`DefaultNum n a`**: Numeric types with a type-level default value `n`+- **`Solo a`**: Standard parsing without special behaviour (equivalent to `TypeParser`)+- **Custom witnesses**: You can define your own by implementing the `TypeParserW` class++More built-in witnesses will be provided.++For more complex parsing needs, witnesses provide a way to augment behaviour without polluting your domain types with wrapper noise.
+ app/EnumExample.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Data.Env.EnumParser+import Data.Env.TypeParser++data Gender = Male | Female+ deriving (Show, Eq, Enum, Bounded)+ deriving TypeParser via (EnumParser Gender)++main :: IO ()+main = do+ print $ parseType @Gender "Male" -- Right Male+ print $ parseType @Gender "Female" -- Right Female
+ app/NewtypeExample.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Data.Env+import Data.Env.TypeParser+import Data.Word+import GHC.Generics++-- Define a newtype wrapper for the port+newtype PsqlPort = PsqlPort Word16+ deriving (Show, Eq)++-- Implement custom parsing with default value+instance TypeParser PsqlPort where+ parseType "" = Right (PsqlPort 5432) -- Default to 5432+ parseType str = case parseType str of+ Right port -> Right (PsqlPort port)+ Left err -> Left err++data Config = Config+ { psqlPort :: PsqlPort+ , dbName :: String }+ deriving (Show, Generic, EnvSchema)++-- Now when you use your config, you have to constantly unwrap the value:+unpackPort :: PsqlPort -> Word16+unpackPort (PsqlPort port) = port++main :: IO ()+main = do+ errOrConfig <- validateEnv @Config+ case errOrConfig of+ Left err -> putStrLn $ "Parse failed: " ++ err+ Right cfg -> putStrLn $ "Port: " ++ show (unpackPort $ psqlPort cfg)
+ app/QuickstartExample.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Data.Env+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
+ app/WitnessExample.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Data.Env.RecordParserW+import Data.Env.Witness.DefaultNum+import qualified Data.Map as M+import Data.Word+import GHC.Generics+import Data.Env.TypeParserW++data Config c = Config+ { psqlPort :: Column c (DefaultNum 5432 Word16) Word16 -- Defaults to 5432+ , dbName :: Column c (Solo String) String }+ deriving (Generic)++deriving stock instance Show (Config 'Res) -- For printing the result++-- Parse with defaults+main :: IO ()+main = do+ -- Example 1: Empty environment (uses defaults)+ let emptyEnv = M.empty+ case parseRecordW @(Config 'Dec) emptyEnv of+ Left err -> putStrLn $ "Parse failed: " ++ err+ Right cfg -> putStrLn $ "Config with defaults: " ++ show cfg++ -- Example 2: Custom values+ let customEnv = M.fromList [("psqlPort", "8080"), ("dbName", "mydb")]+ case parseRecordW @(Config 'Dec) customEnv of+ Left err -> putStrLn $ "Parse failed: " ++ err+ Right cfg -> putStrLn $ "Config with custom values: " ++ show cfg
mmzk-env.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: mmzk-env-version: 0.1.2.0+version: 0.2.0.0 synopsis: Read environment variables into a user-defined data type description:@@ -24,11 +24,13 @@ ghc-options: -Wall default-extensions: BlockArguments+ DataKinds DeriveAnyClass DeriveGeneric DerivingVia FlexibleContexts FlexibleInstances+ ImportQualifiedPost InstanceSigs LambdaCase MultiWayIf@@ -37,6 +39,7 @@ OverloadedStrings PolyKinds ScopedTypeVariables+ StandaloneDeriving TupleSections TypeApplications TypeFamilies@@ -51,7 +54,10 @@ Data.Env.EnumParser Data.Env.ExtractFields Data.Env.RecordParser+ Data.Env.RecordParserW Data.Env.TypeParser+ Data.Env.TypeParserW+ Data.Env.Witness.DefaultNum build-depends: base >=4.16 && <5, containers >= 0.6.7 && < 0.7,@@ -63,19 +69,53 @@ test-suite test import: settings main-is: Spec.hs- type: exitcode-stdio-1.0 other-modules:- Data.Env- Data.Env.EnumParser- Data.Env.ExtractFields- Data.Env.RecordParser- Data.Env.TypeParser+ TypeParserSpec+ RecordParserSpec+ RecordParserWSpec+ type: exitcode-stdio-1.0 build-depends: base >=4.16 && <5, containers,- gigaparsec, hspec ^>=2.11,+ mmzk-env, text,- hs-source-dirs:- src- test+ build-tool-depends:+ hspec-discover:hspec-discover ^>=2.11+ hs-source-dirs: test+++executable quickstart-example+ main-is: QuickstartExample.hs+ build-depends:+ base >=4.16 && <5,+ mmzk-env,+ hs-source-dirs: app+ default-language: Haskell2010+++executable enum-example+ main-is: EnumExample.hs+ build-depends:+ base >=4.16 && <5,+ mmzk-env,+ hs-source-dirs: app+ default-language: Haskell2010++executable newtype-example+ main-is: NewtypeExample.hs+ build-depends:+ base >=4.16 && <5,+ mmzk-env,+ hs-source-dirs: app+ default-language: Haskell2010+++executable witness-example+ main-is: WitnessExample.hs+ build-depends:+ base >=4.16 && <5,+ containers,+ mmzk-env,+ hs-source-dirs: app+ default-language: Haskell2010
src/Data/Env.hs view
@@ -4,11 +4,11 @@ -- -- This module provides functionality to validate environment variables against -- a schema (a type that implements the `EnvSchema` class).-module Data.Env (EnvSchema(..)) where+module Data.Env ( EnvSchema(..) ) where -import Control.Monad.IO.Class-import Data.Env.ExtractFields-import Data.Env.RecordParser+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
src/Data/Env/EnumParser.hs view
@@ -18,13 +18,14 @@ -- > parseType @Gender "Other" `shouldSatisfy` isLeft module Data.Env.EnumParser where -import Data.Env.TypeParser (TypeParser(..))+import Data.Env.TypeParser ( TypeParser(..) ) -- | A helper type for parsing Bounded Enums. newtype EnumParser a = EnumParser a deriving (Show, Eq) instance (Enum a, Show a, Bounded a) => TypeParser (EnumParser a) where+ parseType :: (Enum a, Show a, Bounded a) => String -> Either String (EnumParser a) parseType s = case lookup s enumMap of Just v -> Right (EnumParser v) Nothing -> Left $ "Cannot parse value: " ++ s ++ ". Valid values are: " ++ show (map fst enumMap)
src/Data/Env/ExtractFields.hs view
@@ -10,23 +10,22 @@ -- variables based on these field names, with options for different naming -- conventions. module Data.Env.ExtractFields (- 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+import Control.Monad+import Control.Monad.IO.Class+import Data.Char+import Data.Map ( Map )+import Data.Map qualified as M+import Data.Maybe+import Data.Proxy+import GHC.Generics+import System.Environment -- | Type class for extracting field names from a record type. class ExtractFields a where
src/Data/Env/RecordParser.hs view
@@ -12,15 +12,24 @@ RecordParser (..), ) where -import Data.Env.TypeParser-import Data.Map (Map)-import GHC.Generics-import qualified Data.Map as M-import Data.Maybe+import Data.Env.TypeParser+import Data.Map ( Map )+import Data.Map qualified as M+import Data.Maybe+import GHC.Generics -- | Type class for validating environment schemas. class RecordParser a where parseRecord :: Map String String -> Either String a++ -- | Parse a record, converting 'Either' to 'Maybe' and dropping any error messages.+ --+ -- This is a convenience function that calls 'parseRecord' and converts the result+ -- from 'Either String a' to 'Maybe a', discarding the error message on failure.+ parseRecord' :: Map String String -> Maybe a+ parseRecord' env = case parseRecord env of+ Right val -> Just val+ Left _ -> Nothing instance (Generic a, GRecordParser (Rep a)) => RecordParser a where
+ src/Data/Env/RecordParserW.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE AllowAmbiguousTypes #-}++-- |+-- Module: Data.Env.RecordParserW+-- Description: Type class that provides parsers for records with witnesses.+--+-- This module provides a type class 'RecordParserW' that provides parsers for+-- records with witness types. The parsers are used to parse environment variables+-- into records based on their string representation, with the witness types+-- providing custom parsing behavior per field.+module Data.Env.RecordParserW (+ RecordParserW (..),+ ColumnType (..),+ Column,+ Di,+) where++import Data.Data+import Data.Env.TypeParserW+import Data.Kind+import Data.Map ( Map )+import Data.Map qualified as M+import Data.Maybe+import GHC.Generics++-- | Column type indicator for distinguishing between declaration and result types.+--+-- * 'Dec' - Declaration type containing both witness and value types+-- * 'Res' - Result type containing only value types+data ColumnType = Dec | Res+ deriving stock (Eq, Show)++-- | Type family that maps column types to their representation.+--+-- For 'Dec' (declaration) columns, the type is a pair of witness and value.+-- For 'Res' (result) columns, the type is just the value.+--+-- In application code, typically only 'Res is used to access the parsed record values,+-- while 'Dec is used for the schema definition during parsing.+type family Column (t :: ColumnType) (p :: Type) (a :: Type) where+ Column 'Dec p a = (p, a)+ Column 'Res p a = a++-- | Type class for parsing environment schemas with witness types.+class RecordParserW a where+ -- | The result type after parsing, which removes witness types.+ type RecordParsedType a++ -- | Parse a record from environment variables using witness types.+ parseRecordW :: Map String String -> Either String (RecordParsedType a)++ -- | Parse a record, converting 'Either' to 'Maybe' and dropping any error messages.+ --+ -- This is a convenience function that calls 'parseRecordW' and converts the result+ -- from 'Either String a' to 'Maybe a', discarding the error message on failure.+ parseRecordW' :: Map String String -> Maybe (RecordParsedType a)+ parseRecordW' env = case parseRecordW @a env of+ Right val -> Just val+ Left _ -> Nothing++instance+ ( Generic (a 'Dec)+ , GRecordParserW (Rep (a 'Dec))+ , Generic (a Res)+ , GRecordParsedType (Rep (a 'Dec)) () ~ Rep (a 'Res) ()+ ) => RecordParserW (a 'Dec) where+ type RecordParsedType (a 'Dec) = a 'Res++ parseRecordW :: Map String String -> Either String (RecordParsedType (a 'Dec))+ parseRecordW a = to <$> gParseRecord @(Rep (a 'Dec)) a+++--------------------------------------------------------------------------------+-- Generic instances+--------------------------------------------------------------------------------++-- | Generic validation class.+class GRecordParserW f where+ type GRecordParsedType f :: k -> Type++ gParseRecord :: Map String String -> Either String ((GRecordParsedType f) r)++-- | Handle metadata (wrapping fields in `M1`)+instance GRecordParserW f => GRecordParserW (M1 D c f) where+ type GRecordParsedType (M1 D c f) = M1 D c (GRecordParsedType f)++ gParseRecord :: Map String String -> Either String (GRecordParsedType (M1 D c f) r)+ gParseRecord env = M1 <$> gParseRecord @f env++-- | Handle metadata (wrapping fields in `M1`)+instance GRecordParserW f => GRecordParserW (M1 C c f) where+ type GRecordParsedType (M1 C c f) = M1 C c (GRecordParsedType f)++ gParseRecord :: Map String String -> Either String (GRecordParsedType (M1 C c f) r)+ gParseRecord env = M1 <$> gParseRecord @f env++-- | Handle multiple fields in a record+instance (GRecordParserW f, GRecordParserW g) => GRecordParserW (f :*: g) where+ type GRecordParsedType (f :*: g) = GRecordParsedType f :*: GRecordParsedType g++ gParseRecord :: Map String String -> Either String ((GRecordParsedType f :*: GRecordParsedType g) p)+ gParseRecord env = (:*:) <$> gParseRecord @f env <*> gParseRecord @g env++-- | Handle individual fields+instance (TypeParserW p a, Selector s) => GRecordParserW (M1 S s (K1 i (p, a))) where+ type GRecordParsedType (M1 S s (K1 i (p, a))) = M1 S s (K1 i a)++ gParseRecord :: Map String String -> Either String (M1 S s (K1 i a) r)+ gParseRecord env =+ let key = selName (undefined :: M1 S s (K1 i a) p)+ in M1 . K1 <$> case parseTypeW @p Proxy (fromMaybe "" $ M.lookup key env) of+ Left err -> Left $ "Field " ++ show key ++ " parsing error:\n" ++ err+ Right val -> Right val++-- | Type alias for declaring fields with polymorphic witness types.+--+-- This is a convenience alias for 'Column c (f a) a', commonly used to declare+-- record fields with a witness type @f a@ and a result type @a@.+type Di f c a = Column c (f a) a
src/Data/Env/TypeParser.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE CPP #-} -- | -- Module: Data.Env.TypeParser@@ -13,18 +13,19 @@ TypeParser (..), ) where -import Data.Int (Int8, Int16, Int32, Int64)-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-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+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@@ -36,6 +37,16 @@ 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@@ -146,6 +157,16 @@ 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@).
+ src/Data/Env/TypeParserW.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE FunctionalDependencies #-}++module Data.Env.TypeParserW (+ TypeParserW (..),+ Solo,+) where++import Data.Env.TypeParser+import Data.Proxy ( Proxy(..) )+import Data.Tuple ( Solo )++-- | Type class for parsers parameterized by a witness type.+--+-- This is similar to 'Data.Env.TypeParser.TypeParser''s 'Data.Env.TypeParser.parseType',+-- but allows a witness object to define how to parse. The witness type @p@ determines+-- the parsing strategy, giving you explicit control over the parsing behavior.+--+-- The witness pattern is useful when:+--+-- * You need multiple different parsing strategies for the same type+-- * [TODO] You want to compose parsers in different ways+--+-- The functional dependency @p -> a@ ensures that each witness type uniquely+-- determines the result type. For example, 'Data.Env.Witness.DefaultNum.DefaultNum' 5432 Int+-- uniquely determines the result type as Int.+--+-- See 'Data.Env.Witness.DefaultNum.DefaultNum' for an example of how to use witness types with+-- this class.+class TypeParserW p a | p -> a where+ -- | Parse a value by its string representation using the witness type @p@.+ --+ -- The 'Proxy' parameter carries the witness type information that determines+ -- how the parsing should be performed.+ parseTypeW :: Proxy p -> String -> Either String a++ -- | Parse a value, converting 'Either' to 'Maybe' and dropping any error messages.+ --+ -- This is a convenience function that calls 'parseTypeW' and converts the result+ -- from 'Either String a' to 'Maybe a', discarding the error message on failure.+ parseTypeW' :: Proxy p -> String -> Maybe a+ parseTypeW' proxy str = case parseTypeW proxy str of+ Right val -> Just val+ Left _ -> Nothing++instance TypeParser a => TypeParserW (Solo a) a where+ parseTypeW :: Proxy (Solo a) -> String -> Either String a+ parseTypeW _ = parseType++instance (TypeParserW p1 String, TypeParserW p2 String) => TypeParserW (p1, p2) String where+ parseTypeW :: Proxy (p1, p2) -> String -> Either String String+ parseTypeW _ str = parseTypeW @p1 Proxy str >>= parseTypeW @p2 Proxy
+ src/Data/Env/Witness/DefaultNum.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE MultiParamTypeClasses #-}++{- |+Module : Data.Env.Witness.DefaultNum+Description : Witness type for parsing numeric types with default values+Copyright : (c) 2025+License : MIT+Maintainer : maintainer@example.com+Stability : experimental++This module provides the 'DefaultNum' witness type, which allows you to parse+numeric environment variables with a type-level default value. When the environment+variable is missing or empty, the default value is used instead.++== Usage Example++@+import Data.Env.RecordParserW+import Data.Env.Witness.DefaultNum+import GHC.Generics++data Config c = Config+ { port :: Di (DefaultNum 5432) c Int+ , timeout :: Di (DefaultNum 30) c Int+ }+ deriving (Generic)++-- When parsing with an empty map, defaults are used:+-- parseRecordW \@(Config 'Dec) mempty+-- => Right (Config 5432 30)++-- When values are provided, they override the defaults:+-- parseRecordW \@(Config 'Dec) (fromList [("port", "8080")])+-- => Right (Config 8080 30)+@+-}+module Data.Env.Witness.DefaultNum (+ DefaultNum,+) where++import Data.Env.TypeParser+import Data.Env.TypeParserW+import Data.Proxy+import GHC.TypeLits++-- | Witness type for parsing numeric types with default values.+--+-- The type parameter @n@ is a type-level natural number that specifies the+-- default value. The type parameter @a@ is the numeric type to parse.+--+-- This witness allows you to specify a default numeric value that will be used+-- when the environment variable is missing or empty.+--+-- ==== __Examples__+--+-- >>> parseTypeW (Proxy @(DefaultNum 5432 Int)) ""+-- Right 5432+--+-- >>> parseTypeW (Proxy @(DefaultNum 5432 Int)) "8080"+-- Right 8080+--+-- >>> parseTypeW' (Proxy @(DefaultNum 5432 Int)) "invalid"+-- Nothing+data DefaultNum (n :: Nat) a++-- | Parse a numeric value with a default fallback.+--+-- When the input string is empty, returns the default value specified by the+-- type-level natural @n@. Otherwise, attempts to parse the string as type @a@.+instance (TypeParser a, Num a, KnownNat n) => TypeParserW (DefaultNum n a) a where+ parseTypeW :: Proxy (DefaultNum n a) -> String -> Either String a+ parseTypeW _ "" = Right (fromInteger $ natVal @n Proxy)+ parseTypeW _ str = parseType str+ {-# INLINE parseTypeW #-}
+ test/RecordParserSpec.hs view
@@ -0,0 +1,50 @@+module RecordParserSpec ( spec ) where++import Data.Coerce ( coerce )+import Data.Either ( isLeft )+import Data.Env.EnumParser+import Data.Env.RecordParser+import Data.Env.TypeParser+import Data.Map qualified as M+import GHC.Generics+import Test.Hspec++data Gender = Male | Female+ deriving (Show, Eq, Enum, Bounded)+ deriving TypeParser via (EnumParser Gender)++newtype Age = Age Int+ deriving (Show, Eq)++instance TypeParser Age where+ parseType :: String -> Either String Age+ parseType str = case parseType @Int str of+ Right a -> if a >= 0 && a <= 120 then Right $ Age a else Left "Age is not within range!"+ left -> coerce left++data Person = Person+ { name :: String+ , age :: Age+ , gender :: Maybe Gender+ } deriving (Show, Eq, Generic)++spec :: Spec+spec = describe "parseRecord" do+ it "parses a valid record" do+ let env = M.fromList [("name", "Alice"), ("age", "30")]+ parseRecord @Person env `shouldBe` Right (Person "Alice" (Age 30) Nothing)+ it "allows extra fields" do+ let env = M.fromList [("name", "Alice"), ("age", "30"), ("extra", "value")]+ parseRecord @Person env `shouldBe` Right (Person "Alice" (Age 30) Nothing)+ it "parses optional fields" do+ let env = M.fromList [("name", "Alice"), ("age", "30"), ("gender", "Female")]+ parseRecord @Person env `shouldBe` Right (Person "Alice" (Age 30) (Just Female))+ it "fails to parse an invalid record" do+ let env = M.fromList [("name", "Alice"), ("age", "30"), ("gender", "Other")]+ parseRecord @Person env `shouldSatisfy` isLeft+ it "fails to parse an invalid age" do+ let env = M.fromList [("name", "Alice"), ("age", "150"), ("gender", "Female")]+ parseRecord @Person env `shouldSatisfy` isLeft+ it "fails to parse a missing field" do+ let env = M.fromList [("name", "Alice")]+ parseRecord @Person env `shouldSatisfy` isLeft
+ test/RecordParserWSpec.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE MultiParamTypeClasses #-}++module RecordParserWSpec (spec) where++import Data.Data+import Data.Either ( isLeft )+import Data.Env.RecordParserW+import Data.Env.TypeParser+import Data.Env.TypeParserW+import Data.Env.Witness.DefaultNum+import Data.Map qualified as M+import GHC.Generics+import Test.Hspec++data NameW++instance TypeParserW NameW String where+ parseTypeW :: Proxy NameW -> String -> Either String String+ parseTypeW _ str = case parseType str of+ Right s -> if length s > 16 then Left "Name Too Long!!" else Right s+ left -> left+ {-# INLINE parseTypeW #-}+++data Config c = Config+ { port :: Di (DefaultNum 5432) c Int+ , env :: Di Solo c String+ , name :: Column c NameW String+ }+ deriving (Generic)++deriving instance Show (Config 'Res)+deriving instance Eq (Config 'Res)++spec :: Spec+spec = describe "parseRecordW for Config" do+ it "uses default port 5432 when port not specified" do+ let envMap = M.fromList [("env", "production"), ("name", "name")]+ parseRecordW @(Config 'Dec) envMap `shouldBe` Right (Config 5432 "production" "name")+ it "parses config with custom port" do+ let envMap = M.fromList [("port", "8080"), ("env", "development"), ("name", "name")]+ parseRecordW @(Config 'Dec) envMap `shouldBe` Right (Config 8080 "development" "name")+ it "fails to parse invalid port value" do+ let envMap = M.fromList [("port", "not-a-number"), ("env", "test"), ("name", "name")]+ parseRecordW @(Config 'Dec) envMap `shouldSatisfy` isLeft+ it "fails to parse invalid name value" do+ let envMap = M.fromList [("env", "test"), ("name", "1234567890poiuytrewq")]+ parseRecordW @(Config 'Dec) envMap `shouldSatisfy` isLeft
test/Spec.hs view
@@ -1,187 +1,1 @@-import Data.Either (isLeft)-import Data.Env.EnumParser-import Data.Env.RecordParser-import Data.Env.TypeParser-import Data.Int (Int8, Int16, Int32, Int64)-import qualified Data.Map as M-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-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 strict Text" do- it "parses non-empty string" do- parseType @T.Text "hello" `shouldBe` Right "hello"- it "does not parse empty string" do- parseType @T.Text "" `shouldSatisfy` isLeft- describe "parse lazy Text" do- it "parses non-empty string" do- parseType @TL.Text "hello" `shouldBe` Right "hello"- it "does not parse empty string" do- parseType @TL.Text "" `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 Gender = Male | Female- deriving (Show, Eq, Enum, Bounded)- deriving TypeParser via (EnumParser Gender)--data Person = Person- { name :: String- , age :: Int- , gender :: Maybe Gender- } 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 Nothing)- it "allows extra fields" do- let env = M.fromList [("name", "Alice"), ("age", "30"), ("extra", "value")]- parseRecord @Person env `shouldBe` Right (Person "Alice" 30 Nothing)- it "parses optional fields" do- let env = M.fromList [("name", "Alice"), ("age", "30"), ("gender", "Female")]- parseRecord @Person env `shouldBe` Right (Person "Alice" 30 (Just Female))- it "fails to parse an invalid record" do- let env = M.fromList [("name", "Alice"), ("age", "30"), ("gender", "Other")]- parseRecord @Person env `shouldSatisfy` isLeft- it "fails to parse a missing field" do- let env = M.fromList [("name", "Alice")]- parseRecord @Person env `shouldSatisfy` isLeft+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/TypeParserSpec.hs view
@@ -0,0 +1,151 @@+module TypeParserSpec (spec) where++import Data.Either ( isLeft )+import Data.Env.TypeParser+import Data.Int ( Int8, Int16, Int32, Int64 )+import Data.Text qualified as T+import Data.Text.Lazy qualified as TL+import Data.Word ( Word8, Word16, Word32, Word64 )+import Test.Hspec++spec :: Spec+spec = 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 strict Text" do+ it "parses non-empty string" do+ parseType @T.Text "hello" `shouldBe` Right "hello"+ it "does not parse empty string" do+ parseType @T.Text "" `shouldSatisfy` isLeft+ describe "parse lazy Text" do+ it "parses non-empty string" do+ parseType @TL.Text "hello" `shouldBe` Right "hello"+ it "does not parse empty string" do+ parseType @TL.Text "" `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