packages feed

mmzk-env 0.2.0.0 → 0.2.1.0

raw patch · 16 files changed

+488/−224 lines, 16 filesnew-component:exe:custom-mapping-examplePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Data.Env.ExtractFields: getEnvRawLowerToUpperSnake :: forall {k} (a :: k) m. (MonadIO m, ExtractFields a) => m (Map String String)
+ Data.Env: class (ExtractFields a, RecordParserW a) => EnvSchemaW (a :: k)
+ Data.Env: validateEnvW :: (EnvSchemaW a, MonadIO m) => m (Either String (RecordParsedType a))
+ Data.Env: validateEnvWWith :: (EnvSchemaW a, MonadIO m) => (String -> String) -> m (Either String (RecordParsedType a))
+ Data.Env.Witness.DefaultString: data DefaultString (s :: Symbol) (a :: k)
+ Data.Env.Witness.DefaultString: instance (Data.Env.TypeParser.TypeParser a, GHC.Internal.Data.String.IsString a, GHC.Internal.TypeLits.KnownSymbol s) => Data.Env.TypeParserW.TypeParserW (Data.Env.Witness.DefaultString.DefaultString s a) a

Files

CHANGELOG.md view
@@ -1,6 +1,15 @@ # Revision history for mmzk-env  +## 0.2.1.0 -- 2025-11-30++* Add the missing `validateEnvW` and `validateEnvWWith` functions for validating environment variables with witness types.++* Add `DefaultString` witness type for providing default string values.++* Add more texts and examples.++ ## 0.2.0.0 -- 2025-11-29  * Witnessed record parsing (witness types carry type-level information that determines parsing behaviour):
README.md view
@@ -9,6 +9,7 @@ - [mmzk-env](#mmzk-env)   - [Contents](#contents)   - [Quick Start](#quick-start)+    - [Custom Environment Variable Mapping](#custom-environment-variable-mapping)   - [Enum Support](#enum-support)   - [Witness Types: Avoiding Newtype Boilerplate](#witness-types-avoiding-newtype-boilerplate)     - [The Problem: Newtype Boilerplate](#the-problem-newtype-boilerplate)@@ -18,7 +19,7 @@  ## Quick Start -**[Full example →](app/QuickstartExample.hs)**+**[Full example →][quickstart-example]**  ```Haskell {-# LANGUAGE DeriveAnyClass #-}@@ -49,9 +50,37 @@  If any variable is missing or has an incorrect type, the validation will fail, and an error message will be printed. +### Custom Environment Variable Mapping++**[Full example →][custom-mapping-example]**++By default, the library converts camelCase field names to UPPER_SNAKE_CASE (e.g., `mainHost` → `MAIN_HOST`).++If you want to use uppercase environment variable names without underscores (like `MAINHOST` instead of `MAIN_HOST`), you can use `validateEnvWith` (or `validateEnvWWith` for witness types) with a custom mapping function:++```Haskell+data Config = Config+    { port      :: Int+    , name      :: String+    , main_host :: String  -- Will map to "MAINHOST" with custom mapping+    , debug     :: Maybe Bool }+    deriving (Show, Generic, EnvSchema)++main :: IO ()+main = do+  errOrEnv <- validateEnvWith @Config (map toUpper)+  case errOrEnv of+    Left err  -> putStrLn $ "Validation failed: " ++ err+    Right cfg -> putStrLn $ "Config loaded successfully: " ++ show cfg+```++With `validateEnvWith (map toUpper)`, the field `main_host` will look for the environment variable `MAINHOST` instead of `MAIN_HOST`.++You can provide any custom mapping function to `validateEnvWith` to transform field names to environment variable names according to your needs.+ ## Enum Support -**[Full example →](app/EnumExample.hs)**+**[Full example →][enum-example]**  The library also supports automatic parsing of enumerated types. You can define an enum and derive the `TypeParser` instance using the helper type `EnumParser`. @@ -75,7 +104,7 @@  ### The Problem: Newtype Boilerplate -**[Full example →](app/NewtypeExample.hs)**+**[Full example →][newtype-example]**  Let's say you want to parse a PostgreSQL port that defaults to 5432. Without witnesses, you might create a newtype wrapper: @@ -122,7 +151,7 @@  ### The Solution: Witnesses -**[Full example →](app/WitnessExample.hs)**+**[Full example →][witness-example]**  With witness types, you can specify parsing behaviour at the type level while keeping the final value unwrapped: @@ -134,7 +163,7 @@ {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeApplications #-} -import Data.Env.RecordParserW+import Data.Env import Data.Env.Witness.DefaultNum import Data.Word import GHC.Generics@@ -143,16 +172,17 @@ data Config c = Config   { psqlPort :: Column c (DefaultNum 5432 Word16) Word16  -- Defaults to 5432   , dbName   :: Column c (Solo String) String }-  deriving (Generic)+  deriving (Generic, EnvSchemaW) +instance EnvSchemaW (Config \'Dec) deriving stock instance Show (Config 'Res)  -- For printing the result --- Parse with defaults+-- Validate environment variables with defaults main :: IO () main = do-  errOrConfig <- parseRecordW @(Config 'Dec) mempty+  errOrConfig <- validateEnvW @(Config 'Dec)   case errOrConfig of-    Left err  -> putStrLn $ "Parse failed: " ++ err+    Left err  -> putStrLn $ "Validation failed: " ++ err     Right cfg -> connectToDatabase cfg  -- cfg :: Config 'Res ``` @@ -161,7 +191,7 @@ - **`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`+- **`Column c witness a`**: Expands to `(witness, a)` when `c = 'Dec`, or just `a` when `c = 'Res`  Now your final config has no wrappers: @@ -180,10 +210,17 @@  ### Available Witnesses -- **`DefaultNum n a`**: Numeric types with a type-level default value `n` - **`Solo a`**: Standard parsing without special behaviour (equivalent to `TypeParser`)+- **`DefaultNum n a`**: Numeric types with a type-level default value `n`+- - **`DefaultString s a`**: String types with a type-level default value `s` - **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.++[quickstart-example]: https://github.com/MMZK1526/mmzk-env/blob/v0.2.1.0/app/QuickstartExample.hs+[custom-mapping-example]: https://github.com/MMZK1526/mmzk-env/blob/v0.2.1.0/app/CustomMappingExample.hs+[enum-example]: https://github.com/MMZK1526/mmzk-env/blob/v0.2.1.0/app/EnumExample.hs+[newtype-example]: https://github.com/MMZK1526/mmzk-env/blob/v0.2.1.0/app/NewtypeExample.hs+[witness-example]: https://github.com/MMZK1526/mmzk-env/blob/v0.2.1.0/app/WitnessExample.hs
+ app/CustomMappingExample.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import Data.Char (toUpper)+import Data.Env+import GHC.Generics++-- | Example: Define an environment schema with custom mapping+data Config = Config+    { port      :: Int+    , name      :: String+    , main_host :: String+    , debug     :: Maybe Bool }+    deriving (Show, Generic, EnvSchema)++-- | Run the validation with custom mapping+-- This will look for environment variables: PORT, NAME, MAINHOST, DEBUG+-- Note: main_host maps to MAINHOST (not MAIN_HOST) with map toUpper+main :: IO ()+main = do+  errOrEnv <- validateEnvWith @Config (map toUpper)+  case errOrEnv of+    Left err  -> putStrLn $ "Validation failed: " ++ err+    Right cfg -> putStrLn $ "Config loaded successfully: " ++ show cfg
app/WitnessExample.hs view
@@ -7,11 +7,11 @@  module Main where -import Data.Env.RecordParserW+import Data.Env import Data.Env.Witness.DefaultNum-import qualified Data.Map as M import Data.Word import GHC.Generics+import Data.Env.RecordParserW import Data.Env.TypeParserW  data Config c = Config@@ -19,19 +19,13 @@   , dbName   :: Column c (Solo String) String }   deriving (Generic) +instance EnvSchemaW (Config 'Dec) deriving stock instance Show (Config 'Res)  -- For printing the result --- Parse with defaults+-- Validate environment variables 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+  errOrConfig <- validateEnvW @(Config 'Dec)+  case errOrConfig of+    Left err  -> putStrLn $ "Validation 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.2.0.0+version:            0.2.1.0  synopsis:           Read environment variables into a user-defined data type description:@@ -58,6 +58,7 @@         Data.Env.TypeParser         Data.Env.TypeParserW         Data.Env.Witness.DefaultNum+        Data.Env.Witness.DefaultString     build-depends:         base >=4.16 && <5,         containers >= 0.6.7 && < 0.7,@@ -70,7 +71,9 @@     import:           settings     main-is:          Spec.hs     other-modules:-        TypeParserSpec+        BasicTypeParserSpec+        DefaultWitnessSpec+        EnumParserSpec         RecordParserSpec         RecordParserWSpec     type:             exitcode-stdio-1.0@@ -87,6 +90,15 @@  executable quickstart-example     main-is:          QuickstartExample.hs+    build-depends:+        base >=4.16 && <5,+        mmzk-env,+    hs-source-dirs:   app+    default-language: Haskell2010+++executable custom-mapping-example+    main-is:          CustomMappingExample.hs     build-depends:         base >=4.16 && <5,         mmzk-env,
src/Data/Env.hs view
@@ -1,14 +1,17 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+ -- | -- 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+module Data.Env ( EnvSchema(..), EnvSchemaW(..) ) where  import Control.Monad.IO.Class import Data.Env.ExtractFields import Data.Env.RecordParser+import Data.Env.RecordParserW  -- | Type class for validating environment schemas. class (ExtractFields a, RecordParser a) => EnvSchema a where@@ -26,3 +29,35 @@   validateEnvWith transform = do     envRaw <- getEnvRaw @a transform     return $ parseRecord envRaw++-- | Type class for validating environment schemas using witness types.+--+-- Uses the witness pattern to enhance parsing (e.g., default values) without+-- wrapping final values in newtypes. Use @Config \'Dec@ for parsing and get+-- @Config \'Res@ with unwrapped values as the result.+--+-- @+-- data Config c = Config+--   { psqlPort :: Column c (DefaultNum 5432 Word16) Word16+--   , dbName   :: Column c (Solo String) String }+--   deriving (Generic)+--+-- instance EnvSchemaW (Config \'Dec)+--+-- loadConfig :: IO (Either String (Config \'Res))+-- loadConfig = validateEnvW \@(Config \'Dec)+-- @+class (ExtractFields a, RecordParserW a) => EnvSchemaW a where+  -- | Validate environment variables, transforming field names from camelCase+  -- to UPPER_SNAKE_CASE. Returns the parsed configuration with unwrapped values.+  validateEnvW :: MonadIO m => m (Either String (RecordParsedType a))+  validateEnvW = do+    envRaw <- getEnvRawCamelCaseToUpperSnake @a+    return $ parseRecordW @a envRaw++  -- | Validate environment variables with a custom field name transformation+  -- function. Returns the parsed configuration with unwrapped values.+  validateEnvWWith :: MonadIO m => (String -> String) -> m (Either String (RecordParsedType a))+  validateEnvWWith transform = do+    envRaw <- getEnvRaw @a transform+    return $ parseRecordW @a envRaw
src/Data/Env/ExtractFields.hs view
@@ -13,7 +13,6 @@   ExtractFields (..),   extractFields,   getEnvRaw,-  getEnvRawLowerToUpperSnake,   getEnvRawCamelCaseToUpperSnake, ) where @@ -29,7 +28,7 @@  -- | Type class for extracting field names from a record type. class ExtractFields a where-  -- | Extract field names from a record type. It uses a 'Proxy' to avoid+  -- | Extract field names from a record type. It uses a 'Data.Proxy.Proxy' to avoid   -- needing a value of type $a$, and we recommend using 'extractFields'   -- instead.   extractFields' :: Proxy a -> [String]@@ -48,13 +47,6 @@   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 #-}  -- | Retrieve environment variables based on field names, converting them from -- camel case (record field naming) to upper snake case (environment variable
src/Data/Env/RecordParser.hs view
@@ -46,12 +46,12 @@ class GRecordParser f where   gParseRecord :: Map String String -> Either String (f p) --- | Handle metadata (wrapping fields in `M1`)+-- | Handle metadata (wrapping fields in 'GHC.Generics.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`)+-- | Handle metadata (wrapping fields in 'GHC.Generics.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
src/Data/Env/RecordParserW.hs view
@@ -82,14 +82,14 @@    gParseRecord :: Map String String -> Either String ((GRecordParsedType f) r) --- | Handle metadata (wrapping fields in `M1`)+-- | Handle metadata (wrapping fields in 'GHC.Generics.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`)+-- | Handle metadata (wrapping fields in 'GHC.Generics.M1') instance GRecordParserW f => GRecordParserW (M1 C c f) where   type GRecordParsedType (M1 C c f) = M1 C c (GRecordParsedType f) 
src/Data/Env/TypeParserW.hs view
@@ -1,5 +1,13 @@ {-# LANGUAGE FunctionalDependencies #-} +-- |+-- Module: Data.Env.TypeParserW+-- Description: Type class provides parsers for types parameterized by witness types.+--+-- This module provides the 'TypeParserW' type class, which allows parsing+-- environment variables using witness types to control parsing behavior.+-- This enables multiple parsing strategies for the same type by using+-- different witness types. module Data.Env.TypeParserW (   TypeParserW (..),   Solo,@@ -29,7 +37,7 @@ 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+  -- The 'Data.Proxy.Proxy' parameter carries the witness type information that determines   -- how the parsing should be performed.   parseTypeW :: Proxy p -> String -> Either String a 
src/Data/Env/Witness/DefaultNum.hs view
@@ -1,38 +1,7 @@ {-# 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)-@+Witness type for parsing numeric types with type-level default values. -} module Data.Env.Witness.DefaultNum (   DefaultNum,
+ src/Data/Env/Witness/DefaultString.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE MultiParamTypeClasses #-}++{- |+Witness type for parsing string types with type-level default values.+-}+module Data.Env.Witness.DefaultString (+  DefaultString,+) where++import Data.Env.TypeParser+import Data.Env.TypeParserW+import Data.Proxy+import Data.String+import GHC.TypeLits++-- | Witness type for parsing string types with default values.+--+-- The type parameter @s@ is a type-level symbol that specifies the+-- default value. The type parameter @a@ is the string type to parse.+--+-- This witness allows you to specify a default string value that will be used+-- when the environment variable is missing or empty.+--+-- ==== __Examples__+--+-- >>> parseTypeW (Proxy @(DefaultString "localhost" String)) ""+-- Right "localhost"+--+-- >>> parseTypeW (Proxy @(DefaultString "localhost" String)) "example.com"+-- Right "example.com"+--+data DefaultString (s :: Symbol) a++-- | Parse a string value with a default fallback.+--+-- When the input string is empty, returns the default value specified by the+-- type-level symbol @s@. Otherwise, attempts to parse the string as type @a@.+instance (TypeParser a, IsString a, KnownSymbol s) => TypeParserW (DefaultString s a) a where+  parseTypeW :: Proxy (DefaultString s a) -> String -> Either String a+  parseTypeW _ ""  = Right (fromString $ symbolVal @s Proxy)+  parseTypeW _ str = parseType str+  {-# INLINE parseTypeW #-}
+ test/BasicTypeParserSpec.hs view
@@ -0,0 +1,151 @@+module BasicTypeParserSpec ( 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
+ test/DefaultWitnessSpec.hs view
@@ -0,0 +1,96 @@+module DefaultWitnessSpec ( spec ) where++import Data.Either ( isLeft )+import Data.Env.TypeParserW+import Data.Env.Witness.DefaultNum+import Data.Env.Witness.DefaultString+import Data.Int ( Int16, Int32, Int64 )+import Data.Proxy+import Data.Text qualified as T+import Data.Text.Lazy qualified as TL+import Data.Word ( Word16, Word32, Word64 )+import Test.Hspec++spec :: Spec+spec = do+  describe "DefaultNum witness" do+    describe "with Int" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultNum 5432 Int)) "" `shouldBe` Right 5432+      it "parses non-empty valid value" do+        parseTypeW (Proxy @(DefaultNum 5432 Int)) "8080" `shouldBe` Right 8080+      it "parses negative value" do+        parseTypeW (Proxy @(DefaultNum 5432 Int)) "-100" `shouldBe` Right (-100)+      it "fails to parse invalid value" do+        parseTypeW (Proxy @(DefaultNum 5432 Int)) "invalid" `shouldSatisfy` isLeft+        parseTypeW (Proxy @(DefaultNum 5432 Int)) "1.5" `shouldSatisfy` isLeft++    describe "with Word16" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultNum 3000 Word16)) "" `shouldBe` Right 3000+      it "parses non-empty valid value" do+        parseTypeW (Proxy @(DefaultNum 3000 Word16)) "8080" `shouldBe` Right 8080+      it "fails to parse out-of-range value" do+        parseTypeW (Proxy @(DefaultNum 3000 Word16)) "70000" `shouldSatisfy` isLeft+        parseTypeW (Proxy @(DefaultNum 3000 Word16)) "-1" `shouldSatisfy` isLeft++    describe "with Int16" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultNum 100 Int16)) "" `shouldBe` Right 100+      it "parses non-empty valid value" do+        parseTypeW (Proxy @(DefaultNum 100 Int16)) "32000" `shouldBe` Right 32000+      it "fails to parse out-of-range value" do+        parseTypeW (Proxy @(DefaultNum 100 Int16)) "40000" `shouldSatisfy` isLeft++    describe "with Int32" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultNum 1000 Int32)) "" `shouldBe` Right 1000+      it "parses large value" do+        parseTypeW (Proxy @(DefaultNum 1000 Int32)) "2000000000" `shouldBe` Right 2000000000++    describe "with Int64" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultNum 999 Int64)) "" `shouldBe` Right 999+      it "parses very large value" do+        parseTypeW (Proxy @(DefaultNum 999 Int64)) "9000000000000" `shouldBe` Right 9000000000000++    describe "with Word32" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultNum 500 Word32)) "" `shouldBe` Right 500+      it "parses large value" do+        parseTypeW (Proxy @(DefaultNum 500 Word32)) "4000000000" `shouldBe` Right 4000000000++    describe "with Word64" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultNum 777 Word64)) "" `shouldBe` Right 777+      it "parses very large value" do+        parseTypeW (Proxy @(DefaultNum 777 Word64)) "18000000000000000000" `shouldBe` Right 18000000000000000000++  describe "DefaultString witness" do+    describe "with String" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultString "localhost" String)) "" `shouldBe` Right "localhost"+      it "parses non-empty valid value" do+        parseTypeW (Proxy @(DefaultString "localhost" String)) "example.com" `shouldBe` Right "example.com"+      it "handles special characters" do+        parseTypeW (Proxy @(DefaultString "default" String)) "hello world!" `shouldBe` Right "hello world!"++    describe "with strict Text" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultString "default-host" T.Text)) "" `shouldBe` Right "default-host"+      it "parses non-empty valid value" do+        parseTypeW (Proxy @(DefaultString "default-host" T.Text)) "production.com" `shouldBe` Right "production.com"++    describe "with lazy Text" do+      it "returns default value for empty string" do+        parseTypeW (Proxy @(DefaultString "lazy-default" TL.Text)) "" `shouldBe` Right "lazy-default"+      it "parses non-empty valid value" do+        parseTypeW (Proxy @(DefaultString "lazy-default" TL.Text)) "actual-value" `shouldBe` Right "actual-value"++    describe "different default values" do+      it "handles empty default string" do+        parseTypeW (Proxy @(DefaultString "" String)) "" `shouldBe` Right ""+      it "handles URL as default" do+        parseTypeW (Proxy @(DefaultString "http://localhost:8080" String)) "" `shouldBe` Right "http://localhost:8080"+      it "handles path as default" do+        parseTypeW (Proxy @(DefaultString "/var/log/app.log" String)) "" `shouldBe` Right "/var/log/app.log"
+ test/EnumParserSpec.hs view
@@ -0,0 +1,43 @@+module EnumParserSpec ( spec ) where++import Data.Either ( isLeft )+import Data.Env.EnumParser+import Data.Env.TypeParser+import Test.Hspec++-- Test enum types+data Gender = Male | Female+  deriving (Show, Eq, Enum, Bounded)+  deriving TypeParser via (EnumParser Gender)++data LogLevel = Debug | Info | Warning | Error+  deriving (Show, Eq, Enum, Bounded)+  deriving TypeParser via (EnumParser LogLevel)++spec :: Spec+spec = describe "EnumParser" do+  describe "parse Gender enum" do+    it "parses Male" do+      parseType @Gender "Male" `shouldBe` Right Male+    it "parses Female" do+      parseType @Gender "Female" `shouldBe` Right Female+    it "fails to parse invalid value" do+      parseType @Gender "Other" `shouldSatisfy` isLeft+      parseType @Gender "male" `shouldSatisfy` isLeft+      parseType @Gender "MALE" `shouldSatisfy` isLeft+      parseType @Gender "" `shouldSatisfy` isLeft++  describe "parse LogLevel enum" do+    it "parses Debug" do+      parseType @LogLevel "Debug" `shouldBe` Right Debug+    it "parses Info" do+      parseType @LogLevel "Info" `shouldBe` Right Info+    it "parses Warning" do+      parseType @LogLevel "Warning" `shouldBe` Right Warning+    it "parses Error" do+      parseType @LogLevel "Error" `shouldBe` Right Error+    it "fails to parse invalid values" do+      parseType @LogLevel "debug" `shouldSatisfy` isLeft+      parseType @LogLevel "INFO" `shouldSatisfy` isLeft+      parseType @LogLevel "Warn" `shouldSatisfy` isLeft+      parseType @LogLevel "" `shouldSatisfy` isLeft
− test/TypeParserSpec.hs
@@ -1,151 +0,0 @@-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