git.iko.soy/typeable/hjsonschema
on master help sign in
1.3.0.0.
260d9ceIan Grant Jeffries
README.md+8−4
@@ -1,12 +1,16 @@
1![hjsonschema logo](./logo.jpg)
1![hjsonschema logo](./logo.jpg)
2
2
3−A Haskell implementation of the current [JSON Schema](http://json-schema.org/) specification (Draft 4).
3+A Haskell implementation of the most commonly used [JSON Schema](http://json-schema.org/) specification ([Draft 4](https://github.com/json-schema-org/json-schema-spec/wiki/Specification-Links#draft-4)).
4
4
5[Hackage](https://hackage.haskell.org/package/hjsonschema) / [GitHub](https://github.com/seagreen/hjsonschema) / [Travis CI](https://travis-ci.org/seagreen/hjsonschema)
5[Hackage](https://hackage.haskell.org/package/hjsonschema) / [GitHub](https://github.com/seagreen/hjsonschema) / [Travis CI](https://travis-ci.org/seagreen/hjsonschema)
6
6
7−Requires [pcre](http://www.pcre.org/) (`pkgs.pcre` in Nixpkgs).
7+# Notes
8
8
9++ As of 2017 [json-schema-org](https://github.com/json-schema-org/json-schema-spec) has begun releasing new drafts of the standard. Once this work stabilizes `hjsonschema` will add coverage for the latest draft.
10+
11++ Requires [pcre](http://www.pcre.org/) (`pkgs.pcre` in Nixpkgs).
12+
9−NOTE: Schemas with circular references can cause infinite loops. hjsonschema does loop detection but it may not be solid yet -- please open an issue if you find a situation where it fails.
13++ Schemas with circular references can cause infinite loops. hjsonschema does loop detection but it may not be solid yet -- please open an issue if you find a situation where it fails.
10
14
11# Example
15# Example
12
16
@@ -46,7 +50,7 @@ Run remote tests (makes GETs to json-schema.org, also temporarily starts an HTTP
46
50
47+ Currently doesn't support the optional `"format"` validators.
51+ Currently doesn't support the optional `"format"` validators.
48
52
49−## Notes
53+## Vendoring
50
54
51+ `JSON-Schema-Test-Suite` is vendored from commit # aabcb3427745ade7a0b4d49ff016ad7eda8b898b [here](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
55+ `JSON-Schema-Test-Suite` is vendored from commit # aabcb3427745ade7a0b4d49ff016ad7eda8b898b [here](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
52
56
changelog.txt+28−33
@@ -1,3 +1,17 @@
1+# 1.3.0.0
2+
3++ Rewrite failure messages.
4+
5+We used to return a list of every failed validator, no matter how far it was buried within subschemas of the original schema.
6+
7+Now we return a tree of failure messages, so that if the first schema had only three validators, then no more than three top level failures will be returned.
8+
9++ Move the code to parse each validator from JSON from `Data.Validator.Draft4` into the validators' modules themselves.
10+
11++ Switch from Prelude to Protolude.
12+
13++ Switch `Data.JsonSchema.Fetch` from lazy to strict bytestrings.
14+
1# 1.2.0.2
15# 1.2.0.2
2
16
3+ Bump hspec.
17+ Bump hspec.
@@ -8,8 +22,7 @@
8
22
9# 1.2.0.0
23# 1.2.0.0
10
24
11−+ Return `AdditionalPropertiesObject` error correctly (was mistakenly
25++ Return `AdditionalPropertiesObject` error correctly (was mistakenly returning `AdditionalItemsObject` instead.
12−returning `AdditionalItemsObject` instead.
13+ Don't silence errors resulting from subschemas of "anyOf" or "oneOf".
26+ Don't silence errors resulting from subschemas of "anyOf" or "oneOf".
14
27
15# 1.1.0.1
28# 1.1.0.1
@@ -18,33 +31,23 @@ returning `AdditionalItemsObject` instead.
18
31
19# 1.1.0.0
32# 1.1.0.0
20
33
21−+ Rename `schemaForSchemas` to `metaSchema` and `schemaForSchemasBytes` to
34++ Rename `schemaForSchemas` to `metaSchema` and `schemaForSchemasBytes` to `metaSchemaBytes`.
22−`metaSchemaBytes`.
23
35
24# 1.0.0.0
36# 1.0.0.0
25
37
26## Bug fixes:
38## Bug fixes:
27
39
28−+ Fix JSON Pointer bug. Pointers were being built in reverse order and so were
40++ Fix JSON Pointer bug. Pointers were being built in reverse order and so were totally invalid.
29−totally invalid.
30−+ Use `.:!` instead of `.:?` to parse the draft 4 schema. The only way to omit
31−optional fields in JSON Schema Draft 4 is to omit them entirely, `"null"`
41++ Use `.:!` instead of `.:?` to parse the draft 4 schema. The only way to omit optional fields in JSON Schema Draft 4 is to omit them entirely, `"null"` can't be used for this.
32−can't be used for this.
33
42
34## API Changes:
43## API Changes:
35
44
36+ Add referenced schema loop detection.
45+ Add referenced schema loop detection.
37+ Add a new `referencesValidity` function.
46+ Add a new `referencesValidity` function.
38−+ `checkSchema` now checks referenced schema's validity in addition to the
39−starting schema's validity. This change bubbles up to the one-step validation
47++ `checkSchema` now checks referenced schema's validity in addition to the starting schema's validity. This change bubbles up to the one-step validation functions as well.
40−functions as well.
41−+ Switch most of the fetching code to use `URISchemaMap` instead of
42−`ReferencedSchemas`. It didn't need to know about the more complicated data
48++ Switch most of the fetching code to use `URISchemaMap` instead of `ReferencedSchemas`. It didn't need to know about the more complicated data type.
43−type.
44−+ Rething failure related names. Change `Invalid` to `Failure`, add a new
49++ Rething failure related names. Change `Invalid` to `Failure`, add a new `Invalid` type alias which is only used for final results.
45−`Invalid` type alias which is only used for final results.
46−+ Failures now include the failing part of the data as well as a JSON Pointer to
50++ Failures now include the failing part of the data as well as a JSON Pointer to it, so you don't have to worry about resolving the pointer.
47−it, so you don't have to worry about resolving the pointer.
48
51
49## Fundamental Changes:
52## Fundamental Changes:
50
53
@@ -73,8 +76,7 @@ it, so you don't have to worry about resolving the pointer.
73
76
74+ Rewrite fetching internals.
77+ Rewrite fetching internals.
75+ Fix reference resolution defects, add more tests.
78+ Fix reference resolution defects, add more tests.
76−+ Switch to a Perl style regex library, which is closer to ECMAScript regexes
79++ Switch to a Perl style regex library, which is closer to ECMAScript regexes than the previous Posix style one.
77−than the previous Posix style one.
78+ Add one-step validation functions ('fetchFilesystemAndValidate' and 'fetchHTTPAndValidate').
80+ Add one-step validation functions ('fetchFilesystemAndValidate' and 'fetchHTTPAndValidate').
79+ Alias the validation failure type exported by 'Data.JsonSchema.Draft4' to
81+ Alias the validation failure type exported by 'Data.JsonSchema.Draft4' to
80'Invalid', change its field names.
82'Invalid', change its field names.
@@ -90,11 +92,8 @@ folder for how to use the new one.
90
92
91+ Improve scope updating and resolving.
93+ Improve scope updating and resolving.
92+ Rename RawSchema's _rsObject field to _rsData.
94+ Rename RawSchema's _rsObject field to _rsData.
93−+ Make RawSchema's _rsURI field a Maybe. This way schemas without a starting
95++ Make RawSchema's _rsURI field a Maybe. This way schemas without a starting URI can say so explicitly with Nothing instead of with "".
94−URI can say so explicitly with Nothing instead of with "".
95−+ Rename Graph to SchemaGraph. Declare it with data instead of type. Give it a
96−field referencing the starting schema. This field is used to find the curent
96++ Rename Graph to SchemaGraph. Declare it with data instead of type. Give it a field referencing the starting schema. This field is used to find the curent schema if no URI is in scope and a self-referencing $ref is found (e.g. "#").
97−schema if no URI is in scope and a self-referencing $ref is found (e.g. "#").
98+ Change the order of the last two arguments to fetchReferencedSchemas.
97+ Change the order of the last two arguments to fetchReferencedSchemas.
99
98
100# 0.7.1
99# 0.7.1
@@ -105,13 +104,10 @@ schema if no URI is in scope and a self-referencing $ref is found (e.g. "#").
105
104
106Change error type from Text to ValidationFailure.
105Change error type from Text to ValidationFailure.
107
106
108−Revert the 0.6 changes to validate. Also switch from Vector
107+Revert the 0.6 changes to validate. Also switch from Vector to list. Validate is now:
109−to list. Validate is now:
110 Schema err -> Value -> [ValidationFailure err]
108 Schema err -> Value -> [ValidationFailure err]
111
109
112−Add fetchReferencedSchemas', which lets the user provide their
110+Add fetchReferencedSchemas', which lets the user provide their own MonadIO function to be used when fetching schemas. This lets them do things like only fetch schemas from particular domains.
113−own MonadIO function to be used when fetching schemas. This lets
114−them do things like only fetch schemas from particular domains.
115
111
116# 0.6
112# 0.6
117
113
@@ -131,8 +127,7 @@ Change Schema
131
127
132# 0.5.2
128# 0.5.2
133
129
134−+ Add convenience function for validating and compiling draft 4 schemas
130++ Add convenience function for validating and compiling draft 4 schemas simultaneously.
135−simultaneously.
136
131
137# 0.5.1
132# 0.5.1
138
133
examples/AlternateSchema.hs+86−73
@@ -5,34 +5,28 @@
5-- to copy this module instead of the 'Data.JsonSchema.Draft4'. While it's
5-- to copy this module instead of the 'Data.JsonSchema.Draft4'. While it's
6-- less convenient to write schemas in Haskell without a record type, you
6-- less convenient to write schemas in Haskell without a record type, you
7-- can get the implementation finished with far fewer lines of code.
7-- can get the implementation finished with far fewer lines of code.
8−--
9−-- Note that this module imports the the failure sum type and failure related
10−-- helper functions from the library. If you're implementing a custom schema
11−-- with different error types from JSON Schema Draft 4 you'll have to make
12−-- your own.
13
8
14module AlternateSchema where
9module AlternateSchema where
15
10
11+import Protolude
12+
16−import Data.Aeson (FromJSON(..), Value(..),
13+import Data.Aeson (FromJSON(..), Value(..),
17− decode)
14+ decode)
18−import qualified Data.Aeson as AE
15+import qualified Data.Aeson as AE
19−import qualified Data.ByteString.Lazy as LBS
16+import qualified Data.ByteString.Lazy as LBS
20−import qualified Data.HashMap.Strict as HM
17+import qualified Data.HashMap.Strict as HM
21−import Data.Maybe (fromMaybe)
18+import Data.Maybe (fromMaybe)
22−import Data.Monoid
23−import Data.Profunctor (Profunctor (..))
19+import Data.Profunctor (Profunctor (..))
24−import Data.Text (Text)
20+
25−
26−import Data.JsonSchema.Draft4 (metaSchemaBytes)
21+import Data.JsonSchema.Draft4 (ValidatorFailure(..),
22+ metaSchemaBytes)
27−import Data.JsonSchema.Draft4.Failure
23+import Data.JsonSchema.Fetch (ReferencedSchemas(..),
28−import Data.JsonSchema.Fetch (ReferencedSchemas(..),
29− SchemaWithURI(..))
24+ SchemaWithURI(..))
30−import qualified Data.JsonSchema.Fetch as FE
25+import qualified Data.JsonSchema.Fetch as FE
31−import Data.JsonSchema.Types (Schema(..), Spec(..))
26+import Data.JsonSchema.Types (Schema(..), Spec(..))
32−import qualified Data.JsonSchema.Types as JT
27+import qualified Data.JsonSchema.Types as JT
33−import qualified Data.Validator.Draft4 as D4
28+import qualified Data.Validator.Draft4 as D4
34−import qualified Data.Validator.Draft4.Any as AN
35−import Data.Validator.Reference (updateResolutionScope)
29+import Data.Validator.Reference (updateResolutionScope)
36
30
37--------------------------------------------------
31--------------------------------------------------
38-- * Basic fetching tools
32-- * Basic fetching tools
@@ -44,12 +38,13 @@ referencesViaHTTP
44referencesViaHTTP = FE.referencesViaHTTP' draft4FetchInfo
38referencesViaHTTP = FE.referencesViaHTTP' draft4FetchInfo
45
39
46draft4FetchInfo :: FE.FetchInfo Schema
40draft4FetchInfo :: FE.FetchInfo Schema
47−draft4FetchInfo = FE.FetchInfo embedded (get "id") (get "$ref")
41+draft4FetchInfo = FE.FetchInfo embedded (lookup "id") (lookup "$ref")
48 where
42 where
49− get :: Text -> Schema -> Maybe Text
43+ lookup :: Text -> Schema -> Maybe Text
44+ lookup k (Schema s) =
50− get k (Schema s) = case HM.lookup k s of
45+ case HM.lookup k s of
51− Just (String t) -> Just t
46+ Just (String t) -> Just t
52− _ -> Nothing
47+ _ -> Nothing
53
48
54embedded :: Schema -> ([Schema], [Schema])
49embedded :: Schema -> ([Schema], [Schema])
55embedded s = JT.embedded (d4Spec (ReferencedSchemas s mempty) mempty Nothing) s
50embedded s = JT.embedded (d4Spec (ReferencedSchemas s mempty) mempty Nothing) s
@@ -63,26 +58,26 @@ validate
63 -> Maybe Text
58 -> Maybe Text
64 -> Schema
59 -> Schema
65 -> Value
60 -> Value
66− -> [Failure]
61+ -> [ValidatorFailure]
67−validate rs = continueValidating rs (AN.VisitedSchemas [(Nothing, Nothing)])
62+validate rs = continueValidating rs (D4.VisitedSchemas [(Nothing, Nothing)])
68
63
69-- A schema for schemas themselves, using @src/draft4.json@ which is loaded
64-- A schema for schemas themselves, using @src/draft4.json@ which is loaded
70-- at compile time.
65-- at compile time.
71metaSchema :: Schema
66metaSchema :: Schema
72metaSchema =
67metaSchema =
73− fromMaybe (error "Schema decode failed (this should never happen)")
68+ fromMaybe (panic "Schema decode failed (this should never happen)")
74 . decode
69 . decode
75 . LBS.fromStrict
70 . LBS.fromStrict
76 $ metaSchemaBytes
71 $ metaSchemaBytes
77
72
78−checkSchema :: Schema -> [Failure]
73+checkSchema :: Schema -> [ValidatorFailure]
79checkSchema = validate referenced Nothing metaSchema . Object . _unSchema
74checkSchema = validate referenced Nothing metaSchema . Object . _unSchema
80 where
75 where
81 referenced :: ReferencedSchemas Schema
76 referenced :: ReferencedSchemas Schema
82 referenced = ReferencedSchemas
77 referenced = ReferencedSchemas
83 metaSchema
78 metaSchema
84 (HM.singleton "http://json-schema.org/draft-04/schema"
79 (HM.singleton "http://json-schema.org/draft-04/schema"
85− metaSchema)
80+ metaSchema)
86
81
87--------------------------------------------------
82--------------------------------------------------
88-- * Spec
83-- * Spec
@@ -90,11 +85,11 @@ checkSchema = validate referenced Nothing metaSchema . Object . _unSchema
90
85
91continueValidating
86continueValidating
92 :: ReferencedSchemas Schema
87 :: ReferencedSchemas Schema
93− -> AN.VisitedSchemas
88+ -> D4.VisitedSchemas
94 -> Maybe Text
89 -> Maybe Text
95 -> Schema
90 -> Schema
96 -> Value
91 -> Value
97− -> [Failure]
92+ -> [ValidatorFailure]
98continueValidating referenced visited mURI sc =
93continueValidating referenced visited mURI sc =
99 JT.validate (d4Spec referenced visited newScope) sc
94 JT.validate (d4Spec referenced visited newScope) sc
100 where
95 where
@@ -108,39 +103,52 @@ continueValidating referenced visited mURI sc =
108
103
109d4Spec
104d4Spec
110 :: ReferencedSchemas Schema
105 :: ReferencedSchemas Schema
111− -> AN.VisitedSchemas
106+ -> D4.VisitedSchemas
112 -> Maybe Text
107 -> Maybe Text
113− -> Spec Schema ValidatorChain
108+ -> Spec Schema ValidatorFailure
109+ -- ^ Here we reuses 'ValidatorFailure' from
110+ -- 'Data.JsonSchema.Draft4.Failure'. If your validators have different
111+ -- failure possibilities you'll need to create your own validator
112+ -- failure type.
114d4Spec referenced visited scope =
113d4Spec referenced visited scope =
115 Spec
114 Spec
116− [ dimap f (const MultipleOf) D4.multipleOf
115+ [ dimap f FailureMultipleOf D4.multipleOfValidator
117− , dimap f maxE D4.maximumVal
116+ , dimap f FailureMaximum D4.maximumValidator
118− , dimap f minE D4.minimumVal
117+ , dimap f FailureMinimum D4.minimumValidator
119−
118+
120− , dimap f (const MaxLength) D4.maxLength
119+ , dimap f FailureMaxLength D4.maxLengthValidator
121− , dimap f (const MinLength) D4.minLength
120+ , dimap f FailureMinLength D4.minLengthValidator
122− , dimap f (const PatternValidator) D4.patternVal
121+ , dimap f FailurePattern D4.patternValidator
123−
122+
124− , dimap f (const MaxItems) D4.maxItems
123+ , dimap f FailureMaxItems D4.maxItemsValidator
125− , dimap f (const MinItems) D4.minItems
124+ , dimap f FailureMinItems D4.minItemsValidator
126− , dimap f (const UniqueItems) D4.uniqueItems
125+ , dimap f FailureUniqueItems D4.uniqueItemsValidator
127− , dimap f itemsE (D4.items descend)
126+ , dimap
128−
127+ (fromMaybe D4.emptyItems . f)
129− , dimap f (const MaxProperties) D4.maxProperties
128+ (\err -> case err of
130− , dimap f (const MinProperties) D4.minProperties
129+ D4.IRInvalidItems e -> FailureItems e
131− , dimap f (const Required) D4.required
130+ D4.IRInvalidAdditional e -> FailureAdditionalItems e)
132− , dimap f depsE (D4.dependencies descend)
131+ (D4.itemsRelatedValidator descend)
133− , dimap f propE (D4.properties descend)
132+
134− , dimap f patPropE (D4.patternProperties descend)
133+ , dimap f FailureMaxProperties D4.maxPropertiesValidator
135− , dimap f addPropE (D4.additionalProperties descend)
134+ , dimap f FailureMinProperties D4.minPropertiesValidator
136−
135+ , dimap f FailureRequired D4.requiredValidator
136+ , dimap f FailureDependencies (D4.dependenciesValidator descend)
137+ , dimap
138+ (fromMaybe D4.emptyProperties . f)
139+ FailurePropertiesRelated
140+ (D4.propertiesRelatedValidator descend)
141+
142+ , dimap
143+ f
144+ FailureRef
137− , dimap f refE (D4.ref visited scope (FE.getReference referenced) refVal)
145+ (D4.refValidator visited scope (FE.getReference referenced) getRef)
138− , dimap f (const Enum) D4.enumVal
146+ , dimap f FailureEnum D4.enumValidator
139− , dimap f (const TypeValidator) D4.typeVal
147+ , dimap f FailureType D4.typeValidator
140− , dimap f AllOf (D4.allOf lateral)
148+ , dimap f FailureAllOf (D4.allOfValidator lateral)
141− , dimap f AnyOf (D4.anyOf lateral)
149+ , dimap f FailureAnyOf (D4.anyOfValidator lateral)
142− , dimap f oneOfE (D4.oneOf lateral)
150+ , dimap f FailureOneOf (D4.oneOfValidator lateral)
143− , dimap f (const NotValidator) (D4.notVal lateral)
151+ , dimap f FailureNot (D4.notValidator lateral)
144 ]
152 ]
145 where
153 where
146 f :: FromJSON a => Schema -> Maybe a
154 f :: FromJSON a => Schema -> Maybe a
@@ -148,13 +156,18 @@ d4Spec referenced visited scope =
148 AE.Error _ -> Nothing
156 AE.Error _ -> Nothing
149 AE.Success b -> Just b
157 AE.Success b -> Just b
150
158
151− -- 'Maybe Text' is the URI the refernced schema is fetch from,
159+ -- 'Maybe Text' is the URI the referenced schema is fetched from,
152 -- this probably needs a 'newtype' wrapper.
160 -- this probably needs a 'newtype' wrapper.
161+ getRef
153− refVal :: AN.VisitedSchemas -> Maybe Text -> Schema -> Value -> [Failure]
162+ :: D4.VisitedSchemas
163+ -> Maybe Text
164+ -> Schema
165+ -> Value
166+ -> [ValidatorFailure]
154− refVal = continueValidating referenced
167+ getRef = continueValidating referenced
155−
168+
156− descend :: Schema -> Value -> [Failure]
169+ descend :: Schema -> Value -> [ValidatorFailure]
157 descend = continueValidating referenced mempty scope
170 descend = continueValidating referenced mempty scope
158
171
159− lateral :: Schema -> Value -> [Failure]
172+ lateral :: Schema -> Value -> [ValidatorFailure]
160 lateral = continueValidating referenced visited scope
173 lateral = continueValidating referenced visited scope
examples/Full.hsdeleted−40
@@ -1,40 +0,0 @@
1−-- | Two differences from @examples/Simple.hs@:
2−--
3−-- * This shows how to write the starting schema in Haskell instead
4−-- of parsing it from JSON.
5−--
6−-- * Validation is done in two steps using 'D4.referencesViaFilesystem' and
7−-- 'D4.checkSchema' instead of 'D4.fetchFilesystemAndValidate'. This means
8−-- that the actual validation involves no IO.
9−
10−module Full where
11−
12−import Data.Aeson (Value (..), toJSON)
13−
14−import qualified Data.JsonSchema.Draft4 as D4
15−
16−schema :: D4.Schema
17−schema = D4.emptySchema { D4._schemaRef = Just "./unique.json" }
18−
19−schemaContext :: D4.SchemaWithURI D4.Schema
20−schemaContext = D4.SchemaWithURI
21− { D4._swSchema = schema
22− , D4._swURI = Just "./examples/json/start.json"
23− }
24−
25−badData :: Value
26−badData = toJSON (["foo", "foo"] :: [String])
27−
28−example :: IO ()
29−example = do
30− res <- D4.referencesViaFilesystem schemaContext
31− let references = case res of
32− Left _ -> error "Couldn't fetch referenced schemas."
33− Right a -> a
34− validate = case D4.checkSchema references schemaContext of
35− Left _ -> error "Not a valid schema."
36− Right f -> f
37− case validate badData of
38− [] -> error "We validated bad data."
39− [D4.Failure (D4.Ref D4.UniqueItems) _ _ _] -> return () -- Success
40− _ -> error "We got a different failure than expected."
examples/Simple.hs+21−20
@@ -1,10 +1,8 @@
1−-- | Demonstrate 'D4.fetchFilesystemAndValidate'.
2−--
3−-- To fetch schemas using HTTP instead of from the filesystem use
4−-- 'D4.fetchHTTPAndValidate'.
5
1
6module Simple where
2module Simple where
7
3
4+import Protolude
5+
8import Data.Aeson (Value(..), decode, toJSON)
6import Data.Aeson (Value(..), decode, toJSON)
9import qualified Data.ByteString.Lazy as LBS
7import qualified Data.ByteString.Lazy as LBS
10import qualified Data.List.NonEmpty as NE
8import qualified Data.List.NonEmpty as NE
@@ -13,25 +11,28 @@ import Data.Maybe (fromMaybe)
13import qualified Data.JsonSchema.Draft4 as D4
11import qualified Data.JsonSchema.Draft4 as D4
14
12
15badData :: Value
13badData :: Value
16−badData = toJSON (["foo", "foo"] :: [String])
14+badData = toJSON [True, True]
17
15
18example :: IO ()
16example :: IO ()
19example = do
17example = do
18+ bts <- LBS.readFile "./examples/json/unique.json"
19+ let schema = fromMaybe (panic "Invalid schema JSON.") (decode bts)
20+ schemaWithURI = D4.SchemaWithURI
21+ schema
22+ Nothing -- This would be the URI of the schema
23+ -- if it had one. It's used if the schema
24+ -- has relative references to other
25+ -- schemas.
20
26
21− -- Get the starting schema.
22− bts <- LBS.readFile "./examples/json/start.json"
23− let schema = fromMaybe (error "Invalid schema JSON.") (decode bts)
24− schemaWithURI = D4.SchemaWithURI schema (Just "./examples/json/start.json")
25−
26− -- Fetch any referenced schemas, check that our original schema itself
27+ -- Fetch any referenced schemas over HTTP, check that our original schema
27− -- is valid, and validate the data.
28+ -- itself is valid, and validate the data.
28− res <- D4.fetchFilesystemAndValidate schemaWithURI badData
29+ res <- D4.fetchHTTPAndValidate schemaWithURI badData
29 case res of
30 case res of
30− Right () -> error "We validated bad data."
31+ Right () -> panic "We validated bad data."
31− Left (D4.FVRead _) -> error ("Error fetching a referenced schema"
32+ Left (D4.HVRequest _) -> panic ("Error fetching a referenced schema"
32− ++ " (either during IO or parsing).")
33+ <> " (either during IO or parsing).")
33− Left (D4.FVSchema _) -> error "Our 'schema' itself was invalid."
34+ Left (D4.HVSchema _) -> panic "Our 'schema' itself was invalid."
34− Left (D4.FVData failures) ->
35+ Left (D4.HVData (D4.Invalid _ _ failures)) ->
35 case NE.toList failures of
36 case NE.toList failures of
36− [D4.Failure (D4.Ref D4.UniqueItems) _ _ _] -> return () -- Success.
37+ [D4.FailureUniqueItems _] -> pure () -- Success.
37− _ -> error "Got more invalidation reasons than we expected."
38+ _ -> panic "We got a different failure than expected."
examples/TwoStep.hsnew file+49
@@ -0,0 +1,49 @@
1+-- | Differences from @examples/Simple.hs@:
2+--
3+-- * We demonstrate how to do the validation step without IO by first
4+-- getting the references with 'D4.referencesViaFilesystem'.
5+--
6+-- * This shows how to write the starting schema in Haskell instead
7+-- of parsing it from JSON.
8+--
9+-- * This schema references other schemas. Previously we used
10+-- 'fetchHTTPAndValidate' which fetched references over HTTP (though it
11+-- didn't matter because the last schema didn't actually have any).
12+-- This time we need to get them from the filesystem.
13+
14+module TwoStep where
15+
16+import Protolude
17+
18+import Data.Aeson (Value (..), toJSON)
19+import qualified Data.List.NonEmpty as NE
20+
21+import qualified Data.JsonSchema.Draft4 as D4
22+import qualified Data.Validator.Draft4 as VAL
23+
24+schema :: D4.Schema
25+schema = D4.emptySchema { D4._schemaRef = Just "./unique.json" }
26+
27+schemaContext :: D4.SchemaWithURI D4.Schema
28+schemaContext = D4.SchemaWithURI
29+ { D4._swSchema = schema
30+ , D4._swURI = Just "./examples/json/start.json"
31+ }
32+
33+badData :: Value
34+badData = toJSON [True, True]
35+
36+example :: IO ()
37+example = do
38+ res <- D4.referencesViaFilesystem schemaContext
39+ let references = case res of
40+ Left _ -> panic "Couldn't fetch referenced schemas."
41+ Right a -> a
42+ validate = case D4.checkSchema references schemaContext of
43+ Left _ -> panic "Not a valid schema."
44+ Right f -> f
45+ case validate badData of
46+ [] -> panic "We validated bad data."
47+ [D4.FailureRef (VAL.RefInvalid _ _ (D4.FailureUniqueItems _ NE.:|[])) ] ->
48+ pure () -- Success
49+ _ -> panic "We got a different failure than expected."
hjsonschema.cabal+16−9
@@ -1,5 +1,5 @@
1name: hjsonschema
1name: hjsonschema
2−version: 1.2.0.2
2+version: 1.3.0.0
3synopsis: JSON Schema library
3synopsis: JSON Schema library
4homepage: https://github.com/seagreen/hjsonschema
4homepage: https://github.com/seagreen/hjsonschema
5license: MIT
5license: MIT
@@ -26,15 +26,15 @@ library
26 src
26 src
27 default-language: Haskell2010
27 default-language: Haskell2010
28 default-extensions:
28 default-extensions:
29+ DeriveFunctor
30+ DeriveGeneric
31+ GeneralizedNewtypeDeriving
29 NoImplicitPrelude
32 NoImplicitPrelude
30 OverloadedStrings
33 OverloadedStrings
31 ScopedTypeVariables
34 ScopedTypeVariables
35+ TupleSections
32 if impl(ghc >= 8)
36 if impl(ghc >= 8)
33 default-extensions: StrictData
37 default-extensions: StrictData
34− other-extensions:
35− DeriveFunctor
36− GeneralizedNewtypeDeriving
37− TemplateHaskell
38 ghc-options:
38 ghc-options:
39 -Wall
39 -Wall
40 exposed-modules:
40 exposed-modules:
@@ -50,7 +50,6 @@ library
50 , Data.Validator.Draft4.Number
50 , Data.Validator.Draft4.Number
51 , Data.Validator.Draft4.Object
51 , Data.Validator.Draft4.Object
52 , Data.Validator.Draft4.String
52 , Data.Validator.Draft4.String
53− , Data.Validator.Failure
54 , Data.Validator.Reference
53 , Data.Validator.Reference
55 , Data.Validator.Types
54 , Data.Validator.Types
56 , Data.Validator.Utils
55 , Data.Validator.Utils
@@ -60,17 +59,19 @@ library
60 build-depends:
59 build-depends:
61 base >= 4.7 && < 4.10
60 base >= 4.7 && < 4.10
62 -- 0.11 is for `.:!`:
61 -- 0.11 is for `.:!`:
63− , aeson >= 0.11 && < 1.1
62+ , aeson >= 0.11 && < 1.2
64 , bytestring >= 0.10 && < 0.11
63 , bytestring >= 0.10 && < 0.11
65 , containers >= 0.5 && < 0.6
64 , containers >= 0.5 && < 0.6
66 , file-embed >= 0.0.8 && < 0.1
65 , file-embed >= 0.0.8 && < 0.1
67 , filepath >= 1.3 && < 1.5
66 , filepath >= 1.3 && < 1.5
67+ , hashable >= 1.2 && < 1.3
68− , hjsonpointer >= 0.3 && < 1.1
68+ , hjsonpointer >= 1.1 && < 1.2
69 -- 0.4.30 is for parseUrlThrow:
69 -- 0.4.30 is for parseUrlThrow:
70 , http-client >= 0.4.30 && < 0.6
70 , http-client >= 0.4.30 && < 0.6
71 , http-types >= 0.8 && < 0.10
71 , http-types >= 0.8 && < 0.10
72 , pcre-heavy >= 1.0 && < 1.1
72 , pcre-heavy >= 1.0 && < 1.1
73 , profunctors >= 5.0 && < 5.3
73 , profunctors >= 5.0 && < 5.3
74+ , protolude >= 0.1.10 && < 1.2
74 , QuickCheck >= 2.8 && < 2.10
75 , QuickCheck >= 2.8 && < 2.10
75 , scientific >= 0.3 && < 0.4
76 , scientific >= 0.3 && < 0.4
76 , semigroups >= 0.18 && < 0.19
77 , semigroups >= 0.18 && < 0.19
@@ -89,6 +90,8 @@ test-suite local
89 -Wall
90 -Wall
90 -fno-warn-orphans
91 -fno-warn-orphans
91 default-extensions:
92 default-extensions:
93+ DeriveGeneric
94+ NoImplicitPrelude
92 OverloadedStrings
95 OverloadedStrings
93 ScopedTypeVariables
96 ScopedTypeVariables
94 other-modules:
97 other-modules:
@@ -98,8 +101,8 @@ test-suite local
98 , Shared
101 , Shared
99 -- from ./examples:
102 -- from ./examples:
100 , AlternateSchema
103 , AlternateSchema
101− , Full
102 , Simple
104 , Simple
105+ , TwoStep
103 build-depends:
106 build-depends:
104 base
107 base
105 , aeson
108 , aeson
@@ -108,6 +111,7 @@ test-suite local
108 , hjsonpointer
111 , hjsonpointer
109 , hjsonschema
112 , hjsonschema
110 , profunctors
113 , profunctors
114+ , protolude
111 , semigroups
115 , semigroups
112 , text
116 , text
113 , QuickCheck
117 , QuickCheck
@@ -129,6 +133,8 @@ test-suite remote
129 -Wall
133 -Wall
130 -fno-warn-orphans
134 -fno-warn-orphans
131 default-extensions:
135 default-extensions:
136+ DeriveGeneric
137+ NoImplicitPrelude
132 OverloadedStrings
138 OverloadedStrings
133 ScopedTypeVariables
139 ScopedTypeVariables
134 other-modules:
140 other-modules:
@@ -144,6 +150,7 @@ test-suite remote
144 , hjsonpointer
150 , hjsonpointer
145 , hjsonschema
151 , hjsonschema
146 , profunctors
152 , profunctors
153+ , protolude
147 , semigroups
154 , semigroups
148 , text
155 , text
149 , unordered-containers
156 , unordered-containers
shell.nix+3
@@ -6,5 +6,8 @@ haskell.lib.buildStackProject {
6 buildInputs = [
6 buildInputs = [
7 pkgconfig
7 pkgconfig
8 pcre
8 pcre
9+
10+ # for `git:` references in stack.yml:
11+ git
9 ];
12 ];
10}
13}
src/Data/JsonSchema/Draft4.hs+58−46
@@ -10,7 +10,7 @@ module Data.JsonSchema.Draft4
10 , fetchHTTPAndValidate
10 , fetchHTTPAndValidate
11 , HTTPValidationFailure(..)
11 , HTTPValidationFailure(..)
12 , FE.HTTPFailure(..)
12 , FE.HTTPFailure(..)
13− , InvalidSchema
13+ , SchemaInvalid(..)
14
14
15 -- * One-step validation (getting references from the filesystem)
15 -- * One-step validation (getting references from the filesystem)
16 , fetchFilesystemAndValidate
16 , fetchFilesystemAndValidate
@@ -18,13 +18,12 @@ module Data.JsonSchema.Draft4
18 , FE.FilesystemFailure(..)
18 , FE.FilesystemFailure(..)
19
19
20 -- * Validation failure
20 -- * Validation failure
21− , Invalid
21+ , Invalid(..)
22− , Failure
22+ , ValidatorFailure(..)
23− , FR.Fail(..)
24− , ValidatorChain(..)
25
23
26 -- * Fetching tools
24 -- * Fetching tools
27 , ReferencedSchemas(..)
25 , ReferencedSchemas(..)
26+ , FE.URISchemaMap(..)
28 , referencesViaHTTP
27 , referencesViaHTTP
29 , referencesViaFilesystem
28 , referencesViaFilesystem
30
29
@@ -38,9 +37,8 @@ module Data.JsonSchema.Draft4
38 ) where
37 ) where
39
38
40import Import
39import Import
41−import Prelude
42
40
43−import Control.Arrow (first, left)
41+import Control.Arrow (left)
44import qualified Data.ByteString as BS
42import qualified Data.ByteString as BS
45import Data.FileEmbed (embedFile,
43import Data.FileEmbed (embedFile,
46 makeRelativeToProject)
44 makeRelativeToProject)
@@ -48,25 +46,24 @@ import qualified Data.HashMap.Strict as HM
48import qualified Data.List.NonEmpty as NE
46import qualified Data.List.NonEmpty as NE
49import Data.Maybe (fromMaybe)
47import Data.Maybe (fromMaybe)
50
48
51−import Data.JsonSchema.Draft4.Failure (Failure, Invalid,
49+import Data.JsonSchema.Draft4.Failure (Invalid(..),
52− InvalidSchema,
50+ SchemaInvalid(..),
53− ValidatorChain(..))
51+ ValidatorFailure(..))
54import Data.JsonSchema.Draft4.Schema (Schema)
52import Data.JsonSchema.Draft4.Schema (Schema)
55import qualified Data.JsonSchema.Draft4.Schema as SC
53import qualified Data.JsonSchema.Draft4.Schema as SC
56import qualified Data.JsonSchema.Draft4.Spec as Spec
54import qualified Data.JsonSchema.Draft4.Spec as Spec
57import Data.JsonSchema.Fetch (ReferencedSchemas(..),
55import Data.JsonSchema.Fetch (ReferencedSchemas(..),
58 SchemaWithURI(..))
56 SchemaWithURI(..))
59import qualified Data.JsonSchema.Fetch as FE
57import qualified Data.JsonSchema.Fetch as FE
60−import qualified Data.Validator.Failure as FR
61
58
62data HTTPValidationFailure
59data HTTPValidationFailure
63 = HVRequest FE.HTTPFailure
60 = HVRequest FE.HTTPFailure
64− | HVSchema InvalidSchema
61+ | HVSchema SchemaInvalid
65 | HVData Invalid
62 | HVData Invalid
66 deriving Show
63 deriving Show
67
64
68-- | Fetch recursively referenced schemas over HTTP, check that both the
65-- | Fetch recursively referenced schemas over HTTP, check that both the
69−-- original and referenced schemas are valid, and then validate data.
66+-- original and referenced schemas are valid, then validate then data.
70fetchHTTPAndValidate
67fetchHTTPAndValidate
71 :: SchemaWithURI Schema
68 :: SchemaWithURI Schema
72 -> Value
69 -> Value
@@ -76,23 +73,27 @@ fetchHTTPAndValidate sw v = do
76 pure (g =<< f =<< left HVRequest res)
73 pure (g =<< f =<< left HVRequest res)
77 where
74 where
78 f :: FE.URISchemaMap Schema
75 f :: FE.URISchemaMap Schema
79− -> Either HTTPValidationFailure (Value -> [Failure])
76+ -> Either HTTPValidationFailure (Value -> [ValidatorFailure])
80 f references = left HVSchema (checkSchema references sw)
77 f references = left HVSchema (checkSchema references sw)
81
78
82− g :: (Value -> [Failure]) -> Either HTTPValidationFailure ()
79+ g :: (Value -> [ValidatorFailure]) -> Either HTTPValidationFailure ()
83− g validate = case NE.nonEmpty (validate v) of
80+ g val = case NE.nonEmpty (val v) of
84− Nothing -> Right ()
81+ Nothing -> Right ()
85− Just failures -> Left (HVData failures)
82+ Just failures -> Left (HVData Invalid
83+ { _invalidSchema = _swSchema sw
84+ , _invalidInstance = v
85+ , _invalidFailures = failures
86+ })
86
87
87data FilesystemValidationFailure
88data FilesystemValidationFailure
88 = FVRead FE.FilesystemFailure
89 = FVRead FE.FilesystemFailure
89− | FVSchema InvalidSchema
90+ | FVSchema SchemaInvalid
90 | FVData Invalid
91 | FVData Invalid
91 deriving (Show, Eq)
92 deriving (Show, Eq)
92
93
93−-- | Fetch recursively referenced schemas from the filesystem, check that
94+-- | Fetch recursively referenced schemas from the filesystem, check
94−-- both the original and referenced schemas are valid, and then
95+-- that both the original and referenced schemas are valid, then validate
95−-- validate data.
96+-- the data.
96fetchFilesystemAndValidate
97fetchFilesystemAndValidate
97 :: SchemaWithURI Schema
98 :: SchemaWithURI Schema
98 -> Value
99 -> Value
@@ -102,13 +103,17 @@ fetchFilesystemAndValidate sw v = do
102 pure (g =<< f =<< left FVRead res)
103 pure (g =<< f =<< left FVRead res)
103 where
104 where
104 f :: FE.URISchemaMap Schema
105 f :: FE.URISchemaMap Schema
105− -> Either FilesystemValidationFailure (Value -> [Failure])
106+ -> Either FilesystemValidationFailure (Value -> [ValidatorFailure])
106 f references = left FVSchema (checkSchema references sw)
107 f references = left FVSchema (checkSchema references sw)
107
108
108− g :: (Value -> [Failure]) -> Either FilesystemValidationFailure ()
109+ g :: (Value -> [ValidatorFailure]) -> Either FilesystemValidationFailure ()
109− g validate = case NE.nonEmpty (validate v) of
110+ g val = case NE.nonEmpty (val v) of
110− Nothing -> Right ()
111+ Nothing -> Right ()
111− Just invalid -> Left (FVData invalid)
112+ Just invalid -> Left (FVData Invalid
113+ { _invalidSchema = _swSchema sw
114+ , _invalidInstance = v
115+ , _invalidFailures = invalid
116+ })
112
117
113-- | An instance of 'Data.JsonSchema.Fetch.FetchInfo' specialized for
118-- | An instance of 'Data.JsonSchema.Fetch.FetchInfo' specialized for
114-- JSON Schema Draft 4.
119-- JSON Schema Draft 4.
@@ -128,27 +133,31 @@ referencesViaFilesystem
128 -> IO (Either FE.FilesystemFailure (FE.URISchemaMap Schema))
133 -> IO (Either FE.FilesystemFailure (FE.URISchemaMap Schema))
129referencesViaFilesystem = FE.referencesViaFilesystem' draft4FetchInfo
134referencesViaFilesystem = FE.referencesViaFilesystem' draft4FetchInfo
130
135
131−-- | A helper function.
132−--
133−-- Checks if a schema and a set of referenced schemas are valid.
136+-- | Checks if a schema and a set of referenced schemas are valid.
134--
137--
135-- Return a function to validate data.
138-- Return a function to validate data.
136checkSchema
139checkSchema
137 :: FE.URISchemaMap Schema
140 :: FE.URISchemaMap Schema
138 -> SchemaWithURI Schema
141 -> SchemaWithURI Schema
139− -> Either InvalidSchema (Value -> [Failure])
142+ -> Either SchemaInvalid (Value -> [ValidatorFailure])
140checkSchema sm sw =
143checkSchema sm sw =
141 case NE.nonEmpty failures of
144 case NE.nonEmpty failures of
145+ Just fs -> Left (SchemaInvalid fs)
146+ Nothing -> Right (Spec.specValidate
142− Nothing -> Right (Spec.validate (ReferencedSchemas (_swSchema sw) sm) sw)
147+ (ReferencedSchemas (_swSchema sw)
143− Just fs -> Left fs
148+ (FE._unURISchemaMap sm))
149+ sw)
144 where
150 where
145− failures :: [(Maybe Text, Failure)]
151+ failures :: [(Maybe Text, NonEmpty ValidatorFailure)]
152+ failures =
153+ let refFailures = first Just <$> referencesValidity sm
146− failures = ((\v -> (Nothing, v)) <$> schemaValidity (_swSchema sw))
154+ in case NE.nonEmpty (schemaValidity (_swSchema sw)) of
147− <> (first Just <$> referencesValidity sm)
155+ Nothing -> refFailures
156+ Just errs -> (Nothing,errs) : refFailures
148
157
149metaSchema :: Schema
158metaSchema :: Schema
150metaSchema =
159metaSchema =
151− fromMaybe (error "Schema decode failed (this should never happen)")
160+ fromMaybe (panic "Schema decode failed (this should never happen)")
152 . decodeStrict
161 . decodeStrict
153 $ metaSchemaBytes
162 $ metaSchemaBytes
154
163
@@ -158,26 +167,29 @@ metaSchemaBytes =
158
167
159-- | Check that a schema itself is valid
168-- | Check that a schema itself is valid
160-- (if so the returned list will be empty).
169-- (if so the returned list will be empty).
161−schemaValidity :: Schema -> [Failure]
170+schemaValidity :: Schema -> [ValidatorFailure]
162schemaValidity =
171schemaValidity =
163− Spec.validate referenced (SchemaWithURI metaSchema Nothing) . toJSON
172+ Spec.specValidate referenced (SchemaWithURI metaSchema Nothing) . toJSON
164 where
173 where
165 referenced :: ReferencedSchemas Schema
174 referenced :: ReferencedSchemas Schema
166 referenced = ReferencedSchemas
175 referenced = ReferencedSchemas
167 metaSchema
176 metaSchema
168 (HM.singleton "http://json-schema.org/draft-04/schema"
177 (HM.singleton "http://json-schema.org/draft-04/schema"
169− metaSchema)
178+ metaSchema)
170
179
171-- | Check that a set of referenced schemas are valid
180-- | Check that a set of referenced schemas are valid
172-- (if so the returned list will be empty).
181-- (if so the returned list will be empty).
173referencesValidity
182referencesValidity
174 :: FE.URISchemaMap Schema
183 :: FE.URISchemaMap Schema
175− -> [(Text, Failure)]
184+ -> [(Text, NonEmpty ValidatorFailure)]
176− -- ^ The first value in the tuple is the URI of a referenced schema.
185+ -- ^ The first item of the tuple is the URI of a schema, the second
186+ -- is that schema's validation errors.
177−referencesValidity = HM.foldlWithKey' f mempty
187+referencesValidity = HM.foldlWithKey' f mempty . FE._unURISchemaMap
178 where
188 where
179− f :: [(Text, Failure)]
189+ f :: [(Text, NonEmpty ValidatorFailure)]
180 -> Text
190 -> Text
181 -> Schema
191 -> Schema
182− -> [(Text, Failure)]
192+ -> [(Text, NonEmpty ValidatorFailure)]
183− f acc k v = ((\a -> (k,a)) <$> schemaValidity v) <> acc
193+ f acc k v = case NE.nonEmpty (schemaValidity v) of
194+ Nothing -> acc
195+ Just errs -> (k,errs) : acc
src/Data/JsonSchema/Draft4/Failure.hs+49−113
@@ -1,123 +1,59 @@
1
1
2module Data.JsonSchema.Draft4.Failure where
2module Data.JsonSchema.Draft4.Failure where
3
3
4−import Prelude
4+import Import
5−
5+
6−import Data.List.NonEmpty (NonEmpty)
6+import Data.JsonSchema.Draft4.Schema (Schema)
7−import Data.Text (Text)
7+import qualified Data.Validator.Draft4 as D4
8−
8+
9−import qualified Data.Validator.Draft4.Any as AN
9+-- | Used to report an entire instance being invalidated, as opposed
10−import qualified Data.Validator.Draft4.Array as AR
10+-- to the failure of a single validator.
11−import qualified Data.Validator.Draft4.Number as NU
11+data Invalid = Invalid
12−import qualified Data.Validator.Draft4.Object as OB
12+ { _invalidSchema :: Schema
13−import qualified Data.Validator.Failure as FR
13+ , _invalidInstance :: Value
14+ , _invalidFailures :: NonEmpty ValidatorFailure
15+ } deriving (Eq, Show)
16+
17+data ValidatorFailure
18+ = FailureMultipleOf D4.MultipleOfInvalid
19+ | FailureMaximum D4.MaximumInvalid
20+ | FailureMinimum D4.MinimumInvalid
21+
22+ | FailureMaxLength D4.MaxLengthInvalid
23+ | FailureMinLength D4.MinLengthInvalid
24+ | FailurePattern D4.PatternInvalid
25+
26+ | FailureMaxItems D4.MaxItemsInvalid
27+ | FailureMinItems D4.MinItemsInvalid
28+ | FailureUniqueItems D4.UniqueItemsInvalid
29+ | FailureItems (D4.ItemsInvalid ValidatorFailure)
30+ | FailureAdditionalItems (D4.AdditionalItemsInvalid ValidatorFailure)
31+
32+ | FailureMaxProperties D4.MaxPropertiesInvalid
33+ | FailureMinProperties D4.MinPropertiesInvalid
34+ | FailureRequired ()
35+ | FailureDependencies (D4.DependenciesInvalid ValidatorFailure)
36+ | FailurePropertiesRelated (D4.PropertiesRelatedInvalid ValidatorFailure)
37+
38+ | FailureRef (D4.RefInvalid ValidatorFailure)
39+ | FailureEnum D4.EnumInvalid
40+ | FailureType D4.TypeValidatorInvalid
41+ | FailureAllOf (D4.AllOfInvalid ValidatorFailure)
42+ | FailureAnyOf (D4.AnyOfInvalid ValidatorFailure)
43+ | FailureOneOf (D4.OneOfInvalid ValidatorFailure)
44+ | FailureNot D4.NotValidatorInvalid
45+ deriving (Eq, Show)
14
46
15-- | A description of why a schema (or one of its reference) is itself
47-- | A description of why a schema (or one of its reference) is itself
16-- invalid.
48-- invalid.
17--
49--
18-- 'Nothing' indicates the starting schema. 'Just' indicates a referenced
50-- 'Nothing' indicates the starting schema. 'Just' indicates a referenced
19−-- schema -- the contents of the 'Just' is the schema's URI.
51+-- schema. The contents of the 'Just' is the schema's URI.
20--
52--
21−-- NOTE: 'HashMap (Maybe Text) Invalid' would be a nicer way of defining
53+-- NOTE: 'HashMap (Maybe Text) Invalid' would be a nicer way of
22−-- this, but then we lose the guarantee that there's at least one key.
54+-- defining this, but then we lose the guarantee that there's at least
23−type InvalidSchema = NonEmpty (Maybe Text, Failure)
55+-- one key.
24−
56+newtype SchemaInvalid
25−type Invalid = NonEmpty Failure
57+ = SchemaInvalid {
26−
58+ _unSchemaInvalid :: NonEmpty (Maybe Text, NonEmpty ValidatorFailure) }
27−type Failure = FR.Fail ValidatorChain
28−
29−-- | Distinguish all the different possible causes of failure for
30−-- Draft 4 validation.
31−data ValidatorChain
32− = MultipleOf
33− | Maximum
34− | ExclusiveMaximum
35− | Minimum
36− | ExclusiveMinimum
37−
38− | MaxLength
39− | MinLength
40− | PatternValidator
41−
42− | MaxItems
43− | MinItems
44− | UniqueItems
45− | Items ValidatorChain
46− | AdditionalItemsBool
47− | AdditionalItemsObject ValidatorChain
48−
49− | MaxProperties
50− | MinProperties
51− | Required
52− | SchemaDependency ValidatorChain
53− | PropertyDependency
54− | Properties ValidatorChain
55− | PatternProperties ValidatorChain
56− | AdditionalPropertiesBool
57− | AdditionalPropertiesObject ValidatorChain
58−
59− | RefResolution
60− -- ^ Indicates a reference that failed to resolve.
61− --
62− -- NOTE: The language agnostic test suite doesn't specify if this should
63− -- cause a validation error or should allow data to pass. We choose to
64− -- return a validation error.
65− --
66− -- Also note that ideally we would enforce in the type system that any
67− -- failing references be dealt with before valididation. Then this could
68− -- be removed entirely.
69− | RefLoop
70− | Ref ValidatorChain
71− | Enum
72− | TypeValidator
73− | AllOf ValidatorChain
74− | AnyOf ValidatorChain
75− | OneOfTooManySuccesses
76− | OneOfNoSuccesses ValidatorChain
77− | NotValidator
78 deriving (Eq, Show)
59 deriving (Eq, Show)
79−
80−maxE :: NU.MaximumInvalid -> ValidatorChain
81−maxE NU.Maximum = Maximum
82−maxE NU.ExclusiveMaximum = ExclusiveMaximum
83−
84−minE :: NU.MinimumInvalid -> ValidatorChain
85−minE NU.Minimum = Minimum
86−minE NU.ExclusiveMinimum = ExclusiveMinimum
87−
88−itemsE :: AR.ItemsInvalid ValidatorChain -> ValidatorChain
89−itemsE (AR.Items err) = Items err
90−itemsE AR.AdditionalItemsBoolInvalid = AdditionalItemsBool
91−itemsE (AR.AdditionalItemsObjectInvalid err) = AdditionalItemsObject err
92−
93−depsE :: OB.DependencyInvalid ValidatorChain -> ValidatorChain
94−depsE (OB.SchemaDependencyInvalid err) = SchemaDependency err
95−depsE OB.PropertyDependencyInvalid = PropertyDependency
96−
97−propE :: OB.PropertiesInvalid ValidatorChain -> ValidatorChain
98−propE (OB.PropertiesInvalid err) = Properties err
99−propE (OB.PropPatternInvalid err) = PatternProperties err
100−propE (OB.PropAdditionalInvalid a) =
101− case a of
102− OB.APBoolInvalid -> AdditionalPropertiesBool
103− OB.APObjectInvalid err -> AdditionalPropertiesObject err
104−
105−patPropE :: OB.PatternPropertiesInvalid ValidatorChain -> ValidatorChain
106−patPropE (OB.PPInvalid err) = PatternProperties err
107−patPropE (OB.PPAdditionalPropertiesInvalid a) =
108− case a of
109− OB.APBoolInvalid -> AdditionalPropertiesBool
110− OB.APObjectInvalid err -> AdditionalPropertiesObject err
111−
112−addPropE :: OB.AdditionalPropertiesInvalid ValidatorChain -> ValidatorChain
113−addPropE OB.APBoolInvalid = AdditionalPropertiesBool
114−addPropE (OB.APObjectInvalid err) = AdditionalPropertiesObject err
115−
116−refE :: AN.RefInvalid ValidatorChain -> ValidatorChain
117−refE AN.RefResolution = RefResolution
118−refE AN.RefLoop = RefLoop
119−refE (AN.RefInvalid err) = Ref err
120−
121−oneOfE :: AN.OneOfInvalid ValidatorChain -> ValidatorChain
122−oneOfE AN.TooManySuccesses = OneOfTooManySuccesses
123−oneOfE (AN.NoSuccesses err) = OneOfNoSuccesses err
src/Data/JsonSchema/Draft4/Schema.hs+24−20
@@ -1,17 +1,16 @@
1
1
2module Data.JsonSchema.Draft4.Schema where
2module Data.JsonSchema.Draft4.Schema where
3
3
4−import Import
4+import Import hiding (mapMaybe)
5−import Prelude
6
5
7−import qualified Data.HashMap.Strict as HM
6+import qualified Data.HashMap.Strict as HM
8−import Data.List.NonEmpty (NonEmpty)
7+import Data.List.NonEmpty (NonEmpty)
9−import Data.Maybe (fromJust, isJust)
8+import Data.Maybe (fromJust, isJust)
10import Data.Scientific
9import Data.Scientific
10+import qualified Data.Set as Set
11+import qualified Data.Text as T
11
12
12−import qualified Data.Validator.Draft4.Any as AN
13+import qualified Data.Validator.Draft4 as D4
13−import qualified Data.Validator.Draft4.Array as AR
14−import qualified Data.Validator.Draft4.Object as OB
15import Data.Validator.Utils
14import Data.Validator.Utils
16
15
17data Schema = Schema
16data Schema = Schema
@@ -26,6 +25,10 @@ data Schema = Schema
26 -- contain schemas anywhere (not just in "definitions" or any
25 -- contain schemas anywhere (not just in "definitions" or any
27 -- of the other official keys) we save any leftover key/value
26 -- of the other official keys) we save any leftover key/value
28 -- pairs not covered by them here.
27 -- pairs not covered by them here.
28+ --
29+ -- TODO: This field is the source of most of the complication in this
30+ -- module and needs to be removed. It should be doable, though it will
31+ -- involve some modification to the fetching code.
29
32
30 , _schemaMultipleOf :: Maybe Scientific
33 , _schemaMultipleOf :: Maybe Scientific
31 , _schemaMaximum :: Maybe Scientific
34 , _schemaMaximum :: Maybe Scientific
@@ -40,22 +43,22 @@ data Schema = Schema
40 , _schemaMaxItems :: Maybe Int
43 , _schemaMaxItems :: Maybe Int
41 , _schemaMinItems :: Maybe Int
44 , _schemaMinItems :: Maybe Int
42 , _schemaUniqueItems :: Maybe Bool
45 , _schemaUniqueItems :: Maybe Bool
43− , _schemaItems :: Maybe (AR.Items Schema)
46+ , _schemaItems :: Maybe (D4.Items Schema)
44 -- Note that '_schemaAdditionalItems' is left out of 'runValidate'
47 -- Note that '_schemaAdditionalItems' is left out of 'runValidate'
45 -- because its functionality is handled by '_schemaItems'. It always
48 -- because its functionality is handled by '_schemaItems'. It always
46 -- validates data unless 'Items' is present.
49 -- validates data unless 'Items' is present.
47− , _schemaAdditionalItems :: Maybe (AR.AdditionalItems Schema)
50+ , _schemaAdditionalItems :: Maybe (D4.AdditionalItems Schema)
48
51
49 , _schemaMaxProperties :: Maybe Int
52 , _schemaMaxProperties :: Maybe Int
50 , _schemaMinProperties :: Maybe Int
53 , _schemaMinProperties :: Maybe Int
51− , _schemaRequired :: Maybe OB.Required
54+ , _schemaRequired :: Maybe (Set Text)
52− , _schemaDependencies :: Maybe (HashMap Text (OB.Dependency Schema))
55+ , _schemaDependencies :: Maybe (HashMap Text (D4.Dependency Schema))
53 , _schemaProperties :: Maybe (HashMap Text Schema)
56 , _schemaProperties :: Maybe (HashMap Text Schema)
54 , _schemaPatternProperties :: Maybe (HashMap Text Schema)
57 , _schemaPatternProperties :: Maybe (HashMap Text Schema)
55− , _schemaAdditionalProperties :: Maybe (OB.AdditionalProperties Schema)
58+ , _schemaAdditionalProperties :: Maybe (D4.AdditionalProperties Schema)
56
59
57− , _schemaEnum :: Maybe AN.EnumVal
60+ , _schemaEnum :: Maybe (NonEmpty Value)
58− , _schemaType :: Maybe AN.TypeVal
61+ , _schemaType :: Maybe D4.TypeValidator
59 , _schemaAllOf :: Maybe (NonEmpty Schema)
62 , _schemaAllOf :: Maybe (NonEmpty Schema)
60 , _schemaAnyOf :: Maybe (NonEmpty Schema)
63 , _schemaAnyOf :: Maybe (NonEmpty Schema)
61 , _schemaOneOf :: Maybe (NonEmpty Schema)
64 , _schemaOneOf :: Maybe (NonEmpty Schema)
@@ -262,9 +265,9 @@ instance Arbitrary Schema where
262 a <- maybeGen arbitraryText
265 a <- maybeGen arbitraryText
263 b <- maybeGen arbitraryText
266 b <- maybeGen arbitraryText
264 c <- maybeGen arbitraryText
267 c <- maybeGen arbitraryText
265− -- NOTE: The next two fields are empty to generate cleaner schemas,
268+ -- NOTE: The next two fields are empty to generate cleaner
266− -- but note that this means we don't test e.g. the invertability
269+ -- schemas, but note that this means we don't test the
267− -- of these fields.
270+ -- invertability of these fields.
268 d <- pure Nothing -- _schemaDefinitions
271 d <- pure Nothing -- _schemaDefinitions
269 e <- pure mempty -- _otherPairs
272 e <- pure mempty -- _otherPairs
270
273
@@ -286,13 +289,14 @@ instance Arbitrary Schema where
286
289
287 s <- maybeGen (getPositive <$> arbitrary)
290 s <- maybeGen (getPositive <$> arbitrary)
288 t <- maybeGen (getPositive <$> arbitrary)
291 t <- maybeGen (getPositive <$> arbitrary)
289− u <- arbitrary
292+ u <- maybeGen (Set.map T.pack <$> arbitrary)
290 v <- maybeRecurse n arbitraryHashMap
293 v <- maybeRecurse n arbitraryHashMap
291 w <- maybeRecurse n arbitraryHashMap
294 w <- maybeRecurse n arbitraryHashMap
292 x <- maybeRecurse n arbitraryHashMap
295 x <- maybeRecurse n arbitraryHashMap
293 y <- maybeRecurse n arbitrary
296 y <- maybeRecurse n arbitrary
294
297
298+ z <- maybeRecurse n ( fmap _unArbitraryValue . _unNonEmpty'
295− z <- arbitrary
299+ <$> arbitrary)
296 a2 <- arbitrary
300 a2 <- arbitrary
297 b2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
301 b2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
298 c2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
302 c2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
src/Data/JsonSchema/Draft4/Spec.hs+68−77
@@ -2,10 +2,8 @@
2module Data.JsonSchema.Draft4.Spec where
2module Data.JsonSchema.Draft4.Spec where
3
3
4import Import
4import Import
5−-- Hiding is for GHCs before 7.10:
6−import Prelude hiding (concat)
7
5
8−import Data.Maybe (fromMaybe, isNothing)
6+import Data.Maybe (fromMaybe)
9import Data.Profunctor (Profunctor(..))
7import Data.Profunctor (Profunctor(..))
10
8
11import Data.JsonSchema.Draft4.Failure
9import Data.JsonSchema.Draft4.Failure
@@ -15,33 +13,26 @@ import Data.JsonSchema.Fetch (ReferencedSchemas(..),
15import qualified Data.JsonSchema.Fetch as FE
13import qualified Data.JsonSchema.Fetch as FE
16import Data.JsonSchema.Types (Spec(..))
14import Data.JsonSchema.Types (Spec(..))
17import qualified Data.JsonSchema.Types as JT
15import qualified Data.JsonSchema.Types as JT
18−import qualified Data.Validator.Draft4 as D4
19−import qualified Data.Validator.Draft4.Any as AN
16+import Data.Validator.Draft4
20import Data.Validator.Reference (updateResolutionScope)
17import Data.Validator.Reference (updateResolutionScope)
21
18
22embedded :: Schema -> ([Schema], [Schema])
19embedded :: Schema -> ([Schema], [Schema])
23embedded s = JT.embedded (d4Spec (ReferencedSchemas s mempty) mempty Nothing) s
20embedded s = JT.embedded (d4Spec (ReferencedSchemas s mempty) mempty Nothing) s
24
21
25−-- | For internal use.
22+specValidate
26−--
27−-- A specialized version of 'const' that prevents overwriting
28−-- useful information.
29−toss :: a -> () -> a
30−toss = const
31−
32−validate
33 :: ReferencedSchemas Schema
23 :: ReferencedSchemas Schema
34 -> SchemaWithURI Schema
24 -> SchemaWithURI Schema
35 -> Value
25 -> Value
36− -> [Failure]
26+ -> [ValidatorFailure]
27+specValidate rs =
37−validate rs = continueValidating rs (AN.VisitedSchemas [(Nothing, Nothing)])
28+ continueValidating rs (VisitedSchemas [(Nothing, Nothing)])
38
29
39continueValidating
30continueValidating
40 :: ReferencedSchemas Schema
31 :: ReferencedSchemas Schema
41− -> AN.VisitedSchemas
32+ -> VisitedSchemas
42 -> SchemaWithURI Schema
33 -> SchemaWithURI Schema
43 -> Value
34 -> Value
44− -> [Failure]
35+ -> [ValidatorFailure]
45continueValidating referenced visited sw =
36continueValidating referenced visited sw =
46 JT.validate (d4Spec referenced visited currentScope)
37 JT.validate (d4Spec referenced visited currentScope)
47 (_swSchema sw)
38 (_swSchema sw)
@@ -53,85 +44,85 @@ continueValidating referenced visited sw =
53
44
54d4Spec
45d4Spec
55 :: ReferencedSchemas Schema
46 :: ReferencedSchemas Schema
56− -> AN.VisitedSchemas
47+ -> VisitedSchemas
57 -> Maybe Text
48 -> Maybe Text
58− -> Spec Schema ValidatorChain
49+ -> Spec Schema ValidatorFailure
59d4Spec referenced visited scope = Spec
50d4Spec referenced visited scope = Spec
60− [ dimap
61− (fmap D4.MultipleOf . _schemaMultipleOf)
51+ [ dimap (fmap MultipleOf . _schemaMultipleOf) FailureMultipleOf multipleOfValidator
62− (toss MultipleOf)
63− D4.multipleOf
64 , dimap
52 , dimap
65− (\s -> D4.MaximumContext (fromMaybe False (_schemaExclusiveMaximum s))
53+ (\s -> Maximum (fromMaybe False (_schemaExclusiveMaximum s)) <$> _schemaMaximum s)
66− <$> _schemaMaximum s)
54+ FailureMaximum
67− maxE
55+ maximumValidator
68− D4.maximumVal
69 , dimap
56 , dimap
70− (\s -> D4.MinimumContext (fromMaybe False (_schemaExclusiveMinimum s))
57+ (\s -> Minimum (fromMaybe False (_schemaExclusiveMinimum s)) <$> _schemaMinimum s)
71− <$> _schemaMinimum s)
58+ FailureMinimum
72− minE
59+ minimumValidator
73− D4.minimumVal
74
60
75− , dimap (fmap D4.MaxLength . _schemaMaxLength) (toss MaxLength) D4.maxLength
61+ , dimap (fmap MaxLength . _schemaMaxLength) FailureMaxLength maxLengthValidator
76− , dimap (fmap D4.MinLength . _schemaMinLength) (toss MinLength) D4.minLength
62+ , dimap (fmap MinLength . _schemaMinLength) FailureMinLength minLengthValidator
77− , dimap (fmap D4.PatternVal . _schemaPattern) (toss PatternValidator) D4.patternVal
63+ , dimap (fmap PatternValidator . _schemaPattern) FailurePattern patternValidator
78
64
79− , dimap (fmap D4.MaxItems . _schemaMaxItems) (toss MaxItems) D4.maxItems
65+ , dimap (fmap MaxItems . _schemaMaxItems) FailureMaxItems maxItemsValidator
80− , dimap (fmap D4.MinItems . _schemaMinItems) (toss MinItems) D4.minItems
66+ , dimap (fmap MinItems . _schemaMinItems) FailureMinItems minItemsValidator
81− , dimap (fmap D4.UniqueItems . _schemaUniqueItems) (toss UniqueItems) D4.uniqueItems
67+ , dimap (fmap UniqueItems . _schemaUniqueItems) FailureUniqueItems uniqueItemsValidator
82 , dimap
68 , dimap
83− (\s -> D4.ItemsContext (_schemaAdditionalItems s) <$> _schemaItems s)
69+ (\s -> ItemsRelated
84− itemsE
70+ { _irItems = _schemaItems s
85− (D4.items descend)
71+ , _irAdditional = _schemaAdditionalItems s
86− , lmap (fmap D4.AdditionalItemsContext . _schemaAdditionalItems) D4.additionalItemsEmbedded
72+ })
73+ (\err -> case err of
74+ IRInvalidItems e -> FailureItems e
75+ IRInvalidAdditional e -> FailureAdditionalItems e)
76+ (itemsRelatedValidator descend)
87− , lmap (fmap D4.Definitions . _schemaDefinitions) D4.definitionsEmbedded
77+ , lmap (fmap Definitions . _schemaDefinitions) definitionsEmbedded
88
78
89− , dimap (fmap D4.MaxProperties . _schemaMaxProperties) (toss MaxProperties) D4.maxProperties
90− , dimap (fmap D4.MinProperties . _schemaMinProperties) (toss MinProperties) D4.minProperties
91− , dimap (fmap D4.RequiredContext . _schemaRequired) (toss Required) D4.required
92− , dimap (fmap D4.DependenciesContext . _schemaDependencies) depsE (D4.dependencies descend)
93 , dimap
79 , dimap
94− (\s -> D4.PropertiesContext
80+ (fmap MaxProperties . _schemaMaxProperties)
95− (_schemaPatternProperties s)
81+ FailureMaxProperties
96− (_schemaAdditionalProperties s)
82+ maxPropertiesValidator
97− <$> _schemaProperties s)
83+ , dimap
98− propE
84+ (fmap MinProperties . _schemaMinProperties)
99− (D4.properties descend)
85+ FailureMinProperties
86+ minPropertiesValidator
87+ , dimap (fmap Required . _schemaRequired) FailureRequired requiredValidator
100 , dimap
88 , dimap
101− (\s -> D4.PatternPropertiesContext
89+ (fmap DependenciesValidator . _schemaDependencies)
102− (isNothing (_schemaProperties s))
90+ FailureDependencies
103− (_schemaAdditionalProperties s)
91+ (dependenciesValidator descend)
104− <$> _schemaPatternProperties s)
105− patPropE
106− (D4.patternProperties descend)
107 , dimap
92 , dimap
108− (\s -> D4.AdditionalPropertiesContext
93+ (\s -> PropertiesRelated
109− (isNothing (_schemaProperties s)
94+ { _propProperties = _schemaProperties s
110− && isNothing (_schemaPatternProperties s))
95+ , _propPattern = _schemaPatternProperties s
111− <$> _schemaAdditionalProperties s)
96+ , _propAdditional = _schemaAdditionalProperties s
112− addPropE
97+ })
113− (D4.additionalProperties descend)
98+ FailurePropertiesRelated
99+ (propertiesRelatedValidator descend)
114
100
115 , dimap
101 , dimap
116− (\s -> D4.Ref <$> _schemaRef s)
102+ (\s -> Ref <$> _schemaRef s)
117− refE
103+ FailureRef
118− (D4.ref visited scope (FE.getReference referenced) refVal)
104+ (refValidator visited scope (FE.getReference referenced) getRef)
119− , dimap (fmap D4.EnumContext . _schemaEnum) (toss Enum) D4.enumVal
105+ , dimap (fmap EnumValidator . _schemaEnum) FailureEnum enumValidator
120− , dimap (fmap D4.TypeContext . _schemaType) (toss TypeValidator) D4.typeVal
106+ , dimap (fmap TypeContext . _schemaType) FailureType typeValidator
121− , dimap (fmap D4.AllOf . _schemaAllOf) AllOf (D4.allOf lateral)
107+ , dimap (fmap AllOf . _schemaAllOf) FailureAllOf (allOfValidator lateral)
122− , dimap (fmap D4.AnyOf . _schemaAnyOf) AnyOf (D4.anyOf lateral)
108+ , dimap (fmap AnyOf . _schemaAnyOf) FailureAnyOf (anyOfValidator lateral)
123− , dimap (fmap D4.OneOf . _schemaOneOf) oneOfE (D4.oneOf lateral)
109+ , dimap (fmap OneOf . _schemaOneOf) FailureOneOf (oneOfValidator lateral)
124− , dimap (fmap D4.NotVal . _schemaNot) (toss NotValidator) (D4.notVal lateral)
110+ , dimap (fmap NotValidator . _schemaNot) FailureNot (notValidator lateral)
125 ]
111 ]
126 where
112 where
113+ getRef
127− refVal :: AN.VisitedSchemas -> Maybe Text -> Schema -> Value -> [Failure]
114+ :: VisitedSchemas
115+ -> Maybe Text
116+ -> Schema
117+ -> Value
118+ -> [ValidatorFailure]
128− refVal newVisited newScope schema =
119+ getRef newVisited newScope schema =
129 continueValidating referenced newVisited (SchemaWithURI schema newScope)
120 continueValidating referenced newVisited (SchemaWithURI schema newScope)
130
121
131− descend :: Schema -> Value -> [Failure]
122+ descend :: Schema -> Value -> [ValidatorFailure]
132 descend schema =
123 descend schema =
133 continueValidating referenced mempty (SchemaWithURI schema scope)
124 continueValidating referenced mempty (SchemaWithURI schema scope)
134
125
135− lateral :: Schema -> Value -> [Failure]
126+ lateral :: Schema -> Value -> [ValidatorFailure]
136 lateral schema =
127 lateral schema =
137 continueValidating referenced visited (SchemaWithURI schema scope)
128 continueValidating referenced visited (SchemaWithURI schema scope)
src/Data/JsonSchema/Fetch.hs+26−26
@@ -2,15 +2,13 @@
2module Data.JsonSchema.Fetch where
2module Data.JsonSchema.Fetch where
3
3
4import Import
4import Import
5−-- Hiding is for GHCs before 7.10:
6−import Prelude hiding (concat, sequence)
7
5
8import Control.Arrow (left)
6import Control.Arrow (left)
9−import Control.Exception (catch)
7+import Control.Exception (IOException, catch)
10import Control.Monad (foldM)
8import Control.Monad (foldM)
11−import qualified Data.ByteString.Lazy as LBS
12import qualified Data.ByteString as BS
9import qualified Data.ByteString as BS
13−import qualified Data.HashMap.Strict as H
10+import qualified Data.ByteString.Lazy as LBS
11+import qualified Data.HashMap.Strict as HM
14import qualified Data.Text as T
12import qualified Data.Text as T
15import qualified Network.HTTP.Client as NC
13import qualified Network.HTTP.Client as NC
16
14
@@ -50,11 +48,14 @@ data ReferencedSchemas schema = ReferencedSchemas
50 -- }
48 -- }
51 -- }
49 -- }
52 -- }
50 -- }
53− , _rsSchemaMap :: !(URISchemaMap schema)
51+ , _rsSchemaMap :: !(HashMap Text schema)
52+ -- ^ Map of URIs to schemas.
54 } deriving (Eq, Show)
53 } deriving (Eq, Show)
55
54
56-- | Keys are URIs (without URI fragments).
55-- | Keys are URIs (without URI fragments).
57−type URISchemaMap schema = HashMap Text schema
56+newtype URISchemaMap schema
57+ = URISchemaMap { _unURISchemaMap :: HashMap Text schema }
58+ deriving (Eq, Show)
58
59
59data SchemaWithURI schema = SchemaWithURI
60data SchemaWithURI schema = SchemaWithURI
60 { _swSchema :: !schema
61 { _swSchema :: !schema
@@ -68,7 +69,7 @@ data SchemaWithURI schema = SchemaWithURI
68
69
69getReference :: ReferencedSchemas schema -> Maybe Text -> Maybe schema
70getReference :: ReferencedSchemas schema -> Maybe Text -> Maybe schema
70getReference referenced Nothing = Just (_rsStarting referenced)
71getReference referenced Nothing = Just (_rsStarting referenced)
71−getReference referenced (Just t) = H.lookup t (_rsSchemaMap referenced)
72+getReference referenced (Just t) = HM.lookup t (_rsSchemaMap referenced)
72
73
73--------------------------------------------------
74--------------------------------------------------
74-- * Fetch via HTTP
75-- * Fetch via HTTP
@@ -88,13 +89,13 @@ referencesViaHTTP'
88 -> IO (Either HTTPFailure (URISchemaMap schema))
89 -> IO (Either HTTPFailure (URISchemaMap schema))
89referencesViaHTTP' info sw = do
90referencesViaHTTP' info sw = do
90 manager <- NC.newManager NC.defaultManagerSettings
91 manager <- NC.newManager NC.defaultManagerSettings
91− let f = referencesMethodAgnostic (get manager) info sw
92+ let f = referencesMethodAgnostic (getURL manager) info sw
92 catch (left HTTPParseFailure <$> f) handler
93 catch (left HTTPParseFailure <$> f) handler
93 where
94 where
94− get :: NC.Manager -> Text -> IO LBS.ByteString
95+ getURL :: NC.Manager -> Text -> IO BS.ByteString
95− get man url = do
96+ getURL man url = do
96 request <- NC.parseUrlThrow (T.unpack url)
97 request <- NC.parseUrlThrow (T.unpack url)
97− NC.responseBody <$> NC.httpLbs request man
98+ LBS.toStrict . NC.responseBody <$> NC.httpLbs request man
98
99
99 handler
100 handler
100 :: NC.HttpException
101 :: NC.HttpException
@@ -107,7 +108,7 @@ referencesViaHTTP' info sw = do
107
108
108data FilesystemFailure
109data FilesystemFailure
109 = FSParseFailure Text
110 = FSParseFailure Text
110− | FSReadFailure IOError
111+ | FSReadFailure IOException
111 deriving (Show, Eq)
112 deriving (Show, Eq)
112
113
113referencesViaFilesystem'
114referencesViaFilesystem'
@@ -118,13 +119,10 @@ referencesViaFilesystem'
118referencesViaFilesystem' info sw = catch (left FSParseFailure <$> f) handler
119referencesViaFilesystem' info sw = catch (left FSParseFailure <$> f) handler
119 where
120 where
120 f :: IO (Either Text (URISchemaMap schema))
121 f :: IO (Either Text (URISchemaMap schema))
121− f = referencesMethodAgnostic readFile' info sw
122+ f = referencesMethodAgnostic (BS.readFile . T.unpack) info sw
122−
123− readFile' :: Text -> IO LBS.ByteString
124− readFile' = fmap LBS.fromStrict . BS.readFile . T.unpack
125
123
126 handler
124 handler
127− :: IOError
125+ :: IOException
128 -> IO (Either FilesystemFailure (URISchemaMap schema))
126 -> IO (Either FilesystemFailure (URISchemaMap schema))
129 handler = pure . Left . FSReadFailure
127 handler = pure . Left . FSReadFailure
130
128
@@ -137,16 +135,16 @@ referencesViaFilesystem' info sw = catch (left FSParseFailure <$> f) handler
137-- e.g. rejecting non-local URIs.
135-- e.g. rejecting non-local URIs.
138referencesMethodAgnostic
136referencesMethodAgnostic
139 :: forall schema. FromJSON schema
137 :: forall schema. FromJSON schema
140− => (Text -> IO LBS.ByteString)
138+ => (Text -> IO BS.ByteString)
141 -> FetchInfo schema
139 -> FetchInfo schema
142 -> SchemaWithURI schema
140 -> SchemaWithURI schema
143 -> IO (Either Text (URISchemaMap schema))
141 -> IO (Either Text (URISchemaMap schema))
144referencesMethodAgnostic fetchRef info =
142referencesMethodAgnostic fetchRef info =
145− getRecursiveReferences fetchRef info mempty
143+ getRecursiveReferences fetchRef info (URISchemaMap mempty)
146
144
147getRecursiveReferences
145getRecursiveReferences
148 :: forall schema. FromJSON schema
146 :: forall schema. FromJSON schema
149− => (Text -> IO LBS.ByteString)
147+ => (Text -> IO BS.ByteString)
150 -> FetchInfo schema
148 -> FetchInfo schema
151 -> URISchemaMap schema
149 -> URISchemaMap schema
152 -> SchemaWithURI schema
150 -> SchemaWithURI schema
@@ -158,21 +156,23 @@ getRecursiveReferences fetchRef info referenced sw =
158 -> SchemaWithURI schema
156 -> SchemaWithURI schema
159 -> IO (Either Text (URISchemaMap schema))
157 -> IO (Either Text (URISchemaMap schema))
160 f (Left e) _ = pure (Left e)
158 f (Left e) _ = pure (Left e)
161− f (Right g) (SchemaWithURI schema mUri) =
159+ f (Right (URISchemaMap usm)) (SchemaWithURI schema mUri) =
162 case newRef of
160 case newRef of
163− Nothing -> pure (Right g)
161+ Nothing -> pure (Right (URISchemaMap usm))
164 Just uri -> do
162 Just uri -> do
165 bts <- fetchRef uri
163 bts <- fetchRef uri
166− case eitherDecode bts of
164+ case eitherDecodeStrict bts of
167 Left e -> pure . Left . T.pack $ e
165 Left e -> pure . Left . T.pack $ e
168 Right schm -> getRecursiveReferences
166 Right schm -> getRecursiveReferences
169− fetchRef info (H.insert uri schm g)
167+ fetchRef
168+ info
169+ (URISchemaMap (HM.insert uri schm usm))
170 (SchemaWithURI schm (Just uri))
170 (SchemaWithURI schm (Just uri))
171 where
171 where
172 newRef :: Maybe Text
172 newRef :: Maybe Text
173 newRef
173 newRef
174 | Just (Just uri,_) <- resolveReference mUri <$> _fiRef info schema
174 | Just (Just uri,_) <- resolveReference mUri <$> _fiRef info schema
175− = case H.lookup uri g of
175+ = case HM.lookup uri usm of
176 Nothing -> Just uri
176 Nothing -> Just uri
177 Just _ -> Nothing
177 Just _ -> Nothing
178 | otherwise = Nothing
178 | otherwise = Nothing
src/Data/JsonSchema/Types.hs+4−6
@@ -1,12 +1,9 @@
1−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2
1
3module Data.JsonSchema.Types where
2module Data.JsonSchema.Types where
4
3
5−import Prelude
6import Import
4import Import
7
5
8−import Data.Validator.Failure (Fail)
6+import Data.Validator.Types (Validator(..))
9−import Data.Validator.Types (Validator(..))
10
7
11newtype Spec schema err
8newtype Spec schema err
12 = Spec { _unSpec :: [Validator schema schema err] }
9 = Spec { _unSpec :: [Validator schema schema err] }
@@ -25,8 +22,9 @@ validate
25 :: Spec schema err
22 :: Spec schema err
26 -> schema
23 -> schema
27 -> Value
24 -> Value
28− -> [Fail err]
25+ -> [err]
29−validate spec schema v = _unSpec spec >>= (\val -> _validate val schema v)
26+validate spec schema v =
27+ (\val -> _validate val schema v) =<< _unSpec spec
30
28
31-- | A basic schema type that doesn't impose much structure.
29-- | A basic schema type that doesn't impose much structure.
32--
30--
src/Data/Validator/Draft4.hs+142−450
@@ -1,38 +1,36 @@
1-- | Turn the validation functions into actual 'Validator's.
1-- | Turn the validation functions into actual 'Validator's.
2−--
3−-- From this point on they know how to parse themselves from JSON
4−-- and also know how to extract subschemas embedded within themselves.
5
2
6−module Data.Validator.Draft4 where
3+module Data.Validator.Draft4
4+ ( module Data.Validator.Draft4
5+ , module Export
6+ ) where
7
7
8−import Prelude
9import Import
8import Import
10
9
11−import Data.Aeson.Types (Parser)
12import qualified Data.HashMap.Strict as HM
10import qualified Data.HashMap.Strict as HM
13−import Data.List.NonEmpty (NonEmpty)
14import qualified Data.List.NonEmpty as NE
11import qualified Data.List.NonEmpty as NE
15−import Data.Maybe (catMaybes, isNothing,
12+import Data.Maybe (catMaybes, maybe, maybeToList)
16− maybe, maybeToList)
17−import Data.Scientific (Scientific)
18import Data.Text (Text)
13import Data.Text (Text)
19
14
20−import qualified Data.Validator.Draft4.Any as AN
15+import Data.Validator.Draft4.Any as Export
21−import qualified Data.Validator.Draft4.Array as AR
16+import Data.Validator.Draft4.Array as Export
22−import qualified Data.Validator.Draft4.Number as NU
17+import Data.Validator.Draft4.Number as Export
23−import qualified Data.Validator.Draft4.Object as OB
18+import Data.Validator.Draft4.Object as Export
24−import qualified Data.Validator.Draft4.String as ST
19+import Data.Validator.Draft4.String as Export
25−import Data.Validator.Failure (Fail(..))
26import Data.Validator.Types (Validator(..))
20import Data.Validator.Types (Validator(..))
27import Data.Validator.Utils (fromJSONEither)
21import Data.Validator.Utils (fromJSONEither)
28
22
29-- | For internal use.
23-- | For internal use.
24+--
25+-- Take a validation function, a possibly existing validator, and some data.
26+-- If the validator is exists and can validate the type of data we have,
27+-- attempt to do so and return any failures.
30−run :: FromJSON b => (a -> b -> [c]) -> Maybe a -> Value -> [c]
28+run :: FromJSON b => (a -> b -> Maybe c) -> Maybe a -> Value -> [c]
31run _ Nothing _ = mempty
29run _ Nothing _ = mempty
32run f (Just a) b =
30run f (Just a) b =
33 case fromJSONEither b of
31 case fromJSONEither b of
34 Left _ -> mempty
32 Left _ -> mempty
35− Right c -> f a c
33+ Right c -> maybeToList (f a c)
36
34
37-- | For internal use.
35-- | For internal use.
38noEmbedded :: a -> ([b], [b])
36noEmbedded :: a -> ([b], [b])
@@ -42,190 +40,116 @@ noEmbedded = const (mempty, mempty)
42-- * Numbers
40-- * Numbers
43--------------------------------------------------
41--------------------------------------------------
44
42
45−newtype MultipleOf
46− = MultipleOf { _unMultipleOf :: Scientific }
47− deriving (Eq, Show)
48−
49−instance FromJSON MultipleOf where
50− parseJSON = withObject "MultipleOf" $ \o ->
51− MultipleOf <$> o .: "multipleOf"
52−
53−multipleOf :: Validator a (Maybe MultipleOf) ()
43+multipleOfValidator :: Validator a (Maybe MultipleOf) MultipleOfInvalid
54−multipleOf =
44+multipleOfValidator = Validator noEmbedded (run multipleOfVal)
55− Validator
56− noEmbedded
57− (run (fmap maybeToList . NU.multipleOf . _unMultipleOf))
58
45
59−data MaximumContext
46+maximumValidator :: Validator a (Maybe Maximum) MaximumInvalid
60− = MaximumContext Bool Scientific
47+maximumValidator = Validator noEmbedded (run maximumVal)
61− deriving (Eq, Show)
62
48
63−instance FromJSON MaximumContext where
64− parseJSON = withObject "MaximumContext" $ \o -> MaximumContext
65− <$> o .:! "exclusiveMaximum" .!= False
66− <*> o .: "maximum"
67−
68−maximumVal :: Validator a (Maybe MaximumContext) NU.MaximumInvalid
69−maximumVal =
70− Validator
71− noEmbedded
72− (run (\(MaximumContext a b) -> maybeToList . NU.maximumVal a b))
73−
74−data MinimumContext
75− = MinimumContext Bool Scientific
76− deriving (Eq, Show)
77−
78−instance FromJSON MinimumContext where
79− parseJSON = withObject "MinimumContext" $ \o -> MinimumContext
80− <$> o .:! "exclusiveMinimum" .!= False
81− <*> o .: "minimum"
82−
83−minimumVal :: Validator a (Maybe MinimumContext) NU.MinimumInvalid
49+minimumValidator :: Validator a (Maybe Minimum) MinimumInvalid
84−minimumVal =
50+minimumValidator = Validator noEmbedded (run minimumVal)
85− Validator
86− noEmbedded
87− (run (\(MinimumContext a b) -> maybeToList . NU.minimumVal a b))
88
51
89--------------------------------------------------
52--------------------------------------------------
90-- * Strings
53-- * Strings
91--------------------------------------------------
54--------------------------------------------------
92
55
93−newtype MaxLength
56+maxLengthValidator :: Validator a (Maybe MaxLength) MaxLengthInvalid
94− = MaxLength { _unMaxLength :: Int }
57+maxLengthValidator = Validator noEmbedded (run maxLengthVal)
95− deriving (Eq, Show)
96
58
97−instance FromJSON MaxLength where
59+minLengthValidator :: Validator a (Maybe MinLength) MinLengthInvalid
98− parseJSON = withObject "MaxLength" $ \o ->
60+minLengthValidator = Validator noEmbedded (run minLengthVal)
99− MaxLength <$> o .: "maxLength"
100
61
101−maxLength :: Validator a (Maybe MaxLength) ()
62+patternValidator :: Validator a (Maybe PatternValidator) PatternInvalid
102−maxLength =
63+patternValidator = Validator noEmbedded (run patternVal)
103− Validator
104− noEmbedded
105− (run (fmap maybeToList . ST.maxLength . _unMaxLength))
106−
107−newtype MinLength
108− = MinLength { _unMinLength :: Int }
109− deriving (Eq, Show)
110−
111−instance FromJSON MinLength where
112− parseJSON = withObject "MinLength" $ \o ->
113− MinLength <$> o .: "minLength"
114−
115−minLength :: Validator a (Maybe MinLength) ()
116−minLength =
117− Validator
118− noEmbedded
119− (run (fmap maybeToList . ST.minLength . _unMinLength))
120−
121−newtype PatternVal
122− = PatternVal { _unPatternVal :: Text }
123− deriving (Eq, Show)
124−
125−instance FromJSON PatternVal where
126− parseJSON = withObject "PatternVal" $ \o ->
127− PatternVal <$> o .: "pattern"
128−
129−patternVal :: Validator a (Maybe PatternVal) ()
130−patternVal =
131− Validator
132− noEmbedded
133− (run (fmap maybeToList . ST.patternVal . _unPatternVal))
134
64
135--------------------------------------------------
65--------------------------------------------------
136-- * Arrays
66-- * Arrays
137--------------------------------------------------
67--------------------------------------------------
138
68
139−newtype MaxItems
69+maxItemsValidator :: Validator a (Maybe MaxItems) MaxItemsInvalid
140− = MaxItems { _unMaxItems :: Int }
70+maxItemsValidator = Validator noEmbedded (run maxItemsVal)
141− deriving (Eq, Show)
71+
142−
72+minItemsValidator :: Validator a (Maybe MinItems) MinItemsInvalid
143−instance FromJSON MaxItems where
73+minItemsValidator = Validator noEmbedded (run minItemsVal)
144− parseJSON = withObject "MaxItems" $ \o ->
74+
145− MaxItems <$> o .: "maxItems"
75+uniqueItemsValidator :: Validator a (Maybe UniqueItems) UniqueItemsInvalid
146−
76+uniqueItemsValidator = Validator noEmbedded (run uniqueItemsVal)
147−maxItems :: Validator a (Maybe MaxItems) ()
77+
148−maxItems =
78+itemsRelatedValidator
149− Validator
79+ :: (schema -> Value -> [err])
150− noEmbedded
80+ -> Validator schema (ItemsRelated schema) (ItemsRelatedInvalid err)
151− (run (fmap maybeToList . AR.maxItems . _unMaxItems))
81+itemsRelatedValidator f =
152−
82+ Validator
153−newtype MinItems
83+ (\a -> ( mempty
154− = MinItems { _unMinItems :: Int }
84+ , case _irItems a of
155− deriving (Eq, Show)
85+ Just (ItemsObject b) -> pure b
156−
86+ Just (ItemsArray cs) -> cs
157−instance FromJSON MinItems where
87+ Nothing -> mempty
158− parseJSON = withObject "MinItems" $ \o ->
88+ <> case _irAdditional a of
159− MinItems <$> o .: "minItems"
89+ Just (AdditionalObject b) -> pure b
160−
90+ _ -> mempty
161−minItems :: Validator a (Maybe MinItems) ()
91+ ))
162−minItems =
92+ (\a b -> case fromJSONEither b of
163− Validator
93+ Left _ -> mempty
164− noEmbedded
94+ Right c -> itemsRelatedVal f a c)
165− (run (fmap maybeToList . AR.minItems . _unMinItems))
166−
167−newtype UniqueItems
168− = UniqueItems { _unUniqueItems :: Bool }
169− deriving (Eq, Show)
170
95
171−instance FromJSON UniqueItems where
96+--------------------------------------------------
172− parseJSON = withObject "UniqueItems" $ \o ->
97+-- * Objects
173− UniqueItems <$> o .: "uniqueItems"
98+--------------------------------------------------
174
99
175−uniqueItems :: Validator a (Maybe UniqueItems) ()
100+maxPropertiesValidator
176−uniqueItems =
101+ :: Validator
177− Validator
102+ a
178− noEmbedded
103+ (Maybe MaxProperties)
179− (run (fmap maybeToList . AR.uniqueItems . _unUniqueItems))
104+ MaxPropertiesInvalid
105+maxPropertiesValidator = Validator noEmbedded (run maxPropertiesVal)
180
106
181−data ItemsContext schema =
107+minPropertiesValidator
182− ItemsContext
108+ :: Validator
183− (Maybe (AR.AdditionalItems schema))
109+ a
184− (AR.Items schema)
110+ (Maybe MinProperties)
185− deriving (Eq, Show)
111+ MinPropertiesInvalid
112+minPropertiesValidator = Validator noEmbedded (run minPropertiesVal)
186
113
187−instance FromJSON schema => FromJSON (ItemsContext schema) where
114+requiredValidator :: Validator a (Maybe Required) ()
188− parseJSON = withObject "ItemsContext" $ \o -> ItemsContext
115+requiredValidator = Validator noEmbedded (run requiredVal)
189− <$> o .:! "additionalItems"
190− <*> o .: "items"
191
116
192−items
117+dependenciesValidator
193− :: (schema -> Value -> [Fail err])
118+ :: (schema -> Value -> [err])
194− -> Validator schema (Maybe (ItemsContext schema)) (AR.ItemsInvalid err)
119+ -> Validator
195−items f =
120+ schema
121+ (Maybe (DependenciesValidator schema))
122+ (DependenciesInvalid err)
123+dependenciesValidator f =
196 Validator
124 Validator
197− (\a -> case a of
125+ (maybe mempty ( (\a -> (mempty, a))
198− Nothing -> mempty
126+ . catMaybes . fmap checkDependency
199− Just (ItemsContext _ b) ->
127+ . HM.elems . _unDependenciesValidator
200− case b of
128+ ))
201− AR.ItemsObject c -> (mempty, pure c)
129+ (run (dependenciesVal f))
202− AR.ItemsArray cs -> (mempty, cs))
130+ where
203− (run (\(ItemsContext a b) -> AR.items f a b))
131+ checkDependency :: Dependency schema -> Maybe schema
204−
132+ checkDependency (PropertyDependency _) = Nothing
205−newtype AdditionalItemsContext schema
133+ checkDependency (SchemaDependency s) = Just s
206− = AdditionalItemsContext
207− { _unAdditionalItemsContext :: AR.AdditionalItems schema }
208− deriving (Eq, Show)
209−
210−instance FromJSON schema => FromJSON (AdditionalItemsContext schema) where
211− parseJSON = withObject "AdditionalItemsContext" $ \o ->
212− AdditionalItemsContext <$> o .: "additionalItems"
213
134
214−-- | Since 'items' will always take care of validating 'additionalItems'
135+propertiesRelatedValidator
215−-- as well, the actual validation side of 'additionalItemsEmbedded' is
136+ :: (schema -> Value -> [err])
216−-- disabled.
217−additionalItemsEmbedded
218− :: Validator
137+ -> Validator
219 schema
138 schema
220− (Maybe (AdditionalItemsContext schema))
139+ (PropertiesRelated schema)
221− err
140+ (PropertiesRelatedInvalid err)
222−additionalItemsEmbedded=
141+propertiesRelatedValidator f =
223− Validator
142+ Validator
143+ (\a -> ( mempty
144+ , HM.elems (fromMaybe mempty (_propProperties a))
145+ <> HM.elems (fromMaybe mempty (_propPattern a))
224− (\a -> case a of
146+ <> case _propAdditional a of
225− Just (AdditionalItemsContext (AR.AdditionalObject b)) ->
147+ Just (AdditionalPropertiesObject b) -> [b]
226− (mempty, pure b)
227− _ -> (mempty, mempty))
148+ _ -> mempty
149+ ))
150+ (\a b -> case fromJSONEither b of
228− (const (const mempty))
151+ Left _ -> mempty
152+ Right c -> maybeToList (propertiesRelatedVal f a c))
229
153
230newtype Definitions schema
154newtype Definitions schema
231 = Definitions { _unDefinitions :: HashMap Text schema }
155 = Definitions { _unDefinitions :: HashMap Text schema }
@@ -235,11 +159,9 @@ instance FromJSON schema => FromJSON (Definitions schema) where
235 parseJSON = withObject "Definitions" $ \o ->
159 parseJSON = withObject "Definitions" $ \o ->
236 Definitions <$> o .: "definitions"
160 Definitions <$> o .: "definitions"
237
161
238−-- | Placing this here since it's similar to @"additionalItems"@.
239−-- in that its validator doesn't run.
240−--
241−-- TODO: Add tests to the language agnostic test suite for both
162+-- TODO: Add tests to the language agnostic test suite to
163+-- make sure these schemas are embedded correctly
242−-- @"additionalItems"@ and this.
164+-- (and do so for @"additionalItems"@ as well).
243definitionsEmbedded
165definitionsEmbedded
244 :: Validator
166 :: Validator
245 schema
167 schema
@@ -252,299 +174,69 @@ definitionsEmbedded =
252 Nothing -> (mempty, mempty))
174 Nothing -> (mempty, mempty))
253 (const (const mempty))
175 (const (const mempty))
254
176
255−--------------------------------------------------
256−-- * Objects
257−--------------------------------------------------
258−
259−newtype MaxProperties
260− = MaxProperties { _unMaxProperties :: Int }
261− deriving (Eq, Show)
262−
263−instance FromJSON MaxProperties where
264− parseJSON = withObject "MaxProperties" $ \o ->
265− MaxProperties <$> o .: "maxProperties"
266−
267−maxProperties :: Validator a (Maybe MaxProperties) ()
268−maxProperties =
269− Validator
270− noEmbedded
271− (run (fmap maybeToList . OB.maxProperties . _unMaxProperties))
272−
273−newtype MinProperties
274− = MinProperties { _unMinProperties :: Int }
275− deriving (Eq, Show)
276−
277−instance FromJSON MinProperties where
278− parseJSON = withObject "MinProperties" $ \o ->
279− MinProperties <$> o .: "minProperties"
280−
281−minProperties :: Validator a (Maybe MinProperties) ()
282−minProperties =
283− Validator
284− noEmbedded
285− (run (fmap maybeToList . OB.minProperties . _unMinProperties))
286−
287−newtype RequiredContext
288− = RequiredContext { _unRequiredContext :: OB.Required }
289− deriving (Eq, Show)
290−
291−instance FromJSON RequiredContext where
292− parseJSON = withObject "RequiredContext" $ \o ->
293− RequiredContext <$> o .: "required"
294−
295−required :: Validator a (Maybe RequiredContext) ()
296−required =
297− Validator
298− noEmbedded
299− (run (fmap maybeToList . OB.required . _unRequiredContext))
300−
301−newtype DependenciesContext schema
302− = DependenciesContext
303− { _unDependenciesContext :: HashMap Text (OB.Dependency schema) }
304− deriving (Eq, Show)
305−
306−instance FromJSON schema => FromJSON (DependenciesContext schema) where
307− parseJSON = withObject "DependenciesContext" $ \o ->
308− DependenciesContext <$> o .: "dependencies"
309−
310−dependencies
311− :: (schema -> Value -> [Fail err])
312− -> Validator
313− schema
314− (Maybe (DependenciesContext schema))
315− (OB.DependencyInvalid err)
316−dependencies f =
317− Validator
318− (maybe mempty ( (\a -> (mempty, a))
319− . catMaybes . fmap checkDependency
320− . HM.elems . _unDependenciesContext
321− ))
322− (run (OB.dependencies f . _unDependenciesContext))
323− where
324− checkDependency :: OB.Dependency schema -> Maybe schema
325− checkDependency (OB.PropertyDependency _) = Nothing
326− checkDependency (OB.SchemaDependency s) = Just s
327−
328−data PropertiesContext schema
329− = PropertiesContext
330− (Maybe (HashMap Text schema))
331− (Maybe (OB.AdditionalProperties schema))
332− (HashMap Text schema)
333− deriving (Eq, Show)
334−
335−instance FromJSON schema => FromJSON (PropertiesContext schema) where
336− parseJSON = withObject "PropertiesContext" $ \o -> PropertiesContext
337− <$> o .:! "patternProperties"
338− <*> o .:! "additionalProperties"
339− <*> o .: "properties"
340−
341−properties
342− :: (schema -> Value -> [Fail err])
343− -> Validator
344− schema
345− (Maybe (PropertiesContext schema))
346− (OB.PropertiesInvalid err)
347−properties f =
348− Validator
349− (\a -> case a of
350− Just (PropertiesContext _ _ b) -> (mempty, HM.elems b)
351− Nothing -> (mempty, mempty))
352− (run (\(PropertiesContext a b c) -> OB.properties f a b c))
353−
354−-- | The first argument is whether the validator should be run.
355−-- If @"properties"@ exists it will be parsed to 'False'.
356−data PatternPropertiesContext schema
357− = PatternPropertiesContext
358− Bool
359− (Maybe (OB.AdditionalProperties schema))
360− (HashMap Text schema)
361− deriving (Eq, Show)
362−
363−instance FromJSON schema => FromJSON (PatternPropertiesContext schema) where
364− parseJSON = withObject "PatternPropertiesContext" $ \o ->
365− PatternPropertiesContext
366− <$> shouldRun o
367− <*> o .:! "additionalProperties"
368− <*> o .: "patternProperties"
369− where
370− shouldRun :: HashMap Text Value -> Parser Bool
371− shouldRun o = do
372− a <- o .:! "properties"
373− pure $ isNothing (a :: Maybe (HashMap Text schema))
374−
375−patternProperties
376− :: (schema -> Value -> [Fail err])
377− -> Validator
378− schema
379− (Maybe (PatternPropertiesContext schema))
380− (OB.PatternPropertiesInvalid err)
381−patternProperties f =
382− Validator
383− (\a -> case a of
384− Just (PatternPropertiesContext _ _ b) -> (mempty, HM.elems b)
385− Nothing -> (mempty, mempty))
386− (run (\(PatternPropertiesContext a b c) -> OB.patternProperties f a b c))
387−
388−-- | The first argument is whether the validator should be run.
389−-- If @"properties"@ or @"patternProperties"@ exist it will be parsed
390−-- to 'False'.
391−data AdditionalPropertiesContext schema
392− = AdditionalPropertiesContext
393− Bool
394− (OB.AdditionalProperties schema)
395− deriving (Eq, Show)
396−
397−instance FromJSON schema => FromJSON (AdditionalPropertiesContext schema) where
398− parseJSON = withObject "AdditionalPropertiesContext" $ \o ->
399− AdditionalPropertiesContext
400− <$> shouldRun o
401− <*> o .: "additionalProperties"
402− where
403− shouldRun :: HashMap Text Value -> Parser Bool
404− shouldRun o = do
405− a <- o .:! "properties"
406− b <- o .:! "patternProperties"
407− pure $ isNothing (a :: Maybe (HashMap Text schema))
408− && isNothing (b :: Maybe (HashMap Text schema))
409−
410−additionalProperties
411− :: (schema -> Value -> [Fail err])
412− -> Validator
413− schema
414− (Maybe (AdditionalPropertiesContext schema))
415− (OB.AdditionalPropertiesInvalid err)
416−additionalProperties f =
417− Validator
418− (\a -> case a of
419− Nothing -> mempty
420− Just (AdditionalPropertiesContext _ b) ->
421− case b of
422− OB.AdditionalPropertiesBool _ -> (mempty, mempty)
423− OB.AdditionalPropertiesObject c -> (mempty, pure c))
424− (run (\(AdditionalPropertiesContext a b) -> OB.additionalProperties f a b))
425−
426--------------------------------------------------
177--------------------------------------------------
427-- * Any
178-- * Any
428--------------------------------------------------
179--------------------------------------------------
429
180
430−newtype Ref
181+refValidator
431− = Ref { _unRef :: Text }
432− deriving (Eq, Show)
433−
434−instance FromJSON Ref where
435− parseJSON = withObject "Ref" $ \o ->
436− Ref <$> o .: "$ref"
437−
438−ref
439 :: (FromJSON schema, ToJSON schema)
182 :: (FromJSON schema, ToJSON schema)
440− => AN.VisitedSchemas
183+ => VisitedSchemas
441 -> Maybe Text
184 -> Maybe Text
442 -> (Maybe Text -> Maybe schema)
185 -> (Maybe Text -> Maybe schema)
443− -> (AN.VisitedSchemas -> Maybe Text -> schema -> Value -> [Fail err])
186+ -> (VisitedSchemas -> Maybe Text -> schema -> Value -> [err])
444− -> Validator a (Maybe Ref) (AN.RefInvalid err)
187+ -> Validator a (Maybe Ref) (RefInvalid err)
445−ref visited scope getRef f =
188+refValidator visited scope getRef f =
446 Validator
189 Validator
447 noEmbedded
190 noEmbedded
448− (run (AN.ref visited scope getRef f . _unRef))
191+ (run (refVal visited scope getRef f))
449
192
450−newtype EnumContext
193+enumValidator :: Validator a (Maybe EnumValidator) EnumInvalid
451− = EnumContext { _unEnumContext :: AN.EnumVal }
194+enumValidator = Validator noEmbedded (run enumVal)
452− deriving (Eq, Show)
453
195
454−instance FromJSON EnumContext where
196+typeValidator :: Validator a (Maybe TypeContext) TypeValidatorInvalid
455− parseJSON = withObject "EnumContext" $ \o ->
197+typeValidator = Validator noEmbedded (run typeVal)
456− EnumContext <$> o .: "enum"
457
198
458−enumVal :: Validator a (Maybe EnumContext) ()
199+allOfValidator
459−enumVal =
460− Validator
461− noEmbedded
462− (run (fmap maybeToList . AN.enumVal . _unEnumContext))
463−
464−newtype TypeContext
465− = TypeContext { _unTypeContext :: AN.TypeVal }
466− deriving (Eq, Show)
467−
468−instance FromJSON TypeContext where
469− parseJSON = withObject "TypeContext" $ \o ->
470− TypeContext <$> o .: "type"
471−
472−typeVal :: Validator a (Maybe TypeContext) ()
473−typeVal =
474− Validator
475− noEmbedded
476− (run (fmap maybeToList . AN.typeVal . _unTypeContext))
477−
478−newtype AllOf schema
479− = AllOf { _unAllOf :: NonEmpty schema }
480− deriving (Eq, Show)
481−
482−instance FromJSON schema => FromJSON (AllOf schema) where
483− parseJSON = withObject "AllOf" $ \o ->
484− AllOf <$> o .: "allOf"
485−
486−allOf
487− :: (schema -> Value -> [Fail err])
200+ :: (schema -> Value -> [err])
488− -> Validator schema (Maybe (AllOf schema)) err
201+ -> Validator schema (Maybe (AllOf schema)) (AllOfInvalid err)
489−allOf f =
202+allOfValidator f =
490 Validator
203 Validator
491 (\a -> case a of
204 (\a -> case a of
492 Just (AllOf b) -> (NE.toList b, mempty)
205 Just (AllOf b) -> (NE.toList b, mempty)
493 Nothing -> (mempty, mempty))
206 Nothing -> (mempty, mempty))
494− (run (AN.allOf f . _unAllOf))
207+ (run (allOfVal f))
495−
496−newtype AnyOf schema
497− = AnyOf { _unAnyOf :: NonEmpty schema }
498− deriving (Eq, Show)
499
208
500−instance FromJSON schema => FromJSON (AnyOf schema) where
209+anyOfValidator
501− parseJSON = withObject "AnyOf" $ \o ->
502− AnyOf <$> o .: "anyOf"
503−
504−anyOf
505− :: (schema -> Value -> [Fail err])
210+ :: (schema -> Value -> [err])
506− -> Validator schema (Maybe (AnyOf schema)) err
211+ -> Validator schema (Maybe (AnyOf schema)) (AnyOfInvalid err)
507−anyOf f =
212+anyOfValidator f =
508 Validator
213 Validator
509 (\a -> case a of
214 (\a -> case a of
510 Just (AnyOf b) -> (NE.toList b, mempty)
215 Just (AnyOf b) -> (NE.toList b, mempty)
511 Nothing -> (mempty, mempty))
216 Nothing -> (mempty, mempty))
512− (run (AN.anyOf f . _unAnyOf))
217+ (run (anyOfVal f))
513−
514−newtype OneOf schema
515− = OneOf { _unOneOf :: NonEmpty schema }
516− deriving (Eq, Show)
517
218
518−instance FromJSON schema => FromJSON (OneOf schema) where
219+oneOfValidator
519− parseJSON = withObject "OneOf" $ \o ->
520− OneOf <$> o .: "oneOf"
521−
522−oneOf
523 :: ToJSON schema
220 :: ToJSON schema
524− => (schema -> Value -> [Fail err])
221+ => (schema -> Value -> [err])
525− -> Validator schema (Maybe (OneOf schema)) (AN.OneOfInvalid err)
222+ -> Validator schema (Maybe (OneOf schema)) (OneOfInvalid err)
526−oneOf f =
223+oneOfValidator f =
527 Validator
224 Validator
528 (\a -> case a of
225 (\a -> case a of
529 Just (OneOf b) -> (NE.toList b, mempty)
226 Just (OneOf b) -> (NE.toList b, mempty)
530 Nothing -> (mempty, mempty))
227 Nothing -> (mempty, mempty))
531− (run (AN.oneOf f . _unOneOf))
228+ (run (oneOfVal f))
532
229
533−newtype NotVal schema
230+notValidator
534− = NotVal { _unNotVal :: schema }
535− deriving (Eq, Show)
536−
537−instance FromJSON schema => FromJSON (NotVal schema) where
538− parseJSON = withObject "NotVal" $ \o ->
539− NotVal <$> o .: "not"
540−
541−notVal
542 :: ToJSON schema
231 :: ToJSON schema
543− => (schema -> Value -> [Fail err])
232+ => (schema -> Value -> [err])
544− -> Validator schema (Maybe (NotVal schema)) ()
233+ -> Validator
545−notVal f =
234+ schema
235+ (Maybe (NotValidator schema))
236+ NotValidatorInvalid
237+notValidator f =
546 Validator
238 Validator
547 (\a -> case a of
239 (\a -> case a of
548− Just (NotVal b) -> (pure b, mempty)
240+ Just (NotValidator b) -> (pure b, mempty)
549− Nothing -> (mempty, mempty))
241+ Nothing -> (mempty, mempty))
550− (run (fmap maybeToList . AN.notVal f . _unNotVal))
242+ (run (notVal f))
src/Data/Validator/Draft4/Any.hs+221−107
@@ -1,22 +1,16 @@
1−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2
1
3module Data.Validator.Draft4.Any where
2module Data.Validator.Draft4.Any where
4
3
5import Import
4import Import
6−-- Hiding is for GHCs before 7.10:
7−import Prelude hiding (any, concat, elem)
8
5
9−import Control.Monad
10−import Data.Aeson.Types (Parser)
11−import Data.List (partition)
12−import Data.List.NonEmpty (NonEmpty)
6+import Data.List.NonEmpty (NonEmpty((:|)))
13import qualified Data.List.NonEmpty as NE
7import qualified Data.List.NonEmpty as NE
14−import Data.Maybe
15import qualified Data.Scientific as SCI
8import qualified Data.Scientific as SCI
9+import Data.Semigroup (Semigroup) -- for older GHCs
16import Data.Set (Set)
10import Data.Set (Set)
17import qualified Data.Set as S
11import qualified Data.Set as S
12+import qualified JSONPointer as JP
18
13
19−import Data.Validator.Failure (Fail(..))
20import qualified Data.Validator.Utils as UT
14import qualified Data.Validator.Utils as UT
21import Data.Validator.Reference (URIBaseAndFragment,
15import Data.Validator.Reference (URIBaseAndFragment,
22 resolveFragment, resolveReference)
16 resolveFragment, resolveReference)
@@ -25,43 +19,61 @@ import Data.Validator.Reference (URIBaseAndFragment,
25-- * $ref
19-- * $ref
26--------------------------------------------------
20--------------------------------------------------
27
21
22+newtype Ref
23+ = Ref { _unRef :: Text }
24+ deriving (Eq, Show)
25+
26+instance FromJSON Ref where
27+ parseJSON = withObject "Ref" $ \o ->
28+ Ref <$> o .: "$ref"
29+
28data RefInvalid err
30data RefInvalid err
29− = RefResolution
31+ = RefResolution Text
30− | RefLoop
32+ -- ^ Indicates a reference that failed to resolve.
33+ --
34+ -- NOTE: The language agnostic test suite doesn't specify if this should
35+ -- cause a validation error or should allow data to pass. We choose to
36+ -- return a validation error.
37+ --
38+ -- Also note that ideally we would enforce in the type system that any
39+ -- failing references be dealt with before valididation. Then this could
40+ -- be removed entirely.
41+ | RefPointerResolution Text
42+ | RefLoop Text VisitedSchemas URIBaseAndFragment
31− | RefInvalid err
43+ | RefInvalid Text Value (NonEmpty err)
44+ -- ^ 'Text' is the URI and 'Value' is the linked schema.
32 deriving (Eq, Show)
45 deriving (Eq, Show)
33
46
34newtype VisitedSchemas
47newtype VisitedSchemas
35 = VisitedSchemas { _unVisited :: [URIBaseAndFragment] }
48 = VisitedSchemas { _unVisited :: [URIBaseAndFragment] }
36− deriving (Eq, Show, Monoid)
49+ deriving (Eq, Show, Semigroup, Monoid)
37
50
38−ref
51+refVal
39 :: forall err schema. (FromJSON schema, ToJSON schema)
52 :: forall err schema. (FromJSON schema, ToJSON schema)
40 => VisitedSchemas
53 => VisitedSchemas
41 -> Maybe Text
54 -> Maybe Text
42 -> (Maybe Text -> Maybe schema)
55 -> (Maybe Text -> Maybe schema)
43− -> (VisitedSchemas -> Maybe Text -> schema -> Value -> [Fail err])
56+ -> (VisitedSchemas -> Maybe Text -> schema -> Value -> [err])
44− -> Text
57+ -> Ref
45 -> Value
58 -> Value
46− -> [Fail (RefInvalid err)]
59+ -> Maybe (RefInvalid err)
47−ref visited scope getRef f reference x
60+refVal visited scope getRef f (Ref reference) x
48− | (mUri, mFragment) `elem` _unVisited visited =
61+ | (mURI, mFragment) `elem` _unVisited visited =
49− pure $ Failure RefLoop (toJSON reference) mempty x
62+ Just (RefLoop reference visited (mURI, mFragment))
50 | otherwise =
63 | otherwise =
51− case getRef mUri of
64+ case getRef mURI of
52− Nothing -> pure failureDNE
65+ Nothing -> Just (RefResolution reference)
53 Just schema ->
66 Just schema ->
54 case resolveFragment mFragment schema of
67 case resolveFragment mFragment schema of
55− Nothing -> pure failureDNE
68+ Nothing -> Just (RefPointerResolution reference)
56 Just s ->
69 Just s ->
57− let newVisited = (VisitedSchemas [(mUri, mFragment)]
70+ let newVisited = (VisitedSchemas [(mURI, mFragment)]
58 <> visited)
71 <> visited)
59− in fmap RefInvalid <$> f newVisited mUri s x
72+ errs = f newVisited mURI s x
73+ in RefInvalid reference (toJSON schema)
74+ <$> NE.nonEmpty errs
60 where
75 where
61− (mUri, mFragment) = resolveReference scope reference
76+ (mURI, mFragment) = resolveReference scope reference
62−
63− failureDNE :: Fail (RefInvalid err)
64− failureDNE = Failure RefResolution (toJSON reference) mempty x
65
77
66--------------------------------------------------
78--------------------------------------------------
67-- * enum
79-- * enum
@@ -76,77 +88,84 @@ ref visited scope getRef f reference x
76--
88--
77-- NOTE: We don't enforce the uniqueness constraint in the haskell code,
89-- NOTE: We don't enforce the uniqueness constraint in the haskell code,
78-- but we do in the 'FromJSON' instance.
90-- but we do in the 'FromJSON' instance.
79−newtype EnumVal
91+newtype EnumValidator
80− = EnumVal { _unEnumVal :: NonEmpty Value }
92+ = EnumValidator { _unEnumValidator :: NonEmpty Value }
81 -- Given a choice, we'd prefer to enforce uniqueness through the type
93 -- Given a choice, we'd prefer to enforce uniqueness through the type
82 -- system over having at least one element. To use a 'Set' though we'd
94 -- system over having at least one element. To use a 'Set' though we'd
83 -- have to use 'OrdValue' here (there's no 'Ord' instance for plain Values)
95 -- have to use 'OrdValue' here (there's no 'Ord' instance for plain Values)
84 -- and we'd rather not make users mess with 'OrdValue'.
96 -- and we'd rather not make users mess with 'OrdValue'.
85 deriving (Eq, Show)
97 deriving (Eq, Show)
86
98
87−instance FromJSON EnumVal where
99+instance FromJSON EnumValidator where
88− parseJSON v = checkUnique . UT._unNonEmpty' =<< parseJSON v
100+ parseJSON = withObject "EnumValidator" $ \o ->
89− where
101+ EnumValidator <$> o .: "enum"
90− checkUnique :: NonEmpty Value -> Parser EnumVal
91− checkUnique a
92− | UT.allUniqueValues' a = pure (EnumVal a)
93− | otherwise = fail "All elements of the Enum validator must be unique."
94−
95−instance ToJSON EnumVal where
96− toJSON = toJSON . UT.NonEmpty' . _unEnumVal
97
102
98−instance Arbitrary EnumVal where
103+instance Arbitrary EnumValidator where
99 arbitrary = do
104 arbitrary = do
100 xs <- (fmap.fmap) UT._unArbitraryValue arbitrary
105 xs <- (fmap.fmap) UT._unArbitraryValue arbitrary
101 case NE.nonEmpty (toUnique xs) of
106 case NE.nonEmpty (toUnique xs) of
102− Nothing -> EnumVal . pure . UT._unArbitraryValue <$> arbitrary
107+ Nothing -> EnumValidator . pure . UT._unArbitraryValue <$> arbitrary
103− Just ne -> pure (EnumVal ne)
108+ Just ne -> pure (EnumValidator ne)
104 where
109 where
105 toUnique :: [Value] -> [Value]
110 toUnique :: [Value] -> [Value]
106 toUnique = fmap UT._unOrdValue . S.toList . S.fromList . fmap UT.OrdValue
111 toUnique = fmap UT._unOrdValue . S.toList . S.fromList . fmap UT.OrdValue
107
112
113+data EnumInvalid
114+ = EnumInvalid EnumValidator Value
115+ deriving (Eq, Show)
116+
108−enumVal :: EnumVal -> Value -> Maybe (Fail ())
117+enumVal :: EnumValidator -> Value -> Maybe EnumInvalid
109−enumVal (EnumVal vs) x
118+enumVal a@(EnumValidator vs) x
110 | not (UT.allUniqueValues' vs) = Nothing
119 | not (UT.allUniqueValues' vs) = Nothing
111 | x `elem` vs = Nothing
120 | x `elem` vs = Nothing
112− | otherwise = Just $ Failure () (toJSON (UT.NonEmpty' vs))
121+ | otherwise = Just $ EnumInvalid a x
113− mempty x
114
122
115--------------------------------------------------
123--------------------------------------------------
116-- * type
124-- * type
117--------------------------------------------------
125--------------------------------------------------
118
126
119−data TypeVal
127+-- | This is separate from 'TypeValidator' so that 'TypeValidator' can
120− = TypeValString Text
128+-- be used to write 'Data.JsonSchema.Draft4.Schema.Schema' without
121− | TypeValArray (Set Text)
129+-- messing up the 'FromJSON' instance of that data type.
130+newtype TypeContext
131+ = TypeContext { _unTypeContext :: TypeValidator }
122 deriving (Eq, Show)
132 deriving (Eq, Show)
123
133
124−instance FromJSON TypeVal where
134+instance FromJSON TypeContext where
125− parseJSON v = fmap TypeValString (parseJSON v)
135+ parseJSON = withObject "TypeContext" $ \o ->
126− <|> fmap TypeValArray (parseJSON v)
136+ TypeContext <$> o .: "type"
137+
138+data TypeValidator
139+ = TypeValidatorString Text
140+ | TypeValidatorArray (Set Text)
141+ deriving (Eq, Show)
127
142
128−instance ToJSON TypeVal where
143+instance FromJSON TypeValidator where
129− toJSON (TypeValString t) = toJSON t
144+ parseJSON v = fmap TypeValidatorString (parseJSON v)
130− toJSON (TypeValArray ts) = toJSON ts
145+ <|> fmap TypeValidatorArray (parseJSON v)
131
146
147+instance ToJSON TypeValidator where
148+ toJSON (TypeValidatorString t) = toJSON t
149+ toJSON (TypeValidatorArray ts) = toJSON ts
150+
132−instance Arbitrary TypeVal where
151+instance Arbitrary TypeValidator where
133− arbitrary = oneof [ TypeValString <$> UT.arbitraryText
152+ arbitrary = oneof [ TypeValidatorString <$> UT.arbitraryText
134− , TypeValArray <$> UT.arbitrarySetOfText
153+ , TypeValidatorArray <$> UT.arbitrarySetOfText
135 ]
154 ]
136
155
137−setFromTypeVal :: TypeVal -> Set Text
156+data TypeValidatorInvalid
138−setFromTypeVal (TypeValString t) = S.singleton t
157+ = TypeValidatorInvalid TypeValidator Value
139−setFromTypeVal (TypeValArray ts) = ts
158+ deriving (Eq, Show)
140
159
141−typeVal :: TypeVal -> Value -> Maybe (Fail ())
160+typeVal :: TypeContext -> Value -> Maybe TypeValidatorInvalid
142−typeVal tv x
161+typeVal (TypeContext tv) x
143− | S.null matches = Just (Failure () (toJSON tv) mempty x)
162+ | S.null matches = Just (TypeValidatorInvalid tv x)
144 | otherwise = Nothing
163 | otherwise = Nothing
145 where
164 where
146 -- There can be more than one match because a 'Value' can be both a
165 -- There can be more than one match because a 'Value' can be both a
147 -- @"number"@ and an @"integer"@.
166 -- @"number"@ and an @"integer"@.
148 matches :: Set Text
167 matches :: Set Text
149− matches = S.intersection okTypes (setFromTypeVal tv)
168+ matches = S.intersection okTypes (setFromTypeValidator tv)
150
169
151 okTypes :: Set Text
170 okTypes :: Set Text
152 okTypes =
171 okTypes =
@@ -161,76 +180,171 @@ typeVal tv x
161 then S.fromList ["number", "integer"]
180 then S.fromList ["number", "integer"]
162 else S.singleton "number"
181 else S.singleton "number"
163
182
183+-- | Internal.
184+setFromTypeValidator :: TypeValidator -> Set Text
185+setFromTypeValidator (TypeValidatorString t) = S.singleton t
186+setFromTypeValidator (TypeValidatorArray ts) = ts
187+
164--------------------------------------------------
188--------------------------------------------------
165-- * allOf
189-- * allOf
166--------------------------------------------------
190--------------------------------------------------
167
191
192+newtype AllOf schema
193+ = AllOf { _unAllOf :: NonEmpty schema }
194+ deriving (Eq, Show)
195+
196+instance FromJSON schema => FromJSON (AllOf schema) where
197+ parseJSON = withObject "AllOf" $ \o ->
168−allOf
198+ AllOf <$> o .: "allOf"
199+
200+newtype AllOfInvalid err
201+ = AllOfInvalid (NonEmpty (JP.Index, NonEmpty err))
202+ deriving (Eq, Show)
203+
204+allOfVal
205+ :: forall err schema.
169− :: (schema -> Value -> [Fail err])
206+ (schema -> Value -> [err])
170− -> NonEmpty schema
207+ -> AllOf schema
171 -> Value
208 -> Value
172− -> [Fail err]
209+ -> Maybe (AllOfInvalid err)
210+allOfVal f (AllOf subSchemas) x = AllOfInvalid <$> NE.nonEmpty failures
211+ where
212+ perhapsFailures :: [(JP.Index, [err])]
213+ perhapsFailures = zip (JP.Index <$> [0..])
173−allOf f subSchemas x = NE.toList subSchemas >>= flip f x
214+ (flip f x <$> NE.toList subSchemas)
215+
216+ failures :: [(JP.Index, NonEmpty err)]
217+ failures = mapMaybe (traverse NE.nonEmpty) perhapsFailures
174
218
175--------------------------------------------------
219--------------------------------------------------
176-- * anyOf
220-- * anyOf
177--------------------------------------------------
221--------------------------------------------------
178
222
223+newtype AnyOf schema
224+ = AnyOf { _unAnyOf :: NonEmpty schema }
225+ deriving (Eq, Show)
226+
227+instance FromJSON schema => FromJSON (AnyOf schema) where
228+ parseJSON = withObject "AnyOf" $ \o ->
179−anyOf
229+ AnyOf <$> o .: "anyOf"
230+
231+newtype AnyOfInvalid err
232+ = AnyOfInvalid (NonEmpty (JP.Index, NonEmpty err))
233+ deriving (Eq, Show)
234+
235+anyOfVal
180 :: forall err schema.
236 :: forall err schema.
181− (schema -> Value -> [Fail err])
237+ (schema -> Value -> [err])
182− -> NonEmpty schema
238+ -> AnyOf schema
183 -> Value
239 -> Value
184− -> [Fail err]
240+ -> Maybe (AnyOfInvalid err)
185−anyOf f subSchemas x
241+anyOfVal f (AnyOf subSchemas) x
186− | any null results = mempty
242+ -- Replace with @null@ once we drop GHC 7.8:
187− | otherwise = concat results
243+ | any (((==) 0 . length) . snd) perhapsFailures = Nothing
244+ | otherwise = AnyOfInvalid <$> NE.nonEmpty failures
188 where
245 where
189− results :: [[Fail err]]
246+ perhapsFailures :: [(JP.Index, [err])]
247+ perhapsFailures = zip (JP.Index <$> [0..])
190− results = flip f x <$> NE.toList subSchemas
248+ (flip f x <$> NE.toList subSchemas)
249+
250+ failures :: [(JP.Index, NonEmpty err)]
251+ failures = mapMaybe (traverse NE.nonEmpty) perhapsFailures
191
252
192--------------------------------------------------
253--------------------------------------------------
193-- * oneOf
254-- * oneOf
194--------------------------------------------------
255--------------------------------------------------
195
256
257+newtype OneOf schema
258+ = OneOf { _unOneOf :: NonEmpty schema }
259+ deriving (Eq, Show)
260+
261+instance FromJSON schema => FromJSON (OneOf schema) where
262+ parseJSON = withObject "OneOf" $ \o ->
263+ OneOf <$> o .: "oneOf"
264+
196data OneOfInvalid err
265data OneOfInvalid err
197− = TooManySuccesses
266+ = TooManySuccesses (NonEmpty (JP.Index, Value)) Value
267+ -- ^ The NonEmpty lists contains tuples whose contents
268+ -- are the index of a schema that validated the data
269+ -- and the contents of that schema.
198− | NoSuccesses err
270+ | NoSuccesses (NonEmpty (JP.Index, NonEmpty err)) Value
271+ -- ^ The NonEmpty lists contains tuples whose contents
272+ -- are the index of a schema that failed to validate the data
273+ -- and the failures it produced.
199 deriving (Eq, Show)
274 deriving (Eq, Show)
200
275
201−oneOf
276+oneOfVal
202 :: forall err schema. ToJSON schema
277 :: forall err schema. ToJSON schema
203− => (schema -> Value -> [Fail err])
278+ => (schema -> Value -> [err])
204− -> NonEmpty schema
279+ -> OneOf schema
205 -> Value
280 -> Value
206− -> [Fail (OneOfInvalid err)]
281+ -> Maybe (OneOfInvalid err)
207−oneOf f subSchemas x
282+oneOfVal f (OneOf (firstSubSchema :| otherSubSchemas)) x =
208− | length successes == 1 = mempty
283+ -- Producing the NonEmpty lists needed by the error constructors
209− | length successes > 1 = pure tooManySuccesses
284+ -- is a little tricky. If we had a partition function like this
210− | otherwise = fmap NoSuccesses <$> concat failures
285+ -- it might help:
286+ -- @
287+ -- (a -> Either b c) -> NonEmpty a -> Either (NonEmpty b, [c])
288+ -- ([b], NonEmpty c)
289+ -- @
290+ case (firstSuccess, otherSuccesses) of
291+ (Right _, Nothing) -> Nothing
292+ (Right a, Just successes) -> Just (TooManySuccesses
293+ (a NE.<| successes) x)
294+ (Left e, Nothing) -> Just (NoSuccesses (e :| otherFailures) x)
295+ (Left _, Just (_ :| [])) -> Nothing
296+ (Left _, Just successes) -> Just (TooManySuccesses successes x)
211 where
297 where
212− (successes, failures) = partition null results
298+ firstSuccess :: Either (JP.Index, NonEmpty err) (JP.Index, Value)
213−
299+ firstSuccess =
214− results :: [[Fail err]]
300+ case NE.nonEmpty (f firstSubSchema x) of
215− results = flip f x <$> NE.toList subSchemas
301+ Nothing -> Right (JP.Index 0, toJSON firstSubSchema)
216−
302+ Just errs -> Left (JP.Index 0, errs)
217− -- NOTE: This could also return information about the specific
303+
218− -- validators that succeeded.
304+ otherPerhapsFailures :: [(JP.Index, Value, [err])]
219− tooManySuccesses :: Fail (OneOfInvalid err)
305+ otherPerhapsFailures =
220− tooManySuccesses = Failure TooManySuccesses
306+ zipWith
221− (toJSON (UT.NonEmpty' subSchemas)) mempty x
307+ (\index schema -> (index, toJSON schema, f schema x))
308+ (JP.Index <$> [0..])
309+ otherSubSchemas
310+
311+ otherSuccesses :: Maybe (NonEmpty (JP.Index, Value))
312+ otherSuccesses = NE.nonEmpty
313+ $ mapMaybe (\(index,val,errs) ->
314+ case errs of
315+ [] -> Just (index,val)
316+ _ -> Nothing
317+ ) otherPerhapsFailures
318+
319+ otherFailures :: [(JP.Index, NonEmpty err)]
320+ otherFailures = mapMaybe (traverse NE.nonEmpty . mid) otherPerhapsFailures
321+
322+ mid :: (a,b,c) -> (a,c)
323+ mid (a,_,c) = (a,c)
222
324
223--------------------------------------------------
325--------------------------------------------------
224-- * not
326-- * not
225--------------------------------------------------
327--------------------------------------------------
226
328
329+newtype NotValidator schema
330+ = NotValidator { _unNotValidator :: schema }
331+ deriving (Eq, Show)
332+
333+instance FromJSON schema => FromJSON (NotValidator schema) where
334+ parseJSON = withObject "NotValidator" $ \o ->
335+ NotValidator <$> o .: "not"
336+
337+data NotValidatorInvalid
338+ = NotValidatorInvalid Value Value
339+ deriving (Eq, Show)
340+
227notVal
341notVal
228− :: ToJSON schema
342+ :: ToJSON schema =>
229− => (schema -> Value -> [Fail err])
343+ (schema -> Value -> [err])
230− -> schema
344+ -> NotValidator schema
231 -> Value
345 -> Value
232− -> Maybe (Fail ())
346+ -> Maybe NotValidatorInvalid
233−notVal f schema x =
347+notVal f (NotValidator schema) x =
234 case f schema x of
348 case f schema x of
235− [] -> Just (Failure () (toJSON schema) mempty x)
349+ [] -> Just (NotValidatorInvalid (toJSON schema) x)
236 _ -> Nothing
350 _ -> Nothing
src/Data/Validator/Draft4/Array.hs+138−98
@@ -2,53 +2,101 @@
2module Data.Validator.Draft4.Array where
2module Data.Validator.Draft4.Array where
3
3
4import Import
4import Import
5−import Prelude
6
5
7−import Control.Monad
8−import qualified Data.Aeson.Pointer as AP
6+import qualified Data.List.NonEmpty as NE
9−import qualified Data.Text as T
10−import qualified Data.Vector as V
7+import qualified Data.Vector as V
11−import Text.Read (readMaybe)
8+import qualified JSONPointer as JP
12
9
13−import Data.Validator.Failure (Fail(..), prependToPath)
14−import Data.Validator.Utils (allUniqueValues)
10+import Data.Validator.Utils (allUniqueValues)
15
11
16--------------------------------------------------
12--------------------------------------------------
17-- * maxItems
13-- * maxItems
18--------------------------------------------------
14--------------------------------------------------
19
15
16+newtype MaxItems
17+ = MaxItems { _unMaxItems :: Int }
18+ deriving (Eq, Show)
19+
20+instance FromJSON MaxItems where
21+ parseJSON = withObject "MaxItems" $ \o ->
22+ MaxItems <$> o .: "maxItems"
23+
24+data MaxItemsInvalid
25+ = MaxItemsInvalid MaxItems (Vector Value)
26+ deriving (Eq, Show)
27+
20-- | The spec requires @"maxItems"@ to be non-negative.
28-- | The spec requires @"maxItems"@ to be non-negative.
21−maxItems :: Int -> Vector Value -> Maybe (Fail ())
29+maxItemsVal :: MaxItems -> Vector Value -> Maybe MaxItemsInvalid
22−maxItems n xs
30+maxItemsVal a@(MaxItems n) xs
23 | n < 0 = Nothing
31 | n < 0 = Nothing
24− | V.length xs > n = Just (Failure () (toJSON n) mempty (Array xs))
32+ | V.length xs > n = Just (MaxItemsInvalid a xs)
25 | otherwise = Nothing
33 | otherwise = Nothing
26
34
27--------------------------------------------------
35--------------------------------------------------
28-- * minItems
36-- * minItems
29--------------------------------------------------
37--------------------------------------------------
30
38
39+newtype MinItems
40+ = MinItems { _unMinItems :: Int }
41+ deriving (Eq, Show)
42+
43+instance FromJSON MinItems where
44+ parseJSON = withObject "MinItems" $ \o ->
45+ MinItems <$> o .: "minItems"
46+
47+data MinItemsInvalid
48+ = MinItemsInvalid MinItems (Vector Value)
49+ deriving (Eq, Show)
50+
31-- | The spec requires @"minItems"@ to be non-negative.
51-- | The spec requires @"minItems"@ to be non-negative.
32−minItems :: Int -> Vector Value -> Maybe (Fail ())
52+minItemsVal :: MinItems -> Vector Value -> Maybe MinItemsInvalid
33−minItems n xs
53+minItemsVal a@(MinItems n) xs
34 | n < 0 = Nothing
54 | n < 0 = Nothing
35− | V.length xs < n = Just (Failure () (toJSON n) mempty (Array xs))
55+ | V.length xs < n = Just (MinItemsInvalid a xs)
36 | otherwise = Nothing
56 | otherwise = Nothing
37
57
38--------------------------------------------------
58--------------------------------------------------
39-- * uniqueItems
59-- * uniqueItems
40--------------------------------------------------
60--------------------------------------------------
41
61
42−uniqueItems :: Bool -> Vector Value -> Maybe (Fail ())
62+newtype UniqueItems
43−uniqueItems True xs
63+ = UniqueItems { _unUniqueItems :: Bool }
64+ deriving (Eq, Show)
65+
66+instance FromJSON UniqueItems where
67+ parseJSON = withObject "UniqueItems" $ \o ->
68+ UniqueItems <$> o .: "uniqueItems"
69+
70+data UniqueItemsInvalid
71+ = UniqueItemsInvalid (Vector Value)
72+ deriving (Eq, Show)
73+
74+uniqueItemsVal :: UniqueItems -> Vector Value -> Maybe UniqueItemsInvalid
75+uniqueItemsVal (UniqueItems True) xs
44 | allUniqueValues xs = Nothing
76 | allUniqueValues xs = Nothing
45− | otherwise = Just (Failure () (Bool True) mempty (Array xs))
77+ | otherwise = Just (UniqueItemsInvalid xs)
46−uniqueItems False _ = Nothing
78+uniqueItemsVal (UniqueItems False) _ = Nothing
47
79
48--------------------------------------------------
80--------------------------------------------------
49-- * items
81-- * items
50--------------------------------------------------
82--------------------------------------------------
51
83
84+data ItemsRelated schema = ItemsRelated
85+ { _irItems :: Maybe (Items schema)
86+ , _irAdditional :: Maybe (AdditionalItems schema)
87+ } deriving (Eq, Show)
88+
89+instance FromJSON schema => FromJSON (ItemsRelated schema) where
90+ parseJSON = withObject "ItemsRelated" $ \o -> ItemsRelated
91+ <$> o .:! "items"
92+ <*> o .:! "additionalItems"
93+
94+emptyItems :: ItemsRelated schema
95+emptyItems = ItemsRelated
96+ { _irItems = Nothing
97+ , _irAdditional = Nothing
98+ }
99+
52data Items schema
100data Items schema
53 = ItemsObject schema
101 = ItemsObject schema
54 | ItemsArray [schema]
102 | ItemsArray [schema]
@@ -67,82 +115,73 @@ instance Arbitrary schema => Arbitrary (Items schema) where
67 , ItemsArray <$> arbitrary
115 , ItemsArray <$> arbitrary
68 ]
116 ]
69
117
118+data ItemsRelatedInvalid err
119+ = IRInvalidItems (ItemsInvalid err)
120+ | IRInvalidAdditional (AdditionalItemsInvalid err)
121+ deriving (Eq, Show)
122+
70data ItemsInvalid err
123data ItemsInvalid err
71− = Items err
124+ = ItemsObjectInvalid (NonEmpty (JP.Index, NonEmpty err))
72− | AdditionalItemsBoolInvalid
125+ | ItemsArrayInvalid (NonEmpty (JP.Index, NonEmpty err))
73− | AdditionalItemsObjectInvalid err
74 deriving (Eq, Show)
126 deriving (Eq, Show)
75
127
76−items
128+-- | @"additionalItems"@ only matters if @"items"@ exists
129+-- and is a JSON Array.
130+itemsRelatedVal
131+ :: forall err schema.
132+ (schema -> Value -> [err])
133+ -> ItemsRelated schema
134+ -> Vector Value
135+ -> [ItemsRelatedInvalid err] -- NOTE: 'Data.These' would help here.
136+itemsRelatedVal f a xs =
137+ let (itemsFailure, remaining) = case _irItems a of
138+ Nothing -> (Nothing, mempty)
139+ Just b -> itemsVal f b xs
140+ additionalFailure = (\b -> additionalItemsVal f b remaining)
141+ =<< _irAdditional a
142+ in catMaybes [ IRInvalidItems <$> itemsFailure
143+ , IRInvalidAdditional <$> additionalFailure
144+ ]
145+
146+-- | Internal.
147+--
148+-- This is because 'itemsRelated' handles @"items"@ validation.
149+itemsVal
77 :: forall err schema.
150 :: forall err schema.
78− (schema -> Value -> [Fail err])
151+ (schema -> Value -> [err])
79− -> Maybe (AdditionalItems schema)
80 -> Items schema
152 -> Items schema
81 -> Vector Value
153 -> Vector Value
82− -> [Fail (ItemsInvalid err)]
154+ -> (Maybe (ItemsInvalid err), [(JP.Index, Value)])
155+ -- ^ The second item in the tuple is the elements of the original
156+ -- JSON Array still remaining to be checked by @"additionalItems"@.
157+itemsVal f a xs =
158+ case a of
83−items f _ (ItemsObject subSchema) xs =
159+ ItemsObject subSchema ->
84− zip [0..] (V.toList xs) >>= g
160+ case NE.nonEmpty (mapMaybe (validateElem subSchema) indexed) of
161+ Nothing -> (Nothing, mempty)
162+ Just errs -> (Just (ItemsObjectInvalid errs), mempty)
163+ ItemsArray subSchemas ->
164+ let remaining = drop (length subSchemas) indexed
165+ res = catMaybes (zipWith validateElem subSchemas indexed)
166+ in case NE.nonEmpty res of
167+ Nothing -> (Nothing, remaining)
168+ Just errs -> (Just (ItemsArrayInvalid errs), remaining)
85 where
169 where
86− g :: (Int, Value) -> [Fail (ItemsInvalid err)]
170+ indexed :: [(JP.Index, Value)]
87− g (index,x) = fmap Items
171+ indexed = zip (JP.Index <$> [0..]) (V.toList xs)
88− . prependToPath (AP.Token (T.pack (show index)))
89− <$> f subSchema x
90
172
91−items f mAdditional (ItemsArray subSchemas) xs =
173+ validateElem
92− itemFailures <> additionalItemFailures
174+ :: schema
93− where
175+ -> (JP.Index, Value)
94− indexedValues :: [(Int, Value)]
176+ -> Maybe (JP.Index, NonEmpty err)
95− indexedValues = zip [0..] (V.toList xs)
177+ validateElem schema (index,x) = (index,) <$> NE.nonEmpty (f schema x)
96−
97− itemFailures :: [Fail (ItemsInvalid err)]
98− itemFailures = join (zipWith g subSchemas indexedValues)
99− where
100− g :: schema -> (Int, Value) -> [Fail (ItemsInvalid err)]
101− g schema (index,x) = fmap Items
102− . prependToPath (AP.Token (T.pack (show index)))
103− <$> f schema x
104−
105− additionalItemFailures :: [Fail (ItemsInvalid err)]
106− additionalItemFailures =
107− case mAdditional of
108− Nothing -> mempty
109− Just adi -> fmap correctName
110− . correctIndexes
111− <$> additionalItems f adi extras
112− where
113− -- It's not great that we convert back to Vector again.
114− extras :: Vector Value
115− extras = V.fromList . fmap snd
116− . drop (length subSchemas) $ indexedValues
117−
118− -- Since 'additionalItems' only sees part of the array, but starts
119− -- indexing from zero, we need to modify the paths it reports to
120− -- represent invalid data so they actually represent the correct
121− -- offsets.
122− correctIndexes
123− :: Fail (AdditionalItemsInvalid err)
124− -> Fail (AdditionalItemsInvalid err)
125− correctIndexes (Failure a b c d) = Failure a b (fixIndex c) d
126− where
127− fixIndex :: AP.Pointer -> AP.Pointer
128− fixIndex (AP.Pointer (tok:toks)) =
129− case readMaybe . T.unpack . AP._unToken $ tok of
130− Nothing -> AP.Pointer $ tok:toks
131− Just n -> AP.Pointer $
132− (AP.Token . T.pack . show $ n + length subSchemas):toks
133− fixIndex (AP.Pointer []) = AP.Pointer []
134−
135− correctName :: AdditionalItemsInvalid err -> ItemsInvalid err
136− correctName AdditionalBoolInvalid = AdditionalItemsBoolInvalid
137− correctName (AdditionalObjectInvalid err) =
138− AdditionalItemsObjectInvalid err
139
178
140--------------------------------------------------
179--------------------------------------------------
141-- * additionalItems
180-- * additionalItems
142--------------------------------------------------
181--------------------------------------------------
143
182
144data AdditionalItems schema
183data AdditionalItems schema
145− = AdditionalBool Bool
184+ = AdditionalBool Bool
146 | AdditionalObject schema
185 | AdditionalObject schema
147 deriving (Eq, Show)
186 deriving (Eq, Show)
148
187
@@ -160,25 +199,26 @@ instance Arbitrary schema => Arbitrary (AdditionalItems schema) where
160 ]
199 ]
161
200
162data AdditionalItemsInvalid err
201data AdditionalItemsInvalid err
163− = AdditionalBoolInvalid
202+ = AdditionalItemsBoolInvalid (NonEmpty (JP.Index, Value))
164− | AdditionalObjectInvalid err
203+ | AdditionalItemsObjectInvalid (NonEmpty (JP.Index, NonEmpty err))
165 deriving (Eq, Show)
204 deriving (Eq, Show)
166
205
206+-- | Internal.
207+--
167−additionalItems
208+-- This is because 'itemsRelated' handles @"additionalItems"@ validation.
209+additionalItemsVal
168 :: forall err schema.
210 :: forall err schema.
169− (schema -> Value -> [Fail err])
211+ (schema -> Value -> [err])
170 -> AdditionalItems schema
212 -> AdditionalItems schema
171− -> Vector Value
213+ -> [(JP.Index, Value)]
214+ -- ^ The elements remaining to validate after the ones covered by
215+ -- @"items"@ have been removed.
172− -> [Fail (AdditionalItemsInvalid err)]
216+ -> Maybe (AdditionalItemsInvalid err)
173−additionalItems _ (AdditionalBool b) xs
217+additionalItemsVal _ (AdditionalBool True) _ = Nothing
174− | b = mempty
218+additionalItemsVal _ (AdditionalBool False) xs =
175− | V.length xs > 0 = pure (Failure AdditionalBoolInvalid (Bool b)
219+ AdditionalItemsBoolInvalid <$> NE.nonEmpty xs
176− mempty (toJSON xs))
177− | otherwise = mempty
178−additionalItems f (AdditionalObject subSchema) xs =
220+additionalItemsVal f (AdditionalObject subSchema) xs =
179− zip [0..] (V.toList xs) >>= g
221+ let res = mapMaybe
180− where
222+ (\(index,x) -> (index,) <$> NE.nonEmpty (f subSchema x))
181− g :: (Int, Value) -> [Fail (AdditionalItemsInvalid err)]
223+ xs
182− g (index,x) = fmap AdditionalObjectInvalid
224+ in AdditionalItemsObjectInvalid <$> NE.nonEmpty res
183− . prependToPath (AP.Token (T.pack (show index)))
184− <$> f subSchema x
src/Data/Validator/Draft4/Number.hs+51−32
@@ -2,64 +2,83 @@
2module Data.Validator.Draft4.Number where
2module Data.Validator.Draft4.Number where
3
3
4import Import
4import Import
5−import Prelude
6
5
7−import Data.Fixed (mod')
6+import Data.Fixed (mod')
8−import Data.Scientific (Scientific)
7+import Data.Scientific (Scientific)
9−
10−import Data.Validator.Failure (Fail(..))
11
8
12--------------------------------------------------
9--------------------------------------------------
13-- * multipleOf
10-- * multipleOf
14--------------------------------------------------
11--------------------------------------------------
15
12
13+newtype MultipleOf
14+ = MultipleOf { _unMultipleOf :: Scientific }
15+ deriving (Eq, Show)
16+
17+instance FromJSON MultipleOf where
18+ parseJSON = withObject "MultipleOf" $ \o ->
19+ MultipleOf <$> o .: "multipleOf"
20+
21+data MultipleOfInvalid
22+ = MultipleOfInvalid MultipleOf Scientific
23+ deriving (Eq, Show)
24+
16-- | The spec requires @"multipleOf"@ to be positive.
25-- | The spec requires @"multipleOf"@ to be positive.
17−multipleOf :: Scientific -> Scientific -> Maybe (Fail ())
26+multipleOfVal :: MultipleOf -> Scientific -> Maybe MultipleOfInvalid
18−multipleOf n x
27+multipleOfVal a@(MultipleOf n) x
19 | n <= 0 = Nothing
28 | n <= 0 = Nothing
20− | x `mod'` n /= 0 = Just (Failure () (toJSON n) mempty (Number x))
29+ | x `mod'` n /= 0 = Just (MultipleOfInvalid a x)
21 | otherwise = Nothing
30 | otherwise = Nothing
22
31
23--------------------------------------------------
32--------------------------------------------------
24-- * maximum
33-- * maximum
25--------------------------------------------------
34--------------------------------------------------
26
35
36+data Maximum = Maximum
37+ { _maximumExclusive :: Bool
38+ , _maximumValue :: Scientific
39+ } deriving (Eq, Show)
40+
41+instance FromJSON Maximum where
42+ parseJSON = withObject "Maximum" $ \o -> Maximum
43+ <$> o .:! "exclusiveMaximum" .!= False
44+ <*> o .: "maximum"
45+
27data MaximumInvalid
46data MaximumInvalid
28− = Maximum
47+ = MaximumInvalid Maximum Scientific
29− | ExclusiveMaximum
30 deriving (Eq, Show)
48 deriving (Eq, Show)
31
49
32maximumVal
50maximumVal
33− :: Bool
51+ :: Maximum
34− -> Scientific
35 -> Scientific
52 -> Scientific
36− -> Maybe (Fail MaximumInvalid)
53+ -> Maybe MaximumInvalid
37−maximumVal exclusive n x
54+maximumVal a@(Maximum exclusive n) x
38− | x `greaterThan` n = Just (Failure err (toJSON n) mempty (Number x))
55+ | exclusive && x >= n = Just (MaximumInvalid a x)
56+ | x > n = Just (MaximumInvalid a x)
39− | otherwise = Nothing
57+ | otherwise = Nothing
40− where
41− (greaterThan, err) = if exclusive
42− then ((>=), ExclusiveMaximum)
43− else ((>), Maximum)
44
58
45--------------------------------------------------
59--------------------------------------------------
46-- * minimum
60-- * minimum
47--------------------------------------------------
61--------------------------------------------------
48
62
63+data Minimum = Minimum
64+ { _minimumExclusive :: Bool
65+ , _minimumValue :: Scientific
66+ } deriving (Eq, Show)
67+
68+instance FromJSON Minimum where
69+ parseJSON = withObject "Minimum" $ \o -> Minimum
70+ <$> o .:! "exclusiveMinimum" .!= False
71+ <*> o .: "minimum"
72+
49data MinimumInvalid
73data MinimumInvalid
50− = Minimum
74+ = MinimumInvalid Minimum Scientific
51− | ExclusiveMinimum
52 deriving (Eq, Show)
75 deriving (Eq, Show)
53
76
54minimumVal
77minimumVal
55− :: Bool
78+ :: Minimum
56− -> Scientific
57 -> Scientific
79 -> Scientific
58− -> Maybe (Fail MinimumInvalid)
80+ -> Maybe MinimumInvalid
59−minimumVal exclusive n x
81+minimumVal a@(Minimum exclusive n) x
60− | x `lessThan` n = Just (Failure err (toJSON n) mempty (Number x))
82+ | exclusive && x <= n = Just (MinimumInvalid a x)
83+ | x < n = Just (MinimumInvalid a x)
61− | otherwise = Nothing
84+ | otherwise = Nothing
62− where
63− (lessThan, err) = if exclusive
64− then ((<=), ExclusiveMinimum)
65− else ((<), Minimum)
src/Data/Validator/Draft4/Object.hs+88−66
@@ -1,4 +1,3 @@
1−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2
1
3module Data.Validator.Draft4.Object
2module Data.Validator.Draft4.Object
4 ( module Data.Validator.Draft4.Object
3 ( module Data.Validator.Draft4.Object
@@ -6,41 +5,67 @@ module Data.Validator.Draft4.Object
6 ) where
5 ) where
7
6
8import Import
7import Import
9−-- Hiding is for GHCs before 7.10:
10−import Prelude hiding (all, concat,
11− foldl)
12
8
13−import Data.Aeson.Types (Parser)
14−import qualified Data.HashMap.Strict as H
9+import qualified Data.HashMap.Strict as HM
10+import qualified Data.List.NonEmpty as NE
15import Data.Set (Set)
11import Data.Set (Set)
16import qualified Data.Set as S
12import qualified Data.Set as S
17import qualified Data.Text as T
13import qualified Data.Text as T
18
14
19import Data.Validator.Draft4.Object.Properties
15import Data.Validator.Draft4.Object.Properties
20−import Data.Validator.Failure (Fail(..))
21import Data.Validator.Utils
16import Data.Validator.Utils
22
17
23--------------------------------------------------
18--------------------------------------------------
24-- * maxProperties
19-- * maxProperties
25--------------------------------------------------
20--------------------------------------------------
26
21
22+newtype MaxProperties
23+ = MaxProperties { _unMaxProperties :: Int }
24+ deriving (Eq, Show)
25+
26+instance FromJSON MaxProperties where
27+ parseJSON = withObject "MaxProperties" $ \o ->
28+ MaxProperties <$> o .: "maxProperties"
29+
30+data MaxPropertiesInvalid
31+ = MaxPropertiesInvalid MaxProperties (HashMap Text Value)
32+ deriving (Eq, Show)
33+
27-- | The spec requires @"maxProperties"@ to be non-negative.
34-- | The spec requires @"maxProperties"@ to be non-negative.
35+maxPropertiesVal
36+ :: MaxProperties
28−maxProperties :: Int -> HashMap Text Value -> Maybe (Fail ())
37+ -> HashMap Text Value
29−maxProperties n x
38+ -> Maybe MaxPropertiesInvalid
39+maxPropertiesVal a@(MaxProperties n) x
30− | n < 0 = Nothing
40+ | n < 0 = Nothing
31− | H.size x > n = Just (Failure () (toJSON n) mempty (Object x))
41+ | HM.size x > n = Just (MaxPropertiesInvalid a x)
32− | otherwise = Nothing
42+ | otherwise = Nothing
33
43
34--------------------------------------------------
44--------------------------------------------------
35-- * minProperties
45-- * minProperties
36--------------------------------------------------
46--------------------------------------------------
37
47
48+newtype MinProperties
49+ = MinProperties { _unMinProperties :: Int }
50+ deriving (Eq, Show)
51+
52+instance FromJSON MinProperties where
53+ parseJSON = withObject "MinProperties" $ \o ->
54+ MinProperties <$> o .: "minProperties"
55+
56+data MinPropertiesInvalid
57+ = MinPropertiesInvalid MinProperties (HashMap Text Value)
58+ deriving (Eq, Show)
59+
38-- | The spec requires @"minProperties"@ to be non-negative.
60-- | The spec requires @"minProperties"@ to be non-negative.
61+minPropertiesVal
62+ :: MinProperties
39−minProperties :: Int -> HashMap Text Value -> Maybe (Fail ())
63+ -> HashMap Text Value
40−minProperties n x
64+ -> Maybe MinPropertiesInvalid
65+minPropertiesVal a@(MinProperties n) x
41− | n < 0 = Nothing
66+ | n < 0 = Nothing
42− | H.size x < n = Just (Failure () (toJSON n) mempty (Object x))
67+ | HM.size x < n = Just (MinPropertiesInvalid a x)
43− | otherwise = Nothing
68+ | otherwise = Nothing
44
69
45--------------------------------------------------
70--------------------------------------------------
46-- * required
71-- * required
@@ -51,28 +76,13 @@ minProperties n x
51-- > The value of this keyword MUST be an array.
76-- > The value of this keyword MUST be an array.
52-- > This array MUST have at least one element.
77-- > This array MUST have at least one element.
53-- > Elements of this array MUST be strings, and MUST be unique.
78-- > Elements of this array MUST be strings, and MUST be unique.
54−--
55−-- We don't enfore that 'Required' has at least one element in the
56−-- haskell code, but we do in the 'FromJSON' instance.
57newtype Required
79newtype Required
58 = Required { _unRequired :: Set Text }
80 = Required { _unRequired :: Set Text }
59− deriving (Eq, Show, ToJSON)
81+ deriving (Eq, Show)
60
82
61instance FromJSON Required where
83instance FromJSON Required where
62− parseJSON v = checkUnique =<< checkSize =<< parseJSON v
84+ parseJSON = withObject "Required" $ \o ->
63− where
85+ Required <$> o .: "required"
64− checkSize :: [Text] -> Parser [Text]
65− checkSize a
66− | null a = fail "Required validator must not be empty."
67− | otherwise = pure a
68−
69− checkUnique :: [Text] -> Parser Required
70− checkUnique a =
71− let b = S.fromList a
72− -- NOTE: Can use length instead of S.size in GHC 7.10 or later.
73− in if length a == S.size b
74− then pure (Required b)
75− else fail "All elements of the Required validator must be unique."
76
86
77instance Arbitrary Required where
87instance Arbitrary Required where
78 arbitrary = do
88 arbitrary = do
@@ -80,23 +90,31 @@ instance Arbitrary Required where
80 xs <- (fmap.fmap) T.pack arbitrary
90 xs <- (fmap.fmap) T.pack arbitrary
81 pure . Required . S.fromList $ x:xs
91 pure . Required . S.fromList $ x:xs
82
92
83−required :: Required -> HashMap Text Value -> Maybe (Fail ())
93+requiredVal :: Required -> HashMap Text Value -> Maybe ()
84−required (Required ts) x
94+requiredVal (Required ts) x
85 -- NOTE: When we no longer need to support GHCs before 7.10
95 -- NOTE: When we no longer need to support GHCs before 7.10
86 -- we can use null from Prelude throughout the library
96 -- we can use null from Prelude throughout the library
87 -- instead of specialized versions.
97 -- instead of specialized versions.
88− | S.null ts = Nothing
98+ | S.null ts = Nothing
89− | H.null (H.difference hm x) = Nothing
99+ | HM.null (HM.difference hm x) = Nothing
90− | otherwise = Just (Failure () (toJSON ts)
100+ | otherwise = Just () -- TODO
91− mempty (Object x))
92 where
101 where
93 hm :: HashMap Text Bool
102 hm :: HashMap Text Bool
94− hm = foldl (\b a -> H.insert a True b) mempty ts
103+ hm = foldl (\b a -> HM.insert a True b) mempty ts
95
104
96--------------------------------------------------
105--------------------------------------------------
97-- * dependencies
106-- * dependencies
98--------------------------------------------------
107--------------------------------------------------
99
108
109+newtype DependenciesValidator schema
110+ = DependenciesValidator
111+ { _unDependenciesValidator :: HashMap Text (Dependency schema) }
112+ deriving (Eq, Show)
113+
114+instance FromJSON schema => FromJSON (DependenciesValidator schema) where
115+ parseJSON = withObject "DependenciesValidator" $ \o ->
116+ DependenciesValidator <$> o .: "dependencies"
117+
100data Dependency schema
118data Dependency schema
101 = SchemaDependency schema
119 = SchemaDependency schema
102 | PropertyDependency (Set Text)
120 | PropertyDependency (Set Text)
@@ -115,9 +133,13 @@ instance Arbitrary schema => Arbitrary (Dependency schema) where
115 , PropertyDependency <$> arbitrarySetOfText
133 , PropertyDependency <$> arbitrarySetOfText
116 ]
134 ]
117
135
118−data DependencyInvalid err
136+data DependencyMemberInvalid err
119− = SchemaDependencyInvalid err
137+ = SchemaDepInvalid (NonEmpty err)
120− | PropertyDependencyInvalid
138+ | PropertyDepInvalid (Set Text) (HashMap Text Value)
139+ deriving (Eq, Show)
140+
141+data DependenciesInvalid err
142+ = DependenciesInvalid (HashMap Text (DependencyMemberInvalid err))
121 deriving (Eq, Show)
143 deriving (Eq, Show)
122
144
123-- | From the spec:
145-- | From the spec:
@@ -132,26 +154,26 @@ data DependencyInvalid err
132-- > If the value is an array, it MUST have at least one element.
154-- > If the value is an array, it MUST have at least one element.
133-- > Each element MUST be a string, and elements in the array MUST be unique.
155-- > Each element MUST be a string, and elements in the array MUST be unique.
134-- > This is called a property dependency.
156-- > This is called a property dependency.
135−dependencies
157+dependenciesVal
136 :: forall err schema.
158 :: forall err schema.
137− (schema -> Value -> [Fail err])
159+ (schema -> Value -> [err])
138− -> HashMap Text (Dependency schema)
160+ -> DependenciesValidator schema
139 -> HashMap Text Value
161 -> HashMap Text Value
140− -> [Fail (DependencyInvalid err)]
162+ -> Maybe (DependenciesInvalid err)
141−dependencies f hm x = concat . fmap (uncurry g) . H.toList $ hm
163+dependenciesVal f (DependenciesValidator hm) x =
164+ let res = HM.mapMaybeWithKey g hm
165+ in if HM.null res
166+ then Nothing
167+ else Just (DependenciesInvalid res)
142− where
168+ where
143− g :: Text -> Dependency schema -> [Fail (DependencyInvalid err)]
169+ g :: Text -> Dependency schema -> Maybe (DependencyMemberInvalid err)
144− g k (SchemaDependency schema)
170+ g k (SchemaDependency schema)
145− | H.member k x =
171+ | HM.member k x = SchemaDepInvalid
146− fmap SchemaDependencyInvalid <$> f schema (Object x)
172+ <$> NE.nonEmpty (f schema (Object x))
147− | otherwise = mempty
173+ | otherwise = Nothing
148− g k (PropertyDependency ts)
174+ g k (PropertyDependency ts)
149− | H.member k x && not allPresent =
175+ | HM.member k x && not allPresent = Just (PropertyDepInvalid ts x)
150− pure $ Failure PropertyDependencyInvalid
151− (toJSON (H.singleton k ts))
152− mempty
153− (Object x)
154− | otherwise = mempty
176+ | otherwise = Nothing
155− where
177+ where
156− allPresent :: Bool
178+ allPresent :: Bool
157− allPresent = all (`H.member` x) ts
179+ allPresent = all (`HM.member` x) ts
src/Data/Validator/Draft4/Object/Properties.hs+152−157
@@ -2,213 +2,208 @@
2module Data.Validator.Draft4.Object.Properties where
2module Data.Validator.Draft4.Object.Properties where
3
3
4import Import
4import Import
5−import Prelude
6
5
7−import Control.Monad
8−import qualified Data.Aeson.Pointer as AP
6+import qualified Data.Hashable as HA
9−import Data.Functor (($>))
10−import qualified Data.HashMap.Strict as HM
7+import qualified Data.HashMap.Strict as HM
8+import qualified Data.List.NonEmpty as NE
11−import Data.Text.Encoding (encodeUtf8)
9+import Data.Text.Encoding (encodeUtf8)
10+import qualified JSONPointer as JP
12−import qualified Text.Regex.PCRE.Heavy as RE
11+import qualified Text.Regex.PCRE.Heavy as RE
12+
13+data PropertiesRelated schema = PropertiesRelated
14+ { _propProperties :: Maybe (HashMap Text schema)
15+ , _propPattern :: Maybe (HashMap Text schema)
16+ , _propAdditional :: Maybe (AdditionalProperties schema)
17+ } deriving (Eq, Show)
18+
19+instance FromJSON schema => FromJSON (PropertiesRelated schema) where
20+ parseJSON = withObject "PropertiesRelated" $ \o -> PropertiesRelated
21+ <$> o .:! "properties"
22+ <*> o .:! "patternProperties"
23+ <*> o .:! "additionalProperties"
24+
25+emptyProperties :: PropertiesRelated schema
26+emptyProperties = PropertiesRelated
27+ { _propProperties = Nothing
28+ , _propPattern = Nothing
29+ , _propAdditional = Nothing
30+ }
13
31
14−import Data.Validator.Failure (Fail(..), prependToPath)
32+data AdditionalProperties schema
33+ = AdditionalPropertiesBool Bool
34+ | AdditionalPropertiesObject schema
35+ deriving (Eq, Show)
15
36
16−-- | For internal use.
37+instance FromJSON schema => FromJSON (AdditionalProperties schema) where
17−newtype Remaining
38+ parseJSON v = fmap AdditionalPropertiesBool (parseJSON v)
18− = Remaining { _unRemaining :: HashMap Text Value }
39+ <|> fmap AdditionalPropertiesObject (parseJSON v)
19
40
20−--------------------------------------------------
41+instance ToJSON schema => ToJSON (AdditionalProperties schema) where
21−-- * properties
42+ toJSON (AdditionalPropertiesBool b) = toJSON b
22−--------------------------------------------------
43+ toJSON (AdditionalPropertiesObject hm) = toJSON hm
23
44
24−data PropertiesInvalid err
45+instance Arbitrary schema => Arbitrary (AdditionalProperties schema) where
25− = PropertiesInvalid err
46+ arbitrary = oneof [ AdditionalPropertiesBool <$> arbitrary
26− | PropPatternInvalid err
47+ , AdditionalPropertiesObject <$> arbitrary
27− | PropAdditionalInvalid (AdditionalPropertiesInvalid err)
48+ ]
28− deriving (Eq, Show)
29
49
30−-- | In order of what's tried: @"properties"@, @"patternProperties"@,
50+-- | A glorified @type@ alias.
31−-- @"additionalProperties"@.
51+newtype Regex
32−properties
52+ = Regex { _unRegex :: Text }
33− :: forall err schema.
53+ deriving (Eq, Show, Generic)
34− (schema -> Value -> [Fail err])
54+
35− -> Maybe (HashMap Text schema)
55+instance HA.Hashable Regex
36− -> Maybe (AdditionalProperties schema)
56+
37− -> HashMap Text schema
57+-- NOTE: We'd like to enforce that at least one error exists here.
38− -> HashMap Text Value
58+data PropertiesRelatedInvalid err = PropertiesRelatedInvalid
39− -> [Fail (PropertiesInvalid err)]
59+ { _prInvalidProperties :: HashMap Text [err]
40−properties f mPat mAdd propertiesHm x =
60+ , _prInvalidPattern :: HashMap (Regex, JP.Key) [err]
41− fmap (fmap PropertiesInvalid) propFailures
61+ , _prInvalidAdditional :: Maybe (APInvalid err)
42− <> fmap (fmap PropPatternInvalid) patternFailures
62+ } deriving (Eq, Show)
43− <> fmap (fmap PropAdditionalInvalid) additionalFailures
63+
44− where
64+data APInvalid err
45− propertiesAndUnmatched :: ([Fail err], Remaining)
65+ = APBoolInvalid (HashMap Text Value)
46− propertiesAndUnmatched = ( failures
66+ | APObjectInvalid (HashMap Text (NonEmpty err))
47− , Remaining (HM.difference x propertiesHm)
48− )
49− where
50− failures :: [Fail err]
51− failures = HM.toList (HM.intersectionWith f propertiesHm x)
52− >>= (\(k,vs) -> fmap (prependToPath (AP.Token k)) vs)
53−
54− (propFailures, remaining1) = propertiesAndUnmatched
55−
56− mPatProp :: Maybe (HashMap Text Value -> ([Fail err], Remaining))
57− mPatProp = patternAndUnmatched f <$> mPat
58−
59− patternFailures :: [Fail err]
60− patternFailures = case mPatProp of
61− Nothing -> mempty
62− Just val -> fst (val x)
63−
64− remaining2 :: Remaining
65− remaining2 = case mPatProp of
66− Nothing -> remaining1
67− Just val -> snd . val . _unRemaining $ remaining1
68−
69− additionalFailures :: [Fail (AdditionalPropertiesInvalid err)]
70− additionalFailures =
71− case mAdd of
72− Nothing -> mempty
73− Just a -> additionalProperties f True a (_unRemaining remaining2)
74−
75−--------------------------------------------------
76−-- * patternProperties
77−--------------------------------------------------
78−
79−data PatternPropertiesInvalid err
80− = PPInvalid err
81− | PPAdditionalPropertiesInvalid (AdditionalPropertiesInvalid err)
82 deriving (Eq, Show)
67 deriving (Eq, Show)
83
68
84−patternProperties
69+-- | First @"properties"@ and @"patternProperties"@ are run simultaneously
70+-- on the data, then @"additionalProperties"@ is run on the remainder.
71+propertiesRelatedVal
85 :: forall err schema.
72 :: forall err schema.
86− (schema -> Value -> [Fail err])
73+ (schema -> Value -> [err])
87− -> Bool
74+ -> PropertiesRelated schema
88− -> Maybe (AdditionalProperties schema)
89− -> HashMap Text schema
90 -> HashMap Text Value
75 -> HashMap Text Value
91− -> [Fail (PatternPropertiesInvalid err)]
76+ -> Maybe (PropertiesRelatedInvalid err)
92−patternProperties _ False _ _ _ = mempty
77+propertiesRelatedVal f props x
93−patternProperties f _ mAdd patternPropertiesHm x =
78+ | all null (HM.elems propFailures)
94− (fmap PPInvalid <$> ppFailures)
79+ && all null (HM.elems patFailures)
95− <> (fmap PPAdditionalPropertiesInvalid <$> addFailures)
80+ && isNothing addFailures = Nothing
81+ | otherwise =
82+ Just PropertiesRelatedInvalid
83+ { _prInvalidProperties = propFailures
84+ , _prInvalidPattern = patFailures
85+ , _prInvalidAdditional = addFailures
86+ }
96 where
87 where
97− patternProps :: ([Fail err], Remaining)
88+ propertiesHm :: HashMap Text schema
98− patternProps = patternAndUnmatched f patternPropertiesHm x
89+ propertiesHm = fromMaybe mempty (_propProperties props)
90+
91+ patHm :: HashMap Text schema
92+ patHm = fromMaybe mempty (_propPattern props)
93+
94+ propAndUnmatched :: (HashMap Text [err], Remaining)
95+ propAndUnmatched = ( HM.intersectionWith f propertiesHm x
96+ , Remaining (HM.difference x propertiesHm)
97+ )
98+
99+ (propFailures, propRemaining) = propAndUnmatched
100+
101+ patAndUnmatched :: (HashMap (Regex, JP.Key) [err], Remaining)
102+ patAndUnmatched = patternAndUnmatched f patHm x
103+
104+ (patFailures, patRemaining) = patAndUnmatched
99
105
100− (ppFailures, remaining) = patternProps
106+ finalRemaining :: Remaining
107+ finalRemaining = Remaining (HM.intersection (_unRemaining patRemaining)
108+ (_unRemaining propRemaining))
101
109
102− addFailures :: [Fail (AdditionalPropertiesInvalid err)]
110+ addFailures :: Maybe (APInvalid err)
103− addFailures =
111+ addFailures = (\addProp -> additionalProperties f addProp finalRemaining)
104− case mAdd of
112+ =<< _propAdditional props
105− Nothing -> mempty
106− Just a -> additionalProperties f True a (_unRemaining remaining)
107
113
114+-- | Internal.
115+newtype Remaining
116+ = Remaining { _unRemaining :: HashMap Text Value }
117+
118+-- | Internal.
108patternAndUnmatched
119patternAndUnmatched
109 :: forall err schema.
120 :: forall err schema.
110− (schema -> Value -> [Fail err])
121+ (schema -> Value -> [err])
111 -> HashMap Text schema
122 -> HashMap Text schema
112 -> HashMap Text Value
123 -> HashMap Text Value
113− -> ([Fail err], Remaining)
124+ -> (HashMap (Regex, JP.Key) [err], Remaining)
114patternAndUnmatched f patPropertiesHm x =
125patternAndUnmatched f patPropertiesHm x =
115− (HM.foldlWithKey' runVals mempty perhapsMatches, remaining)
126+ (HM.foldlWithKey' runMatches mempty perhapsMatches, remaining)
116 where
127 where
117− perhapsMatches :: HashMap Text (Value, [schema])
128+ -- @[(Regex, schema)]@ will have one item per match.
118− perhapsMatches = HM.foldlWithKey' (matchingSchemas patPropertiesHm) mempty x
129+ perhapsMatches :: HashMap Text ([(Regex, schema)], Value)
130+ perhapsMatches =
131+ HM.foldlWithKey' (matchingSchemas patPropertiesHm) mempty x
119 where
132 where
120 matchingSchemas
133 matchingSchemas
121 :: HashMap Text schema
134 :: HashMap Text schema
122− -> HashMap Text (Value, [schema])
135+ -> HashMap Text ([(Regex, schema)], Value)
123 -> Text
136 -> Text
124 -> Value
137 -> Value
125− -> HashMap Text (Value, [schema])
138+ -> HashMap Text ([(Regex, schema)], Value)
126 matchingSchemas subSchemas acc k v =
139 matchingSchemas subSchemas acc k v =
127− HM.insert k (v, HM.foldlWithKey' (checkKey k) mempty subSchemas) acc
140+ HM.insert k
141+ (HM.foldlWithKey' (checkKey k) mempty subSchemas, v)
142+ acc
128
143
129 checkKey
144 checkKey
130 :: Text
145 :: Text
131− -> [schema]
146+ -> [(Regex, schema)]
132 -> Text
147 -> Text
133 -> schema
148 -> schema
134− -> [schema]
149+ -> [(Regex, schema)]
135 checkKey k acc r subSchema =
150 checkKey k acc r subSchema =
136 case RE.compileM (encodeUtf8 r) mempty of
151 case RE.compileM (encodeUtf8 r) mempty of
137 Left _ -> acc
152 Left _ -> acc
138 Right re -> if k RE.=~ re
153 Right re -> if k RE.=~ re
139− then pure subSchema <> acc
154+ then (Regex r, subSchema) : acc
140 else acc
155 else acc
141
156
142− runVals
157+ runMatches
143− :: [Fail err]
158+ :: HashMap (Regex, JP.Key) [err]
144 -> Text
159 -> Text
145− -> (Value, [schema])
160+ -> ([(Regex, schema)], Value)
146− -> [Fail err]
161+ -> HashMap (Regex, JP.Key) [err]
147− runVals acc k (v,subSchemas) =
162+ runMatches acc k (matches,v) =
148− (subSchemas >>= (\schema -> prependToPath (AP.Token k) <$> f schema v))
163+ foldr runMatch acc matches
149− <> acc
164+ where
165+ runMatch
166+ :: (Regex, schema)
167+ -> HashMap (Regex, JP.Key) [err]
168+ -> HashMap (Regex, JP.Key) [err]
169+ runMatch (r,schema) = HM.insert (r, JP.Key k) (f schema v)
150
170
151 remaining :: Remaining
171 remaining :: Remaining
152− remaining = Remaining . fmap fst . HM.filter (null . snd) $ perhapsMatches
172+ remaining = Remaining . fmap snd . HM.filter (null . fst) $ perhapsMatches
153−
154−--------------------------------------------------
155−-- * additionalProperties
156−--------------------------------------------------
157−
158−data AdditionalProperties schema
159− = AdditionalPropertiesBool Bool
160− | AdditionalPropertiesObject schema
161− deriving (Eq, Show)
162−
163−instance FromJSON schema => FromJSON (AdditionalProperties schema) where
164− parseJSON v = fmap AdditionalPropertiesBool (parseJSON v)
165− <|> fmap AdditionalPropertiesObject (parseJSON v)
166−
167−instance ToJSON schema => ToJSON (AdditionalProperties schema) where
168− toJSON (AdditionalPropertiesBool b) = toJSON b
169− toJSON (AdditionalPropertiesObject hm) = toJSON hm
170−
171−instance Arbitrary schema => Arbitrary (AdditionalProperties schema) where
172− arbitrary = oneof [ AdditionalPropertiesBool <$> arbitrary
173− , AdditionalPropertiesObject <$> arbitrary
174− ]
175−
176−data AdditionalPropertiesInvalid err
177− = APBoolInvalid
178− | APObjectInvalid err
179− deriving (Eq, Show)
180
173
174+-- Internal.
181additionalProperties
175additionalProperties
182 :: forall err schema.
176 :: forall err schema.
183− (schema -> Value -> [Fail err])
177+ (schema -> Value -> [err])
184− -> Bool
185 -> AdditionalProperties schema
178 -> AdditionalProperties schema
186− -> HashMap Text Value
179+ -> Remaining
187− -> [Fail (AdditionalPropertiesInvalid err)]
180+ -> Maybe (APInvalid err)
188−additionalProperties _ False _ _ = mempty
181+additionalProperties f a x =
189−additionalProperties f _ a x =
190 case a of
182 case a of
191 AdditionalPropertiesBool b ->
183 AdditionalPropertiesBool b ->
192− ($> APBoolInvalid) <$> additionalPropertiesBool b x
184+ APBoolInvalid <$> additionalPropertiesBool b x
193 AdditionalPropertiesObject b ->
185 AdditionalPropertiesObject b ->
194− fmap APObjectInvalid <$> additionalPropertiesObject f b x
186+ APObjectInvalid <$> additionalPropertiesObject f b x
195
187
188+-- | Internal.
196additionalPropertiesBool
189additionalPropertiesBool
197 :: Bool
190 :: Bool
198− -> HashMap Text Value
191+ -> Remaining
199− -> [Fail ()]
192+ -> Maybe (HashMap Text Value)
200−additionalPropertiesBool False x
193+additionalPropertiesBool True _ = Nothing
201− | HM.size x > 0 = pure $ Failure () (Bool False) mempty (Object x)
194+additionalPropertiesBool False (Remaining x)
202− | otherwise = mempty
195+ | HM.size x > 0 = Just x
203−additionalPropertiesBool True _ = mempty
196+ | otherwise = Nothing
204−
197+
198+-- | Internal.
205additionalPropertiesObject
199additionalPropertiesObject
206 :: forall err schema.
200 :: forall err schema.
207− (schema -> Value -> [Fail err])
201+ (schema -> Value -> [err])
208 -> schema
202 -> schema
209− -> HashMap Text Value
203+ -> Remaining
210− -> [Fail err]
204+ -> Maybe (HashMap Text (NonEmpty err))
211−additionalPropertiesObject f schema x = HM.toList x >>= g
205+additionalPropertiesObject f schema (Remaining x) =
212− where
206+ let errs = HM.mapMaybe (NE.nonEmpty . f schema) x
213− g :: (Text, Value) -> [Fail err]
207+ in if HM.null errs
214− g (k,v) = prependToPath (AP.Token k) <$> f schema v
208+ then Nothing
209+ else Just errs
src/Data/Validator/Draft4/String.hs+50−16
@@ -2,44 +2,78 @@
2module Data.Validator.Draft4.String where
2module Data.Validator.Draft4.String where
3
3
4import Import
4import Import
5−import Prelude
6
5
7−import qualified Data.Text as T
6+import qualified Data.Text as T
8−import Data.Text.Encoding (encodeUtf8)
7+import Data.Text.Encoding (encodeUtf8)
9−import qualified Text.Regex.PCRE.Heavy as RE
8+import qualified Text.Regex.PCRE.Heavy as RE
10−
11−import Data.Validator.Failure (Fail(..))
12
9
13--------------------------------------------------
10--------------------------------------------------
14-- * maxLength
11-- * maxLength
15--------------------------------------------------
12--------------------------------------------------
16
13
14+newtype MaxLength
15+ = MaxLength { _unMaxLength :: Int }
16+ deriving (Eq, Show)
17+
18+instance FromJSON MaxLength where
19+ parseJSON = withObject "MaxLength" $ \o ->
20+ MaxLength <$> o .: "maxLength"
21+
22+data MaxLengthInvalid
23+ = MaxLengthInvalid MaxLength Text
24+ deriving (Eq, Show)
25+
17-- | The spec requires @"maxLength"@ to be non-negative.
26-- | The spec requires @"maxLength"@ to be non-negative.
18−maxLength :: Int -> Text -> Maybe (Fail ())
27+maxLengthVal :: MaxLength -> Text -> Maybe MaxLengthInvalid
19−maxLength n x
28+maxLengthVal a@(MaxLength n) x
20 | n <= 0 = Nothing
29 | n <= 0 = Nothing
21− | T.length x > n = Just (Failure () (toJSON n) mempty (String x))
30+ | T.length x > n = Just (MaxLengthInvalid a x)
22 | otherwise = Nothing
31 | otherwise = Nothing
23
32
24--------------------------------------------------
33--------------------------------------------------
25-- * minLength
34-- * minLength
26--------------------------------------------------
35--------------------------------------------------
27
36
37+newtype MinLength
38+ = MinLength { _unMinLength :: Int }
39+ deriving (Eq, Show)
40+
41+instance FromJSON MinLength where
42+ parseJSON = withObject "MinLength" $ \o ->
43+ MinLength <$> o .: "minLength"
44+
45+data MinLengthInvalid
46+ = MinLengthInvalid MinLength Text
47+ deriving (Eq, Show)
48+
28-- | The spec requires @"minLength"@ to be non-negative.
49-- | The spec requires @"minLength"@ to be non-negative.
29−minLength :: Int -> Text -> Maybe (Fail ())
50+minLengthVal :: MinLength -> Text -> Maybe MinLengthInvalid
30−minLength n x
51+minLengthVal a@(MinLength n) x
31 | n <= 0 = Nothing
52 | n <= 0 = Nothing
32− | T.length x < n = Just (Failure () (toJSON n) mempty (String x))
53+ | T.length x < n = Just (MinLengthInvalid a x)
33 | otherwise = Nothing
54 | otherwise = Nothing
34
55
35--------------------------------------------------
56--------------------------------------------------
36-- * pattern
57-- * pattern
37--------------------------------------------------
58--------------------------------------------------
38
59
39−patternVal :: Text -> Text -> Maybe (Fail ())
60+newtype PatternValidator
40−patternVal t x =
61+ = PatternValidator { _unPatternValidator :: Text }
62+ deriving (Eq, Show)
63+
64+instance FromJSON PatternValidator where
65+ parseJSON = withObject "PatternValidator" $ \o ->
66+ PatternValidator <$> o .: "pattern"
67+
68+data PatternInvalid
69+ = PatternNotRegex -- TODO: let these pass successfully?
70+ | PatternInvalid PatternValidator Text
71+ deriving (Eq, Show)
72+
73+patternVal :: PatternValidator -> Text -> Maybe PatternInvalid
74+patternVal a@(PatternValidator t) x =
41 case RE.compileM (encodeUtf8 t) mempty of
75 case RE.compileM (encodeUtf8 t) mempty of
42− Left _ -> Just (Failure () (toJSON t) mempty (String x))
76+ Left _ -> Just PatternNotRegex
43 Right re -> if x RE.=~ re
77 Right re -> if x RE.=~ re
44 then Nothing
78 then Nothing
45− else Just (Failure () (toJSON t) mempty (String x))
79+ else Just (PatternInvalid a x)
src/Data/Validator/Failure.hsdeleted−49
@@ -1,49 +0,0 @@
1−{-# LANGUAGE DeriveFunctor #-}
2−
3−module Data.Validator.Failure where
4−
5−import Import
6−-- Hiding is for GHCs before 7.10:
7−import Prelude hiding (concat, sequence)
8−
9−import qualified Data.Aeson.Pointer as AP
10−
11−-- | Validators shouldn't know more about the schema they're going to
12−-- be used with than necessary. If a validator throws errors using the
13−-- error sum type of a particular schema, then it can't be used with
14−-- other schemas later that have different error sum types (at least not
15−-- without writing partial functions).
16−--
17−-- Because of this we make 'Fail' a higher order type, so each validator
18−-- can return a sum type describing only the failures that can occur in that
19−-- validator (or '()' if that validator can only fail in one way).
20−--
21−-- It's the job of a schema's validate function to unify the errors produced
22−-- by the validators it uses into a single error sum type for that schema.
23−-- The schema's validate function will return a 'Fail' with
24−-- that sum type as its type argument.
25−--
26−-- The slightly weird naming ('Fail' and 'Failure') is so that we can define
27−-- a 'type Failure = Fail SchemaErrorType' for each of our schemas, and
28−-- export it along with 'Fail(..)'. This way the users of the library only
29−-- use 'Failure', not 'Fail'.
30−data Fail err = Failure
31− { _failureValidatorsCalled :: !err
32− -- ^ E.g. @Items UniqueItems@ during draft 4 validation.
33− , _failureFinalValidator :: !Value
34− -- ^ The value of the validator that raised the error (e.g. the value of
35− -- @"uniqueItems"@ in the above example.
36− , _failureOffendingPointer :: !AP.Pointer
37− -- ^ A pointer to the part of the data that caused invalidation.
38− , _failureOffendingData :: !Value
39− -- ^ The part of the data that caused invalidation. Usually this is
40− -- identical to the result of resolving '_invalidOffendingPointer'
41− -- against the starting data, but not always (e.g. in the case of
42− -- 'additionalItems' where '_invalidOffendingData' will be the items
43− -- in the array that were not allowed, instead of the entire array).
44− } deriving (Eq, Show, Functor)
45−
46−prependToPath :: AP.Token -> Fail a -> Fail a
47−prependToPath tok failure =
48− let old = _failureOffendingPointer failure
49− in failure { _failureOffendingPointer = AP.Pointer [tok] <> old }
src/Data/Validator/Reference.hs+4−4
@@ -7,14 +7,14 @@
7module Data.Validator.Reference where
7module Data.Validator.Reference where
8
8
9import Import
9import Import
10−import Prelude
11
10
12−import qualified Data.Aeson.Pointer as AP
13import qualified Data.Text as T
11import qualified Data.Text as T
14import Data.Text.Encoding (decodeUtf8, encodeUtf8)
12import Data.Text.Encoding (decodeUtf8, encodeUtf8)
13+import qualified JSONPointer as JP
15import Network.HTTP.Types.URI (urlDecode)
14import Network.HTTP.Types.URI (urlDecode)
16import System.FilePath ((</>), dropFileName)
15import System.FilePath ((</>), dropFileName)
17
16
17+-- | TODO: Replace these with actual URI data types.
18type URIBase = Maybe Text
18type URIBase = Maybe Text
19type URIBaseAndFragment = (Maybe Text, Maybe Text)
19type URIBaseAndFragment = (Maybe Text, Maybe Text)
20
20
@@ -34,8 +34,8 @@ resolveFragment
34resolveFragment Nothing schema = Just schema
34resolveFragment Nothing schema = Just schema
35resolveFragment (Just pointer) schema = do
35resolveFragment (Just pointer) schema = do
36 let urlDecoded = decodeUtf8 . urlDecode True . encodeUtf8 $ pointer
36 let urlDecoded = decodeUtf8 . urlDecode True . encodeUtf8 $ pointer
37− p <- either (const Nothing) Just (AP.unescape urlDecoded)
37+ p <- either (const Nothing) Just (JP.unescape urlDecoded)
38− x <- either (const Nothing) Just (AP.resolve p (toJSON schema))
38+ x <- either (const Nothing) Just (JP.resolve p (toJSON schema))
39 case fromJSON x of
39 case fromJSON x of
40 Error _ -> Nothing
40 Error _ -> Nothing
41 Success schema' -> Just schema'
41 Success schema' -> Just schema'
src/Data/Validator/Types.hs+3−7
@@ -1,13 +1,9 @@
1−{-# LANGUAGE DeriveFunctor #-}
2
1
3module Data.Validator.Types where
2module Data.Validator.Types where
4
3
5−import Prelude
4+import Import
6
5
7−import Data.Aeson (Value)
6+import Data.Profunctor (Profunctor(..))
8−import Data.Profunctor (Profunctor(..))
9−
10−import Data.Validator.Failure (Fail)
11
7
12data Validator schema val err = Validator
8data Validator schema val err = Validator
13 { _embedded :: val -> ([schema], [schema])
9 { _embedded :: val -> ([schema], [schema])
@@ -19,7 +15,7 @@ data Validator schema val err = Validator
19 -- This is done to allow static detection of loops, though this isn't
15 -- This is done to allow static detection of loops, though this isn't
20 -- implemented yet (though the Draft4 code does do live loop detection
16 -- implemented yet (though the Draft4 code does do live loop detection
21 -- during validation).
17 -- during validation).
22− , _validate :: val -> Value -> [Fail err]
18+ , _validate :: val -> Value -> [err]
23 } deriving Functor
19 } deriving Functor
24
20
25instance Profunctor (Validator schema) where
21instance Profunctor (Validator schema) where
src/Data/Validator/Utils.hs+1−3
@@ -2,11 +2,9 @@
2module Data.Validator.Utils where
2module Data.Validator.Utils where
3
3
4import Import
4import Import
5−import Prelude
6
5
7−import Control.Arrow
6+import Control.Monad (fail)
8import qualified Data.HashMap.Strict as HM
7import qualified Data.HashMap.Strict as HM
9−import Data.List.NonEmpty (NonEmpty)
10import qualified Data.List.NonEmpty as NE
8import qualified Data.List.NonEmpty as NE
11import Data.Scientific (Scientific, fromFloatDigits)
9import Data.Scientific (Scientific, fromFloatDigits)
12import Data.Set (Set)
10import Data.Set (Set)
src/Import.hs+6−6
@@ -1,12 +1,12 @@
1
1
2module Import (module Export) where
2module Import (module Export) where
3
3
4−import Control.Applicative as Export
4+import Protolude as Export
5+
5import Data.Aeson as Export
6import Data.Aeson as Export
6−import Data.Foldable as Export
7import Data.HashMap.Strict as Export (HashMap)
7import Data.HashMap.Strict as Export (HashMap)
8−import Data.Monoid as Export
8+import Data.List as Export (length, null) -- for older GHCs
9−import Data.Text as Export (Text)
9+import Data.List.NonEmpty as Export (NonEmpty)
10−import Data.Traversable as Export
11import Data.Vector as Export (Vector)
10import Data.Vector as Export (Vector)
12−import Test.QuickCheck as Export hiding (Failure, Result, Success)
11+import Test.QuickCheck as Export hiding ((.&.), Failure,
12+ Result, Success)
test/Local.hs+13−16
@@ -1,18 +1,17 @@
1
1
2module Main where
2module Main where
3
3
4−import Control.Applicative
4+import Protolude
5−import Control.Monad (unless)
5+
6import Data.Aeson
6import Data.Aeson
7−import Data.Foldable (traverse_)
7+import qualified Data.List.NonEmpty as NE
8−import qualified Data.List.NonEmpty as N
9−import Data.Monoid
10import qualified System.Timeout as TO
8import qualified System.Timeout as TO
11import Test.Hspec
9import Test.Hspec
12import Test.QuickCheck (property)
10import Test.QuickCheck (property)
13
11
14import qualified Data.JsonSchema.Draft4 as D4
12import qualified Data.JsonSchema.Draft4 as D4
15−import Data.JsonSchema.Fetch (ReferencedSchemas(..))
13+import Data.JsonSchema.Fetch (ReferencedSchemas(..),
14+ URISchemaMap(..))
16import qualified Data.JsonSchema.Types as JT
15import qualified Data.JsonSchema.Types as JT
17import qualified Local.Failure
16import qualified Local.Failure
18import qualified Local.Validation
17import qualified Local.Validation
@@ -21,13 +20,13 @@ import Shared
21
20
22-- Examples
21-- Examples
23import qualified AlternateSchema
22import qualified AlternateSchema
24−import qualified Full
25import qualified Simple
23import qualified Simple
24+import qualified TwoStep
26
25
27−dir :: String
26+dir :: FilePath
28dir = "JSON-Schema-Test-Suite/tests/draft4"
27dir = "JSON-Schema-Test-Suite/tests/draft4"
29
28
30−supplementDir :: String
29+supplementDir :: FilePath
31supplementDir = "test/supplement"
30supplementDir = "test/supplement"
32
31
33main :: IO ()
32main :: IO ()
@@ -73,22 +72,20 @@ main = do
73 res <- D4.fetchHTTPAndValidate (D4.SchemaWithURI s Nothing) (_scData sc)
72 res <- D4.fetchHTTPAndValidate (D4.SchemaWithURI s Nothing) (_scData sc)
74 let failures = case res of
73 let failures = case res of
75 Right () -> mempty
74 Right () -> mempty
76− Left (D4.HVData a) -> N.toList a
75+ Left (D4.HVData a) -> NE.toList (D4._invalidFailures a)
77− other -> error ("Local.validate error: "
76+ other -> panic ("Local.validate error: "
78 <> show other)
77 <> show other)
79− traverse_ (checkPointer (_scData sc)) failures
80 assertResult sc failures
78 assertResult sc failures
81
79
82 validateExample :: JT.Schema -> SchemaTestCase -> Expectation
80 validateExample :: JT.Schema -> SchemaTestCase -> Expectation
83 validateExample s sc = do
81 validateExample s sc = do
84 res <- AlternateSchema.referencesViaHTTP (D4.SchemaWithURI s Nothing)
82 res <- AlternateSchema.referencesViaHTTP (D4.SchemaWithURI s Nothing)
85 case res of
83 case res of
86− Left e -> error ("Local.validateExample error: " <> show e)
84+ Left e -> panic ("Local.validateExample error: " <> show e)
87 Right schemaMap -> do
85 Right schemaMap -> do
88 let failures = AlternateSchema.validate
86 let failures = AlternateSchema.validate
89− (ReferencedSchemas s schemaMap)
87+ (ReferencedSchemas s (_unURISchemaMap schemaMap))
90 Nothing s (_scData sc)
88 Nothing s (_scData sc)
91− traverse_ (checkPointer (_scData sc)) failures
92 assertResult sc failures
89 assertResult sc failures
93
90
94quickCheckTests :: Spec
91quickCheckTests :: Spec
@@ -101,5 +98,5 @@ quickCheckTests =
101
98
102exampleTests :: Spec
99exampleTests :: Spec
103exampleTests = do
100exampleTests = do
104− it "Full.example compiles successfully" Full.example
105 it "Simple.example compiles successfully" Simple.example
101 it "Simple.example compiles successfully" Simple.example
102+ it "TwoStep.example compiles successfully" TwoStep.example
test/Local/Failure.hs+59−15
@@ -1,35 +1,41 @@
1
1
2module Local.Failure where
2module Local.Failure where
3
3
4−import Prelude
4+import Protolude
5
5
6import Data.Aeson
6import Data.Aeson
7−import qualified Data.Aeson.Pointer as P
8−import Data.Monoid
9import Test.Hspec
7import Test.Hspec
8+import qualified Data.Vector as V
9+import qualified JSONPointer as JP
10
10
11import Data.JsonSchema.Draft4
11import Data.JsonSchema.Draft4
12−import Data.JsonSchema.Draft4.Spec (validate)
12+import qualified Data.JsonSchema.Draft4.Spec as Spec
13−import qualified Data.Validator.Draft4.Array as AR
13+import qualified Data.Validator.Draft4 as D4
14
14
15spec :: Spec
15spec :: Spec
16spec = do
16spec = do
17 it "items array failures are constructed correctly" itemsArray
17 it "items array failures are constructed correctly" itemsArray
18 it "items object failures are constructed correctly" itemsObject
18 it "items object failures are constructed correctly" itemsObject
19+ it "reports infinitely looping references correctly" loopRef
19
20
20itemsArray :: Expectation
21itemsArray :: Expectation
21itemsArray =
22itemsArray =
22 failures `shouldBe`
23 failures `shouldBe`
23− [Failure (Items UniqueItems) (Bool True) (P.Pointer [P.Token "0"])
24+ [ FailureItems (D4.ItemsArrayInvalid (
24− (toJSON [True, True])
25+ pure ( JP.Index 0
26+ , pure (FailureUniqueItems (D4.UniqueItemsInvalid
27+ (V.fromList [Null, Null])
28+ ))
29+ )
30+ ))
25 ]
31 ]
26 where
32 where
27− failures = validate (ReferencedSchemas schema mempty)
33+ failures :: [ValidatorFailure]
28− sw (toJSON [[True, True]])
34+ failures = Spec.specValidate (ReferencedSchemas schema mempty) sw badData
29
35
30 schema :: Schema
36 schema :: Schema
31 schema = emptySchema
37 schema = emptySchema
32− { _schemaItems = Just $ AR.ItemsArray
38+ { _schemaItems = Just $ D4.ItemsArray
33 [emptySchema { _schemaUniqueItems = Just True }]
39 [emptySchema { _schemaUniqueItems = Just True }]
34 }
40 }
35
41
@@ -39,19 +45,27 @@ itemsArray =
39 , _swURI = Nothing
45 , _swURI = Nothing
40 }
46 }
41
47
48+ badData :: Value
49+ badData = toJSON [[Null, Null]]
50+
42itemsObject :: Expectation
51itemsObject :: Expectation
43itemsObject =
52itemsObject =
44 failures `shouldBe`
53 failures `shouldBe`
45− [Failure (Items UniqueItems) (Bool True) (P.Pointer [P.Token "0"])
54+ [ FailureItems (D4.ItemsObjectInvalid (
46− (toJSON [True, True])
55+ pure ( JP.Index 1
56+ , pure (FailureUniqueItems (D4.UniqueItemsInvalid
57+ (V.fromList [Null, Null])
58+ ))
59+ )
60+ ))
47 ]
61 ]
48 where
62 where
49− failures = validate (ReferencedSchemas schema mempty)
63+ failures :: [ValidatorFailure]
50− sw (toJSON [[True, True]])
64+ failures = Spec.specValidate (ReferencedSchemas schema mempty) sw badData
51
65
52 schema :: Schema
66 schema :: Schema
53 schema = emptySchema
67 schema = emptySchema
54− { _schemaItems = Just $ AR.ItemsObject
68+ { _schemaItems = Just $ D4.ItemsObject
55 (emptySchema { _schemaUniqueItems = Just True })
69 (emptySchema { _schemaUniqueItems = Just True })
56 }
70 }
57
71
@@ -60,3 +74,33 @@ itemsObject =
60 { _swSchema = schema
74 { _swSchema = schema
61 , _swURI = Nothing
75 , _swURI = Nothing
62 }
76 }
77+
78+ badData :: Value
79+ badData = toJSON [Null, toJSON [Null, Null]]
80+
81+loopRef :: Expectation
82+loopRef =
83+ failures `shouldBe`
84+ [ FailureRef (D4.RefLoop
85+ "#"
86+ (D4.VisitedSchemas [(Nothing, Nothing)])
87+ (Nothing, Nothing)
88+ )
89+ ]
90+ where
91+ failures :: [ValidatorFailure]
92+ failures = Spec.specValidate (ReferencedSchemas schema mempty) sw badData
93+
94+ schema :: Schema
95+ schema = emptySchema
96+ { _schemaRef = Just "#"
97+ }
98+
99+ sw :: SchemaWithURI Schema
100+ sw = SchemaWithURI
101+ { _swSchema = schema
102+ , _swURI = Nothing
103+ }
104+
105+ badData :: Value
106+ badData = toJSON [[Null, Null]]
test/Local/Reference.hs+2
@@ -1,6 +1,8 @@
1
1
2module Local.Reference where
2module Local.Reference where
3
3
4+import Protolude
5+
4import Test.Hspec
6import Test.Hspec
5
7
6import Data.Validator.Reference
8import Data.Validator.Reference
test/Local/Validation.hs+3−3
@@ -1,12 +1,12 @@
1
1
2module Local.Validation where
2module Local.Validation where
3
3
4−import Control.Applicative
4+import Protolude hiding (msg)
5+
5import Data.Aeson
6import Data.Aeson
6import qualified Data.Aeson as AE
7import qualified Data.Aeson as AE
7import qualified Data.HashMap.Strict as HM
8import qualified Data.HashMap.Strict as HM
8−import Data.Monoid
9+import Data.String (String)
9−import Data.Text (Text)
10import Test.Hspec
10import Test.Hspec
11
11
12import Data.JsonSchema.Draft4
12import Data.JsonSchema.Draft4
test/Remote.hs+10−12
@@ -1,25 +1,25 @@
1
1
2module Main where
2module Main where
3
3
4−import Control.Applicative
4+import Protolude
5+
5import Control.Concurrent.Async (withAsync)
6import Control.Concurrent.Async (withAsync)
6−import Data.Foldable (traverse_)
7+import qualified Data.List.NonEmpty as NE
7−import qualified Data.List.NonEmpty as N
8−import Data.Monoid
9import Network.Wai.Application.Static (defaultFileServerSettings,
8import Network.Wai.Application.Static (defaultFileServerSettings,
10 staticApp)
9 staticApp)
11import Network.Wai.Handler.Warp (run)
10import Network.Wai.Handler.Warp (run)
12import Test.Hspec
11import Test.Hspec
13
12
14import qualified Data.JsonSchema.Draft4 as D4
13import qualified Data.JsonSchema.Draft4 as D4
15−import Data.JsonSchema.Fetch (ReferencedSchemas(..))
14+import Data.JsonSchema.Fetch (ReferencedSchemas(..),
15+ URISchemaMap(..))
16import qualified Data.JsonSchema.Types as JT
16import qualified Data.JsonSchema.Types as JT
17import Shared
17import Shared
18
18
19-- Examples
19-- Examples
20import qualified AlternateSchema
20import qualified AlternateSchema
21
21
22−dir :: String
22+dir :: FilePath
23dir = "JSON-Schema-Test-Suite/tests/draft4"
23dir = "JSON-Schema-Test-Suite/tests/draft4"
24
24
25main :: IO ()
25main :: IO ()
@@ -36,22 +36,20 @@ main = withAsync serve $ \_ -> do
36 res <- D4.fetchHTTPAndValidate (D4.SchemaWithURI s Nothing) (_scData sc)
36 res <- D4.fetchHTTPAndValidate (D4.SchemaWithURI s Nothing) (_scData sc)
37 let failures = case res of
37 let failures = case res of
38 Right () -> mempty
38 Right () -> mempty
39− Left (D4.HVData a) -> N.toList a
39+ Left (D4.HVData a) -> NE.toList (D4._invalidFailures a)
40− other -> error ("Local.validate error: "
40+ other -> panic ("Local.validate error: "
41 <> show other)
41 <> show other)
42− traverse_ (checkPointer (_scData sc)) failures
43 assertResult sc failures
42 assertResult sc failures
44
43
45 validateExample :: JT.Schema -> SchemaTestCase -> Expectation
44 validateExample :: JT.Schema -> SchemaTestCase -> Expectation
46 validateExample s sc = do
45 validateExample s sc = do
47 res <- AlternateSchema.referencesViaHTTP (D4.SchemaWithURI s Nothing)
46 res <- AlternateSchema.referencesViaHTTP (D4.SchemaWithURI s Nothing)
48 case res of
47 case res of
49− Left e -> error ("Remote.validateExample error: " <> show e)
48+ Left e -> panic ("Remote.validateExample error: " <> show e)
50 Right schemaMap -> do
49 Right schemaMap -> do
51 let failures = AlternateSchema.validate
50 let failures = AlternateSchema.validate
52− (ReferencedSchemas s schemaMap)
51+ (ReferencedSchemas s (_unURISchemaMap schemaMap))
53 Nothing s (_scData sc)
52 Nothing s (_scData sc)
54− traverse_ (checkPointer (_scData sc)) failures
55 assertResult sc failures
53 assertResult sc failures
56
54
57serve :: IO ()
55serve :: IO ()
test/Shared.hs+19−50
@@ -1,29 +1,19 @@
1−{-# LANGUAGE DeriveGeneric #-}
2
1
3module Shared where
2module Shared where
4
3
5−import Control.Applicative
4+import Protolude
6−import Control.Monad
5+
6+import Control.Monad (fail)
7import Data.Aeson
7import Data.Aeson
8−import qualified Data.Aeson.Pointer as AP
9import Data.Aeson.TH
8import Data.Aeson.TH
10−import qualified Data.ByteString.Lazy as LBS
9+import qualified Data.ByteString.Lazy as LBS
11−import Data.Char (toLower)
10+import Data.Char (toLower)
12−import qualified Data.HashMap.Strict as HM
11+import Data.List (stripPrefix, unlines)
13−import Data.List (isInfixOf, stripPrefix)
12+import qualified Data.Text as T
14−import Data.Maybe
13+import qualified System.Directory as SD
15−import Data.Monoid
14+import System.FilePath ((</>))
16−import Data.Text (Text)
17−import qualified Data.Text as T
18−import Data.Traversable
19−import qualified Data.Vector as V
20−import GHC.Generics
21−import qualified System.Directory as SD
22−import System.FilePath ((</>))
23import Test.Hspec
15import Test.Hspec
24
16
25−import qualified Data.JsonSchema.Draft4 as D4
26−
27-- Recursively return the contents of a directory
17-- Recursively return the contents of a directory
28-- (or return itself if given a file as an argument).
18-- (or return itself if given a file as an argument).
29--
19--
@@ -47,35 +37,14 @@ listFilesFullPath path = do
47 fs <- fmap (path </>) <$> SD.listDirectory path
37 fs <- fmap (path </>) <$> SD.listDirectory path
48 concat <$> traverse listFilesFullPath fs
38 concat <$> traverse listFilesFullPath fs
49
39
50−checkPointer :: Value -> D4.Failure -> Expectation
40+isHTTPTest :: FilePath -> Bool
51−checkPointer v failure =
52− case AP.resolve (D4._failureOffendingPointer failure) v of
53− Left e -> error ("Couldn't resolve pointer: " <> show e)
54− Right a -> assertContains a (D4._failureOffendingData failure)
55− where
56− -- Some validators, such as 'additionalItems', only return a subset
57− -- of the data incated by their '_failureOffendingPointer'.
58− -- See the comments on 'Data.Validator.Failure.Failure' for more info.
59− assertContains :: Value -> Value -> Expectation
60− assertContains x y
61− | x == y = pure ()
62− | otherwise =
63− case (x,y) of
64− (Array xs, Array ys) ->
65− V.toList ys `shouldSatisfy` (`isInfixOf` V.toList xs)
66− (Object xhm, Object yhm) ->
67− HM.toList yhm `shouldSatisfy` (`isInfixOf` HM.toList xhm)
68− _ -> expectationFailure
69− "Pointer resolution incorrect: result mismatch"
70−
71−isHTTPTest :: String -> Bool
72isHTTPTest file = (file == "definitions.json")
41isHTTPTest file = (file == "definitions.json")
73 || (file == "ref.json")
42 || (file == "ref.json")
74 || (file == "refRemote.json")
43 || (file == "refRemote.json")
75
44
76-- | We may never support the @"format"@ keywords, and
45-- | We may never support the @"format"@ keywords, and
77-- are currently failing the zeroTerminatedFloats test.
46-- are currently failing the zeroTerminatedFloats test.
78−skipOptional :: String -> Bool
47+skipOptional :: FilePath -> Bool
79skipOptional file = (file == "optional/format.json")
48skipOptional file = (file == "optional/format.json")
80 || (file == "optional/zeroTerminatedFloats.json")
49 || (file == "optional/zeroTerminatedFloats.json")
81
50
@@ -104,9 +73,9 @@ instance FromJSON SchemaTestCase where
104 { fieldLabelModifier = fmap toLower . drop 3 }
73 { fieldLabelModifier = fmap toLower . drop 3 }
105
74
106readSchemaTests
75readSchemaTests
107− :: String
76+ :: FilePath
108 -- ^ The path to a directory.
77 -- ^ The path to a directory.
109− -> (String -> Bool)
78+ -> (FilePath -> Bool)
110 -- ^ A function to decide what we're interested in within that directory.
79 -- ^ A function to decide what we're interested in within that directory.
111 -> IO [SchemaTest]
80 -> IO [SchemaTest]
112readSchemaTests dir filterFunc = do
81readSchemaTests dir filterFunc = do
@@ -114,7 +83,7 @@ readSchemaTests dir filterFunc = do
114 concat <$> traverse fileToCases files
83 concat <$> traverse fileToCases files
115 where
84 where
116 -- Each file contains an array of SchemaTests, not just one.
85 -- Each file contains an array of SchemaTests, not just one.
117− fileToCases :: String -> IO [SchemaTest]
86+ fileToCases :: FilePath -> IO [SchemaTest]
118 fileToCases name = do
87 fileToCases name = do
119 let fullPath = dir </> name
88 let fullPath = dir </> name
120 jsonBS <- LBS.readFile fullPath
89 jsonBS <- LBS.readFile fullPath
@@ -122,7 +91,7 @@ readSchemaTests dir filterFunc = do
122 Left e -> fail $ "couldn't parse file '" <> fullPath <> "': " <> e
91 Left e -> fail $ "couldn't parse file '" <> fullPath <> "': " <> e
123 Right schemaTests -> pure $ prependFileName name <$> schemaTests
92 Right schemaTests -> pure $ prependFileName name <$> schemaTests
124
93
125− prependFileName :: String -> SchemaTest -> SchemaTest
94+ prependFileName :: FilePath -> SchemaTest -> SchemaTest
126 prependFileName fileName s = s
95 prependFileName fileName s = s
127 { _stDescription = T.pack fileName <> ": " <> _stDescription s
96 { _stDescription = T.pack fileName <> ": " <> _stDescription s
128 }
97 }
@@ -138,15 +107,15 @@ toTest validate st =
138 where
107 where
139 schema :: schema
108 schema :: schema
140 schema = case fromJSON (_stSchema st) of
109 schema = case fromJSON (_stSchema st) of
141− Error e -> error ("Couldn't parse schema: " <> show e)
110+ Error e -> panic ("Couldn't parse schema: " <> show e)
142 Success a -> a
111 Success a -> a
143
112
144−assertResult :: SchemaTestCase -> [D4.Failure] -> Expectation
113+assertResult :: Show err => SchemaTestCase -> [err] -> Expectation
145assertResult sc failures
114assertResult sc failures
146 | _scValid sc = assertValid sc failures
115 | _scValid sc = assertValid sc failures
147 | otherwise = assertInvalid sc failures
116 | otherwise = assertInvalid sc failures
148
117
149−assertValid :: SchemaTestCase -> [D4.Failure] -> Expectation
118+assertValid :: Show err => SchemaTestCase -> [err] -> Expectation
150assertValid _ [] = pure ()
119assertValid _ [] = pure ()
151assertValid sc failures =
120assertValid sc failures =
152 expectationFailure $ unlines
121 expectationFailure $ unlines
@@ -156,7 +125,7 @@ assertValid sc failures =
156 , " Validation failures: " <> show failures
125 , " Validation failures: " <> show failures
157 ]
126 ]
158
127
159−assertInvalid :: SchemaTestCase -> [D4.Failure] -> Expectation
128+assertInvalid :: SchemaTestCase -> [err] -> Expectation
160assertInvalid sc [] =
129assertInvalid sc [] =
161 expectationFailure $ unlines
130 expectationFailure $ unlines
162 [ " Validated invalid data"
131 [ " Validated invalid data"