git.iko.soy/typeable/swagger2
on master help sign in
Merge pull request #18 from GetShopTV/validate-#12

JSON validation based on Swagger Schema

commit87906b04a0f3286150309e3fb81346359f7fc06b authorNickolay Kudasov <fizruk@users.noreply.github.com> dateMon 1 Feb 2016, 17:56 (11 years ago) parent0a0c49414bd0e8
7 files changed+641−9
src/Data/Swagger/Internal.hs+4−1 src/Data/Swagger/Internal/Schema/Validation.hs+324 src/Data/Swagger/Lens.hs+3−1 src/Data/Swagger/Schema/Validation.hs+83 stack.yaml+1−1 swagger2.cabal+13−6 test/Data/Swagger/Schema/ValidationSpec.hs+213
src/Data/Swagger/Internal.hs+4−1
@@ -496,6 +496,9 @@ data NamedSchema = NamedSchema
496496 , _namedSchemaSchema :: Schema
497497 } deriving (Eq, Show, Generic, Data, Typeable)
498498
499+-- | Regex pattern for @string@ type.
500+type Pattern = Text
501+
499502data ParamSchema t = ParamSchema
500503 { -- | Declares the value of the parameter that the server will use if none is provided,
501504 -- for example a @"count"@ to control the number of results per page might default to @100@
@@ -513,7 +516,7 @@ data ParamSchema t = ParamSchema
513516 , _paramSchemaExclusiveMinimum :: Maybe Bool
514517 , _paramSchemaMaxLength :: Maybe Integer
515518 , _paramSchemaMinLength :: Maybe Integer
516− , _paramSchemaPattern :: Maybe Text
519+ , _paramSchemaPattern :: Maybe Pattern
517520 , _paramSchemaMaxItems :: Maybe Integer
518521 , _paramSchemaMinItems :: Maybe Integer
519522 , _paramSchemaUniqueItems :: Maybe Bool
src/Data/Swagger/Internal/Schema/Validation.hsnew file+324
@@ -0,0 +1,324 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE FunctionalDependencies #-}
4+{-# LANGUAGE GADTs #-}
5+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+{-# LANGUAGE LambdaCase #-}
7+{-# LANGUAGE MultiParamTypeClasses #-}
8+{-# LANGUAGE PackageImports #-}
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE TemplateHaskell #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+-- |
14+-- Module: Data.Swagger.Internal.Schema.Validation
15+-- Copyright: (c) 2015 GetShopTV
16+-- License: BSD3
17+-- Maintainer: Nickolay Kudasov <nickolay@getshoptv.com>
18+-- Stability: experimental
19+--
20+-- Validate JSON values with Swagger Schema.
21+module Data.Swagger.Internal.Schema.Validation where
22+
23+import Control.Applicative
24+import Control.Lens
25+import Control.Lens.TH
26+import Control.Monad (when)
27+
28+import Data.Aeson hiding (Result)
29+import Data.Foldable (traverse_, for_, sequenceA_)
30+import Data.HashMap.Strict (HashMap)
31+import qualified Data.HashMap.Strict as HashMap
32+import qualified "unordered-containers" Data.HashSet as HashSet
33+import Data.Monoid
34+import Data.Proxy
35+import Data.Scientific (Scientific, isInteger)
36+import Data.Text (Text)
37+import qualified Data.Text as Text
38+import Data.Vector (Vector)
39+import qualified Data.Vector as Vector
40+
41+import Data.Swagger.Declare
42+import Data.Swagger.Internal
43+import Data.Swagger.Internal.Schema
44+import Data.Swagger.Lens
45+
46+-- | Validate @'ToJSON'@ instance matches @'ToSchema'@ for a given value.
47+-- This can be used with QuickCheck to ensure those instances are coherent:
48+--
49+-- prop> validateToJSON (x :: Int) == []
50+--
51+-- /NOTE:/ @'validateToJSON'@ does not perform string pattern validation.
52+-- See @'validateToJSONWithPatternChecker'@.
53+validateToJSON :: forall a. (ToJSON a, ToSchema a) => a -> [ValidationError]
54+validateToJSON = validateToJSONWithPatternChecker (\_pattern _str -> True)
55+
56+-- | Validate @'ToJSON'@ instance matches @'ToSchema'@ for a given value and pattern checker.
57+-- This can be used with QuickCheck to ensure those instances are coherent.
58+--
59+-- For validation without patterns see @'validateToJSON'@.
60+validateToJSONWithPatternChecker :: forall a. (ToJSON a, ToSchema a) =>
61+ (Pattern -> Text -> Bool) -> a -> [ValidationError]
62+validateToJSONWithPatternChecker checker x = case runValidation (validateWithSchema js) cfg schema of
63+ Failed xs -> xs
64+ Passed _ -> mempty
65+ where
66+ (defs, schema) = runDeclare (declareSchema (Proxy :: Proxy a)) mempty
67+ js = toJSON x
68+ cfg = defaultConfig
69+ { configPatternChecker = checker
70+ , configDefinitions = defs }
71+
72+-- | Validation error message.
73+type ValidationError = String
74+
75+-- | Validation result type.
76+data Result a
77+ = Failed [ValidationError] -- ^ Validation failed with a list of error messages.
78+ | Passed a -- ^ Validation passed.
79+ deriving (Eq, Show, Functor)
80+
81+instance Applicative Result where
82+ pure = Passed
83+ Passed f <*> Passed x = Passed (f x)
84+ Failed xs <*> Failed ys = Failed (xs <> ys)
85+ Failed xs <*> _ = Failed xs
86+ _ <*> Failed ys = Failed ys
87+
88+instance Alternative Result where
89+ empty = Failed mempty
90+ Passed x <|> _ = Passed x
91+ _ <|> y = y
92+
93+instance Monad Result where
94+ return = pure
95+ Passed x >>= f = f x
96+ Failed xs >>= f = Failed xs
97+
98+-- | Validation configuration.
99+data Config = Config
100+ { -- | Pattern checker for @'_paramSchemaPattern'@ validation.
101+ configPatternChecker :: Pattern -> Text -> Bool
102+ -- | Schema definitions in scope to resolve references.
103+ , configDefinitions :: Definitions Schema
104+ }
105+
106+-- | Default @'Config'@:
107+--
108+-- @
109+-- defaultConfig = 'Config'
110+-- { 'configPatternChecker' = \\_pattern _str -> True
111+-- , 'configDefinitions' = mempty
112+-- }
113+-- @
114+defaultConfig :: Config
115+defaultConfig = Config
116+ { configPatternChecker = \_pattern _str -> True
117+ , configDefinitions = mempty
118+ }
119+
120+-- | Value validation.
121+newtype Validation s a = Validation { runValidation :: Config -> s -> Result a }
122+ deriving (Functor)
123+
124+instance Applicative (Validation schema) where
125+ pure x = Validation (\_ _ -> pure x)
126+ Validation f <*> Validation x = Validation (\c s -> f c s <*> x c s)
127+
128+instance Alternative (Validation schema) where
129+ empty = Validation (\_ _ -> empty)
130+ Validation x <|> Validation y = Validation (\c s -> x c s <|> y c s)
131+
132+instance Profunctor Validation where
133+ dimap f g (Validation k) = Validation (\c s -> fmap g (k c (f s)))
134+
135+instance Choice Validation where
136+ left' (Validation g) = Validation (\c -> either (fmap Left . g c) (pure . Right))
137+ right' (Validation g) = Validation (\c -> either (pure . Left) (fmap Right . g c))
138+
139+instance Monad (Validation s) where
140+ return = pure
141+ Validation x >>= f = Validation (\c s -> x c s >>= \x -> runValidation (f x) c s)
142+ (>>) = (*>)
143+
144+withConfig :: (Config -> Validation s a) -> Validation s a
145+withConfig f = Validation (\c -> runValidation (f c) c)
146+
147+withSchema :: (s -> Validation s a) -> Validation s a
148+withSchema f = Validation (\c s -> runValidation (f s) c s)
149+
150+-- | Issue an error message.
151+invalid :: String -> Validation schema a
152+invalid msg = Validation (\_ _ -> Failed [msg])
153+
154+-- | Validation passed.
155+valid :: Validation schema ()
156+valid = pure ()
157+
158+-- | Validate schema's property given a lens into that property
159+-- and property checker.
160+check :: Lens' s (Maybe a) -> (a -> Validation s ()) -> Validation s ()
161+check l g = withSchema $ \schema ->
162+ case schema ^. l of
163+ Nothing -> valid
164+ Just x -> g x
165+
166+-- | Validate same value with different schema.
167+sub :: t -> Validation t a -> Validation s a
168+sub = lmap . const
169+
170+-- | Validate same value with a part of the original schema.
171+sub_ :: Getting a s a -> Validation a r -> Validation s r
172+sub_ = lmap . view
173+
174+-- | Validate value against a schema given schema reference and validation function.
175+withRef :: Reference -> (Schema -> Validation s a) -> Validation s a
176+withRef (Reference ref) f = withConfig $ \cfg ->
177+ case HashMap.lookup ref (configDefinitions cfg) of
178+ Nothing -> invalid $ "unknown schema " ++ show ref
179+ Just s -> f s
180+
181+validateWithSchemaRef :: Referenced Schema -> Value -> Validation s ()
182+validateWithSchemaRef (Ref ref) js = withRef ref $ \schema -> sub schema (validateWithSchema js)
183+validateWithSchemaRef (Inline s) js = sub s (validateWithSchema js)
184+
185+-- | Validate JSON @'Value'@ with Swagger @'Schema'@.
186+validateWithSchema :: Value -> Validation Schema ()
187+validateWithSchema value = do
188+ validateSchemaType value
189+ sub_ paramSchema $ validateEnum value
190+
191+-- | Validate JSON @'Value'@ with Swagger @'ParamSchema'@.
192+validateWithParamSchema :: Value -> Validation (ParamSchema t) ()
193+validateWithParamSchema value = do
194+ validateParamSchemaType value
195+ validateEnum value
196+
197+validateInteger :: Scientific -> Validation (ParamSchema t) ()
198+validateInteger n = do
199+ when (not (isInteger n)) $
200+ invalid ("not an integer")
201+ validateNumber n
202+
203+validateNumber :: Scientific -> Validation (ParamSchema t) ()
204+validateNumber n = withConfig $ \cfg -> withSchema $ \schema -> do
205+ let exMax = Just True == schema ^. exclusiveMaximum
206+ exMin = Just True == schema ^. exclusiveMinimum
207+
208+ check maximum_ $ \m ->
209+ when (if exMax then (n >= m) else (n > m)) $
210+ invalid ("value " ++ show n ++ " exceeds maximum (should be " ++ if exMax then "<" else "<=" ++ show m ++ ")")
211+
212+ check minimum_ $ \m ->
213+ when (if exMin then (n <= m) else (n < m)) $
214+ invalid ("value " ++ show n ++ " falls below minimum (should be " ++ if exMin then ">" else ">=" ++ show m ++ ")")
215+
216+ check multipleOf $ \k ->
217+ when (not (isInteger (n / k))) $
218+ invalid ("expected a multiple of " ++ show k ++ " but got " ++ show n)
219+
220+validateString :: Text -> Validation (ParamSchema t) ()
221+validateString s = do
222+ check maxLength $ \n ->
223+ when (len > fromInteger n) $
224+ invalid ("string is too long (length should be <=" ++ show n ++ ")")
225+
226+ check minLength $ \n ->
227+ when (len < fromInteger n) $
228+ invalid ("string is too short (length should be >=" ++ show n ++ ")")
229+
230+ check pattern $ \regex -> do
231+ withConfig $ \cfg -> do
232+ when (not (configPatternChecker cfg regex s)) $
233+ invalid ("string does not match pattern " ++ show regex)
234+ where
235+ len = Text.length s
236+
237+validateArray :: Vector Value -> Validation (ParamSchema t) ()
238+validateArray xs = do
239+ check maxItems $ \n ->
240+ when (len > fromInteger n) $
241+ invalid ("array exceeds maximum size (should be <=" ++ show n ++ ")")
242+
243+ check minItems $ \n ->
244+ when (len < fromInteger n) $
245+ invalid ("array is too short (size should be >=" ++ show n ++ ")")
246+
247+ check items $ \case
248+ SwaggerItemsPrimitive _ itemSchema -> sub itemSchema $ traverse_ validateWithParamSchema xs
249+ SwaggerItemsObject itemSchema -> traverse_ (validateWithSchemaRef itemSchema) xs
250+ SwaggerItemsArray itemSchemas -> do
251+ when (len /= length itemSchemas) $
252+ invalid ("array size is invalid (should be exactly " ++ show (length itemSchemas) ++ ")")
253+ sequenceA_ (zipWith validateWithSchemaRef itemSchemas (Vector.toList xs))
254+
255+ check uniqueItems $ \unique ->
256+ when (unique && not allUnique) $
257+ invalid ("array is expected to contain unique items, but it does not")
258+ where
259+ len = Vector.length xs
260+ allUnique = len == HashSet.size (HashSet.fromList (Vector.toList xs))
261+
262+validateObject :: HashMap Text Value -> Validation Schema ()
263+validateObject o = withSchema $ \schema ->
264+ case schema ^. discriminator of
265+ Just pname -> case fromJSON <$> HashMap.lookup pname o of
266+ Just (Success ref) -> validateWithSchemaRef ref (Object o)
267+ Just (Error msg) -> invalid ("failed to parse discriminator property " ++ show pname ++ ": " ++ show msg)
268+ Nothing -> invalid ("discriminator property " ++ show pname ++ "is missing")
269+ Nothing -> do
270+ check maxProperties $ \n ->
271+ when (size > n) $
272+ invalid ("object size exceeds maximum (total number of properties should be <=" ++ show n ++ ")")
273+
274+ check minProperties $ \n ->
275+ when (size < n) $
276+ invalid ("object size is too small (total number of properties should be >=" ++ show n ++ ")")
277+
278+ validateRequired
279+ validateProps
280+ where
281+ size = fromIntegral (HashMap.size o)
282+
283+ validateRequired = withSchema $ \schema -> traverse_ validateReq (schema ^. required)
284+ validateReq name =
285+ when (not (HashMap.member name o)) $
286+ invalid ("property " ++ show name ++ " is required, but not found in " ++ show (encode o))
287+
288+ validateProps = withSchema $ \schema -> do
289+ for_ (HashMap.toList o) $ \(k, v) ->
290+ case v of
291+ Null | not (k `elem` (schema ^. required)) -> valid -- null is fine for non-required property
292+ _ ->
293+ case HashMap.lookup k (schema ^. properties) of
294+ Nothing -> check additionalProperties $ \s -> sub s $ validateWithSchema v
295+ Just s -> validateWithSchemaRef s v
296+
297+validateEnum :: Value -> Validation (ParamSchema t) ()
298+validateEnum value = do
299+ check enum_ $ \xs ->
300+ when (value `notElem` xs) $
301+ invalid ("expected one of " ++ show (encode xs) ++ " but got " ++ show value)
302+
303+validateSchemaType :: Value -> Validation Schema ()
304+validateSchemaType value = withSchema $ \schema ->
305+ case (schema ^. type_, value) of
306+ (SwaggerNull, Null) -> valid
307+ (SwaggerBoolean, Bool _) -> valid
308+ (SwaggerInteger, Number n) -> sub_ paramSchema (validateInteger n)
309+ (SwaggerNumber, Number n) -> sub_ paramSchema (validateNumber n)
310+ (SwaggerString, String s) -> sub_ paramSchema (validateString s)
311+ (SwaggerArray, Array xs) -> sub_ paramSchema (validateArray xs)
312+ (SwaggerObject, Object o) -> validateObject o
313+ (t, _) -> invalid $ "expected JSON value of type " ++ show t
314+
315+validateParamSchemaType :: Value -> Validation (ParamSchema t) ()
316+validateParamSchemaType value = withSchema $ \schema ->
317+ case (schema ^. type_, value) of
318+ (SwaggerBoolean, Bool _) -> valid
319+ (SwaggerInteger, Number n) -> validateInteger n
320+ (SwaggerNumber, Number n) -> validateNumber n
321+ (SwaggerString, String s) -> validateString s
322+ (SwaggerArray, Array xs) -> validateArray xs
323+ (t, _) -> invalid $ "expected JSON value of type " ++ show t
324+
src/Data/Swagger/Lens.hs+3−1
@@ -83,10 +83,12 @@ instance At Responses where at n = responses . at n
8383instance Ixed Operation where ix n = responses . ix n
8484instance At Operation where at n = responses . at n
8585
86+instance HasParamSchema NamedSchema (ParamSchema Schema) where paramSchema = schema.paramSchema
87+
8688-- HasType instances
8789instance HasType Header (SwaggerType Header) where type_ = paramSchema.type_
8890instance HasType Schema (SwaggerType Schema) where type_ = paramSchema.type_
89−instance HasType NamedSchema (SwaggerType Schema) where type_ = schema.paramSchema.type_
91+instance HasType NamedSchema (SwaggerType Schema) where type_ = paramSchema.type_
9092instance HasType ParamOtherSchema (SwaggerType ParamOtherSchema) where type_ = paramSchema.type_
9193
9294-- HasDefault instances
src/Data/Swagger/Schema/Validation.hsnew file+83
@@ -0,0 +1,83 @@
1+-- |
2+-- Module: Data.Swagger.Schema.Validation
3+-- Copyright: (c) 2015 GetShopTV
4+-- License: BSD3
5+-- Maintainer: Nickolay Kudasov <nickolay@getshoptv.com>
6+-- Stability: experimental
7+--
8+-- Validate JSON values with Swagger Schema.
9+module Data.Swagger.Schema.Validation (
10+ -- * How to use validation
11+ -- $howto
12+
13+ -- ** Examples
14+ -- $examples
15+
16+ -- ** Validating @'Maybe'@
17+ -- $maybe
18+
19+ -- * JSON validation
20+ validateToJSON,
21+ validateToJSONWithPatternChecker,
22+ ValidationError,
23+) where
24+
25+import Data.Swagger.Internal.Schema.Validation
26+
27+-- $setup
28+-- >>> import Control.Lens
29+-- >>> import Data.Aeson
30+-- >>> import Data.Proxy
31+-- >>> import Data.Swagger
32+-- >>> import GHC.Generics
33+-- >>> :set -XDeriveGeneric
34+
35+-- $howto
36+--
37+-- This module provides helpful functions for JSON validation.
38+-- These functions are meant to be used in test suites for your application
39+-- to ensure that JSON respresentation for your data corresponds to
40+-- schemas you're using for the Swagger specification.
41+--
42+-- It is recommended to use validation functions as QuickCheck properties
43+-- (see <http://hackage.haskell.org/package/QuickCheck>).
44+
45+-- $examples
46+--
47+-- >>> validateToJSON "hello"
48+-- []
49+--
50+-- >>> validateToJSON False
51+-- []
52+--
53+-- >>> newtype Nat = Nat Integer deriving Generic
54+-- >>> instance ToJSON Nat where toJSON (Nat n) = toJSON n
55+-- >>> instance ToSchema Nat where declareNamedSchema proxy = genericDeclareNamedSchema defaultSchemaOptions proxy & mapped.minimum_ ?~ 0
56+-- >>> validateToJSON (Nat 10)
57+-- []
58+-- >>> validateToJSON (Nat (-5))
59+-- ["value -5.0 falls below minimum (should be >=0.0)"]
60+
61+-- $maybe
62+--
63+-- Because @'Maybe' a@ has the same schema as @a@, validation
64+-- generally fails for @null@ JSON:
65+--
66+-- >>> validateToJSON (Nothing :: Maybe String)
67+-- ["expected JSON value of type SwaggerString"]
68+-- >>> validateToJSON ([Just "hello", Nothing] :: [Maybe String])
69+-- ["expected JSON value of type SwaggerString"]
70+-- >>> validateToJSON (123, Nothing :: Maybe String)
71+-- ["expected JSON value of type SwaggerString"]
72+--
73+-- However, when @'Maybe' a@ is a type of a record field,
74+-- validation takes @'required'@ property of the @'Schema'@
75+-- into account:
76+--
77+-- >>> data Person = Person { name :: String, age :: Maybe Int } deriving Generic
78+-- >>> instance ToJSON Person
79+-- >>> instance ToSchema Person
80+-- >>> validateToJSON (Person "John" (Just 25))
81+-- []
82+-- >>> validateToJSON (Person "John" Nothing)
83+-- []
stack.yaml+1−1
@@ -2,4 +2,4 @@ flags: {}
22packages:
33- '.'
44extra-deps: []
5−resolver: lts-3.22
5+resolver: nightly-2016-01-26
swagger2.cabal+13−6
@@ -28,11 +28,13 @@ library
2828 Data.Swagger.Lens
2929 Data.Swagger.ParamSchema
3030 Data.Swagger.Schema
31+ Data.Swagger.Schema.Validation
3132 Data.Swagger.SchemaOptions
3233
3334 -- internal modules
3435 Data.Swagger.Internal
3536 Data.Swagger.Internal.Schema
37+ Data.Swagger.Internal.Schema.Validation
3638 Data.Swagger.Internal.ParamSchema
3739 Data.Swagger.Internal.Utils
3840 build-depends: base >=4.7 && <4.10
@@ -41,15 +43,16 @@ library
4143 , containers
4244 , hashable
4345 , http-media
46+ , lens
4447 , mtl
4548 , network
49+ , scientific
4650 , text
4751 , template-haskell
4852 , time
4953 , transformers
5054 , unordered-containers
51− , lens
52− , scientific
55+ , vector
5356 default-language: Haskell2010
5457
5558test-suite spec
@@ -58,14 +61,17 @@ test-suite spec
5861 main-is: Spec.hs
5962 build-depends: base
6063 , base-compat
61− , swagger2
64+ , aeson
65+ , aeson-qq
66+ , containers
67+ , hashable
6268 , hspec
6369 , HUnit
70+ , mtl
6471 , QuickCheck
72+ , swagger2
6573 , text
66− , aeson
67− , aeson-qq
68− , containers
74+ , time
6975 , unordered-containers
7076 , vector
7177 , lens
@@ -73,6 +79,7 @@ test-suite spec
7379 SpecCommon
7480 Data.SwaggerSpec
7581 Data.Swagger.SchemaSpec
82+ Data.Swagger.Schema.ValidationSpec
7683 default-language: Haskell2010
7784
7885test-suite doctest
test/Data/Swagger/Schema/ValidationSpec.hsnew file+213
@@ -0,0 +1,213 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE PackageImports #-}
4+module Data.Swagger.Schema.ValidationSpec where
5+
6+import Control.Applicative
7+import Data.Aeson
8+import Data.Aeson.Types
9+import Data.Int
10+import Data.IntMap (IntMap)
11+import qualified Data.IntMap as IntMap
12+import Data.IntSet (IntSet)
13+import Data.Hashable (Hashable)
14+import "unordered-containers" Data.HashSet (HashSet)
15+import qualified "unordered-containers" Data.HashSet as HashSet
16+import Data.HashMap.Strict (HashMap)
17+import qualified Data.HashMap.Strict as HashMap
18+import Data.Map (Map)
19+import qualified Data.Map as Map
20+import Data.Proxy
21+import Data.Time
22+import qualified Data.Text as T
23+import qualified Data.Text.Lazy as TL
24+import Data.Set (Set)
25+import qualified Data.Set as Set
26+import Data.Word
27+import GHC.Generics
28+
29+import Data.Swagger
30+import Data.Swagger.Declare
31+import Data.Swagger.Schema.Validation
32+
33+import SpecCommon
34+import Test.Hspec
35+import Test.Hspec.QuickCheck
36+import Test.QuickCheck
37+
38+shouldValidate :: (ToJSON a, ToSchema a) => Proxy a -> a -> Bool
39+shouldValidate _ x = validateToJSON x == []
40+
41+spec :: Spec
42+spec = do
43+ describe "Validation" $ do
44+ prop "Bool" $ shouldValidate (Proxy :: Proxy Bool)
45+ prop "Char" $ shouldValidate (Proxy :: Proxy Char)
46+ prop "Double" $ shouldValidate (Proxy :: Proxy Double)
47+ prop "Float" $ shouldValidate (Proxy :: Proxy Float)
48+ prop "Int" $ shouldValidate (Proxy :: Proxy Int)
49+ prop "Int8" $ shouldValidate (Proxy :: Proxy Int8)
50+ prop "Int16" $ shouldValidate (Proxy :: Proxy Int16)
51+ prop "Int32" $ shouldValidate (Proxy :: Proxy Int32)
52+ prop "Int64" $ shouldValidate (Proxy :: Proxy Int64)
53+ prop "Integer" $ shouldValidate (Proxy :: Proxy Integer)
54+ prop "Word" $ shouldValidate (Proxy :: Proxy Word)
55+ prop "Word8" $ shouldValidate (Proxy :: Proxy Word8)
56+ prop "Word16" $ shouldValidate (Proxy :: Proxy Word16)
57+ prop "Word32" $ shouldValidate (Proxy :: Proxy Word32)
58+ prop "Word64" $ shouldValidate (Proxy :: Proxy Word64)
59+ prop "String" $ shouldValidate (Proxy :: Proxy String)
60+ prop "()" $ shouldValidate (Proxy :: Proxy ())
61+ prop "ZonedTime" $ shouldValidate (Proxy :: Proxy ZonedTime)
62+ prop "UTCTime" $ shouldValidate (Proxy :: Proxy UTCTime)
63+ prop "T.Text" $ shouldValidate (Proxy :: Proxy T.Text)
64+ prop "TL.Text" $ shouldValidate (Proxy :: Proxy TL.Text)
65+ prop "[String]" $ shouldValidate (Proxy :: Proxy [String])
66+ -- prop "(Maybe [Int])" $ shouldValidate (Proxy :: Proxy (Maybe [Int]))
67+ prop "(IntMap String)" $ shouldValidate (Proxy :: Proxy (IntMap String))
68+ prop "(Set Bool)" $ shouldValidate (Proxy :: Proxy (Set Bool))
69+ prop "(HashSet Bool)" $ shouldValidate (Proxy :: Proxy (HashSet Bool))
70+ prop "(Either Int String)" $ shouldValidate (Proxy :: Proxy (Either Int String))
71+ prop "(Int, String)" $ shouldValidate (Proxy :: Proxy (Int, String))
72+ prop "(Map String Int)" $ shouldValidate (Proxy :: Proxy (Map String Int))
73+ prop "(Map T.Text Int)" $ shouldValidate (Proxy :: Proxy (Map T.Text Int))
74+ prop "(Map TL.Text Bool)" $ shouldValidate (Proxy :: Proxy (Map TL.Text Bool))
75+ prop "(HashMap String Int)" $ shouldValidate (Proxy :: Proxy (HashMap String Int))
76+ prop "(HashMap T.Text Int)" $ shouldValidate (Proxy :: Proxy (HashMap T.Text Int))
77+ prop "(HashMap TL.Text Bool)" $ shouldValidate (Proxy :: Proxy (HashMap TL.Text Bool))
78+ prop "(Int, String, Double)" $ shouldValidate (Proxy :: Proxy (Int, String, Double))
79+ prop "(Int, String, Double, [Int])" $ shouldValidate (Proxy :: Proxy (Int, String, Double, [Int]))
80+ prop "(Int, String, Double, [Int], Int)" $ shouldValidate (Proxy :: Proxy (Int, String, Double, [Int], Int))
81+ prop "Person" $ shouldValidate (Proxy :: Proxy Person)
82+ prop "Color" $ shouldValidate (Proxy :: Proxy Color)
83+ prop "Paint" $ shouldValidate (Proxy :: Proxy Paint)
84+ prop "MyRoseTree" $ shouldValidate (Proxy :: Proxy MyRoseTree)
85+ prop "Light" $ shouldValidate (Proxy :: Proxy Light)
86+
87+main :: IO ()
88+main = hspec spec
89+
90+-- ========================================================================
91+-- Person (simple record with optional fields)
92+-- ========================================================================
93+data Person = Person
94+ { name :: String
95+ , phone :: Integer
96+ , email :: Maybe String
97+ } deriving (Show, Generic)
98+
99+instance ToJSON Person
100+instance ToSchema Person
101+
102+instance Arbitrary Person where
103+ arbitrary = Person <$> arbitrary <*> arbitrary <*> arbitrary
104+
105+-- ========================================================================
106+-- Color (enum)
107+-- ========================================================================
108+data Color = Red | Green | Blue deriving (Show, Generic, Bounded, Enum)
109+
110+instance ToJSON Color
111+instance ToSchema Color
112+
113+instance Arbitrary Color where
114+ arbitrary = arbitraryBoundedEnum
115+
116+-- ========================================================================
117+-- Paint (record with bounded enum property)
118+-- ========================================================================
119+
120+newtype Paint = Paint { color :: Color }
121+ deriving (Show, Generic)
122+
123+instance ToJSON Paint
124+instance ToSchema Paint
125+
126+instance Arbitrary Paint where
127+ arbitrary = Paint <$> arbitrary
128+
129+-- ========================================================================
130+-- MyRoseTree (custom datatypeNameModifier)
131+-- ========================================================================
132+
133+data MyRoseTree = MyRoseTree
134+ { root :: String
135+ , trees :: [MyRoseTree]
136+ } deriving (Show, Generic)
137+
138+instance ToJSON MyRoseTree
139+
140+instance ToSchema MyRoseTree where
141+ declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions
142+ { datatypeNameModifier = drop (length "My") }
143+
144+instance Arbitrary MyRoseTree where
145+ arbitrary = fmap (cut limit) $ MyRoseTree <$> arbitrary <*> (take limit <$> arbitrary)
146+ where
147+ limit = 4
148+ cut 0 (MyRoseTree x _ ) = MyRoseTree x []
149+ cut n (MyRoseTree x xs) = MyRoseTree x (map (cut (n - 1)) xs)
150+
151+-- ========================================================================
152+-- Light (sum type)
153+-- ========================================================================
154+
155+data Light = NoLight | LightFreq Double | LightColor Color deriving (Show, Generic)
156+
157+instance ToSchema Light
158+
159+instance ToJSON Light where
160+ toJSON = genericToJSON defaultOptions { sumEncoding = ObjectWithSingleField }
161+
162+instance Arbitrary Light where
163+ arbitrary = oneof
164+ [ return NoLight
165+ , LightFreq <$> arbitrary
166+ , LightColor <$> arbitrary
167+ ]
168+
169+-- Arbitrary instances for common types
170+
171+#if MIN_VERSION_QuickCheck(2,8,2)
172+#else
173+instance (Ord k, Arbitrary k, Arbitrary v) => Arbitrary (Map k v) where
174+ arbitrary = Map.fromList <$> arbitrary
175+
176+instance Arbitrary a => Arbitrary (IntMap a) where
177+ arbitrary = IntMap.fromList <$> arbitrary
178+
179+instance (Ord a, Arbitrary a) => Arbitrary (Set a) where
180+ arbitrary = Set.fromList <$> arbitrary
181+#endif
182+
183+instance (Eq k, Hashable k, Arbitrary k, Arbitrary v) => Arbitrary (HashMap k v) where
184+ arbitrary = HashMap.fromList <$> arbitrary
185+
186+instance (Eq a, Hashable a, Arbitrary a) => Arbitrary (HashSet a) where
187+ arbitrary = HashSet.fromList <$> arbitrary
188+
189+instance Arbitrary T.Text where
190+ arbitrary = T.pack <$> arbitrary
191+
192+instance Arbitrary TL.Text where
193+ arbitrary = TL.pack <$> arbitrary
194+
195+instance Arbitrary Day where
196+ arbitrary = liftA3 fromGregorian (fmap ((+ 1) . abs) arbitrary) arbitrary arbitrary
197+
198+instance Arbitrary LocalTime where
199+ arbitrary = LocalTime
200+ <$> arbitrary
201+ <*> liftA3 TimeOfDay (choose (0, 23)) (choose (0, 59)) (fromInteger <$> choose (0, 60))
202+
203+instance Eq ZonedTime where
204+ ZonedTime t (TimeZone x _ _) == ZonedTime t' (TimeZone y _ _) = t == t' && x == y
205+
206+instance Arbitrary ZonedTime where
207+ arbitrary = ZonedTime
208+ <$> arbitrary
209+ <*> liftA3 TimeZone arbitrary arbitrary (vectorOf 3 (elements ['A'..'Z']))
210+
211+instance Arbitrary UTCTime where
212+ arbitrary = UTCTime <$> arbitrary <*> fmap fromInteger (choose (0, 86400))
213+