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 +9/−0
- README.md +48/−11
- app/CustomMappingExample.hs +27/−0
- app/WitnessExample.hs +7/−13
- mmzk-env.cabal +14/−2
- src/Data/Env.hs +36/−1
- src/Data/Env/ExtractFields.hs +1/−9
- src/Data/Env/RecordParser.hs +2/−2
- src/Data/Env/RecordParserW.hs +2/−2
- src/Data/Env/TypeParserW.hs +9/−1
- src/Data/Env/Witness/DefaultNum.hs +1/−32
- src/Data/Env/Witness/DefaultString.hs +42/−0
- test/BasicTypeParserSpec.hs +151/−0
- test/DefaultWitnessSpec.hs +96/−0
- test/EnumParserSpec.hs +43/−0
- test/TypeParserSpec.hs +0/−151
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