registry-hedgehog-aeson-0.2.0.0: src/Data/Registry/Hedgehog/AesonGenerators.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
-- | This module provides generators for JSON values
--
-- Since the Value data type is an ADT with different alternatives and a recursive data type
-- we need to control how many of each variant (Number, String, Array, etc...) we generate
-- and how deeply we recurse when generating arrays and objects which themselves contain Values.
--
-- A registry is used to store all the generators and the current generation configuration
--
-- In order to control the recursivity we tag the type of Values we generate:
--
-- - @Tag "Simple" Value@ is for generating Null, Number, String, Boolean
-- - @Tag "Recurse" Value@ is for generating Array and Object
--
-- And we put a 'Depth` parameter in the registry. That parameter is decremented every time we generate values
-- for an Array or an Object
module Data.Registry.Hedgehog.AesonGenerators where
import Data.Aeson
import Data.Registry
import Data.Scientific as Scientific hiding (normalize)
import Data.String (fromString)
import Data.Vector as Vector (fromList)
import Hedgehog as H
import Hedgehog.Gen as Gen hiding (either)
import Hedgehog.Range as Range
import Protolude
-- | Generator for a default JSON value
genValue :: Gen Value
genValue = genValueFor simpleGens
-- | Generator for a JSON value with an adjusted set of generators
--
-- For example:
-- - change the recursive depth of the generation: genValueWith (setDepth 5)
-- - change the number of elements in an array or an object: genValueWith (setFieldsNb 5)
-- - change the generator used to generate field names in an object: genValueWith (setFieldNames (elements ["a", "b", "c"]))
-- - use a custom text generator: genValueWith (setGen myTextGenerator)
-- - change the range used for generating numbers: genValueWith (setRange (linear @Int 0 20))
genValueWith :: (Registry _ _ -> Registry _ _) -> Gen Value
genValueWith f = genValueFor (f simpleGens)
-- | Specialized generator for a number value
genNumberValue :: Gen Value
genNumberValue = unTag <$> make @(Gen (Tag "Number" Value)) simpleGens
-- | Generate a JSON value with a given set of generators to be used when recursing
genValueFor :: Registry _ _ -> Gen Value
genValueFor gens =
case make @Depth gens of
-- if the depth is 0 generate a Value from the "Simple" Value generation
0 -> make @(Gen Value) gens
-- if the depth is > 0 generate possibly recursive values like arrays and objects
_ -> make @(Gen Value) $ recursiveGens (decrementDepth gens)
-- | Set of generators for JSON values including recursive values like arrays and objects
-- In order to control the recursivity of the 'Value' data type we produce several types
-- for JSON values using tags:
-- @Recurse Value@ is a generated value to be used when generating an array or an object
-- @Simple Value@ is a generated value that is either: a string, a number, a bool, a null value
recursiveGens :: Registry _ _ -> Registry _ _
recursiveGens overrides =
normalize $
-- generator choosing between generated arrays, objects or simple values
gen genRecursiveValue
-- generator for objects
<: gen genObject
-- generator for arrays
<: gen genArray
-- generator for field names (up to 3 by default)
<: fun (listOf @FieldName 1 3)
-- generator for the elements of arrays or objects (up to 3 by default)
<: fun (listOf @(Tag "Recurse" Value) 1 3)
-- generator for a JSON value to be used in an object or an array
<: fun (tag <$> genValueFor overrides :: Gen (Tag "Recurse" Value))
-- simple, non-recursive, generators
<: overrides
-- | Set of generators for non-recursive JSON values
-- Those value are tagged as @Simple@ but we can also extract a @Gen Value@ from this list
simpleGens :: Registry _ _
simpleGens =
gen untagSimpleValue
<: gen genSimpleValue
<: gen genNumber
<: gen genString
<: gen FieldName
<: gen genText
<: gen genBool
<: gen genNull
<: gen (linear 0 5 :: Range Int)
<: gen (linear (-1000) 1000 :: Range Integer)
<: val (Depth 3)
-- * Individual generators
-- | Create a generator for a Value which can possibly be recursive if it is an array or an object
genRecursiveValue :: Tag "Array" Value -> Tag "Object" Value -> Tag "Simple" Value -> Gen Value
genRecursiveValue arrayValue objectValue simpleValue = Gen.element [unTag arrayValue, unTag objectValue, unTag simpleValue]
-- | Drop the tag on a Value
untagSimpleValue :: Tag "Simple" Value -> Value
untagSimpleValue = unTag
-- | Create a generator for a non-recursive Value (i.e. not an array or an object)
genSimpleValue :: Tag "Null" Value -> Tag "Bool" Value -> Tag "Number" Value -> Tag "String" Value -> Gen (Tag "Simple" Value)
genSimpleValue nullValue boolValue numberValue stringValue =
tag <$> Gen.element [unTag nullValue, unTag boolValue, unTag numberValue, unTag stringValue]
-- | Generator for the Null value
genNull :: Gen (Tag "Null" Value)
genNull = pure (tag Null)
-- | Generator for a boolean value
genBool :: Gen (Tag "Bool" Value)
genBool = tag . Bool <$> Gen.bool
-- | Generator for some Text
genText :: Range Int -> Gen Text
genText range = Gen.text range Gen.alphaNum
-- | Generator for a string value
genString :: Text -> Tag "String" Value
genString = tag . String
-- | Generator for a number value
genNumber :: Range Integer -> Gen (Tag "Number" Value)
genNumber range = fmap tag $ Number <$> (scientific <$> Gen.integral range <*> pure 0)
-- | Generator for an array value
genArray :: [Tag "Recurse" Value] -> Tag "Array" Value
genArray = tag . Array . Vector.fromList . fmap unTag
-- | Generator for an object value
genObject :: [FieldName] -> [Tag "Recurse" Value] -> Tag "Object" Value
genObject fields values = tag . object $ zip (fromString . toS . unFieldName <$> fields) (unTag <$> values)
-- * Support functions
-- | Simplification for funTo @Gen when adding a new function to the registry
gen :: forall a b. (ApplyVariadic Gen a b, Typeable a, Typeable b) => a -> Typed b
gen = funTo @Gen
-- | set a specific generator on top of the list of generators
setGen :: (Typeable a) => Gen a -> Registry _ _ -> Registry _ _
setGen g r = fun g +: r
-- | set a specific range on top of the list of generators
setRange :: (Typeable a) => Range a -> Registry _ _ -> Registry _ _
setRange range r = fun range +: r
-- | Generate a list of min' to max' elements
listOf :: forall a. Int -> Int -> Gen a -> Gen [a]
listOf min' max' = Gen.list (linear min' max')
-- | Simplification for setting a new recursion depth on the registry
setDepth :: Depth -> Registry _ _ -> Registry _ _
setDepth d r = normalize $ val d +: r
-- | Decrement the depth of generation during recursion
decrementDepth :: Registry _ _ -> Registry _ _
decrementDepth = tweak (\(d :: Depth) -> d - 1)
-- | Set the number of fields in an object
setFieldsNb :: Int -> Registry _ _ -> Registry _ _
setFieldsNb n r = fun (listOf @FieldName 1 n) +: r
-- | Set a generator for field names
setFieldNames :: Gen FieldName -> Registry _ _ -> Registry _ _
setFieldNames n r = fun n +: r
-- | Depth of generated Values
newtype Depth = Depth {unDepth :: Int}
deriving newtype (Eq, Show, Num)
-- | Newtype for the name of fields in an object
newtype FieldName = FieldName {unFieldName :: Text}
deriving newtype (Eq, Show, Read)