git.iko.soy/typeable/openapi3
on master help sign in
Refactor validation, allow separate ParamSchema validation
16648b7Nickolay Kudasov
src/Data/Swagger/Schema/Validation.hs+222−139
@@ -1,8 +1,14 @@
1{-# LANGUAGE DeriveFunctor #-}
1{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE FunctionalDependencies #-}
2{-# LANGUAGE GADTs #-}
4{-# LANGUAGE GADTs #-}
3{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+{-# LANGUAGE LambdaCase #-}
7+{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE PackageImports #-}
8{-# LANGUAGE PackageImports #-}
5{-# LANGUAGE RankNTypes #-}
9{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE UndecidableInstances #-}
6-- |
12-- |
7-- Module: Data.Swagger.Schema.Validate
13-- Module: Data.Swagger.Schema.Validate
8-- Copyright: (c) 2015 GetShopTV
14-- Copyright: (c) 2015 GetShopTV
@@ -15,178 +21,255 @@ module Data.Swagger.Schema.Validation where
15
21
16import Control.Applicative
22import Control.Applicative
17import Control.Lens
23import Control.Lens
18−import Control.Monad.Reader
24+import Control.Lens.TH
25+import Control.Monad (when)
19
26
20−import Data.Aeson
27+import Data.Aeson hiding (Result)
21import Data.Foldable (traverse_, for_, sequenceA_)
28import Data.Foldable (traverse_, for_, sequenceA_)
29+import Data.HashMap.Strict (HashMap)
22import qualified Data.HashMap.Strict as HashMap
30import qualified Data.HashMap.Strict as HashMap
23import qualified "unordered-containers" Data.HashSet as HashSet
31import qualified "unordered-containers" Data.HashSet as HashSet
32+import Data.Monoid
24−import Data.Scientific (isInteger)
33+import Data.Scientific (Scientific, isInteger)
25import Data.Text (Text)
34import Data.Text (Text)
26import qualified Data.Text as Text
35import qualified Data.Text as Text
36+import Data.Vector (Vector)
27import qualified Data.Vector as Vector
37import qualified Data.Vector as Vector
28
38
29import Data.Swagger.Internal
39import Data.Swagger.Internal
30import Data.Swagger.Lens
40import Data.Swagger.Lens
31
41
32−data ValidationResult a
42+
33− = ValidationFailed [ValidationError]
43+data Result a
34− | ValidationPassed a
44+ = Failed [String]
45+ | Passed a
35 deriving (Eq, Show, Functor)
46 deriving (Eq, Show, Functor)
36
47
37−instance Applicative ValidationResult where
48+instance Applicative Result where
38− pure = ValidationPassed
49+ pure = Passed
39− ValidationPassed f <*> ValidationPassed x = ValidationPassed (f x)
50+ Passed f <*> Passed x = Passed (f x)
40− ValidationFailed xs <*> ValidationFailed ys = ValidationFailed (xs ++ ys)
51+ Failed xs <*> Failed ys = Failed (xs <> ys)
41− ValidationFailed xs <*> _ = ValidationFailed xs
52+ Failed xs <*> _ = Failed xs
42− _ <*> ValidationFailed ys = ValidationFailed ys
53+ _ <*> Failed ys = Failed ys
43
54
44−instance Alternative ValidationResult where
55+instance Alternative Result where
45− empty = ValidationFailed []
56+ empty = Failed mempty
46− ValidationPassed x <|> _ = ValidationPassed x
57+ Passed x <|> _ = Passed x
47− _ <|> y = y
58+ _ <|> y = y
48
59
49−instance Monad ValidationResult where
60+instance Monad Result where
50 return = pure
61 return = pure
51− ValidationPassed x >>= f = f x
62+ Passed x >>= f = f x
52− ValidationFailed msg >>= _ = ValidationFailed msg
63+ Failed xs >>= f = Failed xs
53
64
54−data ValidationConfig = ValidationConfig
65+data Config = Config
55− { validationPatternChecker :: Pattern -> Text -> Bool
66+ { configPatternChecker :: Pattern -> Text -> Bool
56− , validationDefinitions :: Definitions Schema
67+ , configDefinitions :: Definitions Schema
57 }
68 }
58
69
59−-- | Default @'ValidationConfig'@:
70+-- | Default @'Config'@:
60--
71--
61-- @
72-- @
62−-- defaultValidationConfig = 'ValidationConfig'
73+-- defaultConfig = 'Config'
63−-- { 'validationPatternChecker' = \_pattern _str -> True
74+-- { 'configPatternChecker' = \_pattern _str -> True
64−-- , 'validationDefinitions' = mempty
75+-- , 'configDefinitions' = mempty
65-- }
76-- }
66-- @
77-- @
67−defaultValidationConfig :: ValidationConfig
78+defaultConfig :: Config
68−defaultValidationConfig = ValidationConfig
79+defaultConfig = Config
69− { validationPatternChecker = \_pattern _str -> True
80+ { configPatternChecker = \_pattern _str -> True
70− , validationDefinitions = mempty
81+ , configDefinitions = mempty
71 }
82 }
72
83
73−newtype Validation a = Validation { runValidation :: ReaderT ValidationConfig ValidationResult a }
84+newtype Validation s a = Validation { runValidation :: Config -> s -> Result a }
74− deriving (Functor, Applicative, Alternative, Monad, MonadReader ValidationConfig)
85+ deriving (Functor)
86+
87+instance Applicative (Validation schema) where
88+ pure x = Validation (\_ _ -> pure x)
89+ Validation f <*> Validation x = Validation (\c s -> f c s <*> x c s)
90+
91+instance Alternative (Validation schema) where
92+ empty = Validation (\_ _ -> empty)
93+ Validation x <|> Validation y = Validation (\c s -> x c s <|> y c s)
94+
95+instance Profunctor Validation where
96+ dimap f g (Validation k) = Validation (\c s -> fmap g (k c (f s)))
97+
98+instance Choice Validation where
99+ left' (Validation g) = Validation (\c -> either (fmap Left . g c) (pure . Right))
100+ right' (Validation g) = Validation (\c -> either (pure . Left) (fmap Right . g c))
101+
102+instance Monad (Validation s) where
103+ return = pure
104+ Validation x >>= f = Validation (\c s -> x c s >>= \x -> runValidation (f x) c s)
105+ (>>) = (*>)
106+
107+withConfig :: (Config -> Validation s a) -> Validation s a
108+withConfig f = Validation (\c -> runValidation (f c) c)
109+
110+withSchema :: (s -> Validation s a) -> Validation s a
111+withSchema f = Validation (\c s -> runValidation (f s) c s)
75
112
76−invalid :: String -> Validation a
113+invalid :: String -> Validation schema a
77−invalid msg = Validation (ReaderT (const (ValidationFailed [msg])))
114+invalid msg = Validation (\_ _ -> Failed [msg])
78
115
79−valid :: Validation ()
116+valid :: Validation schema ()
80valid = pure ()
117valid = pure ()
81
118
82−unref :: Reference -> Validation Schema
119+check :: Lens' s (Maybe a) -> (a -> Validation s ()) -> Validation s ()
120+check l g = withSchema $ \schema ->
121+ case schema ^. l of
122+ Nothing -> valid
123+ Just x -> g x
124+
125+sub :: t -> Validation t a -> Validation s a
126+sub = lmap . const
127+
128+sub_ :: Getting a s a -> Validation a r -> Validation s r
129+sub_ = lmap . view
130+
131+withRef :: Reference -> (Schema -> Validation s a) -> Validation s a
83−unref (Reference ref) = do
132+withRef (Reference ref) f = withConfig $ \cfg ->
84− ms <- asks (HashMap.lookup ref . validationDefinitions)
133+ case HashMap.lookup ref (configDefinitions cfg) of
85− case ms of
86 Nothing -> invalid $ "unknown schema " ++ show ref
134 Nothing -> invalid $ "unknown schema " ++ show ref
87− Just s -> pure s
135+ Just s -> f s
136+
137+validateWithSchemaRef :: Referenced Schema -> Value -> Validation s ()
138+validateWithSchemaRef (Ref ref) js = withRef ref $ \schema -> sub schema (validateWithSchema js)
139+validateWithSchemaRef (Inline s) js = sub s (validateWithSchema js)
140+
141+validateWithSchema :: Value -> Validation Schema ()
142+validateWithSchema value = do
143+ validateSchemaType value
144+ sub_ paramSchema $ validateEnum value
145+
146+validateWithParamSchema :: Value -> Validation (ParamSchema t) ()
147+validateWithParamSchema value = do
148+ validateParamSchemaType value
149+ validateEnum value
150+
151+validateInteger :: Scientific -> Validation (ParamSchema t) ()
152+validateInteger n = do
153+ when (not (isInteger n)) $
154+ invalid ("not an integer: " ++ show n)
155+ validateNumber n
156+
157+validateNumber :: Scientific -> Validation (ParamSchema t) ()
158+validateNumber n = withConfig $ \cfg -> withSchema $ \schema -> do
159+ let exMax = Just True == schema ^. exclusiveMaximum
160+ exMin = Just True == schema ^. exclusiveMinimum
88
161
89−type ValidationError = String
162+ check maximum_ $ \m ->
163+ when (if exMax then (n >= m) else (n > m)) $
164+ invalid ("value " ++ show n ++ " exceeds maximum (should be " ++ if exMax then "<" else "<=" ++ show m ++ ")")
90
165
91−validateWithSchemaRef :: Referenced Schema -> Value -> Validation ()
166+ check minimum_ $ \m ->
92−validateWithSchemaRef (Ref ref) js = unref ref >>= flip validateWithSchema js
167+ when (if exMin then (n <= m) else (n < m)) $
93−validateWithSchemaRef (Inline s) js = validateWithSchema s js
168+ invalid ("value " ++ show n ++ " falls below minimum (should be " ++ if exMin then ">" else ">=" ++ show m ++ ")")
94
169
95−validateWithSchema :: Schema -> Value -> Validation ()
170+ check multipleOf $ \k ->
96−validateWithSchema schema value
171+ when (not (isInteger (n / k))) $
97− = validateType
172+ invalid ("expected a multiple of " ++ show k ++ " but got " ++ show n)
98− *> validateEnum
99
173
174+validateString :: Text -> Validation (ParamSchema t) ()
175+validateString s = do
176+ check maxLength $ \n ->
177+ when (len > fromInteger n) $
178+ invalid ("string is too long (length should be <=" ++ show n ++ "): " ++ show s)
179+
180+ check minLength $ \n ->
181+ when (len < fromInteger n) $
182+ invalid ("string is too short (length should be >=" ++ show n ++ "): " ++ show s)
183+
184+ check pattern $ \regex -> do
185+ withConfig $ \cfg -> do
186+ when (not (configPatternChecker cfg regex s)) $
187+ invalid ("string does not match pattern " ++ show regex ++ ": " ++ show s)
188+ where
189+ len = Text.length s
190+
191+validateArray :: Vector Value -> Validation (ParamSchema t) ()
192+validateArray xs = do
193+ check maxItems $ \n ->
194+ when (len > fromInteger n) $
195+ invalid ("array exceeds maximum size (should be <=" ++ show n ++ "): " ++ show (encode xs))
196+
197+ check minItems $ \n ->
198+ when (len < fromInteger n) $
199+ invalid ("array is too short (size should be >=" ++ show n ++ "): " ++ show (encode xs))
200+
201+ check items $ \case
202+ SwaggerItemsPrimitive _ itemSchema -> sub itemSchema $ traverse_ validateWithParamSchema xs
203+ SwaggerItemsObject itemSchema -> traverse_ (validateWithSchemaRef itemSchema) xs
204+ SwaggerItemsArray itemSchemas -> do
205+ when (len /= length itemSchemas) $
206+ invalid ("array size is invalid (should be exactly " ++ show (length itemSchemas) ++ "): " ++ show (encode xs))
207+ sequenceA_ (zipWith validateWithSchemaRef itemSchemas (Vector.toList xs))
208+
209+ check uniqueItems $ \unique ->
210+ when (unique && not allUnique) $
211+ invalid ("array is expected to contain unique items, but it does not: " ++ show (encode xs))
212+ where
213+ len = Vector.length xs
214+ allUnique = len == HashSet.size (HashSet.fromList (Vector.toList xs))
215+
216+validateObject :: HashMap Text Value -> Validation Schema ()
217+validateObject o = withSchema $ \schema ->
218+ case schema ^. discriminator of
219+ Just pname -> case fromJSON <$> HashMap.lookup pname o of
220+ Just (Success ref) -> validateWithSchemaRef ref (Object o)
221+ Just (Error msg) -> invalid ("failed to parse discriminator property " ++ show pname ++ ": " ++ show msg)
222+ Nothing -> invalid ("discriminator property " ++ show pname ++ "is missing: " ++ show (encode o))
223+ Nothing -> do
224+ check maxProperties $ \n ->
225+ when (size > n) $
226+ invalid ("object size exceeds maximum (total number of properties should be <=" ++ show n ++ "): " ++ show (encode o))
227+
228+ check minProperties $ \n ->
229+ when (size < n) $
230+ invalid ("object size is too small (total number of properties should be >=" ++ show n ++ "): " ++ show (encode o))
231+
232+ validateRequired
233+ validateProps
100 where
234 where
101− when' :: (a -> Bool) -> Lens' Schema (Maybe a) -> (a -> Validation ()) -> Validation ()
235+ size = fromIntegral (HashMap.size o)
102− when' p l m = do
236+
103− case schema ^. l of
104− Nothing -> valid
105− Just x -> when (p x) (m x)
106−
107− validateType =
108− case (schema ^. type_, value) of
109− (SwaggerNull, Null) -> valid
110− (SwaggerBoolean, Bool _) -> valid
111− (SwaggerInteger, Number n) -> validateInteger n
112− (SwaggerNumber, Number n) -> validateNumber n
113− (SwaggerString, String s) -> validateString s
114− (SwaggerArray, Array xs) -> validateArray xs
115− (SwaggerObject, Object o) -> validateObject o
116− (t, _) -> invalid $ "expected JSON value of type " ++ show t ++ ": " ++ show (encode value)
117−
118− validateInteger n
119− = when (not (isInteger n)) (invalid $ "not an integer: " ++ show n)
120− *> validateNumber n
121−
122− exMax = Just True == schema ^. exclusiveMaximum
123− exMin = Just True == schema ^. exclusiveMinimum
124−
125− validateNumber n
126− = (if exMax
127− then when' (n >=) maximum_ $ \m ->
128− invalid $ "value " ++ show n ++ " exceeds maximum (should be <" ++ show m ++ ")"
129− else when' (n >) maximum_ $ \m ->
130− invalid $ "value " ++ show n ++ " exceeds maximum (should be <=" ++ show m ++ ")")
131− *> (if exMin
132− then when' (n <=) minimum_ $ \m ->
133− invalid $ "value " ++ show n ++ " is less than minimum (should be >" ++ show m ++ ")"
134− else when' (n <) minimum_ $ \m ->
135− invalid $ "value " ++ show n ++ " is less than minimum (should be >=" ++ show m ++ ")")
136− *> when' (not . isInteger . (n /)) multipleOf
137− (\k -> invalid $ "expected a multiple of " ++ show k ++ " but got " ++ show n)
138−
139− validateString s
140− = when' (fromIntegral (Text.length s) >) maxLength
141− (\n -> invalid $ "string exceeds maximum length (should be <=" ++ show n ++ "): " ++ show s)
142− *> when' (fromIntegral (Text.length s) <) minLength
143− (\n -> invalid $ "string is too short (length should be >=" ++ show n ++ "): " ++ show s)
144− *> validatePattern
145− where
146− validatePattern = do
147− check <- asks validationPatternChecker
148− when' (`check` s) pattern
149− (\regex -> invalid $ "string does not match pattern " ++ show regex)
150−
151− validateArray xs
152− = when' (fromIntegral (Vector.length xs) >) maxItems
153− (\n -> invalid $ "array exceeds maximum size (should be <=" ++ show n ++ "): " ++ show (encode xs))
154− *> when' (fromIntegral (Vector.length xs) <) minItems
155− (\n -> invalid $ "array is too short (size should be >=" ++ show n ++ "): " ++ show (encode xs))
156− *> case schema ^. items of
157− Just (SwaggerItemsObject itemSchema) -> traverse_ (validateWithSchemaRef itemSchema) xs
158− Just (SwaggerItemsArray itemSchemas) ->
159− when (Vector.length xs /= length itemSchemas)
160− (invalid ("array size is invalid (should be exactly " ++ show (length itemSchemas) ++ "): " ++ show (encode xs)))
161− *> sequenceA_ (zipWith validateWithSchemaRef itemSchemas (Vector.toList xs))
162− Nothing -> invalid "invalid schema: array item schema expected"
163− *> when ((Just True == schema ^. uniqueItems) && not allUnique)
164− (invalid $ "array is expected to contain unique items, but it does not: " ++ show (encode xs))
165− where
166− allUnique = Vector.length xs == HashSet.size (HashSet.fromList (Vector.toList xs))
167−
168− validateObject o = case schema ^. discriminator of
169− Just name -> validateWithSchemaRef (Ref (Reference name)) value
170− Nothing ->
171− when' (fromIntegral (HashMap.size o) >) maxProperties
172− (\n -> invalid $ "object size exceeds maximum (total number of properties should be <=" ++ show n ++ "): " ++ show (encode o))
173− *> when' (fromIntegral (HashMap.size o) <) minProperties
174− (\n -> invalid $ "object size is too small (total number of properties should be >=" ++ show n ++ "): " ++ show (encode o))
175− *> validateRequired
176− *> validateProps
177− where
178− validateRequired = traverse_ validateReq (schema ^. required)
237+ validateRequired = withSchema $ \schema -> traverse_ validateReq (schema ^. required)
238+ validateReq name =
179− validateReq name = when (not (HashMap.member name o))
239+ when (not (HashMap.member name o)) $
180− (invalid $ "property " ++ show name ++ " is required, but not found in " ++ show (encode o))
240+ invalid ("property " ++ show name ++ " is required, but not found in " ++ show (encode o))
181−
241+
242+ validateProps = withSchema $ \schema -> do
182− validateProps = for_ (HashMap.toList o) $ \(k, v) ->
243+ for_ (HashMap.toList o) $ \(k, v) ->
183− case HashMap.lookup k (schema ^. properties) of
244+ case HashMap.lookup k (schema ^. properties) of
184− Nothing -> case schema ^. additionalProperties of
245+ Nothing -> check additionalProperties $ \s -> sub s $ validateWithSchema v
185− Nothing -> valid -- TODO: issue a warning
186− Just s -> validateWithSchema s v
187− Just s -> validateWithSchemaRef s v
246+ Just s -> validateWithSchemaRef s v
188−
247+
189− validateEnum = case schema ^. enum_ of
248+validateEnum :: Value -> Validation (ParamSchema t) ()
190− Nothing -> valid
249+validateEnum value = do
250+ check enum_ $ \xs ->
251+ when (value `notElem` xs) $
191− Just xs -> when (value `notElem` xs) $ invalid $ "expected one of " ++ show (encode xs) ++ " but got " ++ show value
252+ invalid ("expected one of " ++ show (encode xs) ++ " but got " ++ show value)
253+
254+validateSchemaType :: Value -> Validation Schema ()
255+validateSchemaType value = withSchema $ \schema ->
256+ case (schema ^. type_, value) of
257+ (SwaggerNull, Null) -> valid
258+ (SwaggerBoolean, Bool _) -> valid
259+ (SwaggerInteger, Number n) -> sub_ paramSchema (validateInteger n)
260+ (SwaggerNumber, Number n) -> sub_ paramSchema (validateNumber n)
261+ (SwaggerString, String s) -> sub_ paramSchema (validateString s)
262+ (SwaggerArray, Array xs) -> sub_ paramSchema (validateArray xs)
263+ (SwaggerObject, Object o) -> validateObject o
264+ (t, _) -> invalid $ "expected JSON value of type " ++ show t ++ ": " ++ show (encode value)
265+
266+validateParamSchemaType :: Value -> Validation (ParamSchema t) ()
267+validateParamSchemaType value = withSchema $ \schema ->
268+ case (schema ^. type_, value) of
269+ (SwaggerBoolean, Bool _) -> valid
270+ (SwaggerInteger, Number n) -> validateInteger n
271+ (SwaggerNumber, Number n) -> validateNumber n
272+ (SwaggerString, String s) -> validateString s
273+ (SwaggerArray, Array xs) -> validateArray xs
274+ (t, _) -> invalid $ "expected JSON value of type " ++ show t ++ ": " ++ show (encode value)
192
275
test/Data/Swagger/Schema/ValidationSpec.hs+2−4
@@ -2,7 +2,6 @@
2module Data.Swagger.Schema.ValidationSpec where
2module Data.Swagger.Schema.ValidationSpec where
3
3
4import Control.Applicative
4import Control.Applicative
5−import Control.Monad.Reader
6import Data.Aeson
5import Data.Aeson
7import Data.Int
6import Data.Int
8import Data.IntMap (IntMap)
7import Data.IntMap (IntMap)
@@ -32,10 +31,9 @@ import Test.Hspec.QuickCheck
32import Test.QuickCheck
31import Test.QuickCheck
33
32
34shouldValidate :: (ToJSON a, ToSchema a) => Proxy a -> a -> Bool
33shouldValidate :: (ToJSON a, ToSchema a) => Proxy a -> a -> Bool
35−shouldValidate proxy x = res == ValidationPassed ()
34+shouldValidate proxy x = res == Passed ()
36 where
35 where
37− res = flip runReaderT defaultValidationConfig . runValidation $ do
38− validateWithSchema (toSchema proxy) (toJSON x)
36+ res = runValidation (validateWithSchema (toJSON x)) defaultConfig (toSchema proxy)
39
37
40spec :: Spec
38spec :: Spec
41spec = do
39spec = do