git.iko.soy/typeable/hjsonschema
on master help sign in
Merge pull request #40 from seagreen/develop

1.0.0.0.

commitf2f38f94c2d5900eb2e78136003c4dd14318694e authorIan Jeffries <ianspiral@gmail.com> dateMon 11 Jul 2016, 12:48 (10 years ago) parenteb201448beeabb
48 files changed+3023−1803
JSON-Schema-Test-Suite/.travis.yml+2−2 JSON-Schema-Test-Suite/README.md+75−18 JSON-Schema-Test-Suite/shell.nix+8 JSON-Schema-Test-Suite/tests/draft3/optional/format.json+5 JSON-Schema-Test-Suite/tests/draft3/pattern.json+11 JSON-Schema-Test-Suite/tests/draft3/ref.json+18−3 JSON-Schema-Test-Suite/tests/draft3/type.json+2−2 JSON-Schema-Test-Suite/tests/draft4/optional/format.json+5 JSON-Schema-Test-Suite/tests/draft4/pattern.json+11 JSON-Schema-Test-Suite/tests/draft4/ref.json+18−3 JSON-Schema-Test-Suite/tests/draft4/type.json+2−2 JSON-Schema-Test-Suite/tox.ini+8 README.md+7−3 changelog.txt+36 examples/AlternateSchema.hs+160 examples/CustomSchema.hs−71 examples/Full.hs+20−14 examples/PrettyShowFailure.hs−53 examples/Simple.hs+23−25 examples/json/start.json+3 hjsonschema.cabal+46−22 shell.nix+12 src/Data/JsonSchema/Draft4.hs+153−104 src/Data/JsonSchema/Draft4/Failure.hs+112−47 src/Data/JsonSchema/Draft4/Internal.hs−267 src/Data/JsonSchema/Draft4/Schema.hs+308−304 src/Data/JsonSchema/Draft4/Spec.hs+130 src/Data/JsonSchema/Fetch.hs+116−95 src/Data/JsonSchema/Types.hs+38 src/Data/Validator/Draft4.hs+549−6 src/Data/Validator/Draft4/Any.hs+151−110 src/Data/Validator/Draft4/Array.hs+110−94 src/Data/Validator/Draft4/Number.hs+48−34 src/Data/Validator/Draft4/Object.hs+81−71 src/Data/Validator/Draft4/Object/Properties.hs+139−115 src/Data/Validator/Draft4/String.hs+32−19 src/Data/Validator/Failure.hs+33−26 src/Data/Validator/Reference.hs+27−39 src/Data/Validator/Types.hs+27 src/Data/Validator/Utils.hs+52−41 test/Local.hs+94−26 test/Local/Failure.hs+40−27 test/Local/Filesystem.hs−56 test/Local/Reference.hs+35−35 test/Local/Validation.hs+82 test/Remote.hs+47−9 test/Shared.hs+130−60 test/supplement/recurse.json+17
JSON-Schema-Test-Suite/.travis.yml+2−2
@@ -1,4 +1,4 @@
11language: python
22python: "2.7"
3−install: pip install jsonschema
4−script: bin/jsonschema_suite check
3+install: pip install tox
4+script: tox
JSON-Schema-Test-Suite/README.md+75−18
@@ -60,23 +60,80 @@ Who Uses the Test Suite
6060
6161This suite is being used by:
6262
63− * [jsck (a fast JSON validator in CoffeeScript)](https://github.com/pandastrike/jsck)
64− * [json-schema-validator (Java)](https://github.com/fge/json-schema-validator)
65− * [jsonschema (python)](https://github.com/Julian/jsonschema)
66− * [aeson-schema (haskell)](https://github.com/timjb/aeson-schema)
67− * [direct-schema (javascript)](https://github.com/IreneKnapp/direct-schema)
68− * [jsonschema (javascript)](https://github.com/tdegrunt/jsonschema)
69− * [JaySchema (javascript)](https://github.com/natesilva/jayschema)
70− * [z-schema (javascript)](https://github.com/zaggino/z-schema)
71− * [jassi (javascript)](https://github.com/iclanzan/jassi)
72− * [json-schema-valid (javascript)](https://github.com/ericgj/json-schema-valid)
73− * [jesse (Erlang)](https://github.com/klarna/jesse)
74− * [json-schema (PHP)](https://github.com/justinrainbow/json-schema)
75− * [gojsonschema (Go)](https://github.com/sigu-399/gojsonschema)
76− * [json_schema (Dart)](https://github.com/patefacio/json_schema)
77− * [tv4 (JavaScript)](https://github.com/geraintluff/tv4)
78− * [Jsonary (JavaScript)](https://github.com/jsonary-js/jsonary)
79− * [json-schema (Ruby)](https://github.com/hoxworth/json-schema)
63+### Coffeescript ###
64+
65+* [jsck](https://github.com/pandastrike/jsck)
66+
67+### Dart ###
68+
69+* [json_schema](https://github.com/patefacio/json_schema)
70+
71+### Erlang ###
72+
73+* [jesse](https://github.com/klarna/jesse)
74+
75+### Go ###
76+
77+* [gojsonschema](https://github.com/sigu-399/gojsonschema)
78+* [validate-json](https://github.com/cesanta/validate-json)
79+
80+### Haskell ###
81+
82+* [aeson-schema](https://github.com/timjb/aeson-schema)
83+* [hjsonschema](https://github.com/seagreen/hjsonschema)
84+
85+### Java ###
86+
87+* [json-schema-validator](https://github.com/fge/json-schema-validator)
88+
89+### JavaScript ###
90+
91+* [json-schema-benchmark](https://github.com/Muscula/json-schema-benchmark)
92+* [direct-schema](https://github.com/IreneKnapp/direct-schema)
93+* [is-my-json-valid](https://github.com/mafintosh/is-my-json-valid)
94+* [jassi](https://github.com/iclanzan/jassi)
95+* [JaySchema](https://github.com/natesilva/jayschema)
96+* [json-schema-valid](https://github.com/ericgj/json-schema-valid)
97+* [Jsonary](https://github.com/jsonary-js/jsonary)
98+* [jsonschema](https://github.com/tdegrunt/jsonschema)
99+* [request-validator](https://github.com/bugventure/request-validator)
100+* [skeemas](https://github.com/Prestaul/skeemas)
101+* [tv4](https://github.com/geraintluff/tv4)
102+* [z-schema](https://github.com/zaggino/z-schema)
103+* [jsen](https://github.com/bugventure/jsen)
104+* [ajv](https://github.com/epoberezkin/ajv)
105+
106+### Node.js ###
107+
108+The JSON Schema Test Suite is also available as an
109+[npm](https://www.npmjs.com/package/json-schema-test-suite) package.
110+Node-specific support is maintained on the [node branch](https://github.com/json-schema/JSON-Schema-Test-Suite/tree/node).
111+See [NODE-README.md](https://github.com/json-schema/JSON-Schema-Test-Suite/blob/node/NODE-README.md)
112+for more information.
113+
114+### .NET ###
115+
116+* [Newtonsoft.Json.Schema](https://github.com/JamesNK/Newtonsoft.Json.Schema)
117+
118+### PHP ###
119+
120+* [json-schema](https://github.com/justinrainbow/json-schema)
121+
122+### Python ###
123+
124+* [jsonschema](https://github.com/Julian/jsonschema)
125+
126+### Ruby ###
127+
128+* [json-schema](https://github.com/hoxworth/json-schema)
129+
130+### Rust ###
131+
132+* [valico](https://github.com/rustless/valico)
133+
134+### Swift ###
135+
136+* [JSONSchema](https://github.com/kylef/JSONSchema.swift)
80137
81138If you use it as well, please fork and send a pull request adding yourself to
82139the list :).
@@ -87,5 +144,5 @@ Contributing
87144If you see something missing or incorrect, a pull request is most welcome!
88145
89146There are some sanity checks in place for testing the test suite. You can run
90−them with `bin/jsonschema_suite check`. They will be run automatically by
147+them with `bin/jsonschema_suite check` or `tox`. They will be run automatically by
91148[Travis CI](https://travis-ci.org/) as well.
JSON-Schema-Test-Suite/shell.nixnew file+8
@@ -0,0 +1,8 @@
1+with import <nixpkgs> {};
2+
3+stdenv.mkDerivation {
4+ name = "none";
5+ buildInputs = [
6+ pythonPackages.jsonschema
7+ ];
8+}
JSON-Schema-Test-Suite/tests/draft3/optional/format.json+5
@@ -77,6 +77,11 @@
7777 "data": "http://foo.bar/?baz=qux#quux",
7878 "valid": true
7979 },
80+ {
81+ "description": "a valid protocol-relative URI",
82+ "data": "//foo.bar/?baz=qux#quux",
83+ "valid": true
84+ },
8085 {
8186 "description": "an invalid URI",
8287 "data": "\\\\WINDOWS\\fileshare",
JSON-Schema-Test-Suite/tests/draft3/pattern.json+11
@@ -19,5 +19,16 @@
1919 "valid": true
2020 }
2121 ]
22+ },
23+ {
24+ "description": "pattern is not anchored",
25+ "schema": {"pattern": "a+"},
26+ "tests": [
27+ {
28+ "description": "matches a substring",
29+ "data": "xxaayy",
30+ "valid": true
31+ }
32+ ]
2233 }
2334]
JSON-Schema-Test-Suite/tests/draft3/ref.json+18−3
@@ -86,19 +86,34 @@
8686 },
8787 "tests": [
8888 {
89− "description": "slash",
89+ "description": "slash invalid",
9090 "data": {"slash": "aoeu"},
9191 "valid": false
9292 },
9393 {
94− "description": "tilda",
94+ "description": "tilda invalid",
9595 "data": {"tilda": "aoeu"},
9696 "valid": false
9797 },
9898 {
99− "description": "percent",
99+ "description": "percent invalid",
100100 "data": {"percent": "aoeu"},
101101 "valid": false
102+ },
103+ {
104+ "description": "slash valid",
105+ "data": {"slash": 123},
106+ "valid": true
107+ },
108+ {
109+ "description": "tilda valid",
110+ "data": {"tilda": 123},
111+ "valid": true
112+ },
113+ {
114+ "description": "percent valid",
115+ "data": {"percent": 123},
116+ "valid": true
102117 }
103118 ]
104119 },
JSON-Schema-Test-Suite/tests/draft3/type.json+2−2
@@ -188,7 +188,7 @@
188188 "valid": false
189189 },
190190 {
191− "description": "an array is not an array",
191+ "description": "an array is an array",
192192 "data": [],
193193 "valid": true
194194 },
@@ -234,7 +234,7 @@
234234 "valid": false
235235 },
236236 {
237− "description": "a boolean is not a boolean",
237+ "description": "a boolean is a boolean",
238238 "data": true,
239239 "valid": true
240240 },
JSON-Schema-Test-Suite/tests/draft4/optional/format.json+5
@@ -29,6 +29,11 @@
2929 "data": "http://foo.bar/?baz=qux#quux",
3030 "valid": true
3131 },
32+ {
33+ "description": "a valid protocol-relative URI",
34+ "data": "//foo.bar/?baz=qux#quux",
35+ "valid": true
36+ },
3237 {
3338 "description": "an invalid URI",
3439 "data": "\\\\WINDOWS\\fileshare",
JSON-Schema-Test-Suite/tests/draft4/pattern.json+11
@@ -19,5 +19,16 @@
1919 "valid": true
2020 }
2121 ]
22+ },
23+ {
24+ "description": "pattern is not anchored",
25+ "schema": {"pattern": "a+"},
26+ "tests": [
27+ {
28+ "description": "matches a substring",
29+ "data": "xxaayy",
30+ "valid": true
31+ }
32+ ]
2233 }
2334]
JSON-Schema-Test-Suite/tests/draft4/ref.json+18−3
@@ -86,19 +86,34 @@
8686 },
8787 "tests": [
8888 {
89− "description": "slash",
89+ "description": "slash invalid",
9090 "data": {"slash": "aoeu"},
9191 "valid": false
9292 },
9393 {
94− "description": "tilda",
94+ "description": "tilda invalid",
9595 "data": {"tilda": "aoeu"},
9696 "valid": false
9797 },
9898 {
99− "description": "percent",
99+ "description": "percent invalid",
100100 "data": {"percent": "aoeu"},
101101 "valid": false
102+ },
103+ {
104+ "description": "slash valid",
105+ "data": {"slash": 123},
106+ "valid": true
107+ },
108+ {
109+ "description": "tilda valid",
110+ "data": {"tilda": 123},
111+ "valid": true
112+ },
113+ {
114+ "description": "percent valid",
115+ "data": {"percent": 123},
116+ "valid": true
102117 }
103118 ]
104119 },
JSON-Schema-Test-Suite/tests/draft4/type.json+2−2
@@ -188,7 +188,7 @@
188188 "valid": false
189189 },
190190 {
191− "description": "an array is not an array",
191+ "description": "an array is an array",
192192 "data": [],
193193 "valid": true
194194 },
@@ -234,7 +234,7 @@
234234 "valid": false
235235 },
236236 {
237− "description": "a boolean is not a boolean",
237+ "description": "a boolean is a boolean",
238238 "data": true,
239239 "valid": true
240240 },
JSON-Schema-Test-Suite/tox.ininew file+8
@@ -0,0 +1,8 @@
1+[tox]
2+minversion = 1.6
3+envlist = py27
4+skipsdist = True
5+
6+[testenv]
7+deps = jsonschema
8+commands = {envpython} bin/jsonschema_suite check
README.md+7−3
@@ -4,7 +4,9 @@ A Haskell implementation of the current [JSON Schema](http://json-schema.org/) s
44
55[Hackage](https://hackage.haskell.org/package/hjsonschema) / [GitHub](https://github.com/seagreen/hjsonschema) / [Travis CI](https://travis-ci.org/seagreen/hjsonschema)
66
7−NOTE: You CANNOT use untrusted JSON data to make schemas. Schemas with circular references can cause infinite loops. See the issue list for more info.
7+Requires [pcre](http://www.pcre.org/) (`pkgs.pcre` in Nixpkgs).
8+
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.
810
911# Example
1012
@@ -34,7 +36,7 @@ Run remote tests (makes GETs to json-schema.org, also temporarily starts an HTTP
3436
3537## Good Parts
3638
37−+ Passes all the tests in the [language agnostic test suite](https://github.com/json-schema/JSON-Schema-Test-Suite).
39++ Passes all the required tests in the [language agnostic test suite](https://github.com/json-schema/JSON-Schema-Test-Suite).
3840
3941+ Very modular, which should make it easy to support future versions of the specification.
4042
@@ -42,11 +44,13 @@ Run remote tests (makes GETs to json-schema.org, also temporarily starts an HTTP
4244
4345+ Uses the [pcre-heavy](https://hackage.haskell.org/package/pcre-heavy) regular expression library for the "pattern" validator. It should use a library based on the ECMA 262 regex dialect, which the [spec](http://json-schema.org/latest/json-schema-validation.html#anchor33) requires.
4446
47++ Currently doesn't support the optional `"format"` validators.
48+
4549## Notes
4650
4751+ `JSON-Schema-Test-Suite` is vendored from commit # aabcb3427745ade7a0b4d49ff016ad7eda8b898b [here](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
4852
49−+ `src/draft4.json` is from commit # cc8ec81ce0abe2385ebd6c2a6f2d6deb646f874a [here](https://github.com/json-schema/json-schema).
53++ `src/draft4.json` is from commit # f3d5aeb5ffbe9d9a5a0ceb761dc47c7c4c2efa68 [here](https://github.com/json-schema/json-schema).
5054
5155## Credits
5256
changelog.txt+36
@@ -1,3 +1,39 @@
1+# 1.0.0.0
2+
3+## Bug fixes:
4+
5++ Fix JSON Pointer bug. Pointers were being built in reverse order and so were
6+totally invalid.
7++ Use `.:!` instead of `.:?` to parse the draft 4 schema. The only way to omit
8+optional fields in JSON Schema Draft 4 is to omit them entirely, `"null"`
9+can't be used for this.
10+
11+## API Changes:
12+
13++ Add referenced schema loop detection.
14++ Add a new `referencesValidity` function.
15++ `checkSchema` now checks referenced schema's validity in addition to the
16+starting schema's validity. This change bubbles up to the one-step validation
17+functions as well.
18++ Switch most of the fetching code to use `URISchemaMap` instead of
19+`ReferencedSchemas`. It didn't need to know about the more complicated data
20+type.
21++ Rething failure related names. Change `Invalid` to `Failure`, add a new
22+`Invalid` type alias which is only used for final results.
23++ Failures now include the failing part of the data as well as a JSON Pointer to
24+it, so you don't have to worry about resolving the pointer.
25+
26+## Fundamental Changes:
27+
28++ Make `Fail` (previously `Failure`) an instance of `Functor`.
29++ Add a `Validator` data type which is an instance of `Profunctor`.
30++ Add a `Spec` data type for collections of `Validators`.
31+
32+## General:
33+
34++ Switch from 2 to 4 space indentation.
35++ Update the vendored JSON Schema Test Suite.
36+
137# 0.10.0.3
238
339+ Bump http-client.
examples/AlternateSchema.hsnew file+160
@@ -0,0 +1,160 @@
1+-- | An implementation of JSON Schema Draft 4 based on 'HashMap Text Value'
2+-- instead of a custom record type like 'Data.JsonSchema.Draft4'.
3+--
4+-- If you're writing code for a new schema specification you probably want
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
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+
14+module AlternateSchema where
15+
16+import Data.Aeson (FromJSON(..), Value(..),
17+ decode)
18+import qualified Data.Aeson as AE
19+import qualified Data.ByteString.Lazy as LBS
20+import qualified Data.HashMap.Strict as HM
21+import Data.Maybe (fromMaybe)
22+import Data.Monoid
23+import Data.Profunctor (Profunctor (..))
24+import Data.Text (Text)
25+
26+import Data.JsonSchema.Draft4 (schemaForSchemasBytes)
27+import Data.JsonSchema.Draft4.Failure
28+import Data.JsonSchema.Fetch (ReferencedSchemas(..),
29+ SchemaWithURI(..))
30+import qualified Data.JsonSchema.Fetch as FE
31+import Data.JsonSchema.Types (Schema(..), Spec(..))
32+import qualified Data.JsonSchema.Types as JT
33+import qualified Data.Validator.Draft4 as D4
34+import qualified Data.Validator.Draft4.Any as AN
35+import Data.Validator.Reference (updateResolutionScope)
36+
37+--------------------------------------------------
38+-- * Basic fetching tools
39+--------------------------------------------------
40+
41+referencesViaHTTP
42+ :: SchemaWithURI Schema
43+ -> IO (Either FE.HTTPFailure (FE.URISchemaMap Schema))
44+referencesViaHTTP = FE.referencesViaHTTP' draft4FetchInfo
45+
46+draft4FetchInfo :: FE.FetchInfo Schema
47+draft4FetchInfo = FE.FetchInfo embedded (get "id") (get "$ref")
48+ where
49+ get :: Text -> Schema -> Maybe Text
50+ get k (Schema s) = case HM.lookup k s of
51+ Just (String t) -> Just t
52+ _ -> Nothing
53+
54+embedded :: Schema -> ([Schema], [Schema])
55+embedded s = JT.embedded (d4Spec (ReferencedSchemas s mempty) mempty Nothing) s
56+
57+--------------------------------------------------
58+-- * Main API
59+--------------------------------------------------
60+
61+validate
62+ :: ReferencedSchemas Schema
63+ -> Maybe Text
64+ -> Schema
65+ -> Value
66+ -> [Failure]
67+validate rs = continueValidating rs (AN.VisitedSchemas [(Nothing, Nothing)])
68+
69+-- A schema for schemas themselves, using @src/draft4.json@ which is loaded
70+-- at compile time.
71+schemaForSchemas :: Schema
72+schemaForSchemas =
73+ fromMaybe (error "Schema decode failed (this should never happen)")
74+ . decode
75+ . LBS.fromStrict
76+ $ schemaForSchemasBytes
77+
78+checkSchema :: Schema -> [Failure]
79+checkSchema = validate referenced Nothing schemaForSchemas . Object . _unSchema
80+ where
81+ referenced :: ReferencedSchemas Schema
82+ referenced = ReferencedSchemas
83+ schemaForSchemas
84+ (HM.singleton "http://json-schema.org/draft-04/schema"
85+ schemaForSchemas)
86+
87+--------------------------------------------------
88+-- * Spec
89+--------------------------------------------------
90+
91+continueValidating
92+ :: ReferencedSchemas Schema
93+ -> AN.VisitedSchemas
94+ -> Maybe Text
95+ -> Schema
96+ -> Value
97+ -> [Failure]
98+continueValidating referenced visited mURI sc =
99+ JT.validate (d4Spec referenced visited newScope) sc
100+ where
101+ schemaId :: Maybe Text
102+ schemaId = case HM.lookup "id" (_unSchema sc) of
103+ Just (String t) -> Just t
104+ _ -> Nothing
105+
106+ newScope :: Maybe Text
107+ newScope = updateResolutionScope mURI schemaId
108+
109+d4Spec
110+ :: ReferencedSchemas Schema
111+ -> AN.VisitedSchemas
112+ -> Maybe Text
113+ -> Spec Schema ValidatorChain
114+d4Spec referenced visited scope =
115+ Spec
116+ [ dimap f (const MultipleOf) D4.multipleOf
117+ , dimap f maxE D4.maximumVal
118+ , dimap f minE D4.minimumVal
119+
120+ , dimap f (const MaxLength) D4.maxLength
121+ , dimap f (const MinLength) D4.minLength
122+ , dimap f (const PatternValidator) D4.patternVal
123+
124+ , dimap f (const MaxItems) D4.maxItems
125+ , dimap f (const MinItems) D4.minItems
126+ , dimap f (const UniqueItems) D4.uniqueItems
127+ , dimap f itemsE (D4.items descend)
128+
129+ , dimap f (const MaxProperties) D4.maxProperties
130+ , dimap f (const MinProperties) D4.minProperties
131+ , dimap f (const Required) D4.required
132+ , dimap f depsE (D4.dependencies descend)
133+ , dimap f propE (D4.properties descend)
134+ , dimap f patPropE (D4.patternProperties descend)
135+ , dimap f addPropE (D4.additionalProperties descend)
136+
137+ , dimap f refE (D4.ref visited scope (FE.getReference referenced) refVal)
138+ , dimap f (const Enum) D4.enumVal
139+ , dimap f (const TypeValidator) D4.typeVal
140+ , dimap f AllOf (D4.allOf lateral)
141+ , dimap f (const AnyOf) (D4.anyOf lateral)
142+ , dimap f (const OneOf) (D4.oneOf lateral)
143+ , dimap f (const NotValidator) (D4.notVal lateral)
144+ ]
145+ where
146+ f :: FromJSON a => Schema -> Maybe a
147+ f (Schema a) = case AE.fromJSON (Object a) of
148+ AE.Error _ -> Nothing
149+ AE.Success b -> Just b
150+
151+ -- 'Maybe Text' is the URI the refernced schema is fetch from,
152+ -- this probably needs a 'newtype' wrapper.
153+ refVal :: AN.VisitedSchemas -> Maybe Text -> Schema -> Value -> [Failure]
154+ refVal = continueValidating referenced
155+
156+ descend :: Schema -> Value -> [Failure]
157+ descend = continueValidating referenced mempty scope
158+
159+ lateral :: Schema -> Value -> [Failure]
160+ lateral = continueValidating referenced visited scope
examples/CustomSchema.hsdeleted−71
@@ -1,71 +0,0 @@
1−-- | A custom schema specification using one validator from
2−-- 'Data.Validator.Draft4' and one original validator.
3−--
4−-- This is a simple example because it doesn't allow references (so it
5−-- doesn't need to define an 'embed' function @Schema -> [Schema]@ for use
6−-- with 'fetchReferencedSchemas'.
7−--
8−-- For a full example see 'Data.JsonSchema.Draft4' and its submodules.
9−-- Code use between schema specifications will likely be OK but not great.
10−-- All the 'Data.Validator' code as well as the 'Data.JsonSchema.Fetch' code
11−-- is reusable, but there's a lot of boilerplate to tie it together.
12−
13−module CustomSchema where
14−
15−import Control.Applicative
16−import Data.Aeson
17−import Data.Maybe (maybeToList)
18−import Data.Monoid
19−import Data.Text (Text)
20−import qualified Data.Text as T
21−
22−import qualified Data.Validator.Draft4 as VA
23−import qualified Data.Validator.Failure as FR
24−
25−-- | Our custom validator.
26−oddLength :: Bool -> Text -> Maybe (FR.Failure () )
27−oddLength b t
28− | b == odd (T.length t) = Nothing
29− | otherwise = Just (FR.Invalid () (Bool b) mempty)
30−
31−data CustomError
32− = MaxLength
33− | OddLength
34−
35−-- If we were really using the schema we would also need ToJSON and FromJSON
36−-- instances.
37−data Schema = Schema
38− { _schemaMaxLength :: Maybe Int
39− , _schemaOddLength :: Maybe Bool
40− }
41−
42−-- | If our 'Schema's themselves could be invalid we might want to write
43−-- something like 'Data.JsonSchema.Draft4.schemaValidityy' for them.
44−validate :: Schema -> Value -> [FR.Failure CustomError]
45−validate s (String x) = concat
46− [ f _schemaMaxLength (FR.setFailure MaxLength) (fmap maybeToList . VA.maxLength)
47− , f _schemaOddLength (FR.setFailure OddLength) (fmap maybeToList . oddLength)
48− ]
49− where
50− -- This pattern is overkill here, but is helpful if you have lots of
51− -- validators (e.g. the Draft 4 schema has 27).
52− f :: (Schema -> Maybe val)
53− -> (err -> FR.Failure CustomError)
54− -> (val -> Text -> [err])
55− -> [FR.Failure CustomError]
56− f field modifyError runVal =
57− maybe mempty (\val -> modifyError <$> runVal val x) (field s)
58−validate _ _ = mempty -- Our schema passes everything that isn't a string.
59−
60−example :: IO ()
61−example =
62− case validate schema badData of
63− [] -> error "We validated bad data."
64− [FR.Invalid OddLength _ _] -> return () -- Success.
65− _ -> error "We got a different failure than expected."
66− where
67− schema :: Schema
68− schema = Schema (Just 100) (Just True)
69−
70− badData :: Value
71− badData = String "even"
examples/Full.hs+20−14
@@ -1,9 +1,15 @@
1−-- | Step by step validation using 'D4.referencesViaFilesystem' and
2−-- 'D4.checkSchema'. This means the actual validation invovles no IO.
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.
39
410module Full where
511
6−import Data.Aeson
12+import Data.Aeson (Value (..), toJSON)
713
814import qualified Data.JsonSchema.Draft4 as D4
915
@@ -12,23 +18,23 @@ schema = D4.emptySchema { D4._schemaRef = Just "./unique.json" }
1218
1319schemaContext :: D4.SchemaWithURI D4.Schema
1420schemaContext = D4.SchemaWithURI
15− { D4._swSchema = schema
16− , D4._swURI = Just "./examples/json/imaginary.json"
17− }
21+ { D4._swSchema = schema
22+ , D4._swURI = Just "./examples/json/start.json"
23+ }
1824
1925badData :: Value
2026badData = toJSON (["foo", "foo"] :: [String])
2127
2228example :: IO ()
2329example = do
24− res <- D4.referencesViaFilesystem schemaContext
25− case res of
26− Left _ -> error "Couldn't fetch referenced schemas."
27− Right references -> do
28− let validate = case D4.checkSchema references schemaContext of
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
2935 Left _ -> error "Not a valid schema."
3036 Right f -> f
31− case validate badData of
37+ case validate badData of
3238 [] -> error "We validated bad data."
33− [D4.Invalid (D4.Ref D4.UniqueItems) _ _] -> return () -- Success.
34− e -> error "We got a different failure than expected."
39+ [D4.Failure (D4.Ref D4.UniqueItems) _ _ _] -> return () -- Success
40+ _ -> error "We got a different failure than expected."
examples/PrettyShowFailure.hsdeleted−53
@@ -1,53 +0,0 @@
1−-- | 'D4.Invalid's contain a JSON Pointer to the subset of the data that
2−-- caused validation to fail, but they don't contain that data itself.
3−--
4−-- If you want to display the invalid subset of the data here's how you
5−-- resolve the JSON Pointer (which has the field name
6−-- 'D4._invalidOffendingData') against the original data.
7−--
8−-- NOTE: You have to have hjsonpointer in your build-depends.
9−
10−module PrettyShowFailure where
11−
12−import Data.Aeson
13−import qualified Data.Aeson.Pointer as AP
14−import Data.Monoid
15−
16−import qualified Data.JsonSchema.Draft4 as D4
17−
18−badData :: Value
19−badData = toJSON [1, 2 :: Int]
20−
21−failure :: D4.Invalid
22−failure = D4.Invalid
23− { D4._invalidValidatorsCalled = D4.Items D4.MultipleOf
24− , D4._invalidFinalValidator = Number 2
25− , D4._invalidOffendingData = AP.Pointer [AP.Token "0"]
26− }
27−
28−example :: IO ()
29−example =
30− case AP.resolve (D4._invalidOffendingData failure) badData of
31− Left _ -> error "Couldn't resolve pointer."
32− Right _ -> return () -- Success. We could feed the 'Right' value into
33− -- the otherwise unused 'msg' if we wanted to
34− -- display it.
35−
36−msg :: Value -> String
37−msg subsetOfData = unlines
38− [ "Invalid data. Here's the sequence of validators that caught it:"
39− , ""
40− , " " <> show (D4._invalidValidatorsCalled failure)
41− , ""
42− , "Here's the contents of the final validator in that sequence:"
43− , ""
44− , " " <> show (D4._invalidFinalValidator failure)
45− , ""
46− , "Here's a JSON Pointer to the invalid part of the data:"
47− , ""
48− , " " <> show (D4._invalidOffendingData failure)
49− , ""
50− , "Here's the invalid part of the data:"
51− , ""
52− , " " <> show subsetOfData
53− ]
examples/Simple.hs+23−25
@@ -1,39 +1,37 @@
1−-- | Fetch any referenced schemas, check that our original schema is itself
2−-- valid, then validate our data.
1+-- | Demonstrate 'D4.fetchFilesystemAndValidate'.
32--
43-- To fetch schemas using HTTP instead of from the filesystem use
54-- 'D4.fetchHTTPAndValidate'.
65
76module Simple where
87
9−import Data.Aeson
10−import qualified Data.List.NonEmpty as NE
8+import Data.Aeson (Value(..), decode, toJSON)
9+import qualified Data.ByteString.Lazy as LBS
10+import qualified Data.List.NonEmpty as NE
11+import Data.Maybe (fromMaybe)
1112
1213import qualified Data.JsonSchema.Draft4 as D4
1314
14−schema :: D4.Schema
15−schema = D4.emptySchema { D4._schemaRef = Just "./unique.json" }
16−
17−schemaContext :: D4.SchemaWithURI D4.Schema
18−schemaContext = D4.SchemaWithURI
19− { D4._swSchema = schema
20− , D4._swURI = Just "./examples/json/imaginary.json"
21− -- ^ For this example we're pretending we found 'schema' at this location.
22− -- Its relative links will be resolved from here.
23− }
24−
2515badData :: Value
2616badData = toJSON (["foo", "foo"] :: [String])
2717
2818example :: IO ()
2919example = do
30− res <- D4.fetchFilesystemAndValidate schemaContext badData
31− case res of
32− Right () -> error "We validated bad data."
33− Left (D4.FVRead _) -> error ("Error fetching a referenced schema"
34− ++ " (either during IO or parsing).")
35− Left (D4.FVSchema _) -> error "Our 'schema' itself was invalid."
36− Left (D4.FVData failures) ->
37− case NE.toList failures of
38− [D4.Invalid (D4.Ref D4.UniqueItems) _ _] -> return () -- Success.
39− _ -> error "Got more invalidation reasons than we expected."
20+
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+ -- is valid, and validate the data.
28+ res <- D4.fetchFilesystemAndValidate schemaWithURI badData
29+ case res of
30+ Right () -> error "We validated bad data."
31+ Left (D4.FVRead _) -> error ("Error fetching a referenced schema"
32+ ++ " (either during IO or parsing).")
33+ Left (D4.FVSchema _) -> error "Our 'schema' itself was invalid."
34+ Left (D4.FVData failures) ->
35+ case NE.toList failures of
36+ [D4.Failure (D4.Ref D4.UniqueItems) _ _ _] -> return () -- Success.
37+ _ -> error "Got more invalidation reasons than we expected."
examples/json/start.jsonnew file+3
@@ -0,0 +1,3 @@
1+{
2+ "$ref": "./unique.json"
3+}
hjsonschema.cabal+46−22
@@ -1,5 +1,5 @@
11name: hjsonschema
2−version: 0.10.0.3
2+version: 1.0.0.0
33synopsis: JSON Schema library
44homepage: https://github.com/seagreen/hjsonschema
55license: MIT
@@ -14,33 +14,43 @@ extra-source-files: changelog.txt
1414 JSON-Schema-Test-Suite/remotes/*.json
1515 JSON-Schema-Test-Suite/remotes/folder/*.json
1616 JSON-Schema-Test-Suite/tests/draft4/*.json
17+ JSON-Schema-Test-Suite/tests/draft4/optional/*.json
1718 README.md
1819 src/draft4.json
1920 test/Local/*.json
21+ test/supplement/*.json
2022 examples/json/*.json
2123
2224library
2325 hs-source-dirs: src
2426 exposed-modules: Data.JsonSchema.Draft4
25− , Data.JsonSchema.Draft4.Internal
27+ , Data.JsonSchema.Draft4.Failure
28+ , Data.JsonSchema.Draft4.Schema
29+ , Data.JsonSchema.Draft4.Spec
2630 , Data.JsonSchema.Fetch
31+ , Data.JsonSchema.Types
2732 , Data.Validator.Draft4
28− , Data.Validator.Failure
29− , Data.Validator.Reference
30− , Data.Validator.Utils
31−
32− other-modules: Data.JsonSchema.Draft4.Failure
33− , Data.JsonSchema.Draft4.Schema
3433 , Data.Validator.Draft4.Any
3534 , Data.Validator.Draft4.Array
3635 , Data.Validator.Draft4.Number
3736 , Data.Validator.Draft4.Object
38− , Data.Validator.Draft4.Object.Properties
3937 , Data.Validator.Draft4.String
38+ , Data.Validator.Failure
39+ , Data.Validator.Reference
40+ , Data.Validator.Types
41+ , Data.Validator.Utils
42+
43+ other-modules: Data.Validator.Draft4.Object.Properties
4044 , Import
4145 default-language: Haskell2010
42− default-extensions: ScopedTypeVariables
46+ default-extensions: NoImplicitPrelude
4347 OverloadedStrings
48+ ScopedTypeVariables
49+ if impl(ghc >= 8)
50+ default-extensions: StrictData
51+ other-extensions: DeriveFunctor
52+ GeneralizedNewtypeDeriving
53+ TemplateHaskell
4454 ghc-options: -Wall
4555 build-depends: aeson >= 0.7 && < 0.12
4656 , base >= 4.7 && < 4.10
@@ -49,10 +59,12 @@ library
4959 , file-embed >= 0.0.8 && < 0.1
5060 , filepath >= 1.3 && < 1.5
5161 , hjsonpointer >= 0.3 && < 0.4
52− , http-client >= 0.4 && < 0.6
62+ -- 0.4.30 is for parseUrlThrow:
63+ , http-client >= 0.4.30 && < 0.6
5364 , http-types >= 0.8 && < 0.10
5465 , pcre-heavy >= 1.0 && < 1.1
55− , QuickCheck >= 2.8.1 && < 2.9
66+ , profunctors >= 5.0 && < 5.3
67+ , QuickCheck >= 2.8 && < 2.10
5668 , scientific >= 0.3 && < 0.4
5769 , semigroups >= 0.18 && < 0.19
5870 , unordered-containers >= 0.2 && < 0.3
@@ -65,49 +77,61 @@ test-suite local
6577 examples
6678 main-is: Local.hs
6779 other-modules: Local.Failure
68− , Local.Filesystem
6980 , Local.Reference
81+ , Local.Validation
7082 , Shared
7183 -- from ./examples:
72− , CustomSchema
84+ , AlternateSchema
7385 , Full
74− , PrettyShowFailure
7586 , Simple
7687 default-language: Haskell2010
77− ghc-options: -Wall -fno-warn-orphans
88+ ghc-options: -Wall
89+ -fno-warn-orphans
7890 default-extensions: OverloadedStrings
91+ ScopedTypeVariables
7992 build-depends: aeson
8093 , base
8194 , bytestring
8295 , filepath
8396 , hjsonpointer
8497 , hjsonschema
98+ , profunctors
8599 , semigroups
86100 , text
87101 , QuickCheck
88102 , unordered-containers
89103 , vector
90− , directory >= 1.2 && < 1.3
91− , HUnit >= 1.2 && < 1.4
92− , tasty >= 0.11 && < 0.12
93− , tasty-hunit >= 0.9 && < 0.10
94− , tasty-quickcheck >= 0.8 && < 0.9
104+ -- directory-1.2.5 required for `listDirectory`:
105+ , directory >= 1.2.5 && < 1.3
106+ , HUnit >= 1.2 && < 1.4
107+ , tasty >= 0.11 && < 0.12
108+ , tasty-hunit >= 0.9 && < 0.10
109+ , tasty-quickcheck >= 0.8 && < 0.9
95110
96111test-suite remote
97112 type: exitcode-stdio-1.0
98113 hs-source-dirs: test
114+ examples
99115 main-is: Remote.hs
100116 other-modules: Shared
117+ -- from ./examples:
118+ , AlternateSchema
101119 default-language: Haskell2010
102− ghc-options: -Wall -fno-warn-orphans
120+ ghc-options: -Wall
121+ -fno-warn-orphans
103122 default-extensions: OverloadedStrings
123+ ScopedTypeVariables
104124 build-depends: aeson
105125 , async
106126 , base
107127 , bytestring
108128 , filepath
129+ , hjsonpointer
109130 , hjsonschema
131+ , profunctors
132+ , semigroups
110133 , text
134+ , unordered-containers
111135 , vector
112136 , directory
113137 , HUnit
shell.nixnew file+12
@@ -0,0 +1,12 @@
1+with (import <nixpkgs> {});
2+
3+haskell.lib.buildStackProject {
4+ name = "none";
5+ ghc = haskell.compiler.ghc801;
6+ buildInputs = [
7+ zlib.dev
8+ zlib.out
9+ pkgconfig
10+ pcre
11+ ];
12+}
src/Data/JsonSchema/Draft4.hs+153−104
@@ -1,136 +1,185 @@
1−{-# LANGUAGE ScopedTypeVariables #-}
2−{-# LANGUAGE TemplateHaskell #-}
1+{-# LANGUAGE TemplateHaskell #-}
32
43module Data.JsonSchema.Draft4
5− ( Schema(..)
6− , emptySchema
7−
8− -- * One-step validation
9− , HTTPValidationFailure(..)
10− , fetchHTTPAndValidate
11− , FilesystemValidationFailure(..)
12− , fetchFilesystemAndValidate
13−
14− -- * Fetching tools
15− , SchemaWithURI(..)
16− , ReferencedSchemas(..)
17− , HTTPFailure(..)
18− , referencesViaHTTP
19− , FilesystemFailure(..)
20− , referencesViaFilesystem
21−
22− -- * Failure
23− , Invalid
24− , FR.Failure(..)
25− , ValidatorChain(..)
26−
27− -- * Other Draft 4 things exported just in case
28− , checkSchema
29− , schemaValidity
30− , IN.runValidate
31− , draft4Spec
32− ) where
33−
34−import Control.Applicative
35−import Control.Arrow (left)
36−import Data.Aeson
4+ ( -- * Draft 4 Schema
5+ SchemaWithURI(..)
6+ , Schema(..)
7+ , SC.emptySchema
8+
9+ -- * One-step validation (getting references over HTTP)
10+ , fetchHTTPAndValidate
11+ , HTTPValidationFailure(..)
12+ , FE.HTTPFailure(..)
13+ , InvalidSchema
14+
15+ -- * One-step validation (getting references from the filesystem)
16+ , fetchFilesystemAndValidate
17+ , FilesystemValidationFailure(..)
18+ , FE.FilesystemFailure(..)
19+
20+ -- * Validation failure
21+ , Invalid
22+ , Failure
23+ , FR.Fail(..)
24+ , ValidatorChain(..)
25+
26+ -- * Fetching tools
27+ , ReferencedSchemas(..)
28+ , referencesViaHTTP
29+ , referencesViaFilesystem
30+
31+ -- * Other Draft 4 things exported just in case
32+ , schemaForSchemas
33+ , schemaForSchemasBytes
34+ , schemaValidity
35+ , referencesValidity
36+ , checkSchema
37+ , draft4FetchInfo
38+ ) where
39+
40+import Import
41+import Prelude
42+
43+import Control.Arrow (first, left)
44+import qualified Data.ByteString as BS
3745import qualified Data.ByteString.Lazy as LBS
38−import Data.FileEmbed
39−import qualified Data.HashMap.Strict as H
40−import Data.List.NonEmpty (NonEmpty)
41−import qualified Data.List.NonEmpty as N
46+import Data.FileEmbed (embedFile,
47+ makeRelativeToProject)
48+import qualified Data.HashMap.Strict as HM
49+import qualified Data.List.NonEmpty as NE
4250import Data.Maybe (fromMaybe)
4351
44−import Data.JsonSchema.Draft4.Failure (Invalid, ValidatorChain(..))
45−import qualified Data.JsonSchema.Draft4.Internal as IN
46−import Data.JsonSchema.Draft4.Schema
47−import Data.JsonSchema.Fetch (FilesystemFailure(..),
48− HTTPFailure(..),
49− ReferencedSchemas(..),
52+import Data.JsonSchema.Draft4.Failure (Failure, Invalid,
53+ InvalidSchema,
54+ ValidatorChain(..))
55+import Data.JsonSchema.Draft4.Schema (Schema)
56+import qualified Data.JsonSchema.Draft4.Schema as SC
57+import qualified Data.JsonSchema.Draft4.Spec as Spec
58+import Data.JsonSchema.Fetch (ReferencedSchemas(..),
5059 SchemaWithURI(..))
5160import qualified Data.JsonSchema.Fetch as FE
5261import qualified Data.Validator.Failure as FR
5362
5463data HTTPValidationFailure
55− = HVRequest HTTPFailure
56− | HVSchema (NonEmpty Invalid)
57− | HVData (NonEmpty Invalid)
58− deriving Show
64+ = HVRequest FE.HTTPFailure
65+ | HVSchema InvalidSchema
66+ | HVData Invalid
67+ deriving Show
5968
69+-- | Fetch recursively referenced schemas over HTTP, check that both the
70+-- original and referenced schemas are valid, and then validate data.
6071fetchHTTPAndValidate
61− :: SchemaWithURI Schema
62− -> Value
63− -> IO (Either HTTPValidationFailure ())
72+ :: SchemaWithURI Schema
73+ -> Value
74+ -> IO (Either HTTPValidationFailure ())
6475fetchHTTPAndValidate sw v = do
65− res <- referencesViaHTTP sw
66− pure (g =<< f =<< left HVRequest res)
76+ res <- referencesViaHTTP sw
77+ pure (g =<< f =<< left HVRequest res)
6778 where
68− f :: ReferencedSchemas Schema
69− -> Either HTTPValidationFailure (Value -> [Invalid])
79+ f :: FE.URISchemaMap Schema
80+ -> Either HTTPValidationFailure (Value -> [Failure])
7081 f references = left HVSchema (checkSchema references sw)
7182
72− g :: (Value -> [Invalid]) -> Either HTTPValidationFailure ()
73− g validate = case N.nonEmpty (validate v) of
74− Nothing -> Right ()
75− Just failures -> Left (HVData failures)
83+ g :: (Value -> [Failure]) -> Either HTTPValidationFailure ()
84+ g validate = case NE.nonEmpty (validate v) of
85+ Nothing -> Right ()
86+ Just failures -> Left (HVData failures)
7687
7788data FilesystemValidationFailure
78− = FVRead FilesystemFailure
79− | FVSchema (NonEmpty Invalid)
80− | FVData (NonEmpty Invalid)
81− deriving Show
82−
89+ = FVRead FE.FilesystemFailure
90+ | FVSchema InvalidSchema
91+ | FVData Invalid
92+ deriving (Show, Eq)
93+
94+-- | Fetch recursively referenced schemas from the filesystem, check that
95+-- both the original and referenced schemas are valid, and then
96+-- validate data.
8397fetchFilesystemAndValidate
84− :: SchemaWithURI Schema
85− -> Value
86− -> IO (Either FilesystemValidationFailure ())
98+ :: SchemaWithURI Schema
99+ -> Value
100+ -> IO (Either FilesystemValidationFailure ())
87101fetchFilesystemAndValidate sw v = do
88− res <- referencesViaFilesystem sw
89− pure (g =<< f =<< left FVRead res)
102+ res <- referencesViaFilesystem sw
103+ pure (g =<< f =<< left FVRead res)
90104 where
91− f :: ReferencedSchemas Schema
92− -> Either FilesystemValidationFailure (Value -> [Invalid])
105+ f :: FE.URISchemaMap Schema
106+ -> Either FilesystemValidationFailure (Value -> [Failure])
93107 f references = left FVSchema (checkSchema references sw)
94108
95− g :: (Value -> [Invalid]) -> Either FilesystemValidationFailure ()
96− g validate = case N.nonEmpty (validate v) of
97− Nothing -> Right ()
98− Just failures -> Left (FVData failures)
99−
100−-- | Check the that a schema itself is valid.
101−--
102−-- Return a function to validate data.
103−checkSchema
104− :: ReferencedSchemas Schema
105− -> SchemaWithURI Schema
106− -> Either (NonEmpty Invalid) (Value -> [Invalid])
107−checkSchema referenced schemaWithURI =
108− case N.nonEmpty . schemaValidity . _swSchema $ schemaWithURI of
109− Nothing -> Right (IN.runValidate referenced schemaWithURI)
110− Just failures -> Left failures
109+ g :: (Value -> [Failure]) -> Either FilesystemValidationFailure ()
110+ g validate = case NE.nonEmpty (validate v) of
111+ Nothing -> Right ()
112+ Just invalid -> Left (FVData invalid)
111113
112−draft4Spec :: FE.Spec Schema
113−draft4Spec = FE.Spec IN.embedded _schemaId _schemaRef
114+-- | An instance of 'Data.JsonSchema.Fetch.FetchInfo' specialized for
115+-- JSON Schema Draft 4.
116+draft4FetchInfo :: FE.FetchInfo Schema
117+draft4FetchInfo = FE.FetchInfo Spec.embedded SC._schemaId SC._schemaRef
114118
119+-- | Fetch the schemas recursively referenced by a starting schema over HTTP.
115120referencesViaHTTP
116− :: SchemaWithURI Schema
117− -> IO (Either HTTPFailure (ReferencedSchemas Schema))
118−referencesViaHTTP = FE.referencesViaHTTP' draft4Spec
121+ :: SchemaWithURI Schema
122+ -> IO (Either FE.HTTPFailure (FE.URISchemaMap Schema))
123+referencesViaHTTP = FE.referencesViaHTTP' draft4FetchInfo
119124
125+-- | Fetch the schemas recursively referenced by a starting schema from
126+-- the filesystem.
120127referencesViaFilesystem
121− :: SchemaWithURI Schema
122− -> IO (Either FilesystemFailure (ReferencedSchemas Schema))
123−referencesViaFilesystem = FE.referencesViaFilesystem' draft4Spec
128+ :: SchemaWithURI Schema
129+ -> IO (Either FE.FilesystemFailure (FE.URISchemaMap Schema))
130+referencesViaFilesystem = FE.referencesViaFilesystem' draft4FetchInfo
124131
125−-- | Check that a schema itself is valid.
126−schemaValidity :: Schema -> [Invalid]
127−schemaValidity = IN.runValidate referenced (SchemaWithURI d4 Nothing) . toJSON
132+-- | A helper function.
133+--
134+-- Checks if a schema and a set of referenced schemas are valid.
135+--
136+-- Return a function to validate data.
137+checkSchema
138+ :: FE.URISchemaMap Schema
139+ -> SchemaWithURI Schema
140+ -> Either InvalidSchema (Value -> [Failure])
141+checkSchema sm sw =
142+ case NE.nonEmpty failures of
143+ Nothing -> Right (Spec.validate (ReferencedSchemas (_swSchema sw) sm) sw)
144+ Just fs -> Left fs
145+ where
146+ failures :: [(Maybe Text, Failure)]
147+ failures = ((\v -> (Nothing, v)) <$> schemaValidity (_swSchema sw))
148+ <> (first Just <$> referencesValidity sm)
149+
150+schemaForSchemas :: Schema
151+schemaForSchemas =
152+ fromMaybe (error "Schema decode failed (this should never happen)")
153+ . decode
154+ . LBS.fromStrict
155+ $ schemaForSchemasBytes
156+
157+schemaForSchemasBytes :: BS.ByteString
158+schemaForSchemasBytes =
159+ $(makeRelativeToProject "src/draft4.json" >>= embedFile)
160+
161+-- | Check that a schema itself is valid
162+-- (if so the returned list will be empty).
163+schemaValidity :: Schema -> [Failure]
164+schemaValidity =
165+ Spec.validate referenced (SchemaWithURI schemaForSchemas Nothing) . toJSON
128166 where
129− d4 :: Schema
130− d4 = fromMaybe (error "Schema decode failed (this should never happen)")
131− . decode . LBS.fromStrict $ $(embedFile "src/draft4.json")
132−
133167 referenced :: ReferencedSchemas Schema
134168 referenced = ReferencedSchemas
135− d4
136− (H.singleton "http://json-schema.org/draft-04/schema" d4)
169+ schemaForSchemas
170+ (HM.singleton "http://json-schema.org/draft-04/schema"
171+ schemaForSchemas)
172+
173+-- | Check that a set of referenced schemas are valid
174+-- (if so the returned list will be empty).
175+referencesValidity
176+ :: FE.URISchemaMap Schema
177+ -> [(Text, Failure)]
178+ -- ^ The first value in the tuple is the URI of a referenced schema.
179+referencesValidity = HM.foldlWithKey' f mempty
180+ where
181+ f :: [(Text, Failure)]
182+ -> Text
183+ -> Schema
184+ -> [(Text, Failure)]
185+ f acc k v = ((\a -> (k,a)) <$> schemaValidity v) <> acc
src/Data/JsonSchema/Draft4/Failure.hs+112−47
@@ -1,53 +1,118 @@
11
22module Data.JsonSchema.Draft4.Failure where
33
4−import qualified Data.Validator.Failure as FR
4+import Prelude
55
6−type Invalid = FR.Failure ValidatorChain
6+import Data.List.NonEmpty (NonEmpty)
7+import Data.Text (Text)
78
9+import qualified Data.Validator.Draft4.Any as AN
10+import qualified Data.Validator.Draft4.Array as AR
11+import qualified Data.Validator.Draft4.Number as NU
12+import qualified Data.Validator.Draft4.Object as OB
13+import qualified Data.Validator.Failure as FR
14+
15+-- | A description of why a schema (or one of its reference) is itself
16+-- invalid.
17+--
18+-- 'Nothing' indicates the starting schema. 'Just' indicates a referenced
19+-- schema -- the contents of the 'Just' is the schema's URI.
20+--
21+-- NOTE: 'HashMap (Maybe Text) Invalid' would be a nicer way of defining
22+-- this, but then we lose the guarantee that there's at least one key.
23+type InvalidSchema = NonEmpty (Maybe Text, Failure)
24+
25+type Invalid = NonEmpty Failure
26+
27+type Failure = FR.Fail ValidatorChain
28+
29+-- | Distinguish all the different possible causes of failure for
30+-- Draft 4 validation.
831data ValidatorChain
9− = MultipleOf
10− | Maximum
11− | ExclusiveMaximum
12− | Minimum
13− | ExclusiveMinimum
14−
15− | MaxLength
16− | MinLength
17− | PatternValidator
18−
19− | MaxItems
20− | MinItems
21− | UniqueItems
22− | Items ValidatorChain
23− | AdditionalItemsBool
24− | AdditionalItemsObject ValidatorChain
25−
26− | MaxProperties
27− | MinProperties
28− | Required
29− | SchemaDependency ValidatorChain
30− | PropertyDependency
31− | Properties ValidatorChain
32− | PatternProperties ValidatorChain
33− | AdditionalPropertiesBool
34− | AdditionalPropertiesObject ValidatorChain
35−
36− | RefResolution
37− -- ^ Indicates a reference that failed to resolve.
38− --
39− -- NOTE: The language agnostic test suite doesn't specify if this should
40− -- cause a validation error or should allow data to pass. We choose to
41− -- return a validation error.
42− --
43− -- Also note that ideally we would enforce in the type system that any
44− -- failing references be dealt with before valididation. Then this could
45− -- be removed entirely.
46− | Ref ValidatorChain
47− | Enum
48− | TypeValidator
49− | AllOf ValidatorChain
50− | AnyOf
51− | OneOf
52− | NotValidator
53− deriving (Eq, Show)
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
75+ | OneOf
76+ | NotValidator
77+ deriving (Eq, Show)
78+
79+maxE :: NU.MaximumInvalid -> ValidatorChain
80+maxE NU.Maximum = Maximum
81+maxE NU.ExclusiveMaximum = ExclusiveMaximum
82+
83+minE :: NU.MinimumInvalid -> ValidatorChain
84+minE NU.Minimum = Minimum
85+minE NU.ExclusiveMinimum = ExclusiveMinimum
86+
87+itemsE :: AR.ItemsInvalid ValidatorChain -> ValidatorChain
88+itemsE (AR.Items err) = Items err
89+itemsE AR.AdditionalItemsBoolInvalid = AdditionalItemsBool
90+itemsE (AR.AdditionalItemsObjectInvalid err) = AdditionalItemsObject err
91+
92+depsE :: OB.DependencyInvalid ValidatorChain -> ValidatorChain
93+depsE (OB.SchemaDependencyInvalid err) = SchemaDependency err
94+depsE OB.PropertyDependencyInvalid = PropertyDependency
95+
96+propE :: OB.PropertiesInvalid ValidatorChain -> ValidatorChain
97+propE (OB.PropertiesInvalid err) = Properties err
98+propE (OB.PropPatternInvalid err) = PatternProperties err
99+propE (OB.PropAdditionalInvalid a) =
100+ case a of
101+ OB.APBoolInvalid -> AdditionalPropertiesBool
102+ OB.APObjectInvalid err -> AdditionalPropertiesObject err
103+
104+patPropE :: OB.PatternPropertiesInvalid ValidatorChain -> ValidatorChain
105+patPropE (OB.PPInvalid err) = PatternProperties err
106+patPropE (OB.PPAdditionalPropertiesInvalid a) =
107+ case a of
108+ OB.APBoolInvalid -> AdditionalPropertiesBool
109+ OB.APObjectInvalid err -> AdditionalPropertiesObject err
110+
111+addPropE :: OB.AdditionalPropertiesInvalid ValidatorChain -> ValidatorChain
112+addPropE OB.APBoolInvalid = AdditionalPropertiesBool
113+addPropE (OB.APObjectInvalid err) = AdditionalItemsObject err
114+
115+refE :: AN.RefInvalid ValidatorChain -> ValidatorChain
116+refE AN.RefResolution = RefResolution
117+refE AN.RefLoop = RefLoop
118+refE (AN.RefInvalid err) = Ref err
src/Data/JsonSchema/Draft4/Internal.hsdeleted−267
@@ -1,267 +0,0 @@
1−
2−module Data.JsonSchema.Draft4.Internal where
3−
4−import Data.Aeson
5−import qualified Data.HashMap.Strict as H
6−import qualified Data.List.NonEmpty as N
7−import Data.Maybe (catMaybes, fromMaybe, isJust,
8− maybeToList)
9−import Data.Scientific
10−
11−import Data.JsonSchema.Draft4.Failure
12−import Data.JsonSchema.Draft4.Schema
13−import Data.JsonSchema.Fetch (ReferencedSchemas(..),
14− SchemaWithURI(..))
15−import qualified Data.Validator.Draft4.Any as AN
16−import qualified Data.Validator.Draft4.Array as AR
17−import qualified Data.Validator.Draft4.Number as NU
18−import qualified Data.Validator.Draft4.Object as OB
19−import qualified Data.Validator.Draft4.String as ST
20−import Data.Validator.Failure (modFailure, setFailure)
21−import qualified Data.Validator.Failure as FR
22−import Data.Validator.Reference (updateResolutionScope)
23−import Import
24−
25−-- For GHCs before 7.10:
26−import Prelude hiding (concat)
27−
28−--------------------------------------------------
29−-- * Embedded Schemas
30−--------------------------------------------------
31−
32−-- | Return a schema's immediate subschemas.
33−--
34−-- Pass this to 'fetchReferencedSchemas' so that function can find all the
35−-- subschemas in a document. This allows 'fetchReferencedSchemas' to process
36−-- only "$ref"s and "id"s that are actual schema keywords. For example,
37−-- within a "properties" validator object an "id" key doesn't actually change
38−-- any scope, but instead serves a validator-specific function.
39−embedded :: Schema -> [Schema]
40−embedded schema = concat
41− [ f _schemaItems
42− (\x -> case x of
43− AR.ItemsObject s -> pure s
44− AR.ItemsArray ss -> ss
45− )
46− , f _schemaAdditionalItems
47− (\x -> case x of
48− AR.AdditionalObject s -> pure s
49− _ -> mempty
50− )
51− , f _schemaDependencies (catMaybes . fmap checkDependency . H.elems)
52− , f _schemaProperties H.elems
53− , f _schemaPatternProperties H.elems
54− , f _schemaAdditionalProperties
55− (\x -> case x of
56− OB.AdditionalPropertiesObject s -> pure s
57− _ -> mempty
58− )
59− , f _schemaAllOf N.toList
60− , f _schemaAnyOf N.toList
61− , f _schemaOneOf N.toList
62− , f _schemaNot pure
63− , f _schemaDefinitions H.elems
64− ]
65− where
66− f :: (Schema -> Maybe a) -> (a -> [Schema]) -> [Schema]
67− f field nextLevelEmbedded = maybe mempty nextLevelEmbedded (field schema)
68−
69− checkDependency :: OB.Dependency Schema -> Maybe Schema
70− checkDependency (OB.PropertyDependency _) = Nothing
71− checkDependency (OB.SchemaDependency s) = Just s
72−
73−--------------------------------------------------
74−-- * Validation (Main internal functions)
75−--------------------------------------------------
76−
77−runValidate
78− :: ReferencedSchemas Schema
79− -> SchemaWithURI Schema
80− -> Value
81− -> [Invalid]
82−runValidate referenced sw x = concat
83− [ f _schemaEnum (setFailure Enum) (fmap maybeToList . AN.enumVal)
84− , f _schemaType (setFailure TypeValidator) (fmap maybeToList . AN.typeVal)
85− , f _schemaAllOf (modFailure AllOf) (AN.allOf recurse)
86− , f _schemaAnyOf (setFailure AnyOf) (fmap maybeToList . AN.anyOf recurse)
87− , f _schemaOneOf (setFailure OneOf) (fmap maybeToList . AN.oneOf recurse)
88− , f _schemaNot (setFailure NotValidator) (fmap maybeToList . AN.notVal recurse)
89− , refFailures
90− ] <> specificValidators
91− where
92− specificValidators :: [Invalid]
93− specificValidators =
94− case x of
95− Number y -> validateNumber (_swSchema sw) y
96− String y -> validateString (_swSchema sw) y
97− Array y -> validateArray referenced sw y
98− Object y -> validateObject referenced sw y
99− _ -> mempty
100−
101− f = runSingle (_swSchema sw) x
102−
103− recurse = descendNextLevel referenced sw
104−
105− -- Since the results of the 'AN.ref' validator are fairly complicated [1]
106− -- it's simpler not to use our 'f' helper function for it.
107− --
108− -- [1] A list of errors wrapped in a 'Maybe' where 'Nothing' represents
109− -- if resolving the reference itself failed.
110− refFailures :: [Invalid]
111− refFailures =
112− case _schemaRef (_swSchema sw) of
113− Nothing -> mempty
114− Just reference ->
115− maybe [FR.Invalid RefResolution (toJSON reference) mempty]
116− (fmap (modFailure Ref))
117− $ AN.ref scope
118− getReference
119− (\a b -> runValidate referenced (SchemaWithURI b a))
120− reference
121− x
122− where
123− scope :: Maybe Text
124− scope = updateResolutionScope (_swURI sw) (_schemaId (_swSchema sw))
125−
126− getReference :: Maybe Text -> Maybe Schema
127− getReference Nothing = Just (_rsStarting referenced)
128− getReference (Just t) = H.lookup t (_rsSchemaMap referenced)
129−
130−validateString
131− :: Schema
132− -> Text
133− -> [Invalid]
134−validateString schema x = concat
135− [ f _schemaMaxLength (setFailure MaxLength) (fmap maybeToList . ST.maxLength)
136− , f _schemaMinLength (setFailure MinLength) (fmap maybeToList . ST.minLength)
137− , f _schemaPattern (setFailure PatternValidator) (fmap maybeToList . ST.patternVal)
138− ]
139− where
140− f = runSingle schema x
141−
142−validateNumber
143− :: Schema
144− -> Scientific
145− -> [Invalid]
146−validateNumber schema x = concat
147− [ f _schemaMultipleOf (setFailure MultipleOf) (fmap maybeToList . NU.multipleOf)
148− , f _schemaMaximum
149− (modFailure fMax)
150− ( fmap maybeToList
151− . NU.maximumVal (fromMaybe False (_schemaExclusiveMaximum schema))
152− )
153− , f _schemaMinimum
154− (modFailure fMin)
155− ( fmap maybeToList
156− . NU.minimumVal (fromMaybe False (_schemaExclusiveMinimum schema))
157− )
158− ]
159− where
160− f = runSingle schema x
161−
162− fMax NU.Maximum = Maximum
163− fMax NU.ExclusiveMaximum = ExclusiveMaximum
164−
165− fMin NU.Minimum = Minimum
166− fMin NU.ExclusiveMinimum = ExclusiveMinimum
167−
168−validateArray
169− :: ReferencedSchemas Schema
170− -> SchemaWithURI Schema
171− -> Vector Value
172− -> [Invalid]
173−validateArray referenced (SchemaWithURI schema mUri) x = concat
174− [ f _schemaMaxItems (setFailure MaxItems) (fmap maybeToList . AR.maxItems)
175− , f _schemaMinItems (setFailure MinItems) (fmap maybeToList . AR.minItems)
176− , f _schemaUniqueItems (setFailure UniqueItems) (fmap maybeToList . AR.uniqueItems)
177− , f _schemaItems
178− (modFailure fItems)
179− (AR.items recurse (_schemaAdditionalItems schema))
180− ]
181− where
182− f = runSingle schema x
183−
184− recurse = descendNextLevel referenced (SchemaWithURI schema mUri)
185−
186− fItems (AR.Items err) = Items err
187− fItems AR.AdditionalItemsBoolInvalid = AdditionalItemsBool
188− fItems (AR.AdditionalItemsObjectInvalid err) = AdditionalItemsObject err
189−
190−validateObject
191− :: ReferencedSchemas Schema
192− -> SchemaWithURI Schema
193− -> HashMap Text Value
194− -> [Invalid]
195−validateObject referenced (SchemaWithURI schema mUri) x = concat
196− [ f _schemaMaxProperties (setFailure MaxProperties) (fmap maybeToList . OB.maxProperties)
197− , f _schemaMinProperties (setFailure MinProperties) (fmap maybeToList . OB.minProperties)
198− , f _schemaRequired (setFailure Required) (fmap maybeToList . OB.required)
199− , f _schemaDependencies (modFailure fDeps) (OB.dependencies recurse)
200−
201− , f _schemaProperties
202− (modFailure fProp)
203− (OB.properties recurse
204− (_schemaPatternProperties schema)
205− (_schemaAdditionalProperties schema))
206−
207− , f _schemaPatternProperties
208− (modFailure fPatProp)
209− (case _schemaProperties schema of
210− Just _ -> const (const mempty)
211− Nothing -> OB.patternProperties recurse (_schemaAdditionalProperties schema))
212−
213− , f _schemaAdditionalProperties
214− (modFailure fAddProp)
215− (if isJust (_schemaProperties schema) || isJust (_schemaPatternProperties schema)
216− then const (const mempty)
217− else OB.additionalProperties recurse)
218− ]
219− where
220− f = runSingle schema x
221−
222− recurse = descendNextLevel referenced (SchemaWithURI schema mUri)
223−
224− fDeps (OB.SchemaDependencyInvalid err) = SchemaDependency err
225− fDeps OB.PropertyDependencyInvalid = PropertyDependency
226−
227− fProp (OB.PropertiesInvalid err) = Properties err
228− fProp (OB.PropPatternInvalid err) = PatternProperties err
229− fProp (OB.PropAdditionalInvalid a) =
230− case a of
231− OB.APBoolInvalid -> AdditionalPropertiesBool
232− OB.APObjectInvalid err -> AdditionalPropertiesObject err
233−
234− fPatProp (OB.PPInvalid err) = PatternProperties err
235− fPatProp (OB.PPAdditionalPropertiesInvalid a) =
236− case a of
237− OB.APBoolInvalid -> AdditionalPropertiesBool
238− OB.APObjectInvalid err -> AdditionalPropertiesObject err
239−
240− fAddProp OB.APBoolInvalid = AdditionalPropertiesBool
241− fAddProp (OB.APObjectInvalid err) = AdditionalItemsObject err
242−
243−--------------------------------------------------
244−-- * Validation (Internal utils)
245−--------------------------------------------------
246−
247−descendNextLevel
248− :: ReferencedSchemas Schema
249− -> SchemaWithURI Schema
250− -> Schema
251− -> Value
252− -> [Invalid]
253−descendNextLevel referenced (SchemaWithURI schema mUri) =
254− runValidate referenced . flip SchemaWithURI scope
255− where
256− scope :: Maybe Text
257− scope = updateResolutionScope mUri (_schemaId schema)
258−
259−runSingle
260− :: Schema
261− -> dta
262− -> (Schema -> Maybe val)
263− -> (err -> Invalid)
264− -> (val -> dta -> [err])
265− -> [Invalid]
266−runSingle schema dta field modifyError validate =
267− maybe mempty (\val -> modifyError <$> validate val dta) (field schema)
src/Data/JsonSchema/Draft4/Schema.hs+308−304
@@ -1,7 +1,10 @@
11
22module Data.JsonSchema.Draft4.Schema where
33
4−import qualified Data.HashMap.Strict as H
4+import Import
5+import Prelude
6+
7+import qualified Data.HashMap.Strict as HM
58import Data.List.NonEmpty (NonEmpty)
69import Data.Maybe (fromJust, isJust)
710import Data.Scientific
@@ -10,325 +13,326 @@ import qualified Data.Validator.Draft4.Any as AN
1013import qualified Data.Validator.Draft4.Array as AR
1114import qualified Data.Validator.Draft4.Object as OB
1215import Data.Validator.Utils
13−import Import
1416
1517data Schema = Schema
16− { _schemaVersion :: Maybe Text
17− , _schemaId :: Maybe Text
18− , _schemaRef :: Maybe Text
19− , _schemaDefinitions :: Maybe (HashMap Text Schema)
20− -- ^ A standardized location for embedding schemas
21− -- to be referenced from elsewhere in the document.
22− , _schemaOther :: HashMap Text Value
23− -- ^ Since the JSON document this Schema was built from could
24− -- contain schemas anywhere (not just in "definitions" or any
25− -- of the other official keys) we save any leftover key/value
26− -- pairs not covered by them here.
27−
28− , _schemaMultipleOf :: Maybe Scientific
29− , _schemaMaximum :: Maybe Scientific
30− , _schemaExclusiveMaximum :: Maybe Bool
31− , _schemaMinimum :: Maybe Scientific
32− , _schemaExclusiveMinimum :: Maybe Bool
33−
34− , _schemaMaxLength :: Maybe Int
35− , _schemaMinLength :: Maybe Int
36− , _schemaPattern :: Maybe Text
37−
38− , _schemaMaxItems :: Maybe Int
39− , _schemaMinItems :: Maybe Int
40− , _schemaUniqueItems :: Maybe Bool
41− , _schemaItems :: Maybe (AR.Items Schema)
42− -- Note that '_schemaAdditionalItems' is left out of 'runValidate'
43− -- because its functionality is handled by '_schemaItems'. It always
44− -- validates data unless 'Items' is present.
45− , _schemaAdditionalItems :: Maybe (AR.AdditionalItems Schema)
46−
47− , _schemaMaxProperties :: Maybe Int
48− , _schemaMinProperties :: Maybe Int
49− , _schemaRequired :: Maybe OB.Required
50− , _schemaDependencies :: Maybe (HashMap Text (OB.Dependency Schema))
51− , _schemaProperties :: Maybe (HashMap Text Schema)
52− , _schemaPatternProperties :: Maybe (HashMap Text Schema)
53− , _schemaAdditionalProperties :: Maybe (OB.AdditionalProperties Schema)
54−
55− , _schemaEnum :: Maybe AN.EnumVal
56− , _schemaType :: Maybe AN.TypeVal
57− , _schemaAllOf :: Maybe (NonEmpty Schema)
58− , _schemaAnyOf :: Maybe (NonEmpty Schema)
59− , _schemaOneOf :: Maybe (NonEmpty Schema)
60− , _schemaNot :: Maybe Schema
61− } deriving (Eq, Show)
18+ { _schemaVersion :: Maybe Text
19+ , _schemaId :: Maybe Text
20+ , _schemaRef :: Maybe Text
21+ , _schemaDefinitions :: Maybe (HashMap Text Schema)
22+ -- ^ A standardized location for embedding schemas
23+ -- to be referenced from elsewhere in the document.
24+ , _schemaOther :: HashMap Text Value
25+ -- ^ Since the JSON document this schema was built from could
26+ -- contain schemas anywhere (not just in "definitions" or any
27+ -- of the other official keys) we save any leftover key/value
28+ -- pairs not covered by them here.
29+
30+ , _schemaMultipleOf :: Maybe Scientific
31+ , _schemaMaximum :: Maybe Scientific
32+ , _schemaExclusiveMaximum :: Maybe Bool
33+ , _schemaMinimum :: Maybe Scientific
34+ , _schemaExclusiveMinimum :: Maybe Bool
35+
36+ , _schemaMaxLength :: Maybe Int
37+ , _schemaMinLength :: Maybe Int
38+ , _schemaPattern :: Maybe Text
39+
40+ , _schemaMaxItems :: Maybe Int
41+ , _schemaMinItems :: Maybe Int
42+ , _schemaUniqueItems :: Maybe Bool
43+ , _schemaItems :: Maybe (AR.Items Schema)
44+ -- Note that '_schemaAdditionalItems' is left out of 'runValidate'
45+ -- because its functionality is handled by '_schemaItems'. It always
46+ -- validates data unless 'Items' is present.
47+ , _schemaAdditionalItems :: Maybe (AR.AdditionalItems Schema)
48+
49+ , _schemaMaxProperties :: Maybe Int
50+ , _schemaMinProperties :: Maybe Int
51+ , _schemaRequired :: Maybe OB.Required
52+ , _schemaDependencies :: Maybe (HashMap Text (OB.Dependency Schema))
53+ , _schemaProperties :: Maybe (HashMap Text Schema)
54+ , _schemaPatternProperties :: Maybe (HashMap Text Schema)
55+ , _schemaAdditionalProperties :: Maybe (OB.AdditionalProperties Schema)
56+
57+ , _schemaEnum :: Maybe AN.EnumVal
58+ , _schemaType :: Maybe AN.TypeVal
59+ , _schemaAllOf :: Maybe (NonEmpty Schema)
60+ , _schemaAnyOf :: Maybe (NonEmpty Schema)
61+ , _schemaOneOf :: Maybe (NonEmpty Schema)
62+ , _schemaNot :: Maybe Schema
63+ } deriving (Eq, Show)
6264
6365emptySchema :: Schema
6466emptySchema = Schema
65− { _schemaVersion = Nothing
66− , _schemaId = Nothing
67− , _schemaRef = Nothing
68− , _schemaDefinitions = Nothing
69− , _schemaOther = mempty
70−
71− , _schemaMultipleOf = Nothing
72− , _schemaMaximum = Nothing
73− , _schemaExclusiveMaximum = Nothing
74− , _schemaMinimum = Nothing
75− , _schemaExclusiveMinimum = Nothing
76−
77− , _schemaMaxLength = Nothing
78− , _schemaMinLength = Nothing
79− , _schemaPattern = Nothing
80−
81− , _schemaMaxItems = Nothing
82− , _schemaMinItems = Nothing
83− , _schemaUniqueItems = Nothing
84− , _schemaItems = Nothing
85− , _schemaAdditionalItems = Nothing
86−
87− , _schemaMaxProperties = Nothing
88− , _schemaMinProperties = Nothing
89− , _schemaRequired = Nothing
90− , _schemaDependencies = Nothing
91− , _schemaProperties = Nothing
92− , _schemaPatternProperties = Nothing
93− , _schemaAdditionalProperties = Nothing
94−
95− , _schemaEnum = Nothing
96− , _schemaType = Nothing
97− , _schemaAllOf = Nothing
98− , _schemaAnyOf = Nothing
99− , _schemaOneOf = Nothing
100− , _schemaNot = Nothing
101− }
67+ { _schemaVersion = Nothing
68+ , _schemaId = Nothing
69+ , _schemaRef = Nothing
70+ , _schemaDefinitions = Nothing
71+ , _schemaOther = mempty
72+
73+ , _schemaMultipleOf = Nothing
74+ , _schemaMaximum = Nothing
75+ , _schemaExclusiveMaximum = Nothing
76+ , _schemaMinimum = Nothing
77+ , _schemaExclusiveMinimum = Nothing
78+
79+ , _schemaMaxLength = Nothing
80+ , _schemaMinLength = Nothing
81+ , _schemaPattern = Nothing
82+
83+ , _schemaMaxItems = Nothing
84+ , _schemaMinItems = Nothing
85+ , _schemaUniqueItems = Nothing
86+ , _schemaItems = Nothing
87+ , _schemaAdditionalItems = Nothing
88+
89+ , _schemaMaxProperties = Nothing
90+ , _schemaMinProperties = Nothing
91+ , _schemaRequired = Nothing
92+ , _schemaDependencies = Nothing
93+ , _schemaProperties = Nothing
94+ , _schemaPatternProperties = Nothing
95+ , _schemaAdditionalProperties = Nothing
96+
97+ , _schemaEnum = Nothing
98+ , _schemaType = Nothing
99+ , _schemaAllOf = Nothing
100+ , _schemaAnyOf = Nothing
101+ , _schemaOneOf = Nothing
102+ , _schemaNot = Nothing
103+ }
102104
103105instance FromJSON Schema where
104− parseJSON = withObject "Schema" $ \o -> do
105− a <- o .:? "$schema"
106− b <- o .:? "id"
107− c <- o .:? "$ref"
108− d <- o .:? "definitions"
109− e <- parseJSON (Object (H.difference o internalSchemaHashMap))
110−
111− f <- o .:? "multipleOf"
112− g <- o .:? "maximum"
113− h <- o .:? "exclusiveMaximum"
114− i <- o .:? "minimum"
115− j <- o .:? "exclusiveMinimum"
116−
117− k <- o .:? "maxLength"
118− l <- o .:? "minLength"
119− m <- o .:? "pattern"
120−
121− n <- o .:? "maxItems"
122− o' <- o .:? "minItems"
123− p <- o .:? "uniqueItems"
124− q <- o .:? "items"
125− r <- o .:? "additionalItems"
126−
127− s <- o .:? "maxProperties"
128− t <- o .:? "minProperties"
129− u <- o .:? "required"
130− v <- o .:? "dependencies"
131− w <- o .:? "properties"
132− x <- o .:? "patternProperties"
133− y <- o .:? "additionalProperties"
134−
135− z <- o .:? "enum"
136− a2 <- o .:? "type"
137− b2 <- fmap _unNonEmpty' <$> o .:? "allOf"
138− c2 <- fmap _unNonEmpty' <$> o .:? "anyOf"
139− d2 <- fmap _unNonEmpty' <$> o .:? "oneOf"
140− e2 <- o .:? "not"
141− pure Schema
142− { _schemaVersion = a
143− , _schemaId = b
144− , _schemaRef = c
145− , _schemaDefinitions = d
146− , _schemaOther = e
147−
148− , _schemaMultipleOf = f
149− , _schemaMaximum = g
150− , _schemaExclusiveMaximum = h
151− , _schemaMinimum = i
152− , _schemaExclusiveMinimum = j
153−
154− , _schemaMaxLength = k
155− , _schemaMinLength = l
156− , _schemaPattern = m
157−
158− , _schemaMaxItems = n
159− , _schemaMinItems = o'
160− , _schemaUniqueItems = p
161− , _schemaItems = q
162− , _schemaAdditionalItems = r
163−
164− , _schemaMaxProperties = s
165− , _schemaMinProperties = t
166− , _schemaRequired = u
167− , _schemaDependencies = v
168− , _schemaProperties = w
169− , _schemaPatternProperties = x
170− , _schemaAdditionalProperties = y
171−
172− , _schemaEnum = z
173− , _schemaType = a2
174− , _schemaAllOf = b2
175− , _schemaAnyOf = c2
176− , _schemaOneOf = d2
177− , _schemaNot = e2
178− }
106+ parseJSON = withObject "Schema" $ \o -> do
107+ a <- o .:! "$schema"
108+ b <- o .:! "id"
109+ c <- o .:! "$ref"
110+ d <- o .:! "definitions"
111+ e <- parseJSON (Object (HM.difference o internalSchemaHashMap))
112+
113+ f <- o .:! "multipleOf"
114+ g <- o .:! "maximum"
115+ h <- o .:! "exclusiveMaximum"
116+ i <- o .:! "minimum"
117+ j <- o .:! "exclusiveMinimum"
118+
119+ k <- o .:! "maxLength"
120+ l <- o .:! "minLength"
121+ m <- o .:! "pattern"
122+
123+ n <- o .:! "maxItems"
124+ o' <- o .:! "minItems"
125+ p <- o .:! "uniqueItems"
126+ q <- o .:! "items"
127+ r <- o .:! "additionalItems"
128+
129+ s <- o .:! "maxProperties"
130+ t <- o .:! "minProperties"
131+ u <- o .:! "required"
132+ v <- o .:! "dependencies"
133+ w <- o .:! "properties"
134+ x <- o .:! "patternProperties"
135+ y <- o .:! "additionalProperties"
136+
137+ z <- o .:! "enum"
138+ a2 <- o .:! "type"
139+ b2 <- fmap _unNonEmpty' <$> o .:! "allOf"
140+ c2 <- fmap _unNonEmpty' <$> o .:! "anyOf"
141+ d2 <- fmap _unNonEmpty' <$> o .:! "oneOf"
142+ e2 <- o .:! "not"
143+ pure Schema
144+ { _schemaVersion = a
145+ , _schemaId = b
146+ , _schemaRef = c
147+ , _schemaDefinitions = d
148+ , _schemaOther = e
149+
150+ , _schemaMultipleOf = f
151+ , _schemaMaximum = g
152+ , _schemaExclusiveMaximum = h
153+ , _schemaMinimum = i
154+ , _schemaExclusiveMinimum = j
155+
156+ , _schemaMaxLength = k
157+ , _schemaMinLength = l
158+ , _schemaPattern = m
159+
160+ , _schemaMaxItems = n
161+ , _schemaMinItems = o'
162+ , _schemaUniqueItems = p
163+ , _schemaItems = q
164+ , _schemaAdditionalItems = r
165+
166+ , _schemaMaxProperties = s
167+ , _schemaMinProperties = t
168+ , _schemaRequired = u
169+ , _schemaDependencies = v
170+ , _schemaProperties = w
171+ , _schemaPatternProperties = x
172+ , _schemaAdditionalProperties = y
173+
174+ , _schemaEnum = z
175+ , _schemaType = a2
176+ , _schemaAllOf = b2
177+ , _schemaAnyOf = c2
178+ , _schemaOneOf = d2
179+ , _schemaNot = e2
180+ }
179181
180182instance ToJSON Schema where
181− -- | The way we resolve JSON Pointers to embedded schemas is by
182− -- serializing the containing schema to a value and then resolving the
183− -- pointer against it. This means that FromJSON and ToJSON must be
184− -- isomorphic.
185− --
186− -- This influences the design choices in the library. E.g. right now
187− -- there are two false values for "exclusiveMaximum" -- Nothing and
188− -- Just False. We could have condensed them down by using () instead
189− -- of Bool for "exclusiveMaximum". This would have made writing schemas
190− -- in haskell easier, but we could no longer round trip through/from
191− -- JSON without losing information.
192− toJSON s = Object $ H.union (mapMaybe ($ s) internalSchemaHashMap) (toJSON <$> _schemaOther s)
193− where
194− -- 'mapMaybe' is provided by unordered-containers after unordered-container-2.6.0.0,
195− -- but until that is a little older (and has time to get into Stackage etc.)
196− -- we use our own implementation.
197− mapMaybe :: (v1 -> Maybe v2) -> HashMap k v1 -> HashMap k v2
198− mapMaybe f = fmap fromJust . H.filter isJust . fmap f
183+ -- | The way we resolve JSON Pointers to embedded schemas is by
184+ -- serializing the containing schema to a value and then resolving the
185+ -- pointer against it. This means that FromJSON and ToJSON must be
186+ -- isomorphic.
187+ --
188+ -- This influences the design choices in the library. E.g. right now
189+ -- there are two false values for "exclusiveMaximum" -- Nothing and
190+ -- Just False. We could have condensed them down by using () instead
191+ -- of Bool for "exclusiveMaximum". This would have made writing schemas
192+ -- in haskell easier, but we could no longer round trip through/from
193+ -- JSON without losing information.
194+ toJSON s = Object $ HM.union (mapMaybe ($ s) internalSchemaHashMap)
195+ (toJSON <$> _schemaOther s)
196+ where
197+ -- 'mapMaybe' is provided by unordered-containers after
198+ -- unordered-container-2.6.0.0, but until that is a little older
199+ -- (and has time to get into Stackage etc.) we use our own
200+ -- implementation.
201+ mapMaybe :: (v1 -> Maybe v2) -> HashMap k v1 -> HashMap k v2
202+ mapMaybe f = fmap fromJust . HM.filter isJust . fmap f
199203
200204-- | Internal. Separate from ToJSON because it's also used
201205-- by FromJSON to determine what keys aren't official schema
202206-- keys and therefor should be included in _schemaOther.
203207internalSchemaHashMap :: HashMap Text (Schema -> Maybe Value)
204−internalSchemaHashMap = H.fromList
205− [ ("$schema" , f _schemaVersion)
206− , ("id" , f _schemaId)
207− , ("$ref" , f _schemaRef)
208− , ("definitions" , f _schemaDefinitions)
209−
210− , ("multipleOf" , f _schemaMultipleOf)
211− , ("maximum" , f _schemaMaximum)
212− , ("exclusiveMaximum" , f _schemaExclusiveMaximum)
213− , ("minimum" , f _schemaMinimum)
214− , ("exclusiveMinimum" , f _schemaExclusiveMinimum)
215−
216− , ("maxLength" , f _schemaMaxLength)
217− , ("minLength" , f _schemaMinLength)
218− , ("pattern" , f _schemaPattern)
219−
220− , ("maxItems" , f _schemaMaxItems)
221− , ("minItems" , f _schemaMinItems)
222− , ("uniqueItems" , f _schemaUniqueItems)
223− , ("items" , f _schemaItems)
224− , ("additionalItems" , f _schemaAdditionalItems)
225−
226− , ("maxProperties" , f _schemaMaxProperties)
227− , ("minProperties" , f _schemaMinProperties)
228− , ("required" , f _schemaRequired)
229− , ("dependencies" , f _schemaDependencies)
230− , ("properties" , f _schemaProperties)
231− , ("patternProperties" , f _schemaPatternProperties)
232− , ("additionalProperties", f _schemaAdditionalProperties)
233−
234− , ("enum" , f _schemaEnum)
235− , ("type" , f _schemaType)
236− , ("allOf" , f (fmap NonEmpty' . _schemaAllOf))
237− , ("anyOf" , f (fmap NonEmpty' . _schemaAnyOf))
238− , ("oneOf" , f (fmap NonEmpty' . _schemaOneOf))
239− , ("not" , f _schemaNot)
240− ]
208+internalSchemaHashMap = HM.fromList
209+ [ ("$schema" , f _schemaVersion)
210+ , ("id" , f _schemaId)
211+ , ("$ref" , f _schemaRef)
212+ , ("definitions" , f _schemaDefinitions)
213+
214+ , ("multipleOf" , f _schemaMultipleOf)
215+ , ("maximum" , f _schemaMaximum)
216+ , ("exclusiveMaximum" , f _schemaExclusiveMaximum)
217+ , ("minimum" , f _schemaMinimum)
218+ , ("exclusiveMinimum" , f _schemaExclusiveMinimum)
219+
220+ , ("maxLength" , f _schemaMaxLength)
221+ , ("minLength" , f _schemaMinLength)
222+ , ("pattern" , f _schemaPattern)
223+
224+ , ("maxItems" , f _schemaMaxItems)
225+ , ("minItems" , f _schemaMinItems)
226+ , ("uniqueItems" , f _schemaUniqueItems)
227+ , ("items" , f _schemaItems)
228+ , ("additionalItems" , f _schemaAdditionalItems)
229+
230+ , ("maxProperties" , f _schemaMaxProperties)
231+ , ("minProperties" , f _schemaMinProperties)
232+ , ("required" , f _schemaRequired)
233+ , ("dependencies" , f _schemaDependencies)
234+ , ("properties" , f _schemaProperties)
235+ , ("patternProperties" , f _schemaPatternProperties)
236+ , ("additionalProperties", f _schemaAdditionalProperties)
237+
238+ , ("enum" , f _schemaEnum)
239+ , ("type" , f _schemaType)
240+ , ("allOf" , f (fmap NonEmpty' . _schemaAllOf))
241+ , ("anyOf" , f (fmap NonEmpty' . _schemaAnyOf))
242+ , ("oneOf" , f (fmap NonEmpty' . _schemaOneOf))
243+ , ("not" , f _schemaNot)
244+ ]
241245 where
242246 f :: ToJSON a => (Schema -> Maybe a) -> Schema -> Maybe Value
243247 f = (fmap.fmap) toJSON
244248
245249instance Arbitrary Schema where
246− arbitrary = sized f
247− where
248− maybeGen :: Gen a -> Gen (Maybe a)
249− maybeGen a = oneof [pure Nothing, Just <$> a]
250−
251− maybeRecurse :: Int -> Gen a -> Gen (Maybe a)
252− maybeRecurse n a
253− | n < 1 = pure Nothing
254− | otherwise = maybeGen $ resize (n `div` 10) a
255−
256− f :: Int -> Gen Schema
257− f n = do
258− a <- maybeGen arbitraryText
259− b <- maybeGen arbitraryText
260− c <- maybeGen arbitraryText
261− -- NOTE: The next two fields are empty to generate cleaner schemas,
262− -- but note that this means we don't test e.g. the invertability
263− -- of these fields.
264− d <- pure Nothing -- _schemaDefinitions
265− e <- pure mempty -- _otherPairs
266−
267− f' <- maybeGen arbitraryPositiveScientific
268− g <- maybeGen arbitraryScientific
269− h <- arbitrary
270− i <- maybeGen arbitraryScientific
271− j <- arbitrary
272−
273− k <- maybeGen (getPositive <$> arbitrary)
274− l <- maybeGen (getPositive <$> arbitrary)
275− m <- maybeGen arbitraryText
276−
277− n' <- maybeGen (getPositive <$> arbitrary)
278− o <- maybeGen (getPositive <$> arbitrary)
279− p <- arbitrary
280− q <- maybeRecurse n arbitrary
281− r <- maybeRecurse n arbitrary
282−
283− s <- maybeGen (getPositive <$> arbitrary)
284− t <- maybeGen (getPositive <$> arbitrary)
285− u <- arbitrary
286− v <- maybeRecurse n arbitraryHashMap
287− w <- maybeRecurse n arbitraryHashMap
288− x <- maybeRecurse n arbitraryHashMap
289− y <- maybeRecurse n arbitrary
290−
291− z <- arbitrary
292− a2 <- arbitrary
293− b2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
294− c2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
295− d2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
296− e2 <- maybeRecurse n arbitrary
297− pure Schema
298− { _schemaVersion = a
299− , _schemaId = b
300− , _schemaRef = c
301− , _schemaDefinitions = d
302− , _schemaOther = e
303−
304− , _schemaMultipleOf = f'
305− , _schemaMaximum = g
306− , _schemaExclusiveMaximum = h
307− , _schemaMinimum = i
308− , _schemaExclusiveMinimum = j
309−
310− , _schemaMaxLength = k
311− , _schemaMinLength = l
312− , _schemaPattern = m
313−
314− , _schemaMaxItems = n'
315− , _schemaMinItems = o
316− , _schemaUniqueItems = p
317− , _schemaItems = q
318− , _schemaAdditionalItems = r
319−
320− , _schemaMaxProperties = s
321− , _schemaMinProperties = t
322− , _schemaRequired = u
323− , _schemaDependencies = v
324− , _schemaProperties = w
325− , _schemaPatternProperties = x
326− , _schemaAdditionalProperties = y
327−
328− , _schemaEnum = z
329− , _schemaType = a2
330− , _schemaAllOf = b2
331− , _schemaAnyOf = c2
332− , _schemaOneOf = d2
333− , _schemaNot = e2
334− }
250+ arbitrary = sized f
251+ where
252+ maybeGen :: Gen a -> Gen (Maybe a)
253+ maybeGen a = oneof [pure Nothing, Just <$> a]
254+
255+ maybeRecurse :: Int -> Gen a -> Gen (Maybe a)
256+ maybeRecurse n a
257+ | n < 1 = pure Nothing
258+ | otherwise = maybeGen $ resize (n `div` 10) a
259+
260+ f :: Int -> Gen Schema
261+ f n = do
262+ a <- maybeGen arbitraryText
263+ b <- maybeGen arbitraryText
264+ c <- maybeGen arbitraryText
265+ -- NOTE: The next two fields are empty to generate cleaner schemas,
266+ -- but note that this means we don't test e.g. the invertability
267+ -- of these fields.
268+ d <- pure Nothing -- _schemaDefinitions
269+ e <- pure mempty -- _otherPairs
270+
271+ f' <- maybeGen arbitraryPositiveScientific
272+ g <- maybeGen arbitraryScientific
273+ h <- arbitrary
274+ i <- maybeGen arbitraryScientific
275+ j <- arbitrary
276+
277+ k <- maybeGen (getPositive <$> arbitrary)
278+ l <- maybeGen (getPositive <$> arbitrary)
279+ m <- maybeGen arbitraryText
280+
281+ n' <- maybeGen (getPositive <$> arbitrary)
282+ o <- maybeGen (getPositive <$> arbitrary)
283+ p <- arbitrary
284+ q <- maybeRecurse n arbitrary
285+ r <- maybeRecurse n arbitrary
286+
287+ s <- maybeGen (getPositive <$> arbitrary)
288+ t <- maybeGen (getPositive <$> arbitrary)
289+ u <- arbitrary
290+ v <- maybeRecurse n arbitraryHashMap
291+ w <- maybeRecurse n arbitraryHashMap
292+ x <- maybeRecurse n arbitraryHashMap
293+ y <- maybeRecurse n arbitrary
294+
295+ z <- arbitrary
296+ a2 <- arbitrary
297+ b2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
298+ c2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
299+ d2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
300+ e2 <- maybeRecurse n arbitrary
301+ pure Schema
302+ { _schemaVersion = a
303+ , _schemaId = b
304+ , _schemaRef = c
305+ , _schemaDefinitions = d
306+ , _schemaOther = e
307+
308+ , _schemaMultipleOf = f'
309+ , _schemaMaximum = g
310+ , _schemaExclusiveMaximum = h
311+ , _schemaMinimum = i
312+ , _schemaExclusiveMinimum = j
313+
314+ , _schemaMaxLength = k
315+ , _schemaMinLength = l
316+ , _schemaPattern = m
317+
318+ , _schemaMaxItems = n'
319+ , _schemaMinItems = o
320+ , _schemaUniqueItems = p
321+ , _schemaItems = q
322+ , _schemaAdditionalItems = r
323+
324+ , _schemaMaxProperties = s
325+ , _schemaMinProperties = t
326+ , _schemaRequired = u
327+ , _schemaDependencies = v
328+ , _schemaProperties = w
329+ , _schemaPatternProperties = x
330+ , _schemaAdditionalProperties = y
331+
332+ , _schemaEnum = z
333+ , _schemaType = a2
334+ , _schemaAllOf = b2
335+ , _schemaAnyOf = c2
336+ , _schemaOneOf = d2
337+ , _schemaNot = e2
338+ }
src/Data/JsonSchema/Draft4/Spec.hsnew file+130
@@ -0,0 +1,130 @@
1+
2+module Data.JsonSchema.Draft4.Spec where
3+
4+import Import
5+-- Hiding is for GHCs before 7.10:
6+import Prelude hiding (concat)
7+
8+import Data.Maybe (fromMaybe, isNothing)
9+import Data.Profunctor (Profunctor(..))
10+
11+import Data.JsonSchema.Draft4.Failure
12+import Data.JsonSchema.Draft4.Schema (Schema(..))
13+import Data.JsonSchema.Fetch (ReferencedSchemas(..),
14+ SchemaWithURI(..))
15+import qualified Data.JsonSchema.Fetch as FE
16+import Data.JsonSchema.Types (Spec(..))
17+import qualified Data.JsonSchema.Types as JT
18+import qualified Data.Validator.Draft4 as D4
19+import qualified Data.Validator.Draft4.Any as AN
20+import Data.Validator.Reference (updateResolutionScope)
21+
22+embedded :: Schema -> ([Schema], [Schema])
23+embedded s = JT.embedded (d4Spec (ReferencedSchemas s mempty) mempty Nothing) s
24+
25+validate
26+ :: ReferencedSchemas Schema
27+ -> SchemaWithURI Schema
28+ -> Value
29+ -> [Failure]
30+validate rs = continueValidating rs (AN.VisitedSchemas [(Nothing, Nothing)])
31+
32+continueValidating
33+ :: ReferencedSchemas Schema
34+ -> AN.VisitedSchemas
35+ -> SchemaWithURI Schema
36+ -> Value
37+ -> [Failure]
38+continueValidating referenced visited sw =
39+ JT.validate (d4Spec referenced visited currentScope)
40+ (_swSchema sw)
41+ where
42+ currentScope :: Maybe Text
43+ currentScope = updateResolutionScope
44+ (_swURI sw)
45+ (_schemaId (_swSchema sw))
46+
47+d4Spec
48+ :: ReferencedSchemas Schema
49+ -> AN.VisitedSchemas
50+ -> Maybe Text
51+ -> Spec Schema ValidatorChain
52+d4Spec referenced visited scope = Spec
53+ [ dimap
54+ (fmap D4.MultipleOf . _schemaMultipleOf)
55+ (const MultipleOf)
56+ D4.multipleOf
57+ , dimap
58+ (\s -> D4.MaximumContext (fromMaybe False (_schemaExclusiveMaximum s))
59+ <$> _schemaMaximum s)
60+ maxE
61+ D4.maximumVal
62+ , dimap
63+ (\s -> D4.MinimumContext (fromMaybe False (_schemaExclusiveMinimum s))
64+ <$> _schemaMinimum s)
65+ minE
66+ D4.minimumVal
67+
68+ , dimap (fmap D4.MaxLength . _schemaMaxLength) (const MaxLength) D4.maxLength
69+ , dimap (fmap D4.MinLength . _schemaMinLength) (const MinLength) D4.minLength
70+ , dimap (fmap D4.PatternVal . _schemaPattern) (const PatternValidator) D4.patternVal
71+
72+ , dimap (fmap D4.MaxItems . _schemaMaxItems) (const MaxItems) D4.maxItems
73+ , dimap (fmap D4.MinItems . _schemaMinItems) (const MinItems) D4.minItems
74+ , dimap (fmap D4.UniqueItems . _schemaUniqueItems) (const UniqueItems) D4.uniqueItems
75+ , dimap
76+ (\s -> D4.ItemsContext (_schemaAdditionalItems s) <$> _schemaItems s)
77+ itemsE
78+ (D4.items descend)
79+ , lmap (fmap D4.AdditionalItemsContext . _schemaAdditionalItems) D4.additionalItemsEmbedded
80+ , lmap (fmap D4.Definitions . _schemaDefinitions) D4.definitionsEmbedded
81+
82+ , dimap (fmap D4.MaxProperties . _schemaMaxProperties) (const MaxProperties) D4.maxProperties
83+ , dimap (fmap D4.MinProperties . _schemaMinProperties) (const MinProperties) D4.minProperties
84+ , dimap (fmap D4.RequiredContext . _schemaRequired) (const Required) D4.required
85+ , dimap (fmap D4.DependenciesContext . _schemaDependencies) depsE (D4.dependencies descend)
86+ , dimap
87+ (\s -> D4.PropertiesContext
88+ (_schemaPatternProperties s)
89+ (_schemaAdditionalProperties s)
90+ <$> _schemaProperties s)
91+ propE
92+ (D4.properties descend)
93+ , dimap
94+ (\s -> D4.PatternPropertiesContext
95+ (isNothing (_schemaProperties s))
96+ (_schemaAdditionalProperties s)
97+ <$> _schemaPatternProperties s)
98+ patPropE
99+ (D4.patternProperties descend)
100+ , dimap
101+ (\s -> D4.AdditionalPropertiesContext
102+ (isNothing (_schemaProperties s)
103+ && isNothing (_schemaPatternProperties s))
104+ <$> _schemaAdditionalProperties s)
105+ addPropE
106+ (D4.additionalProperties descend)
107+
108+ , dimap
109+ (\s -> D4.Ref <$> _schemaRef s)
110+ refE
111+ (D4.ref visited scope (FE.getReference referenced) refVal)
112+ , dimap (fmap D4.EnumContext . _schemaEnum) (const Enum) D4.enumVal
113+ , dimap (fmap D4.TypeContext . _schemaType) (const TypeValidator) D4.typeVal
114+ , dimap (fmap D4.AllOf . _schemaAllOf) AllOf (D4.allOf lateral)
115+ , dimap (fmap D4.AnyOf . _schemaAnyOf) (const AnyOf) (D4.anyOf lateral)
116+ , dimap (fmap D4.OneOf . _schemaOneOf) (const OneOf) (D4.oneOf lateral)
117+ , dimap (fmap D4.NotVal . _schemaNot) (const NotValidator) (D4.notVal lateral)
118+ ]
119+ where
120+ refVal :: AN.VisitedSchemas -> Maybe Text -> Schema -> Value -> [Failure]
121+ refVal newVisited newScope schema =
122+ continueValidating referenced newVisited (SchemaWithURI schema newScope)
123+
124+ descend :: Schema -> Value -> [Failure]
125+ descend schema =
126+ continueValidating referenced mempty (SchemaWithURI schema scope)
127+
128+ lateral :: Schema -> Value -> [Failure]
129+ lateral schema =
130+ continueValidating referenced visited (SchemaWithURI schema scope)
src/Data/JsonSchema/Fetch.hs+116−95
@@ -1,83 +1,104 @@
1−{-# LANGUAGE ScopedTypeVariables #-}
21
32module Data.JsonSchema.Fetch where
43
4+import Import
5+-- Hiding is for GHCs before 7.10:
6+import Prelude hiding (concat, sequence)
7+
58import Control.Arrow (left)
69import Control.Exception (catch)
10+import Control.Monad (foldM)
711import qualified Data.ByteString.Lazy as LBS
812import qualified Data.ByteString as BS
913import qualified Data.HashMap.Strict as H
1014import qualified Data.Text as T
11−import Network.HTTP.Client
15+import qualified Network.HTTP.Client as NC
1216
1317import Data.Validator.Reference (resolveReference,
1418 updateResolutionScope)
15−import Import
16−
17−-- For GHCs before 7.10:
18−import Prelude hiding (concat, sequence)
1919
2020--------------------------------------------------
2121-- * Types
2222--------------------------------------------------
2323
24−data Spec schema = Spec
25− { _ssEmbedded :: schema -> [schema]
26− , _ssGetId :: schema -> Maybe Text
27− , _ssGetRef :: schema -> Maybe Text
28− }
24+-- | This is all the fetching functions need to know about a particular
25+-- JSON Schema draft, e.g. JSON Schema Draft 4.
26+data FetchInfo schema = FetchInfo
27+ { _fiEmbedded :: schema -> ([schema], [schema])
28+ , _fiId :: schema -> Maybe Text
29+ , _fiRef :: schema -> Maybe Text
30+ }
2931
30−data SchemaWithURI schema = SchemaWithURI
31− { _swSchema :: !schema
32− , _swURI :: !(Maybe Text)
33− -- ^ Must not include a URI fragment, e.g. use
34− -- "http://example.com/foo" not "http://example.com/foo#bar".
35− --
36− -- This is the URI identifying the document containing the schema.
37− -- It's different than the schema's "id" field, which controls scope
38− -- when resolving references contained in the schema.
39−
40− -- TODO: Make the no URI fragment requirement unnecessary.
41− } deriving (Eq, Show)
32+data ReferencedSchemas schema = ReferencedSchemas
33+ { _rsStarting :: !schema
34+ -- ^ Used to resolve relative references when we don't know what the scope
35+ -- of the current schema is. This only happens with starting schemas
36+ -- because if we're using a remote schema we had to know its URI in order
37+ -- to fetch it.
38+ --
39+ -- Tracking the starting schema (instead of just resolving the reference to
40+ -- the current schema being used for validation) is necessary for cases
41+ -- where schemas are embedded inside one another. For instance in this
42+ -- case not distinguishing the starting and "foo" schemas sends the code
43+ -- into an infinite loop:
44+ --
45+ -- {
46+ -- "additionalProperties": false,
47+ -- "properties": {
48+ -- "foo": {
49+ -- "$ref": "#"
50+ -- }
51+ -- }
52+ -- }
53+ , _rsSchemaMap :: !(URISchemaMap schema)
54+ } deriving (Eq, Show)
4255
4356-- | Keys are URIs (without URI fragments).
4457type URISchemaMap schema = HashMap Text schema
4558
46−data ReferencedSchemas schema = ReferencedSchemas
47− { _rsStarting :: !schema
48− -- ^ Used to resolve relative references.
49− , _rsSchemaMap :: !(URISchemaMap schema)
50− } deriving (Eq, Show)
59+data SchemaWithURI schema = SchemaWithURI
60+ { _swSchema :: !schema
61+ , _swURI :: !(Maybe Text)
62+ -- ^ This is the URI identifying the document containing the schema.
63+ -- It's different than the schema's "id" field, which controls scope
64+ -- when resolving references contained in the schema.
65+
66+ -- TODO: Make the no URI fragment requirement unnecessary.
67+ } deriving (Eq, Show)
68+
69+getReference :: ReferencedSchemas schema -> Maybe Text -> Maybe schema
70+getReference referenced Nothing = Just (_rsStarting referenced)
71+getReference referenced (Just t) = H.lookup t (_rsSchemaMap referenced)
5172
5273--------------------------------------------------
5374-- * Fetch via HTTP
5475--------------------------------------------------
5576
5677data HTTPFailure
57− = HTTPParseFailure Text
58− | HTTPRequestFailure HttpException
59− deriving Show
78+ = HTTPParseFailure Text
79+ | HTTPRequestFailure NC.HttpException
80+ deriving Show
6081
6182-- | Take a schema. Retrieve every document either it or its subschemas
6283-- include via the "$ref" keyword.
6384referencesViaHTTP'
64− :: forall schema. FromJSON schema
65− => Spec schema
66− -> SchemaWithURI schema
67− -> IO (Either HTTPFailure (ReferencedSchemas schema))
68−referencesViaHTTP' spec sw = do
69− manager <- newManager defaultManagerSettings
70− let f = referencesMethodAgnostic (get manager) spec sw
71− catch (left HTTPParseFailure <$> f) handler
85+ :: forall schema. FromJSON schema
86+ => FetchInfo schema
87+ -> SchemaWithURI schema
88+ -> IO (Either HTTPFailure (URISchemaMap schema))
89+referencesViaHTTP' info sw = do
90+ manager <- NC.newManager NC.defaultManagerSettings
91+ let f = referencesMethodAgnostic (get manager) info sw
92+ catch (left HTTPParseFailure <$> f) handler
7293 where
73− get :: Manager -> Text -> IO LBS.ByteString
94+ get :: NC.Manager -> Text -> IO LBS.ByteString
7495 get man url = do
75− request <- parseUrl (T.unpack url)
76− responseBody <$> httpLbs request man
96+ request <- NC.parseUrlThrow (T.unpack url)
97+ NC.responseBody <$> NC.httpLbs request man
7798
7899 handler
79− :: HttpException
80− -> IO (Either HTTPFailure (ReferencedSchemas schema))
100+ :: NC.HttpException
101+ -> IO (Either HTTPFailure (URISchemaMap schema))
81102 handler = pure . Left . HTTPRequestFailure
82103
83104--------------------------------------------------
@@ -85,26 +106,26 @@ referencesViaHTTP' spec sw = do
85106--------------------------------------------------
86107
87108data FilesystemFailure
88− = FSParseFailure Text
89− | FSReadFailure IOError
90− deriving Show
109+ = FSParseFailure Text
110+ | FSReadFailure IOError
111+ deriving (Show, Eq)
91112
92113referencesViaFilesystem'
93− :: forall schema. FromJSON schema
94− => Spec schema
95− -> SchemaWithURI schema
96− -> IO (Either FilesystemFailure (ReferencedSchemas schema))
97−referencesViaFilesystem' spec sw = catch (left FSParseFailure <$> f) handler
114+ :: forall schema. FromJSON schema
115+ => FetchInfo schema
116+ -> SchemaWithURI schema
117+ -> IO (Either FilesystemFailure (URISchemaMap schema))
118+referencesViaFilesystem' info sw = catch (left FSParseFailure <$> f) handler
98119 where
99− f :: IO (Either Text (ReferencedSchemas schema))
100− f = referencesMethodAgnostic readFile' spec sw
120+ f :: IO (Either Text (URISchemaMap schema))
121+ f = referencesMethodAgnostic readFile' info sw
101122
102123 readFile' :: Text -> IO LBS.ByteString
103124 readFile' = fmap LBS.fromStrict . BS.readFile . T.unpack
104125
105126 handler
106− :: IOError
107− -> IO (Either FilesystemFailure (ReferencedSchemas schema))
127+ :: IOError
128+ -> IO (Either FilesystemFailure (URISchemaMap schema))
108129 handler = pure . Left . FSReadFailure
109130
110131--------------------------------------------------
@@ -115,59 +136,59 @@ referencesViaFilesystem' spec sw = catch (left FSParseFailure <$> f) handler
115136-- schemas is provided by the user. This allows restrictions to be added,
116137-- e.g. rejecting non-local URIs.
117138referencesMethodAgnostic
118− :: forall schema. FromJSON schema
119− => (Text -> IO LBS.ByteString)
120− -> Spec schema
121− -> SchemaWithURI schema
122− -> IO (Either Text (ReferencedSchemas schema))
123−referencesMethodAgnostic fetchRef spec sw =
124− (fmap.fmap) (ReferencedSchemas (_swSchema sw))
125− (foldFunction fetchRef spec mempty sw)
126−
127−foldFunction
128− :: forall schema. FromJSON schema
129− => (Text -> IO LBS.ByteString)
130− -> Spec schema
131− -> URISchemaMap schema
132− -> SchemaWithURI schema
133− -> IO (Either Text (URISchemaMap schema))
134−foldFunction fetchRef spec@(Spec _ _ getRef) referenced sw =
135− foldlM f (Right referenced) (includeSubschemas spec sw)
139+ :: forall schema. FromJSON schema
140+ => (Text -> IO LBS.ByteString)
141+ -> FetchInfo schema
142+ -> SchemaWithURI schema
143+ -> IO (Either Text (URISchemaMap schema))
144+referencesMethodAgnostic fetchRef info =
145+ getRecursiveReferences fetchRef info mempty
146+
147+getRecursiveReferences
148+ :: forall schema. FromJSON schema
149+ => (Text -> IO LBS.ByteString)
150+ -> FetchInfo schema
151+ -> URISchemaMap schema
152+ -> SchemaWithURI schema
153+ -> IO (Either Text (URISchemaMap schema))
154+getRecursiveReferences fetchRef info referenced sw =
155+ foldM f (Right referenced) (includeSubschemas info sw)
136156 where
137157 f :: Either Text (URISchemaMap schema)
138158 -> SchemaWithURI schema
139159 -> IO (Either Text (URISchemaMap schema))
140160 f (Left e) _ = pure (Left e)
141161 f (Right g) (SchemaWithURI schema mUri) =
142− case newRef of
143− Nothing -> pure (Right g)
144− Just uri -> do
145− bts <- fetchRef uri
146− case eitherDecode bts of
147− Left e -> pure . Left . T.pack $ e
148− Right schm -> foldFunction fetchRef spec (H.insert uri schm g)
149− (SchemaWithURI schm (Just uri))
162+ case newRef of
163+ Nothing -> pure (Right g)
164+ Just uri -> do
165+ bts <- fetchRef uri
166+ case eitherDecode bts of
167+ Left e -> pure . Left . T.pack $ e
168+ Right schm -> getRecursiveReferences
169+ fetchRef info (H.insert uri schm g)
170+ (SchemaWithURI schm (Just uri))
150171 where
151172 newRef :: Maybe Text
152173 newRef
153− | Just (Just uri,_) <- resolveReference mUri <$> getRef schema
154− = case H.lookup uri g of
155− Nothing -> Just uri
156− Just _ -> Nothing
174+ | Just (Just uri,_) <- resolveReference mUri <$> _fiRef info schema
175+ = case H.lookup uri g of
176+ Nothing -> Just uri
177+ Just _ -> Nothing
157178 | otherwise = Nothing
158179
159180-- | Return the schema passed in as an argument, as well as every
160181-- subschema contained within it.
161182includeSubschemas
162− :: forall schema.
163− Spec schema
164− -> SchemaWithURI schema
165− -> [SchemaWithURI schema]
166−includeSubschemas spec@(Spec embedded getId _) (SchemaWithURI schema mUri) =
167− SchemaWithURI schema mUri
168− : (includeSubschemas spec =<< subSchemas)
183+ :: forall schema.
184+ FetchInfo schema
185+ -> SchemaWithURI schema
186+ -> [SchemaWithURI schema]
187+includeSubschemas info (SchemaWithURI schema mUri) =
188+ SchemaWithURI schema mUri
189+ : (includeSubschemas info =<< subSchemas)
169190 where
170191 subSchemas :: [SchemaWithURI schema]
171192 subSchemas =
172− (\a -> SchemaWithURI a (updateResolutionScope mUri (getId schema)))
173− <$> embedded schema
193+ (\a -> SchemaWithURI a (updateResolutionScope mUri (_fiId info schema)))
194+ <$> uncurry (<>) (_fiEmbedded info schema)
src/Data/JsonSchema/Types.hsnew file+38
@@ -0,0 +1,38 @@
1+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2+
3+module Data.JsonSchema.Types where
4+
5+import Prelude
6+import Import
7+
8+import Data.Validator.Failure (Fail)
9+import Data.Validator.Types (Validator(..))
10+
11+newtype Spec schema err
12+ = Spec { _unSpec :: [Validator schema schema err] }
13+
14+-- | Return a schema's immediate subschemas.
15+--
16+-- The first list is subschemas validating the same level of the document,
17+-- the second list is subschemas validating lower levels (see
18+-- 'Data.Validator.Types.Fail' for a full explanation).
19+embedded :: Spec schema a -> schema -> ([schema], [schema])
20+embedded spec schema =
21+ let embeds = (\val -> _embedded val schema) <$> _unSpec spec
22+ in foldl' (\(a,b) (x,y) -> (x <> a, y <> b)) (mempty, mempty) embeds
23+
24+validate
25+ :: Spec schema err
26+ -> schema
27+ -> Value
28+ -> [Fail err]
29+validate spec schema v = _unSpec spec >>= (\val -> _validate val schema v)
30+
31+-- | A basic schema type that doesn't impose much structure.
32+--
33+-- 'Data.JsonSchema.Draft4' doesn't use this, but instead uses the one
34+-- defined in 'Data.JsonSchema.Draft4.Schema' to make it easier to write
35+-- draft 4 schemas in Haskell.
36+newtype Schema
37+ = Schema { _unSchema :: HashMap Text Value }
38+ deriving (Eq, Show, FromJSON, ToJSON)
src/Data/Validator/Draft4.hs+549−6
@@ -1,8 +1,551 @@
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.
15
2−module Data.Validator.Draft4 (module Export) where
6+module Data.Validator.Draft4 where
37
4−import Data.Validator.Draft4.Any as Export
5−import Data.Validator.Draft4.Array as Export
6−import Data.Validator.Draft4.Number as Export
7−import Data.Validator.Draft4.Object as Export
8−import Data.Validator.Draft4.String as Export
8+import Prelude
9+import Import
10+
11+import Data.Aeson.Types (Parser)
12+import qualified Data.HashMap.Strict as HM
13+import Data.List.NonEmpty (NonEmpty)
14+import qualified Data.List.NonEmpty as NE
15+import Data.Maybe (catMaybes, isNothing,
16+ maybe, maybeToList)
17+import Data.Scientific (Scientific)
18+import Data.Text (Text)
19+
20+import qualified Data.Validator.Draft4.Any as AN
21+import qualified Data.Validator.Draft4.Array as AR
22+import qualified Data.Validator.Draft4.Number as NU
23+import qualified Data.Validator.Draft4.Object as OB
24+import qualified Data.Validator.Draft4.String as ST
25+import Data.Validator.Failure (Fail(..))
26+import Data.Validator.Types (Validator(..))
27+import Data.Validator.Utils (fromJSONEither)
28+
29+-- | For internal use.
30+run :: FromJSON b => (a -> b -> [c]) -> Maybe a -> Value -> [c]
31+run _ Nothing _ = mempty
32+run f (Just a) b =
33+ case fromJSONEither b of
34+ Left _ -> mempty
35+ Right c -> f a c
36+
37+-- | For internal use.
38+noEmbedded :: a -> ([b], [b])
39+noEmbedded = const (mempty, mempty)
40+
41+--------------------------------------------------
42+-- * Numbers
43+--------------------------------------------------
44+
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) ()
54+multipleOf =
55+ Validator
56+ noEmbedded
57+ (run (fmap maybeToList . NU.multipleOf . _unMultipleOf))
58+
59+data MaximumContext
60+ = MaximumContext Bool Scientific
61+ deriving (Eq, Show)
62+
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
84+minimumVal =
85+ Validator
86+ noEmbedded
87+ (run (\(MinimumContext a b) -> maybeToList . NU.minimumVal a b))
88+
89+--------------------------------------------------
90+-- * Strings
91+--------------------------------------------------
92+
93+newtype MaxLength
94+ = MaxLength { _unMaxLength :: Int }
95+ deriving (Eq, Show)
96+
97+instance FromJSON MaxLength where
98+ parseJSON = withObject "MaxLength" $ \o ->
99+ MaxLength <$> o .: "maxLength"
100+
101+maxLength :: Validator a (Maybe MaxLength) ()
102+maxLength =
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+
135+--------------------------------------------------
136+-- * Arrays
137+--------------------------------------------------
138+
139+newtype MaxItems
140+ = MaxItems { _unMaxItems :: Int }
141+ deriving (Eq, Show)
142+
143+instance FromJSON MaxItems where
144+ parseJSON = withObject "MaxItems" $ \o ->
145+ MaxItems <$> o .: "maxItems"
146+
147+maxItems :: Validator a (Maybe MaxItems) ()
148+maxItems =
149+ Validator
150+ noEmbedded
151+ (run (fmap maybeToList . AR.maxItems . _unMaxItems))
152+
153+newtype MinItems
154+ = MinItems { _unMinItems :: Int }
155+ deriving (Eq, Show)
156+
157+instance FromJSON MinItems where
158+ parseJSON = withObject "MinItems" $ \o ->
159+ MinItems <$> o .: "minItems"
160+
161+minItems :: Validator a (Maybe MinItems) ()
162+minItems =
163+ Validator
164+ noEmbedded
165+ (run (fmap maybeToList . AR.minItems . _unMinItems))
166+
167+newtype UniqueItems
168+ = UniqueItems { _unUniqueItems :: Bool }
169+ deriving (Eq, Show)
170+
171+instance FromJSON UniqueItems where
172+ parseJSON = withObject "UniqueItems" $ \o ->
173+ UniqueItems <$> o .: "uniqueItems"
174+
175+uniqueItems :: Validator a (Maybe UniqueItems) ()
176+uniqueItems =
177+ Validator
178+ noEmbedded
179+ (run (fmap maybeToList . AR.uniqueItems . _unUniqueItems))
180+
181+data ItemsContext schema =
182+ ItemsContext
183+ (Maybe (AR.AdditionalItems schema))
184+ (AR.Items schema)
185+ deriving (Eq, Show)
186+
187+instance FromJSON schema => FromJSON (ItemsContext schema) where
188+ parseJSON = withObject "ItemsContext" $ \o -> ItemsContext
189+ <$> o .:! "additionalItems"
190+ <*> o .: "items"
191+
192+items
193+ :: (schema -> Value -> [Fail err])
194+ -> Validator schema (Maybe (ItemsContext schema)) (AR.ItemsInvalid err)
195+items f =
196+ Validator
197+ (\a -> case a of
198+ Nothing -> mempty
199+ Just (ItemsContext _ b) ->
200+ case b of
201+ AR.ItemsObject c -> (mempty, pure c)
202+ AR.ItemsArray cs -> (mempty, cs))
203+ (run (\(ItemsContext a b) -> AR.items f a b))
204+
205+newtype AdditionalItemsContext schema
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+
214+-- | Since 'items' will always take care of validating 'additionalItems'
215+-- as well, the actual validation side of 'additionalItemsEmbedded' is
216+-- disabled.
217+additionalItemsEmbedded
218+ :: Validator
219+ schema
220+ (Maybe (AdditionalItemsContext schema))
221+ err
222+additionalItemsEmbedded=
223+ Validator
224+ (\a -> case a of
225+ Just (AdditionalItemsContext (AR.AdditionalObject b)) ->
226+ (mempty, pure b)
227+ _ -> (mempty, mempty))
228+ (const (const mempty))
229+
230+newtype Definitions schema
231+ = Definitions { _unDefinitions :: HashMap Text schema }
232+ deriving (Eq, Show)
233+
234+instance FromJSON schema => FromJSON (Definitions schema) where
235+ parseJSON = withObject "Definitions" $ \o ->
236+ Definitions <$> o .: "definitions"
237+
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
242+-- @"additionalItems"@ and this.
243+definitionsEmbedded
244+ :: Validator
245+ schema
246+ (Maybe (Definitions schema))
247+ err
248+definitionsEmbedded =
249+ Validator
250+ (\a -> case a of
251+ Just (Definitions b) -> (mempty, HM.elems b)
252+ Nothing -> (mempty, mempty))
253+ (const (const mempty))
254+
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+--------------------------------------------------
427+-- * Any
428+--------------------------------------------------
429+
430+newtype Ref
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)
440+ => AN.VisitedSchemas
441+ -> Maybe Text
442+ -> (Maybe Text -> Maybe schema)
443+ -> (AN.VisitedSchemas -> Maybe Text -> schema -> Value -> [Fail err])
444+ -> Validator a (Maybe Ref) (AN.RefInvalid err)
445+ref visited scope getRef f =
446+ Validator
447+ noEmbedded
448+ (run (AN.ref visited scope getRef f . _unRef))
449+
450+newtype EnumContext
451+ = EnumContext { _unEnumContext :: AN.EnumVal }
452+ deriving (Eq, Show)
453+
454+instance FromJSON EnumContext where
455+ parseJSON = withObject "EnumContext" $ \o ->
456+ EnumContext <$> o .: "enum"
457+
458+enumVal :: Validator a (Maybe EnumContext) ()
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])
488+ -> Validator schema (Maybe (AllOf schema)) err
489+allOf f =
490+ Validator
491+ (\a -> case a of
492+ Just (AllOf b) -> (NE.toList b, mempty)
493+ Nothing -> (mempty, mempty))
494+ (run (AN.allOf f . _unAllOf))
495+
496+newtype AnyOf schema
497+ = AnyOf { _unAnyOf :: NonEmpty schema }
498+ deriving (Eq, Show)
499+
500+instance FromJSON schema => FromJSON (AnyOf schema) where
501+ parseJSON = withObject "AnyOf" $ \o ->
502+ AnyOf <$> o .: "anyOf"
503+
504+anyOf
505+ :: ToJSON schema
506+ => (schema -> Value -> [Fail err])
507+ -> Validator schema (Maybe (AnyOf schema)) ()
508+anyOf f =
509+ Validator
510+ (\a -> case a of
511+ Just (AnyOf b) -> (NE.toList b, mempty)
512+ Nothing -> (mempty, mempty))
513+ (run (fmap maybeToList . AN.anyOf f . _unAnyOf))
514+
515+newtype OneOf schema
516+ = OneOf { _unOneOf :: NonEmpty schema }
517+ deriving (Eq, Show)
518+
519+instance FromJSON schema => FromJSON (OneOf schema) where
520+ parseJSON = withObject "OneOf" $ \o ->
521+ OneOf <$> o .: "oneOf"
522+
523+oneOf
524+ :: ToJSON schema
525+ => (schema -> Value -> [Fail err])
526+ -> Validator schema (Maybe (OneOf schema)) ()
527+oneOf f =
528+ Validator
529+ (\a -> case a of
530+ Just (OneOf b) -> (NE.toList b, mempty)
531+ Nothing -> (mempty, mempty))
532+ (run (fmap maybeToList . AN.oneOf f . _unOneOf))
533+
534+newtype NotVal schema
535+ = NotVal { _unNotVal :: schema }
536+ deriving (Eq, Show)
537+
538+instance FromJSON schema => FromJSON (NotVal schema) where
539+ parseJSON = withObject "NotVal" $ \o ->
540+ NotVal <$> o .: "not"
541+
542+notVal
543+ :: ToJSON schema
544+ => (schema -> Value -> [Fail err])
545+ -> Validator schema (Maybe (NotVal schema)) ()
546+notVal f =
547+ Validator
548+ (\a -> case a of
549+ Just (NotVal b) -> (pure b, mempty)
550+ Nothing -> (mempty, mempty))
551+ (run (fmap maybeToList . AN.notVal f . _unNotVal))
src/Data/Validator/Draft4/Any.hs+151−110
@@ -1,41 +1,66 @@
1+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
12
23module Data.Validator.Draft4.Any where
34
5+import Import
6+-- Hiding is for GHCs before 7.10:
7+import Prelude hiding (any, elem)
8+
49import Control.Monad
510import Data.Aeson.Types (Parser)
611import Data.List.NonEmpty (NonEmpty)
7−import qualified Data.List.NonEmpty as N
12+import qualified Data.List.NonEmpty as NE
813import Data.Maybe
9−import Data.Scientific
14+import qualified Data.Scientific as SCI
1015import Data.Set (Set)
1116import qualified Data.Set as S
1217
13−import Data.Validator.Failure
14−import Data.Validator.Utils
15−import Data.Validator.Reference (resolveFragment, resolveReference)
16−import Import
17−
18−-- For GHCs before 7.10:
19−import Prelude hiding (any, elem)
18+import Data.Validator.Failure (Fail(..))
19+import qualified Data.Validator.Utils as UT
20+import Data.Validator.Reference (URIBaseAndFragment,
21+ resolveFragment, resolveReference)
2022
2123--------------------------------------------------
2224-- * $ref
2325--------------------------------------------------
2426
25−-- | Will return 'Nothing' if the reference can't be resolved.
27+data RefInvalid err
28+ = RefResolution
29+ | RefLoop
30+ | RefInvalid err
31+ deriving (Eq, Show)
32+
33+newtype VisitedSchemas
34+ = VisitedSchemas { _unVisited :: [URIBaseAndFragment] }
35+ deriving (Eq, Show, Monoid)
36+
2637ref
27− :: forall err schema. (FromJSON schema, ToJSON schema, Show schema)
28− => Maybe Text
29− -> (Maybe Text -> Maybe schema)
30− -> (Maybe Text -> schema -> Value -> [Failure err])
31− -> Text
32− -> Value
33− -> Maybe [Failure err]
34−ref scope getRef f reference x = do
35− let (mUri, mFragment) = resolveReference scope reference
36− schema <- getRef mUri
37− s <- resolveFragment mFragment schema
38− Just (f mUri s x)
38+ :: forall err schema. (FromJSON schema, ToJSON schema)
39+ => VisitedSchemas
40+ -> Maybe Text
41+ -> (Maybe Text -> Maybe schema)
42+ -> (VisitedSchemas -> Maybe Text -> schema -> Value -> [Fail err])
43+ -> Text
44+ -> Value
45+ -> [Fail (RefInvalid err)]
46+ref visited scope getRef f reference x
47+ | (mUri, mFragment) `elem` _unVisited visited =
48+ pure $ Failure RefLoop (toJSON reference) mempty x
49+ | otherwise =
50+ case getRef mUri of
51+ Nothing -> pure failureDNE
52+ Just schema ->
53+ case resolveFragment mFragment schema of
54+ Nothing -> pure failureDNE
55+ Just s ->
56+ let newVisited = (VisitedSchemas [(mUri, mFragment)]
57+ <> visited)
58+ in fmap RefInvalid <$> f newVisited mUri s x
59+ where
60+ (mUri, mFragment) = resolveReference scope reference
61+
62+ failureDNE :: Fail (RefInvalid err)
63+ failureDNE = Failure RefResolution (toJSON reference) mempty x
3964
4065--------------------------------------------------
4166-- * enum
@@ -51,128 +76,144 @@ ref scope getRef f reference x = do
5176-- NOTE: We don't enforce the uniqueness constraint in the haskell code,
5277-- but we do in the 'FromJSON' instance.
5378newtype EnumVal
54− = EnumVal { _unEnumVal :: NonEmpty Value }
55− -- Given a choice, we'd prefer to enforce uniqueness through the type
56− -- system over having at least one element. To use a 'Set' though we'd
57− -- have to use 'OrdValue' here (there's no 'Ord' instance for plain Values)
58− -- and we'd rather not make users mess with 'OrdValue'.
59− deriving (Eq, Show)
79+ = EnumVal { _unEnumVal :: NonEmpty Value }
80+ -- Given a choice, we'd prefer to enforce uniqueness through the type
81+ -- system over having at least one element. To use a 'Set' though we'd
82+ -- have to use 'OrdValue' here (there's no 'Ord' instance for plain Values)
83+ -- and we'd rather not make users mess with 'OrdValue'.
84+ deriving (Eq, Show)
6085
6186instance FromJSON EnumVal where
62− parseJSON v = checkUnique . _unNonEmpty' =<< parseJSON v
63− where
64− checkUnique :: NonEmpty Value -> Parser EnumVal
65− checkUnique a
66− | allUniqueValues' a = pure (EnumVal a)
67− | otherwise = fail "All elements of the Enum validator must be unique."
87+ parseJSON v = checkUnique . UT._unNonEmpty' =<< parseJSON v
88+ where
89+ checkUnique :: NonEmpty Value -> Parser EnumVal
90+ checkUnique a
91+ | UT.allUniqueValues' a = pure (EnumVal a)
92+ | otherwise = fail "All elements of the Enum validator must be unique."
6893
6994instance ToJSON EnumVal where
70− toJSON = toJSON . NonEmpty' . _unEnumVal
95+ toJSON = toJSON . UT.NonEmpty' . _unEnumVal
7196
7297instance Arbitrary EnumVal where
73− arbitrary = do
74− xs <- traverse (const arbitraryValue) =<< (arbitrary :: Gen [()])
75− case N.nonEmpty (toUnique xs) of
76− Nothing -> EnumVal . pure <$> arbitraryValue
77− Just ne -> pure (EnumVal ne)
78− where
79− toUnique :: [Value] -> [Value]
80− toUnique = fmap _unOrdValue . S.toList . S.fromList . fmap OrdValue
81−
82−enumVal :: EnumVal -> Value -> Maybe (Failure ())
98+ arbitrary = do
99+ xs <- traverse (const UT.arbitraryValue) =<< (arbitrary :: Gen [()])
100+ case NE.nonEmpty (toUnique xs) of
101+ Nothing -> EnumVal . pure <$> UT.arbitraryValue
102+ Just ne -> pure (EnumVal ne)
103+ where
104+ toUnique :: [Value] -> [Value]
105+ toUnique = fmap UT._unOrdValue . S.toList . S.fromList . fmap UT.OrdValue
106+
107+enumVal :: EnumVal -> Value -> Maybe (Fail ())
83108enumVal (EnumVal vs) x
84− | not (allUniqueValues' vs) = Nothing
85− | x `elem` vs = Nothing
86− | otherwise =
87− Just $ Invalid () (toJSON (NonEmpty' vs)) mempty
109+ | not (UT.allUniqueValues' vs) = Nothing
110+ | x `elem` vs = Nothing
111+ | otherwise = Just $ Failure () (toJSON (UT.NonEmpty' vs))
112+ mempty x
88113
89114--------------------------------------------------
90115-- * type
91116--------------------------------------------------
92117
93118data TypeVal
94− = TypeValString Text
95− | TypeValArray (Set Text)
96− deriving (Eq, Show)
119+ = TypeValString Text
120+ | TypeValArray (Set Text)
121+ deriving (Eq, Show)
97122
98123instance FromJSON TypeVal where
99− parseJSON v = fmap TypeValString (parseJSON v)
100− <|> fmap TypeValArray (parseJSON v)
124+ parseJSON v = fmap TypeValString (parseJSON v)
125+ <|> fmap TypeValArray (parseJSON v)
101126
102127instance ToJSON TypeVal where
103− toJSON (TypeValString t) = toJSON t
104− toJSON (TypeValArray ts) = toJSON ts
128+ toJSON (TypeValString t) = toJSON t
129+ toJSON (TypeValArray ts) = toJSON ts
105130
106131instance Arbitrary TypeVal where
107− arbitrary = oneof [ TypeValString <$> arbitraryText
108− , TypeValArray <$> arbitrarySetOfText
109− ]
110−
111−typeVal :: TypeVal -> Value -> Maybe (Failure ())
112−typeVal (TypeValString t) x = isJsonType x (S.singleton t)
113−typeVal (TypeValArray ts) x = isJsonType x ts
114−
115−isJsonType :: Value -> Set Text -> Maybe (Failure ())
116−isJsonType x ts
117− | S.null (S.intersection okTypes ts) = Just (Invalid () (toJSON ts) mempty)
118− | otherwise = Nothing
132+ arbitrary = oneof [ TypeValString <$> UT.arbitraryText
133+ , TypeValArray <$> UT.arbitrarySetOfText
134+ ]
135+
136+setFromTypeVal :: TypeVal -> Set Text
137+setFromTypeVal (TypeValString t) = S.singleton t
138+setFromTypeVal (TypeValArray ts) = ts
139+
140+typeVal :: TypeVal -> Value -> Maybe (Fail ())
141+typeVal tv x
142+ | S.null matches = Just (Failure () (toJSON tv) mempty x)
143+ | otherwise = Nothing
119144 where
145+ -- There can be more than one match because a 'Value' can be both a
146+ -- @"number"@ and an @"integer"@.
147+ matches :: Set Text
148+ matches = S.intersection okTypes (setFromTypeVal tv)
149+
120150 okTypes :: Set Text
121151 okTypes =
122− case x of
123− Null -> S.singleton "null"
124− (Array _) -> S.singleton "array"
125− (Bool _) -> S.singleton "boolean"
126− (Object _) -> S.singleton "object"
127− (String _) -> S.singleton "string"
128− (Number y) ->
129− case toBoundedInteger y :: Maybe Int of
130− Nothing -> S.singleton "number"
131− Just _ -> S.fromList ["number", "integer"]
152+ case x of
153+ Null -> S.singleton "null"
154+ (Array _) -> S.singleton "array"
155+ (Bool _) -> S.singleton "boolean"
156+ (Object _) -> S.singleton "object"
157+ (String _) -> S.singleton "string"
158+ (Number y) ->
159+ if SCI.isInteger y
160+ then S.fromList ["number", "integer"]
161+ else S.singleton "number"
132162
133163--------------------------------------------------
134−-- * other
164+-- * allOf
135165--------------------------------------------------
136166
137167allOf
138− :: (schema -> Value -> [Failure err])
139− -> NonEmpty schema
140− -> Value
141− -> [Failure err]
142−allOf f subSchemas x = N.toList subSchemas >>= flip f x
168+ :: (schema -> Value -> [Fail err])
169+ -> NonEmpty schema
170+ -> Value
171+ -> [Fail err]
172+allOf f subSchemas x = NE.toList subSchemas >>= flip f x
173+
174+--------------------------------------------------
175+-- * anyOf
176+--------------------------------------------------
143177
144178anyOf
145− :: ToJSON schema
146− => (schema -> Value -> [Failure err])
147− -> NonEmpty schema
148− -> Value
149− -> Maybe (Failure ())
179+ :: ToJSON schema
180+ => (schema -> Value -> [Fail err])
181+ -> NonEmpty schema
182+ -> Value
183+ -> Maybe (Fail ())
150184anyOf f subSchemas x
151− | any null (flip f x <$> subSchemas) = Nothing
152− | otherwise = Just $ Invalid () (toJSON (NonEmpty' subSchemas)) mempty
185+ | any null (flip f x <$> subSchemas) = Nothing
186+ | otherwise = Just $ Failure () (toJSON (UT.NonEmpty' subSchemas))
187+ mempty x
188+
189+--------------------------------------------------
190+-- * oneOf
191+--------------------------------------------------
153192
154193oneOf
155− :: forall err schema. ToJSON schema
156− => (schema -> Value -> [Failure err])
157− -> NonEmpty schema
158− -> Value
159− -> Maybe (Failure ())
194+ :: forall err schema. ToJSON schema
195+ => (schema -> Value -> [Fail err])
196+ -> NonEmpty schema
197+ -> Value
198+ -> Maybe (Fail ())
160199oneOf f subSchemas x
161− | length successes == 1 = Nothing
162− | otherwise = Just $ Invalid ()
163− (toJSON (NonEmpty' subSchemas))
164− mempty
200+ | length successes == 1 = Nothing
201+ | otherwise = Just $ Failure () (toJSON (UT.NonEmpty' subSchemas)) mempty x
165202 where
166− successes :: [[Failure err]]
167− successes = filter null $ flip f x <$> N.toList subSchemas
203+ successes :: [[Fail err]]
204+ successes = filter null $ flip f x <$> NE.toList subSchemas
205+
206+--------------------------------------------------
207+-- * not
208+--------------------------------------------------
168209
169210notVal
170− :: ToJSON schema
171− => (schema -> Value -> [Failure err])
172− -> schema
173− -> Value
174− -> Maybe (Failure ())
211+ :: ToJSON schema
212+ => (schema -> Value -> [Fail err])
213+ -> schema
214+ -> Value
215+ -> Maybe (Fail ())
175216notVal f schema x =
176− case f schema x of
177− [] -> Just (Invalid () (toJSON schema) mempty)
178− _ -> Nothing
217+ case f schema x of
218+ [] -> Just (Failure () (toJSON schema) mempty x)
219+ _ -> Nothing
src/Data/Validator/Draft4/Array.hs+110−94
@@ -1,168 +1,184 @@
11
22module Data.Validator.Draft4.Array where
33
4+import Import
5+import Prelude
6+
47import Control.Monad
5−import qualified Data.Aeson.Pointer as P
8+import qualified Data.Aeson.Pointer as AP
69import qualified Data.Text as T
710import qualified Data.Vector as V
811import Text.Read (readMaybe)
912
10−import Data.Validator.Failure
13+import Data.Validator.Failure (Fail(..), prependToPath)
1114import Data.Validator.Utils (allUniqueValues)
12−import Import
1315
14−-- | The spec requires "maxItems" to be non-negative.
15−maxItems :: Int -> Vector Value -> Maybe (Failure ())
16+--------------------------------------------------
17+-- * maxItems
18+--------------------------------------------------
19+
20+-- | The spec requires @"maxItems"@ to be non-negative.
21+maxItems :: Int -> Vector Value -> Maybe (Fail ())
1622maxItems n xs
17− | n < 0 = Nothing
18− | V.length xs > n = Just (Invalid () (toJSON n) mempty)
19− | otherwise = Nothing
23+ | n < 0 = Nothing
24+ | V.length xs > n = Just (Failure () (toJSON n) mempty (Array xs))
25+ | otherwise = Nothing
2026
21−-- | The spec requires "minItems" to be non-negative.
22−minItems :: Int -> Vector Value -> Maybe (Failure ())
27+--------------------------------------------------
28+-- * minItems
29+--------------------------------------------------
30+
31+-- | The spec requires @"minItems"@ to be non-negative.
32+minItems :: Int -> Vector Value -> Maybe (Fail ())
2333minItems n xs
24− | n < 0 = Nothing
25− | V.length xs < n = Just (Invalid () (toJSON n) mempty)
26− | otherwise = Nothing
34+ | n < 0 = Nothing
35+ | V.length xs < n = Just (Failure () (toJSON n) mempty (Array xs))
36+ | otherwise = Nothing
37+
38+--------------------------------------------------
39+-- * uniqueItems
40+--------------------------------------------------
2741
28−uniqueItems :: Bool -> Vector Value -> Maybe (Failure ())
42+uniqueItems :: Bool -> Vector Value -> Maybe (Fail ())
2943uniqueItems True xs
30− | allUniqueValues xs = Nothing
31− | otherwise = Just (Invalid () (Bool True) mempty)
44+ | allUniqueValues xs = Nothing
45+ | otherwise = Just (Failure () (Bool True) mempty (Array xs))
3246uniqueItems False _ = Nothing
3347
3448--------------------------------------------------
3549-- * items
3650--------------------------------------------------
3751
38−data ItemsInvalid err
39− = Items err
40− | AdditionalItemsBoolInvalid
41− | AdditionalItemsObjectInvalid err
42− deriving (Eq, Show)
43−
4452data Items schema
45− = ItemsObject schema
46− | ItemsArray [schema]
47− deriving (Eq, Show)
53+ = ItemsObject schema
54+ | ItemsArray [schema]
55+ deriving (Eq, Show)
4856
4957instance FromJSON schema => FromJSON (Items schema) where
50− parseJSON v = fmap ItemsObject (parseJSON v)
51− <|> fmap ItemsArray (parseJSON v)
58+ parseJSON v = fmap ItemsObject (parseJSON v)
59+ <|> fmap ItemsArray (parseJSON v)
5260
5361instance ToJSON schema => ToJSON (Items schema) where
54− toJSON (ItemsObject hm) = toJSON hm
55− toJSON (ItemsArray schemas) = toJSON schemas
62+ toJSON (ItemsObject hm) = toJSON hm
63+ toJSON (ItemsArray schemas) = toJSON schemas
5664
5765instance Arbitrary schema => Arbitrary (Items schema) where
58− arbitrary = oneof [ ItemsObject <$> arbitrary
59− , ItemsArray <$> arbitrary
60− ]
66+ arbitrary = oneof [ ItemsObject <$> arbitrary
67+ , ItemsArray <$> arbitrary
68+ ]
69+
70+data ItemsInvalid err
71+ = Items err
72+ | AdditionalItemsBoolInvalid
73+ | AdditionalItemsObjectInvalid err
74+ deriving (Eq, Show)
6175
6276items
63− :: forall err schema.
64− (schema -> Value -> [Failure err])
65− -> Maybe (AdditionalItems schema)
66− -> Items schema
67− -> Vector Value
68− -> [Failure (ItemsInvalid err)]
69−items f _ (ItemsObject subSchema) xs = zip [0..] (V.toList xs) >>= g
77+ :: forall err schema.
78+ (schema -> Value -> [Fail err])
79+ -> Maybe (AdditionalItems schema)
80+ -> Items schema
81+ -> Vector Value
82+ -> [Fail (ItemsInvalid err)]
83+items f _ (ItemsObject subSchema) xs =
84+ zip [0..] (V.toList xs) >>= g
7085 where
71− g :: (Int, Value) -> [Failure (ItemsInvalid err)]
72− g (index,x) = modFailure Items
73− . addToPath (P.Token (T.pack (show index)))
86+ g :: (Int, Value) -> [Fail (ItemsInvalid err)]
87+ g (index,x) = fmap Items
88+ . prependToPath (AP.Token (T.pack (show index)))
7489 <$> f subSchema x
7590
76−items f mAdditional (ItemsArray subSchemas) xs = itemFailures
77− <> additionalItemFailures
91+items f mAdditional (ItemsArray subSchemas) xs =
92+ itemFailures <> additionalItemFailures
7893 where
7994 indexedValues :: [(Int, Value)]
8095 indexedValues = zip [0..] (V.toList xs)
8196
82− itemFailures :: [Failure (ItemsInvalid err)]
97+ itemFailures :: [Fail (ItemsInvalid err)]
8398 itemFailures = join (zipWith g subSchemas indexedValues)
8499 where
85− g :: schema -> (Int, Value) -> [Failure (ItemsInvalid err)]
86− g schema (index,x) = modFailure Items
87− . addToPath (P.Token (T.pack (show index)))
100+ g :: schema -> (Int, Value) -> [Fail (ItemsInvalid err)]
101+ g schema (index,x) = fmap Items
102+ . prependToPath (AP.Token (T.pack (show index)))
88103 <$> f schema x
89104
90− additionalItemFailures :: [Failure (ItemsInvalid err)]
105+ additionalItemFailures :: [Fail (ItemsInvalid err)]
91106 additionalItemFailures =
92− case mAdditional of
93− Nothing -> mempty
94− Just adi -> modFailure correctName
95− . correctIndexes
96− <$> additionalItems f adi extras
107+ case mAdditional of
108+ Nothing -> mempty
109+ Just adi -> fmap correctName
110+ . correctIndexes
111+ <$> additionalItems f adi extras
97112 where
98113 -- It's not great that we convert back to Vector again.
99114 extras :: Vector Value
100− extras =
101− V.fromList . fmap snd . drop (length subSchemas) $ indexedValues
115+ extras = V.fromList . fmap snd
116+ . drop (length subSchemas) $ indexedValues
102117
103118 -- Since 'additionalItems' only sees part of the array, but starts
104119 -- indexing from zero, we need to modify the paths it reports to
105120 -- represent invalid data so they actually represent the correct
106121 -- offsets.
107122 correctIndexes
108− :: Failure (AdditionalItemsInvalid err)
109− -> Failure (AdditionalItemsInvalid err)
110− correctIndexes (Invalid a b c) = Invalid a b (fixIndex c)
123+ :: Fail (AdditionalItemsInvalid err)
124+ -> Fail (AdditionalItemsInvalid err)
125+ correctIndexes (Failure a b c d) = Failure a b (fixIndex c) d
111126 where
112− fixIndex :: P.Pointer -> P.Pointer
113− fixIndex (P.Pointer (tok:toks)) =
114− case readMaybe . T.unpack . P._unToken $ tok of
115− Nothing -> P.Pointer $ tok:toks
116− Just n -> P.Pointer $
117− (P.Token . T.pack . show $ n + length subSchemas):toks
118− fixIndex (P.Pointer []) = P.Pointer []
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 []
119134
120135 correctName :: AdditionalItemsInvalid err -> ItemsInvalid err
121136 correctName AdditionalBoolInvalid = AdditionalItemsBoolInvalid
122137 correctName (AdditionalObjectInvalid err) =
123− AdditionalItemsObjectInvalid err
138+ AdditionalItemsObjectInvalid err
124139
125140--------------------------------------------------
126141-- * additionalItems
127142--------------------------------------------------
128143
129−data AdditionalItemsInvalid err
130− = AdditionalBoolInvalid
131− | AdditionalObjectInvalid err
132− deriving (Eq, Show)
133−
134144data AdditionalItems schema
135− = AdditionalBool Bool
136− | AdditionalObject schema
137− deriving (Eq, Show)
145+ = AdditionalBool Bool
146+ | AdditionalObject schema
147+ deriving (Eq, Show)
138148
139149instance FromJSON schema => FromJSON (AdditionalItems schema) where
140− parseJSON v = fmap AdditionalBool (parseJSON v)
141− <|> fmap AdditionalObject (parseJSON v)
150+ parseJSON v = fmap AdditionalBool (parseJSON v)
151+ <|> fmap AdditionalObject (parseJSON v)
142152
143153instance ToJSON schema => ToJSON (AdditionalItems schema) where
144− toJSON (AdditionalBool b) = toJSON b
145− toJSON (AdditionalObject hm) = toJSON hm
154+ toJSON (AdditionalBool b) = toJSON b
155+ toJSON (AdditionalObject hm) = toJSON hm
146156
147157instance Arbitrary schema => Arbitrary (AdditionalItems schema) where
148− arbitrary = oneof [ AdditionalBool <$> arbitrary
149− , AdditionalObject <$> arbitrary
150− ]
158+ arbitrary = oneof [ AdditionalBool <$> arbitrary
159+ , AdditionalObject <$> arbitrary
160+ ]
161+
162+data AdditionalItemsInvalid err
163+ = AdditionalBoolInvalid
164+ | AdditionalObjectInvalid err
165+ deriving (Eq, Show)
151166
152167additionalItems
153− :: forall err schema.
154− (schema -> Value -> [Failure err])
155− -> AdditionalItems schema
156− -> Vector Value
157− -> [Failure (AdditionalItemsInvalid err)]
168+ :: forall err schema.
169+ (schema -> Value -> [Fail err])
170+ -> AdditionalItems schema
171+ -> Vector Value
172+ -> [Fail (AdditionalItemsInvalid err)]
158173additionalItems _ (AdditionalBool b) xs
159− | b = mempty
160− | V.length xs > 0 = pure (Invalid AdditionalBoolInvalid (Bool b) mempty)
161− | otherwise = mempty
174+ | b = mempty
175+ | V.length xs > 0 = pure (Failure AdditionalBoolInvalid (Bool b)
176+ mempty (toJSON xs))
177+ | otherwise = mempty
162178additionalItems f (AdditionalObject subSchema) xs =
163− zip [0..] (V.toList xs) >>= g
179+ zip [0..] (V.toList xs) >>= g
164180 where
165− g :: (Int, Value) -> [Failure (AdditionalItemsInvalid err)]
166− g (index,x) = modFailure AdditionalObjectInvalid
167− . addToPath (P.Token (T.pack (show index)))
181+ g :: (Int, Value) -> [Fail (AdditionalItemsInvalid err)]
182+ g (index,x) = fmap AdditionalObjectInvalid
183+ . prependToPath (AP.Token (T.pack (show index)))
168184 <$> f subSchema x
src/Data/Validator/Draft4/Number.hs+48−34
@@ -1,51 +1,65 @@
11
22module Data.Validator.Draft4.Number where
33
4+import Import
5+import Prelude
6+
47import Data.Fixed (mod')
5−import Data.Scientific
8+import Data.Scientific (Scientific)
69
7−import Data.Validator.Failure
8−import Import
10+import Data.Validator.Failure (Fail(..))
911
10−-- | The spec requires "multipleOf" to be positive.
11−multipleOf :: Scientific -> Scientific -> Maybe (Failure ())
12+--------------------------------------------------
13+-- * multipleOf
14+--------------------------------------------------
15+
16+-- | The spec requires @"multipleOf"@ to be positive.
17+multipleOf :: Scientific -> Scientific -> Maybe (Fail ())
1218multipleOf n x
13− | n <= 0 = Nothing
14− | x `mod'` n /= 0 = Just (Invalid () (toJSON n) mempty)
15− | otherwise = Nothing
19+ | n <= 0 = Nothing
20+ | x `mod'` n /= 0 = Just (Failure () (toJSON n) mempty (Number x))
21+ | otherwise = Nothing
22+
23+--------------------------------------------------
24+-- * maximum
25+--------------------------------------------------
1626
1727data MaximumInvalid
18− = Maximum
19− | ExclusiveMaximum
20− deriving (Eq, Show)
28+ = Maximum
29+ | ExclusiveMaximum
30+ deriving (Eq, Show)
2131
2232maximumVal
23− :: Bool
24− -> Scientific
25− -> Scientific
26− -> Maybe (Failure MaximumInvalid)
27−maximumVal exclusiveMaximum n x
28− | x `greaterThan` n = Just (Invalid err (toJSON n) mempty)
29− | otherwise = Nothing
33+ :: Bool
34+ -> Scientific
35+ -> Scientific
36+ -> Maybe (Fail MaximumInvalid)
37+maximumVal exclusive n x
38+ | x `greaterThan` n = Just (Failure err (toJSON n) mempty (Number x))
39+ | otherwise = Nothing
3040 where
31− (greaterThan, err) = if exclusiveMaximum
32− then ((>=), ExclusiveMaximum)
33− else ((>), Maximum)
41+ (greaterThan, err) = if exclusive
42+ then ((>=), ExclusiveMaximum)
43+ else ((>), Maximum)
44+
45+--------------------------------------------------
46+-- * minimum
47+--------------------------------------------------
3448
3549data MinimumInvalid
36− = Minimum
37− | ExclusiveMinimum
38− deriving (Eq, Show)
50+ = Minimum
51+ | ExclusiveMinimum
52+ deriving (Eq, Show)
3953
4054minimumVal
41− :: Bool
42− -> Scientific
43− -> Scientific
44− -> Maybe (Failure MinimumInvalid)
45−minimumVal exclusiveMinimum n x
46− | x `lessThan` n = Just (Invalid err (toJSON n) mempty)
47− | otherwise = Nothing
55+ :: Bool
56+ -> Scientific
57+ -> Scientific
58+ -> Maybe (Fail MinimumInvalid)
59+minimumVal exclusive n x
60+ | x `lessThan` n = Just (Failure err (toJSON n) mempty (Number x))
61+ | otherwise = Nothing
4862 where
49− (lessThan, err) = if exclusiveMinimum
50− then ((<=), ExclusiveMinimum)
51− else ((<), Minimum)
63+ (lessThan, err) = if exclusive
64+ then ((<=), ExclusiveMinimum)
65+ else ((<), Minimum)
src/Data/Validator/Draft4/Object.hs+81−71
@@ -1,11 +1,15 @@
11{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2−{-# LANGUAGE ScopedTypeVariables #-}
32
43module Data.Validator.Draft4.Object
54 ( module Data.Validator.Draft4.Object
65 , module Data.Validator.Draft4.Object.Properties
76 ) where
87
8+import Import
9+-- Hiding is for GHCs before 7.10:
10+import Prelude hiding (all, concat,
11+ foldl)
12+
913import Data.Aeson.Types (Parser)
1014import qualified Data.HashMap.Strict as H
1115import Data.Set (Set)
@@ -13,27 +17,30 @@ import qualified Data.Set as S
1317import qualified Data.Text as T
1418
1519import Data.Validator.Draft4.Object.Properties
16−import Data.Validator.Failure
20+import Data.Validator.Failure (Fail(..))
1721import Data.Validator.Utils
18−import Import
1922
20−-- For GHCs before 7.10:
21−import Prelude hiding (all, concat,
22− foldl)
23+--------------------------------------------------
24+-- * maxProperties
25+--------------------------------------------------
2326
24−-- | The spec requires "maxProperties" to be non-negative.
25−maxProperties :: Int -> HashMap Text Value -> Maybe (Failure ())
27+-- | The spec requires @"maxProperties"@ to be non-negative.
28+maxProperties :: Int -> HashMap Text Value -> Maybe (Fail ())
2629maxProperties n x
27− | n < 0 = Nothing
28− | H.size x > n = Just (Invalid () (toJSON n) mempty)
29− | otherwise = Nothing
30+ | n < 0 = Nothing
31+ | H.size x > n = Just (Failure () (toJSON n) mempty (Object x))
32+ | otherwise = Nothing
3033
31−-- | The spec requires "minProperties" to be non-negative.
32−minProperties :: Int -> HashMap Text Value -> Maybe (Failure ())
34+--------------------------------------------------
35+-- * minProperties
36+--------------------------------------------------
37+
38+-- | The spec requires @"minProperties"@ to be non-negative.
39+minProperties :: Int -> HashMap Text Value -> Maybe (Fail ())
3340minProperties n x
34− | n < 0 = Nothing
35− | H.size x < n = Just (Invalid () (toJSON n) mempty)
36− | otherwise = Nothing
41+ | n < 0 = Nothing
42+ | H.size x < n = Just (Failure () (toJSON n) mempty (Object x))
43+ | otherwise = Nothing
3744
3845--------------------------------------------------
3946-- * required
@@ -48,39 +55,40 @@ minProperties n x
4855-- We don't enfore that 'Required' has at least one element in the
4956-- haskell code, but we do in the 'FromJSON' instance.
5057newtype Required
51− = Required { _unRequired :: Set Text }
52− deriving (Eq, Show, ToJSON)
58+ = Required { _unRequired :: Set Text }
59+ deriving (Eq, Show, ToJSON)
5360
5461instance FromJSON Required where
55− parseJSON v = checkUnique =<< checkSize =<< parseJSON v
56− where
57− checkSize :: [Text] -> Parser [Text]
58− checkSize a
59− | null a = fail "Required validator must not be empty."
60− | otherwise = pure a
61−
62− checkUnique :: [Text] -> Parser Required
63− checkUnique a =
64− let b = S.fromList a
65− -- NOTE: Can use length instead of S.size in GHC 7.10 or later.
66− in if length a == S.size b
67− then pure (Required b)
68− else fail "All elements of the Required validator must be unique."
62+ parseJSON v = checkUnique =<< checkSize =<< parseJSON v
63+ where
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."
6976
7077instance Arbitrary Required where
71− arbitrary = do
72− x <- arbitraryText -- Guarantee at least one element.
73− xs <- (fmap.fmap) T.pack arbitrary
74− pure . Required . S.fromList $ x:xs
78+ arbitrary = do
79+ x <- arbitraryText -- Guarantee at least one element.
80+ xs <- (fmap.fmap) T.pack arbitrary
81+ pure . Required . S.fromList $ x:xs
7582
76−required :: Required -> HashMap Text Value -> Maybe (Failure ())
83+required :: Required -> HashMap Text Value -> Maybe (Fail ())
7784required (Required ts) x
78− -- NOTE: When we no longer need to support GHCs before 7.10
79− -- we can use null from Prelude throughout the library
80− -- instead of specialized versions.
81− | S.null ts = Nothing
82− | H.null (H.difference hm x) = Nothing
83− | otherwise = Just (Invalid () (toJSON ts) mempty)
85+ -- NOTE: When we no longer need to support GHCs before 7.10
86+ -- we can use null from Prelude throughout the library
87+ -- instead of specialized versions.
88+ | S.null ts = Nothing
89+ | H.null (H.difference hm x) = Nothing
90+ | otherwise = Just (Failure () (toJSON ts)
91+ mempty (Object x))
8492 where
8593 hm :: HashMap Text Bool
8694 hm = foldl (\b a -> H.insert a True b) mempty ts
@@ -89,28 +97,28 @@ required (Required ts) x
8997-- * dependencies
9098--------------------------------------------------
9199
92−data DependencyInvalid err
93− = SchemaDependencyInvalid err
94− | PropertyDependencyInvalid
95− deriving (Eq, Show)
96−
97100data Dependency schema
98− = SchemaDependency schema
99− | PropertyDependency (Set Text)
100− deriving (Eq, Show)
101+ = SchemaDependency schema
102+ | PropertyDependency (Set Text)
103+ deriving (Eq, Show)
101104
102105instance FromJSON schema => FromJSON (Dependency schema) where
103− parseJSON v = fmap SchemaDependency (parseJSON v)
104− <|> fmap PropertyDependency (parseJSON v)
106+ parseJSON v = fmap SchemaDependency (parseJSON v)
107+ <|> fmap PropertyDependency (parseJSON v)
105108
106109instance ToJSON schema => ToJSON (Dependency schema) where
107− toJSON (SchemaDependency schema) = toJSON schema
108− toJSON (PropertyDependency ts) = toJSON ts
110+ toJSON (SchemaDependency schema) = toJSON schema
111+ toJSON (PropertyDependency ts) = toJSON ts
109112
110113instance Arbitrary schema => Arbitrary (Dependency schema) where
111− arbitrary = oneof [ SchemaDependency <$> arbitrary
112− , PropertyDependency <$> arbitrarySetOfText
113− ]
114+ arbitrary = oneof [ SchemaDependency <$> arbitrary
115+ , PropertyDependency <$> arbitrarySetOfText
116+ ]
117+
118+data DependencyInvalid err
119+ = SchemaDependencyInvalid err
120+ | PropertyDependencyInvalid
121+ deriving (Eq, Show)
114122
115123-- | From the spec:
116124-- <http://json-schema.org/latest/json-schema-validation.html#anchor70>
@@ -125,23 +133,25 @@ instance Arbitrary schema => Arbitrary (Dependency schema) where
125133-- > Each element MUST be a string, and elements in the array MUST be unique.
126134-- > This is called a property dependency.
127135dependencies
128− :: forall err schema. (schema -> Value -> [Failure err])
129− -> HashMap Text (Dependency schema)
130− -> HashMap Text Value
131− -> [Failure (DependencyInvalid err)]
136+ :: forall err schema.
137+ (schema -> Value -> [Fail err])
138+ -> HashMap Text (Dependency schema)
139+ -> HashMap Text Value
140+ -> [Fail (DependencyInvalid err)]
132141dependencies f hm x = concat . fmap (uncurry g) . H.toList $ hm
133142 where
134− g :: Text -> Dependency schema -> [Failure (DependencyInvalid err)]
143+ g :: Text -> Dependency schema -> [Fail (DependencyInvalid err)]
135144 g k (SchemaDependency schema)
136− | H.member k x =
137− modFailure SchemaDependencyInvalid <$> f schema (Object x)
138− | otherwise = mempty
145+ | H.member k x =
146+ fmap SchemaDependencyInvalid <$> f schema (Object x)
147+ | otherwise = mempty
139148 g k (PropertyDependency ts)
140− | H.member k x && not allPresent =
141− pure $ Invalid PropertyDependencyInvalid
142− (toJSON (H.singleton k ts))
143− mempty
144− | otherwise = mempty
149+ | H.member k x && not allPresent =
150+ pure $ Failure PropertyDependencyInvalid
151+ (toJSON (H.singleton k ts))
152+ mempty
153+ (Object x)
154+ | otherwise = mempty
145155 where
146156 allPresent :: Bool
147157 allPresent = all (`H.member` x) ts
src/Data/Validator/Draft4/Object/Properties.hs+139−115
@@ -1,190 +1,214 @@
1−{-# LANGUAGE ScopedTypeVariables #-}
21
32module Data.Validator.Draft4.Object.Properties where
43
4+import Import
5+import Prelude
6+
57import Control.Monad
6−import Data.Aeson
7−import qualified Data.Aeson.Pointer as P
8−import qualified Data.HashMap.Strict as H
9−import qualified Data.Text as T
8+import qualified Data.Aeson.Pointer as AP
9+import Data.Functor (($>))
10+import qualified Data.HashMap.Strict as HM
1011import Data.Text.Encoding (encodeUtf8)
1112import qualified Text.Regex.PCRE.Heavy as RE
1213
13−import Data.Validator.Failure
14−import Import
14+import Data.Validator.Failure (Fail(..), prependToPath)
1515
16−newtype Remaining = Remaining { _unRemaining :: HashMap Text Value }
16+-- | For internal use.
17+newtype Remaining
18+ = Remaining { _unRemaining :: HashMap Text Value }
1719
1820--------------------------------------------------
1921-- * properties
2022--------------------------------------------------
2123
2224data PropertiesInvalid err
23− = PropertiesInvalid err
24− | PropPatternInvalid err
25− | PropAdditionalInvalid (AdditionalPropertiesInvalid err)
26− deriving (Eq, Show)
25+ = PropertiesInvalid err
26+ | PropPatternInvalid err
27+ | PropAdditionalInvalid (AdditionalPropertiesInvalid err)
28+ deriving (Eq, Show)
2729
28−-- | In order of what's tried: "properties", "patternProperties",
29−-- "additionalProperties".
30+-- | In order of what's tried: @"properties"@, @"patternProperties"@,
31+-- @"additionalProperties"@.
3032properties
31− :: forall err schema.
32− (schema -> Value -> [Failure err])
33− -> Maybe (HashMap Text schema)
34− -> Maybe (AdditionalProperties schema)
35− -> HashMap Text schema
36− -> HashMap Text Value
37− -> [Failure (PropertiesInvalid err)]
33+ :: forall err schema.
34+ (schema -> Value -> [Fail err])
35+ -> Maybe (HashMap Text schema)
36+ -> Maybe (AdditionalProperties schema)
37+ -> HashMap Text schema
38+ -> HashMap Text Value
39+ -> [Fail (PropertiesInvalid err)]
3840properties f mPat mAdd propertiesHm x =
39− fmap (modFailure PropertiesInvalid) propFailures
40− <> fmap (modFailure PropPatternInvalid) patternFailures
41− <> fmap (modFailure PropAdditionalInvalid) additionalFailures
41+ fmap (fmap PropertiesInvalid) propFailures
42+ <> fmap (fmap PropPatternInvalid) patternFailures
43+ <> fmap (fmap PropAdditionalInvalid) additionalFailures
4244 where
43− propertiesAndUnmatched :: ([Failure err], Remaining)
45+ propertiesAndUnmatched :: ([Fail err], Remaining)
4446 propertiesAndUnmatched = ( failures
45− , Remaining (H.difference x propertiesHm)
47+ , Remaining (HM.difference x propertiesHm)
4648 )
4749 where
48− failures :: [Failure err]
49− failures = H.toList (H.intersectionWith f propertiesHm x)
50− >>= (\(k,vs) -> fmap (addToPath (P.Token k)) vs)
50+ failures :: [Fail err]
51+ failures = HM.toList (HM.intersectionWith f propertiesHm x)
52+ >>= (\(k,vs) -> fmap (prependToPath (AP.Token k)) vs)
5153
5254 (propFailures, remaining1) = propertiesAndUnmatched
5355
54− mPatProp :: Maybe (HashMap Text Value -> ([Failure err], Remaining))
56+ mPatProp :: Maybe (HashMap Text Value -> ([Fail err], Remaining))
5557 mPatProp = patternAndUnmatched f <$> mPat
5658
57− patternFailures :: [Failure err]
59+ patternFailures :: [Fail err]
5860 patternFailures = case mPatProp of
59− Nothing -> mempty
60− Just val -> fst (val x)
61+ Nothing -> mempty
62+ Just val -> fst (val x)
6163
6264 remaining2 :: Remaining
6365 remaining2 = case mPatProp of
64− Nothing -> remaining1
65− Just val -> snd . val . _unRemaining $ remaining1
66+ Nothing -> remaining1
67+ Just val -> snd . val . _unRemaining $ remaining1
6668
67− additionalFailures :: [Failure (AdditionalPropertiesInvalid err)]
68− additionalFailures = case additionalProperties f <$> mAdd of
69− Nothing -> mempty
70− Just val -> val (_unRemaining remaining2)
69+ additionalFailures :: [Fail (AdditionalPropertiesInvalid err)]
70+ additionalFailures =
71+ case mAdd of
72+ Nothing -> mempty
73+ Just a -> additionalProperties f True a (_unRemaining remaining2)
7174
7275--------------------------------------------------
7376-- * patternProperties
7477--------------------------------------------------
7578
7679data PatternPropertiesInvalid err
77− = PPInvalid err
78− | PPAdditionalPropertiesInvalid (AdditionalPropertiesInvalid err)
79− deriving (Eq, Show)
80+ = PPInvalid err
81+ | PPAdditionalPropertiesInvalid (AdditionalPropertiesInvalid err)
82+ deriving (Eq, Show)
8083
8184patternProperties
82− :: forall err schema.
83− (schema -> Value -> [Failure err])
84− -> Maybe (AdditionalProperties schema)
85− -> HashMap Text schema
86− -> HashMap Text Value
87− -> [Failure (PatternPropertiesInvalid err)]
88−patternProperties f mAdd patternPropertiesHm x =
89− fmap (modFailure PPInvalid) ppFailures
90− <> fmap (modFailure PPAdditionalPropertiesInvalid) addFailures
85+ :: forall err schema.
86+ (schema -> Value -> [Fail err])
87+ -> Bool
88+ -> Maybe (AdditionalProperties schema)
89+ -> HashMap Text schema
90+ -> HashMap Text Value
91+ -> [Fail (PatternPropertiesInvalid err)]
92+patternProperties _ False _ _ _ = mempty
93+patternProperties f _ mAdd patternPropertiesHm x =
94+ (fmap PPInvalid <$> ppFailures)
95+ <> (fmap PPAdditionalPropertiesInvalid <$> addFailures)
9196 where
92− patternProps :: ([Failure err], Remaining)
97+ patternProps :: ([Fail err], Remaining)
9398 patternProps = patternAndUnmatched f patternPropertiesHm x
9499
95100 (ppFailures, remaining) = patternProps
96101
97− addFailures :: [Failure (AdditionalPropertiesInvalid err)]
98− addFailures = case additionalProperties f <$> mAdd of
99− Nothing -> mempty
100− Just val -> val (_unRemaining remaining)
102+ addFailures :: [Fail (AdditionalPropertiesInvalid err)]
103+ addFailures =
104+ case mAdd of
105+ Nothing -> mempty
106+ Just a -> additionalProperties f True a (_unRemaining remaining)
101107
102108patternAndUnmatched
103− :: forall err schema.
104− (schema -> Value -> [Failure err])
105− -> HashMap Text schema
106− -> HashMap Text Value
107− -> ([Failure err], Remaining)
109+ :: forall err schema.
110+ (schema -> Value -> [Fail err])
111+ -> HashMap Text schema
112+ -> HashMap Text Value
113+ -> ([Fail err], Remaining)
108114patternAndUnmatched f patPropertiesHm x =
109− (H.foldlWithKey' runVals mempty perhapsMatches, remaining)
115+ (HM.foldlWithKey' runVals mempty perhapsMatches, remaining)
110116 where
111117 perhapsMatches :: HashMap Text (Value, [schema])
112− perhapsMatches = H.foldlWithKey' (matchingSchemas patPropertiesHm) mempty x
118+ perhapsMatches = HM.foldlWithKey' (matchingSchemas patPropertiesHm) mempty x
113119 where
114120 matchingSchemas
115− :: HashMap Text schema
116− -> HashMap Text (Value, [schema])
117− -> Text
118− -> Value
119− -> HashMap Text (Value, [schema])
121+ :: HashMap Text schema
122+ -> HashMap Text (Value, [schema])
123+ -> Text
124+ -> Value
125+ -> HashMap Text (Value, [schema])
120126 matchingSchemas subSchemas acc k v =
121− H.insert k (v, H.foldlWithKey' (checkKey k) mempty subSchemas) acc
127+ HM.insert k (v, HM.foldlWithKey' (checkKey k) mempty subSchemas) acc
122128
123129 checkKey
124− :: Text
125− -> [schema]
126− -> Text
127− -> schema
128− -> [schema]
130+ :: Text
131+ -> [schema]
132+ -> Text
133+ -> schema
134+ -> [schema]
129135 checkKey k acc r subSchema =
130− case RE.compileM (encodeUtf8 r) mempty of
131− Left _ -> acc
132− Right re -> if k RE.=~ re
133− then pure subSchema <> acc
134− else acc
136+ case RE.compileM (encodeUtf8 r) mempty of
137+ Left _ -> acc
138+ Right re -> if k RE.=~ re
139+ then pure subSchema <> acc
140+ else acc
135141
136142 runVals
137− :: [Failure err]
138− -> Text
139− -> (Value, [schema])
140− -> [Failure err]
143+ :: [Fail err]
144+ -> Text
145+ -> (Value, [schema])
146+ -> [Fail err]
141147 runVals acc k (v,subSchemas) =
142− (subSchemas >>= (\schema -> addToPath (P.Token k) <$> f schema v))
143− <> acc
148+ (subSchemas >>= (\schema -> prependToPath (AP.Token k) <$> f schema v))
149+ <> acc
144150
145151 remaining :: Remaining
146− remaining = Remaining . fmap fst . H.filter (null . snd) $ perhapsMatches
152+ remaining = Remaining . fmap fst . HM.filter (null . snd) $ perhapsMatches
147153
148154--------------------------------------------------
149155-- * additionalProperties
150156--------------------------------------------------
151157
152−data AdditionalPropertiesInvalid err
153− = APBoolInvalid
154− | APObjectInvalid err
155− deriving (Eq, Show)
156−
157158data AdditionalProperties schema
158− = AdditionalPropertiesBool Bool
159− | AdditionalPropertiesObject schema
160− deriving (Eq, Show)
159+ = AdditionalPropertiesBool Bool
160+ | AdditionalPropertiesObject schema
161+ deriving (Eq, Show)
161162
162163instance FromJSON schema => FromJSON (AdditionalProperties schema) where
163− parseJSON v = fmap AdditionalPropertiesBool (parseJSON v)
164− <|> fmap AdditionalPropertiesObject (parseJSON v)
164+ parseJSON v = fmap AdditionalPropertiesBool (parseJSON v)
165+ <|> fmap AdditionalPropertiesObject (parseJSON v)
165166
166167instance ToJSON schema => ToJSON (AdditionalProperties schema) where
167− toJSON (AdditionalPropertiesBool b) = toJSON b
168− toJSON (AdditionalPropertiesObject hm) = toJSON hm
168+ toJSON (AdditionalPropertiesBool b) = toJSON b
169+ toJSON (AdditionalPropertiesObject hm) = toJSON hm
169170
170171instance Arbitrary schema => Arbitrary (AdditionalProperties schema) where
171− arbitrary = oneof [ AdditionalPropertiesBool <$> arbitrary
172− , AdditionalPropertiesObject <$> arbitrary
173− ]
172+ arbitrary = oneof [ AdditionalPropertiesBool <$> arbitrary
173+ , AdditionalPropertiesObject <$> arbitrary
174+ ]
175+
176+data AdditionalPropertiesInvalid err
177+ = APBoolInvalid
178+ | APObjectInvalid err
179+ deriving (Eq, Show)
174180
175181additionalProperties
176− :: forall err schema.
177− (schema -> Value -> [Failure err])
178− -> AdditionalProperties schema
179− -> HashMap Text Value
180− -> [Failure (AdditionalPropertiesInvalid err)]
181−additionalProperties _ (AdditionalPropertiesBool False) x
182− | H.size x > 0 = pure $ Invalid APBoolInvalid (Bool False) mempty
183− | otherwise = mempty
184−additionalProperties _ (AdditionalPropertiesBool True) _ = mempty
185−additionalProperties f (AdditionalPropertiesObject schema) x = H.toList x >>= g
182+ :: forall err schema.
183+ (schema -> Value -> [Fail err])
184+ -> Bool
185+ -> AdditionalProperties schema
186+ -> HashMap Text Value
187+ -> [Fail (AdditionalPropertiesInvalid err)]
188+additionalProperties _ False _ _ = mempty
189+additionalProperties f _ a x =
190+ case a of
191+ AdditionalPropertiesBool b ->
192+ ($> APBoolInvalid) <$> additionalPropertiesBool b x
193+ AdditionalPropertiesObject b ->
194+ fmap APObjectInvalid <$> additionalPropertiesObject f b x
195+
196+additionalPropertiesBool
197+ :: Bool
198+ -> HashMap Text Value
199+ -> [Fail ()]
200+additionalPropertiesBool False x
201+ | HM.size x > 0 = pure $ Failure () (Bool False) mempty (Object x)
202+ | otherwise = mempty
203+additionalPropertiesBool True _ = mempty
204+
205+additionalPropertiesObject
206+ :: forall err schema.
207+ (schema -> Value -> [Fail err])
208+ -> schema
209+ -> HashMap Text Value
210+ -> [Fail err]
211+additionalPropertiesObject f schema x = HM.toList x >>= g
186212 where
187− g :: (Text, Value) -> [Failure (AdditionalPropertiesInvalid err)]
188− g (k,v) = modFailure APObjectInvalid
189− . addToPath (P.Token k)
190− <$> f schema v
213+ g :: (Text, Value) -> [Fail err]
214+ g (k,v) = prependToPath (AP.Token k) <$> f schema v
src/Data/Validator/Draft4/String.hs+32−19
@@ -1,32 +1,45 @@
11
22module Data.Validator.Draft4.String where
33
4−import Data.Aeson
4+import Import
5+import Prelude
6+
57import qualified Data.Text as T
68import Data.Text.Encoding (encodeUtf8)
79import qualified Text.Regex.PCRE.Heavy as RE
810
9−import Data.Validator.Failure
10−import Import
11+import Data.Validator.Failure (Fail(..))
1112
12−-- | The spec requires "maxLength" to be non-negative.
13−maxLength :: Int -> Text -> Maybe (Failure ())
13+--------------------------------------------------
14+-- * maxLength
15+--------------------------------------------------
16+
17+-- | The spec requires @"maxLength"@ to be non-negative.
18+maxLength :: Int -> Text -> Maybe (Fail ())
1419maxLength n x
15− | n <= 0 = Nothing
16− | T.length x > n = Just (Invalid () (toJSON n) mempty)
17− | otherwise = Nothing
20+ | n <= 0 = Nothing
21+ | T.length x > n = Just (Failure () (toJSON n) mempty (String x))
22+ | otherwise = Nothing
23+
24+--------------------------------------------------
25+-- * minLength
26+--------------------------------------------------
1827
19−-- | The spec requires "minLength" to be non-negative.
20−minLength :: Int -> Text -> Maybe (Failure ())
28+-- | The spec requires @"minLength"@ to be non-negative.
29+minLength :: Int -> Text -> Maybe (Fail ())
2130minLength n x
22− | n <= 0 = Nothing
23− | T.length x < n = Just (Invalid () (toJSON n) mempty)
24− | otherwise = Nothing
31+ | n <= 0 = Nothing
32+ | T.length x < n = Just (Failure () (toJSON n) mempty (String x))
33+ | otherwise = Nothing
34+
35+--------------------------------------------------
36+-- * pattern
37+--------------------------------------------------
2538
26−patternVal :: Text -> Text -> Maybe (Failure ())
39+patternVal :: Text -> Text -> Maybe (Fail ())
2740patternVal t x =
28− case RE.compileM (encodeUtf8 t) mempty of
29− Left _ -> Just (Invalid () (toJSON t) mempty)
30− Right re -> if x RE.=~ re
31− then Nothing
32− else Just (Invalid () (toJSON t) mempty)
41+ case RE.compileM (encodeUtf8 t) mempty of
42+ Left _ -> Just (Failure () (toJSON t) mempty (String x))
43+ Right re -> if x RE.=~ re
44+ then Nothing
45+ else Just (Failure () (toJSON t) mempty (String x))
src/Data/Validator/Failure.hs+33−26
@@ -1,42 +1,49 @@
1−{-# LANGUAGE ScopedTypeVariables #-}
1+{-# LANGUAGE DeriveFunctor #-}
22
33module Data.Validator.Failure where
44
5−import qualified Data.Aeson.Pointer as P
6−
75import Import
8−
9−-- For GHCs before 7.10:
6+-- Hiding is for GHCs before 7.10:
107import Prelude hiding (concat, sequence)
118
9+import qualified Data.Aeson.Pointer as AP
10+
1211-- | Validators shouldn't know more about the schema they're going to
1312-- be used with than necessary. If a validator throws errors using the
1413-- error sum type of a particular schema, then it can't be used with
1514-- other schemas later that have different error sum types (at least not
1615-- without writing partial functions).
1716--
18−-- Thus validators that can only fail in one way return 'FailureInfo's.
19−-- Validators that can fail in multiple ways return 'ValidationFailure's
20−-- along with an custom error sum type for that particular validator.
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).
2120--
2221-- It's the job of a schema's validate function to unify the errors produced
2322-- by the validators it uses into a single error sum type for that schema.
24−-- The schema's validate function will return a 'ValidationFailure' with
23+-- The schema's validate function will return a 'Fail' with
2524-- that sum type as its type argument.
26−data Failure err = Invalid
27− { _invalidValidatorsCalled :: !err
28− , _invalidFinalValidator :: !Value
29− , _invalidOffendingData :: !P.Pointer
30− } deriving (Eq, Show)
31−
32−setFailure :: b -> Failure a -> Failure b
33−setFailure e (Invalid _ a b) = Invalid e a b
34−
35−modFailure :: (a -> b) -> Failure a -> Failure b
36−modFailure f v@(Invalid a _ _) = v { _invalidValidatorsCalled = f a }
37−
38−addToPath :: P.Token -> Failure a -> Failure a
39−addToPath tok v@(Invalid _ _ a) = v { _invalidOffendingData = addToken a }
40− where
41− addToken :: P.Pointer -> P.Pointer
42− addToken (P.Pointer ts) = P.Pointer (ts <> pure tok)
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+27−39
@@ -1,56 +1,44 @@
1−
21-- | JSON Reference is described here:
32-- <http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03>
43--
54-- And is extended for JSON Schema here:
65-- <http://json-schema.org/latest/json-schema-core.html#anchor26>
7−--
8−-- Notes on where these functions are used:
9−--
10−-- * 'Data.JsonSchema.Fetch.includeSubschemas' uses 'updateResolutionScope'.
11−--
12−-- * 'Data.JsonSchema.Fetch.foldFunction' uses 'resolveReference'.
13−--
14−-- * 'Data.JsonSchema.Draft4.Internal' uses 'updateResolutionScope'
15−-- throughout.
16−--
17−-- * 'Data.Validator.Draft4.Any.ref' uses 'resolveReference' and
18−-- 'resolveFragment'.
196
207module Data.Validator.Reference where
218
22−import qualified Data.Aeson.Pointer as P
9+import Import
10+import Prelude
11+
12+import qualified Data.Aeson.Pointer as AP
2313import qualified Data.Text as T
24−import Data.Text.Encoding
14+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
2515import Network.HTTP.Types.URI (urlDecode)
2616import System.FilePath ((</>), dropFileName)
2717
28−import Import
29−
3018type URIBase = Maybe Text
3119type URIBaseAndFragment = (Maybe Text, Maybe Text)
3220
3321updateResolutionScope :: URIBase -> Maybe Text -> URIBase
3422updateResolutionScope mScope idKeyword
35− | Just t <- idKeyword = fst . baseAndFragment $ resolveScopeAgainst mScope t
36− | otherwise = mScope
23+ | Just t <- idKeyword = fst . baseAndFragment $ resolveScopeAgainst mScope t
24+ | otherwise = mScope
3725
3826resolveReference :: URIBase -> Text -> URIBaseAndFragment
3927resolveReference mScope t = baseAndFragment $ resolveScopeAgainst mScope t
4028
4129resolveFragment
42− :: (FromJSON schema, ToJSON schema, Show schema)
43− => Maybe Text
44− -> schema
45− -> Maybe schema
30+ :: (FromJSON schema, ToJSON schema)
31+ => Maybe Text
32+ -> schema
33+ -> Maybe schema
4634resolveFragment Nothing schema = Just schema
4735resolveFragment (Just pointer) schema = do
48− let urlDecoded = decodeUtf8 . urlDecode True . encodeUtf8 $ pointer
49− p <- either (const Nothing) Just (P.unescape urlDecoded)
50− x <- either (const Nothing) Just (P.resolve p (toJSON schema))
51− case fromJSON x of
52− Error _ -> Nothing
53− Success schema' -> Just schema'
36+ let urlDecoded = decodeUtf8 . urlDecode True . encodeUtf8 $ pointer
37+ p <- either (const Nothing) Just (AP.unescape urlDecoded)
38+ x <- either (const Nothing) Just (AP.resolve p (toJSON schema))
39+ case fromJSON x of
40+ Error _ -> Nothing
41+ Success schema' -> Just schema'
5442
5543--------------------------------------------------
5644-- * Helpers
@@ -73,19 +61,19 @@ baseAndFragment = f . T.splitOn "#"
7361resolveScopeAgainst :: Maybe Text -> Text -> Text
7462resolveScopeAgainst Nothing t = t
7563resolveScopeAgainst (Just scope) t
76− | isRemoteReference t = t
77− | otherwise = smartAppend
64+ | isRemoteReference t = t
65+ | otherwise = smartAppend
7866 where
7967 -- There shouldn't be a fragment at the end of a scope URI,
8068 -- but just in case a user leaves one in we want to be sure
8169 -- to cut it off before appending.
8270 smartAppend :: Text
8371 smartAppend =
84− case baseAndFragment scope of
85− (Just base,_) ->
86− case T.unpack t of
87− -- We want "/foo" and "#/bar" to combine into
88− -- "/foo#/bar" not "/foo/#/bar".
89− '#':_ -> base <> t
90− _ -> T.pack (dropFileName (T.unpack base) </> T.unpack t)
91− _ -> t
72+ case baseAndFragment scope of
73+ (Just base,_) ->
74+ case T.unpack t of
75+ -- We want "/foo" and "#/bar" to combine into
76+ -- "/foo#/bar" not "/foo/#/bar".
77+ '#':_ -> base <> t
78+ _ -> T.pack (dropFileName (T.unpack base) </> T.unpack t)
79+ _ -> t
src/Data/Validator/Types.hsnew file+27
@@ -0,0 +1,27 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+
3+module Data.Validator.Types where
4+
5+import Prelude
6+
7+import Data.Aeson (Value)
8+import Data.Profunctor (Profunctor(..))
9+
10+import Data.Validator.Failure (Fail)
11+
12+data Validator schema val err = Validator
13+ { _embedded :: val -> ([schema], [schema])
14+ -- ^ The first list is embedded schemas that validate the same piece
15+ -- of data this schema validates (such as schemas embedded in 'allOf').
16+ -- The second is embedded schemas that validate a subset of that data
17+ -- (such as the schemas embedded in 'items').
18+ --
19+ -- 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
21+ -- during validation).
22+ , _validate :: val -> Value -> [Fail err]
23+ } deriving Functor
24+
25+instance Profunctor (Validator schema) where
26+ lmap f (Validator a b) = Validator (lmap f a) (lmap f b)
27+ rmap = fmap
src/Data/Validator/Utils.hs+52−41
@@ -1,18 +1,19 @@
11
22module Data.Validator.Utils where
33
4+import Import
5+import Prelude
6+
47import Control.Arrow
5−import qualified Data.HashMap.Strict as H
8+import qualified Data.HashMap.Strict as HM
69import Data.List.NonEmpty (NonEmpty)
7−import qualified Data.List.NonEmpty as N
10+import qualified Data.List.NonEmpty as NE
811import Data.Scientific (Scientific, fromFloatDigits)
912import Data.Set (Set)
1013import qualified Data.Set as S
1114import qualified Data.Text as T
1215import qualified Data.Vector as V
1316
14−import Import
15−
1617--------------------------------------------------
1718-- * QuickCheck
1819--------------------------------------------------
@@ -36,7 +37,7 @@ arbitraryValue = sized f
3637 | otherwise = oneof $
3738 fmap (Array . V.fromList) (traverse (const (f (n `div` 10)))
3839 =<< (arbitrary :: Gen [()]))
39− : fmap (Object . H.fromList) (traverse (const (g (n `div` 10)))
40+ : fmap (Object . HM.fromList) (traverse (const (g (n `div` 10)))
4041 =<< (arbitrary :: Gen [()]))
4142 : nonRecursive
4243
@@ -45,14 +46,14 @@ arbitraryValue = sized f
4546
4647 nonRecursive :: [Gen Value]
4748 nonRecursive =
48− [ pure Null
49− , Bool <$> arbitrary
50− , String <$> arbitraryText
51− , Number <$> arbitraryScientific
52− ]
49+ [ pure Null
50+ , Bool <$> arbitrary
51+ , String <$> arbitraryText
52+ , Number <$> arbitraryScientific
53+ ]
5354
5455arbitraryHashMap :: Arbitrary a => Gen (HashMap Text a)
55−arbitraryHashMap = H.fromList . fmap (first T.pack) <$> arbitrary
56+arbitraryHashMap = HM.fromList . fmap (first T.pack) <$> arbitrary
5657
5758arbitrarySetOfText :: Gen (Set Text)
5859arbitrarySetOfText = S.fromList . fmap T.pack <$> arbitrary
@@ -60,21 +61,21 @@ arbitrarySetOfText = S.fromList . fmap T.pack <$> arbitrary
6061newtype NonEmpty' a = NonEmpty' { _unNonEmpty' :: NonEmpty a }
6162
6263instance FromJSON a => FromJSON (NonEmpty' a) where
63− parseJSON v = do
64− xs <- parseJSON v
65− case N.nonEmpty xs of
66− Nothing -> fail "Must have at least one item."
67− Just ne -> pure (NonEmpty' ne)
64+ parseJSON v = do
65+ xs <- parseJSON v
66+ case NE.nonEmpty xs of
67+ Nothing -> fail "Must have at least one item."
68+ Just ne -> pure (NonEmpty' ne)
6869
6970instance ToJSON a => ToJSON (NonEmpty' a) where
70− toJSON = toJSON . N.toList . _unNonEmpty'
71+ toJSON = toJSON . NE.toList . _unNonEmpty'
7172
7273instance Arbitrary a => Arbitrary (NonEmpty' a) where
73− arbitrary = do
74− xs <- arbitrary
75− case N.nonEmpty xs of
76− Nothing -> NonEmpty' . pure <$> arbitrary
77− Just ne -> pure (NonEmpty' ne)
74+ arbitrary = do
75+ xs <- arbitrary
76+ case NE.nonEmpty xs of
77+ Nothing -> NonEmpty' . pure <$> arbitrary
78+ Just ne -> pure (NonEmpty' ne)
7879
7980--------------------------------------------------
8081-- * allUniqueValues
@@ -86,7 +87,7 @@ allUniqueValues = allUnique . fmap OrdValue . V.toList
8687-- NOTE: When we no longer support GHC 7.8 we can generalize
8788-- allUnique to work on any Foldable and remove this function.
8889allUniqueValues' :: NonEmpty Value -> Bool
89−allUniqueValues' = allUnique . fmap OrdValue . N.toList
90+allUniqueValues' = allUnique . fmap OrdValue . NE.toList
9091
9192allUnique :: (Ord a) => [a] -> Bool
9293allUnique xs = S.size (S.fromList xs) == length xs
@@ -97,26 +98,36 @@ allUnique xs = S.size (S.fromList xs) == length xs
9798newtype OrdValue = OrdValue { _unOrdValue :: Value } deriving Eq
9899
99100instance Ord OrdValue where
100− (OrdValue Null) `compare` (OrdValue Null) = EQ
101− (OrdValue Null) `compare` _ = LT
102− _ `compare` (OrdValue Null) = GT
101+ (OrdValue Null) `compare` (OrdValue Null) = EQ
102+ (OrdValue Null) `compare` _ = LT
103+ _ `compare` (OrdValue Null) = GT
103104
104− (OrdValue (Bool x)) `compare` (OrdValue (Bool y)) = x `compare` y
105− (OrdValue (Bool _)) `compare` _ = LT
106− _ `compare` (OrdValue (Bool _)) = GT
105+ (OrdValue (Bool x)) `compare` (OrdValue (Bool y)) = x `compare` y
106+ (OrdValue (Bool _)) `compare` _ = LT
107+ _ `compare` (OrdValue (Bool _)) = GT
107108
108− (OrdValue (Number x)) `compare` (OrdValue (Number y)) = x `compare` y
109− (OrdValue (Number _)) `compare` _ = LT
110− _ `compare` (OrdValue (Number _)) = GT
109+ (OrdValue (Number x)) `compare` (OrdValue (Number y)) = x `compare` y
110+ (OrdValue (Number _)) `compare` _ = LT
111+ _ `compare` (OrdValue (Number _)) = GT
111112
112− (OrdValue (String x)) `compare` (OrdValue (String y)) = x `compare` y
113− (OrdValue (String _)) `compare` _ = LT
114− _ `compare` (OrdValue (String _)) = GT
113+ (OrdValue (String x)) `compare` (OrdValue (String y)) = x `compare` y
114+ (OrdValue (String _)) `compare` _ = LT
115+ _ `compare` (OrdValue (String _)) = GT
115116
116− (OrdValue (Array xs)) `compare` (OrdValue (Array ys)) =
117− (OrdValue <$> xs) `compare` (OrdValue <$> ys)
118− (OrdValue (Array _)) `compare` _ = LT
119− _ `compare` (OrdValue (Array _)) = GT
117+ (OrdValue (Array xs)) `compare` (OrdValue (Array ys)) =
118+ (OrdValue <$> xs) `compare` (OrdValue <$> ys)
119+ (OrdValue (Array _)) `compare` _ = LT
120+ _ `compare` (OrdValue (Array _)) = GT
121+
122+ (OrdValue (Object x)) `compare` (OrdValue (Object y)) =
123+ HM.toList (OrdValue <$> x) `compare` HM.toList (OrdValue <$> y)
124+
125+--------------------------------------------------
126+-- * other
127+--------------------------------------------------
120128
121− (OrdValue (Object x)) `compare` (OrdValue (Object y)) =
122− H.toList (OrdValue <$> x) `compare` H.toList (OrdValue <$> y)
129+fromJSONEither :: FromJSON a => Value -> Either Text a
130+fromJSONEither a =
131+ case fromJSON a of
132+ Error e -> Left (T.pack e)
133+ Success b -> Right b
test/Local.hs+94−26
@@ -4,51 +4,119 @@ module Main where
44import Control.Applicative
55import Control.Monad (unless)
66import Data.Aeson
7−import Data.List (isSuffixOf)
7+import Data.Foldable (traverse_)
8+import qualified Data.List.NonEmpty as N
89import Data.Monoid
9−import System.Directory (getDirectoryContents)
10+import qualified System.Timeout as TO
1011import Test.Tasty (TestTree, defaultMain, testGroup)
1112import qualified Test.Tasty.HUnit as HU
1213import Test.Tasty.QuickCheck (testProperty)
1314
15+import qualified Data.JsonSchema.Draft4 as D4
16+import Data.JsonSchema.Fetch (ReferencedSchemas(..))
17+import qualified Data.JsonSchema.Types as JT
18+import Local.Failure (correctPaths)
19+import Local.Validation (fetchFromFilesystem,
20+ generalValidation)
21+import Local.Reference (referenceTests)
22+import Shared
23+
1424-- Examples
15−import qualified CustomSchema
25+import qualified AlternateSchema
1626import qualified Full
17−import qualified PrettyShowFailure
1827import qualified Simple
1928
20−import Data.JsonSchema.Draft4
21−import Local.Failure (correctPaths)
22−import Local.Filesystem (fetchFromFilesystem)
23−import Local.Reference (referenceTests)
24−import Shared (isLocal, readSchemaTests, toTest)
25−
2629dir :: String
2730dir = "JSON-Schema-Test-Suite/tests/draft4"
2831
32+supplementDir :: String
33+supplementDir = "test/supplement"
34+
2935main :: IO ()
3036main = do
31− filenames <- filter isLocal . filter (".json" `isSuffixOf`) <$> getDirectoryContents dir
32− ts <- readSchemaTests dir filenames
33− defaultMain . testGroup "Tests not requiring an HTTP server" $
34− testGroup "Check that examples compile and don't throw errors" exampleTests
35− : testGroup "QuickCheck tests" quickCheckTests
36− : testGroup "Report the path to invalid data correctly" correctPaths
37− : testGroup "Test the referencesViaFilesystem function" fetchFromFilesystem
38− : testGroup "Test the Reference module" referenceTests
39− : fmap toTest ts
37+
38+ -- Language agnostic tests
39+ ts <- readSchemaTests
40+ dir
41+ (\a -> not (isHTTPTest a || skipOptional a))
42+
43+ -- Custom supplements to the language agnostic tests
44+ supplementTs <- readSchemaTests
45+ supplementDir
46+ (\a -> not (isHTTPTest a || skipOptional a))
47+
48+ defaultMain . testGroup "Tests not requiring an HTTP server" $
49+ [ testGroup
50+ "Check that examples compile and don't throw errors"
51+ exampleTests
52+ , testGroup
53+ "QuickCheck tests"
54+ quickCheckTests
55+ , testGroup
56+ "Report the path to invalid data correctly"
57+ correctPaths
58+ , testGroup
59+ "Test the Reference module"
60+ referenceTests
61+ , testGroup
62+ "Test the referencesViaFilesystem function"
63+ fetchFromFilesystem
64+ , testGroup
65+ "Supplementary validation tests written in Haskell"
66+ generalValidation
67+ , testGroup
68+ "Supplementary tests written in JSON (using the record based schema)"
69+ (toTest (fmap timeout . validate) <$> supplementTs)
70+ , testGroup
71+ "Supplementary tests written in JSON (using the 'Value' based schema)"
72+ (toTest (fmap timeout . validateExample) <$> supplementTs)
73+ , testGroup
74+ "Language agnostic local tests (using the record based schema)"
75+ (toTest validate <$> ts)
76+ , testGroup
77+ "Language agnostic local tests (using the 'Value' based schema)"
78+ (toTest validateExample <$> ts)
79+ ]
80+ where
81+ timeout :: HU.Assertion -> HU.Assertion
82+ timeout f = do
83+ res <- TO.timeout 3000000 f
84+ case res of
85+ Nothing -> HU.assertFailure "timeout expired"
86+ Just a -> pure a
87+
88+ validate :: D4.Schema -> SchemaTestCase -> HU.Assertion
89+ validate s sc = do
90+ res <- D4.fetchHTTPAndValidate (D4.SchemaWithURI s Nothing) (_scData sc)
91+ let failures = case res of
92+ Right () -> mempty
93+ Left (D4.HVData a) -> N.toList a
94+ other -> error ("Local.validate error: "
95+ <> show other)
96+ traverse_ (checkPointer (_scData sc)) failures
97+ assertResult sc failures
98+
99+ validateExample :: JT.Schema -> SchemaTestCase -> HU.Assertion
100+ validateExample s sc = do
101+ res <- AlternateSchema.referencesViaHTTP (D4.SchemaWithURI s Nothing)
102+ case res of
103+ Left e -> error ("Local.validateExample error: " <> show e)
104+ Right schemaMap -> do
105+ let failures = AlternateSchema.validate
106+ (ReferencedSchemas s schemaMap)
107+ Nothing s (_scData sc)
108+ traverse_ (checkPointer (_scData sc)) failures
109+ assertResult sc failures
40110
41111quickCheckTests :: [TestTree]
42112quickCheckTests =
43− [testProperty "Invert schemas through JSON without change" invertSchema]
113+ [testProperty "Invert schemas through JSON without change" invertSchema]
44114 where
45− invertSchema :: Schema -> Bool
115+ invertSchema :: D4.Schema -> Bool
46116 invertSchema a = Just a == decode (encode a)
47117
48118exampleTests :: [TestTree]
49119exampleTests =
50− [ HU.testCase "CustomSchema example" CustomSchema.example
51− , HU.testCase "Full example" Full.example
52− , HU.testCase "PrettyShowFailure example" PrettyShowFailure.example
53− , HU.testCase "Simple example" Simple.example
54− ]
120+ [ HU.testCase "Full example" Full.example
121+ , HU.testCase "Simple example" Simple.example
122+ ]
test/Local/Failure.hs+40−27
@@ -1,55 +1,68 @@
11
22module Local.Failure where
33
4+import Prelude
5+
46import Data.Aeson
5−import qualified Data.Aeson.Pointer as P
7+import qualified Data.Aeson.Pointer as P
68import Data.Monoid
7−import Test.Tasty (TestTree)
8−import qualified Test.Tasty.HUnit as HU
9+import Test.Tasty (TestTree)
10+import qualified Test.Tasty.HUnit as HU
911
1012import Data.JsonSchema.Draft4
11−import qualified Data.Validator.Draft4 as VA
13+import Data.JsonSchema.Draft4.Spec (validate)
14+import qualified Data.Validator.Draft4.Array as AR
1215
1316correctPaths :: [TestTree]
1417correctPaths =
15− [ HU.testCase "Items object" itemsObject
16− , HU.testCase "Items array" itemsArray
17− ]
18+ [ HU.testCase "Items object" itemsObject
19+ , HU.testCase "Items array" itemsArray
20+ ]
1821
1922itemsObject :: IO ()
20−itemsObject = HU.assertEqual "Path to invalid data"
21− (P.Pointer [P.Token "0"])
22− (_invalidOffendingData failure)
23+itemsObject =
24+ HU.assertEqual
25+ "Path to invalid data"
26+ [Failure (Items UniqueItems) (Bool True) (P.Pointer [P.Token "0"])
27+ (toJSON [True, True])
28+ ]
29+ failures
2330 where
24− [failure] = runValidate (ReferencedSchemas schema mempty)
25− sw (toJSON [[True, True]])
31+ failures = validate (ReferencedSchemas schema mempty)
32+ sw (toJSON [[True, True]])
2633
2734 schema :: Schema
2835 schema = emptySchema
29− { _schemaItems = Just (VA.ItemsObject (emptySchema { _schemaUniqueItems = Just True }))
30− }
36+ { _schemaItems = Just $ AR.ItemsObject
37+ (emptySchema { _schemaUniqueItems = Just True })
38+ }
3139
3240 sw :: SchemaWithURI Schema
3341 sw = SchemaWithURI
34− { _swSchema = schema
35− , _swURI = Nothing
36− }
42+ { _swSchema = schema
43+ , _swURI = Nothing
44+ }
3745
3846itemsArray :: IO ()
39−itemsArray = HU.assertEqual "Path to invalid data"
40− (P.Pointer [P.Token "0"])
41− (_invalidOffendingData failure)
47+itemsArray =
48+ HU.assertEqual
49+ "Path to invalid data"
50+ [Failure (Items UniqueItems) (Bool True) (P.Pointer [P.Token "0"])
51+ (toJSON [True, True])
52+ ]
53+ failures
4254 where
43− [failure] = runValidate (ReferencedSchemas schema mempty)
44− sw (toJSON [[True, True]])
55+ failures = validate (ReferencedSchemas schema mempty)
56+ sw (toJSON [[True, True]])
4557
4658 schema :: Schema
4759 schema = emptySchema
48− { _schemaItems = Just (VA.ItemsArray [emptySchema { _schemaUniqueItems = Just True }])
49− }
60+ { _schemaItems = Just $ AR.ItemsArray
61+ [emptySchema { _schemaUniqueItems = Just True }]
62+ }
5063
5164 sw :: SchemaWithURI Schema
5265 sw = SchemaWithURI
53− { _swSchema = schema
54− , _swURI = Nothing
55− }
66+ { _swSchema = schema
67+ , _swURI = Nothing
68+ }
test/Local/Filesystem.hsdeleted−56
@@ -1,56 +0,0 @@
1−
2−module Local.Filesystem where
3−
4−import Control.Applicative
5−import Data.Aeson
6−import Data.Monoid
7−import Test.Tasty (TestTree)
8−import qualified Test.Tasty.HUnit as HU
9−import Data.Text (Text)
10−
11−import Data.JsonSchema.Draft4
12−
13−fetchFromFilesystem :: [TestTree]
14−fetchFromFilesystem =
15− [ HU.testCase
16− "readFile exceptions are turned into Lefts"
17− readFileExceptions
18−
19− , HU.testCase
20− "Relative reference to local file"
21− (resolve "test/Local/schema.json")
22− , HU.testCase
23− "Chained relative references to local files"
24− (resolve "./test/Local/schema-with-ref.json")
25− ]
26−
27−readFileExceptions :: IO ()
28−readFileExceptions = do
29− res <- referencesViaFilesystem (SchemaWithURI schema Nothing)
30− case res of
31− Left (FSReadFailure _) -> pure ()
32− a -> error (msg <> show a)
33− where
34− schema :: Schema
35− schema = emptySchema { _schemaRef = Just "does-not-exist.json" }
36−
37− msg :: String
38− msg = "expected referencesViaFilesystem to return ReadFailure,"
39− <> " instead got: "
40−
41−-- * Helpers
42−
43−resolve :: Text -> IO ()
44−resolve ref = do
45− let schema = emptySchema { _schemaRef = Just ref }
46− res <- fetchFilesystemAndValidate (SchemaWithURI schema Nothing) badData
47− case res of
48− Left (FVData _) -> pure ()
49− a -> error (msg <> show a)
50− where
51− badData :: Value
52− badData = toJSON [True, True]
53−
54− msg :: String
55− msg = "expected fetchFilesystemAndValidate to return"
56− <> " Left (FVData [_]), instead got: "
test/Local/Reference.hs+35−35
@@ -8,44 +8,44 @@ import Data.Validator.Reference
88
99referenceTests :: [TestTree]
1010referenceTests =
11− [ HU.testCase "updateResolutionScope test cases" updateResolutionScopeTests
12− , HU.testCase "resolveReference test cases" resolveReferenceTests
13− ]
11+ [ HU.testCase "updateResolutionScope test cases" updateResolutionScopeTests
12+ , HU.testCase "resolveReference test cases" resolveReferenceTests
13+ ]
1414
1515updateResolutionScopeTests :: IO ()
1616updateResolutionScopeTests = do
17− HU.assertEqual
18− "case 1 result"
19− Nothing
20− (updateResolutionScope Nothing Nothing)
21− HU.assertEqual
22− "case 2 result"
23− Nothing
24− (updateResolutionScope Nothing (Just "#"))
25− HU.assertEqual
26− "case 3 result"
27− (Just "foo")
28− (updateResolutionScope Nothing (Just "foo"))
29− HU.assertEqual
30− "case 4 result"
31− (Just "/./bar") -- TODO: Normalize after updateResolutionScope.
32− (updateResolutionScope (Just "/foo") (Just "./bar"))
17+ HU.assertEqual
18+ "case 1 result"
19+ Nothing
20+ (updateResolutionScope Nothing Nothing)
21+ HU.assertEqual
22+ "case 2 result"
23+ Nothing
24+ (updateResolutionScope Nothing (Just "#"))
25+ HU.assertEqual
26+ "case 3 result"
27+ (Just "foo")
28+ (updateResolutionScope Nothing (Just "foo"))
29+ HU.assertEqual
30+ "case 4 result"
31+ (Just "/./bar") -- TODO: Normalize after updateResolutionScope.
32+ (updateResolutionScope (Just "/foo") (Just "./bar"))
3333
3434resolveReferenceTests :: IO ()
3535resolveReferenceTests = do
36− HU.assertEqual
37− "case 1 result"
38− (Just "/bar", Nothing)
39− (resolveReference (Just "/foo") "bar")
40− HU.assertEqual
41− "case 2 result"
42− (Just "/baz", Nothing)
43− (resolveReference (Just "/foo/bar") "/baz")
44− HU.assertEqual
45− "case 3 result"
46− (Nothing, Just "/bar")
47− (resolveReference Nothing "#/bar")
48− HU.assertEqual
49− "case 4 result"
50− (Just "/foo", Just "/bar")
51− (resolveReference (Just "/foo") "#/bar")
36+ HU.assertEqual
37+ "case 1 result"
38+ (Just "/bar", Nothing)
39+ (resolveReference (Just "/foo") "bar")
40+ HU.assertEqual
41+ "case 2 result"
42+ (Just "/baz", Nothing)
43+ (resolveReference (Just "/foo/bar") "/baz")
44+ HU.assertEqual
45+ "case 3 result"
46+ (Nothing, Just "/bar")
47+ (resolveReference Nothing "#/bar")
48+ HU.assertEqual
49+ "case 4 result"
50+ (Just "/foo", Just "/bar")
51+ (resolveReference (Just "/foo") "#/bar")
test/Local/Validation.hsnew file+82
@@ -0,0 +1,82 @@
1+
2+module Local.Validation where
3+
4+import Control.Applicative
5+import Data.Aeson
6+import qualified Data.Aeson as AE
7+import qualified Data.HashMap.Strict as HM
8+import Data.Monoid
9+import Data.Text (Text)
10+import Test.Tasty (TestTree)
11+import qualified Test.Tasty.HUnit as HU
12+
13+import Data.JsonSchema.Draft4
14+import qualified Data.JsonSchema.Types as JT
15+
16+import qualified AlternateSchema as AS
17+
18+fetchFromFilesystem :: [TestTree]
19+fetchFromFilesystem =
20+ [ HU.testCase
21+ "readFile exceptions are turned into Lefts"
22+ readFileExceptions
23+
24+ , HU.testCase
25+ "Relative reference to local file"
26+ (resolve "test/Local/schema.json")
27+ , HU.testCase
28+ "Chained relative references to local files"
29+ (resolve "./test/Local/schema-with-ref.json")
30+ ]
31+
32+readFileExceptions :: IO ()
33+readFileExceptions = do
34+ res <- referencesViaFilesystem (SchemaWithURI schema Nothing)
35+ case res of
36+ Left (FSReadFailure _) -> pure ()
37+ a -> error (msg <> show a)
38+ where
39+ schema :: Schema
40+ schema = emptySchema { _schemaRef = Just "does-not-exist.json" }
41+
42+ msg :: String
43+ msg = "expected referencesViaFilesystem to return ReadFailure,"
44+ <> " instead got: "
45+
46+resolve :: Text -> IO ()
47+resolve ref = do
48+ let schema = emptySchema { _schemaRef = Just ref }
49+ res <- fetchFilesystemAndValidate (SchemaWithURI schema Nothing) badData
50+ case res of
51+ Left (FVData _) -> pure ()
52+ a -> error (msg <> show a)
53+ where
54+ badData :: Value
55+ badData = toJSON [True, True]
56+
57+ msg :: String
58+ msg = "expected fetchFilesystemAndValidate to return"
59+ <> " Left (FVData [_]), instead got: "
60+
61+-- | Validation tests that were hard to write in JSON for whatever reason.
62+generalValidation :: [TestTree]
63+generalValidation =
64+ [ HU.testCase
65+ "Don't parse schemas that have Null in a forbidden location"
66+ forbidNull
67+ , HU.testCase
68+ "The 'Value' based Schema's checkSchema should catch Nulls"
69+ exampleForbidNull
70+ ]
71+
72+forbidNull :: IO ()
73+forbidNull =
74+ case fromJSON (Object (HM.singleton "type" Null)) of
75+ AE.Error _ -> pure ()
76+ AE.Success a -> HU.assertFailure ("parsed to: " <> show (a :: Schema))
77+
78+exampleForbidNull :: IO ()
79+exampleForbidNull =
80+ case AS.checkSchema (JT.Schema (HM.singleton "type" Null)) of
81+ [] -> HU.assertFailure "No checkSchema failures"
82+ _ -> pure ()
test/Remote.hs+47−9
@@ -2,25 +2,63 @@ module Main where
22
33import Control.Applicative
44import Control.Concurrent.Async (withAsync)
5−import Data.List (isSuffixOf)
5+import Data.Foldable (traverse_)
6+import qualified Data.List.NonEmpty as N
7+import Data.Monoid
68import Network.Wai.Application.Static (defaultFileServerSettings,
79 staticApp)
810import Network.Wai.Handler.Warp (run)
9−import System.Directory (getDirectoryContents)
1011import Test.Tasty (defaultMain, testGroup)
12+import qualified Test.Tasty.HUnit as HU
1113
12−import Shared (isLocal, readSchemaTests,
13− toTest)
14+import qualified Data.JsonSchema.Draft4 as D4
15+import Data.JsonSchema.Fetch (ReferencedSchemas(..))
16+import qualified Data.JsonSchema.Types as JT
17+import Shared
18+
19+-- Examples
20+import qualified AlternateSchema
1421
1522dir :: String
1623dir = "JSON-Schema-Test-Suite/tests/draft4"
1724
1825main :: IO ()
1926main = withAsync serve $ \_ -> do
20− filenames <- filter (not . isLocal) . filter (".json" `isSuffixOf`)
21− <$> getDirectoryContents dir
22− ts <- readSchemaTests dir filenames
23− defaultMain . testGroup "Tests that run an HTTP server" $ toTest <$> ts
27+ ts <- readSchemaTests dir isHTTPTest
28+ defaultMain . testGroup "Tests that run an HTTP server" $
29+ [ testGroup
30+ "Language agnostic remote tests (using the record based schema)"
31+ (toTest validate <$> ts)
32+ , testGroup
33+ "Language agnostic remote tests (using the 'Value' based schema)"
34+ (toTest validateExample <$> ts)
35+ ]
36+ where
37+ validate :: D4.Schema -> SchemaTestCase -> HU.Assertion
38+ validate s sc = do
39+ res <- D4.fetchHTTPAndValidate (D4.SchemaWithURI s Nothing) (_scData sc)
40+ let failures = case res of
41+ Right () -> mempty
42+ Left (D4.HVData a) -> N.toList a
43+ other -> error ("Local.validate error: "
44+ <> show other)
45+ traverse_ (checkPointer (_scData sc)) failures
46+ assertResult sc failures
47+
48+ validateExample :: JT.Schema -> SchemaTestCase -> HU.Assertion
49+ validateExample s sc = do
50+ res <- AlternateSchema.referencesViaHTTP (D4.SchemaWithURI s Nothing)
51+ case res of
52+ Left e -> error ("Remote.validateExample error: " <> show e)
53+ Right schemaMap -> do
54+ let failures = AlternateSchema.validate
55+ (ReferencedSchemas s schemaMap)
56+ Nothing s (_scData sc)
57+ traverse_ (checkPointer (_scData sc)) failures
58+ assertResult sc failures
2459
2560serve :: IO ()
26−serve = run 1234 . staticApp . defaultFileServerSettings $ "JSON-Schema-Test-Suite/remotes"
61+serve = run 1234
62+ . staticApp
63+ . defaultFileServerSettings
64+ $ "JSON-Schema-Test-Suite/remotes"
test/Shared.hs+130−60
@@ -5,95 +5,165 @@ module Shared where
55import Control.Applicative
66import Control.Monad
77import Data.Aeson
8+import qualified Data.Aeson.Pointer as AP
89import Data.Aeson.TH
910import qualified Data.ByteString.Lazy as LBS
1011import Data.Char (toLower)
12+import qualified Data.HashMap.Strict as HM
13+import Data.List (isInfixOf, stripPrefix)
14+import Data.Maybe
1115import Data.Monoid
1216import Data.Text (Text)
1317import qualified Data.Text as T
18+import Data.Traversable
19+import qualified Data.Vector as V
1420import GHC.Generics
21+import qualified System.Directory as SD
1522import System.FilePath ((</>))
1623import Test.Tasty (TestTree)
1724import qualified Test.Tasty.HUnit as HU
1825
1926import qualified Data.JsonSchema.Draft4 as D4
2027
21−isLocal :: String -> Bool
22−isLocal file = (file /= "definitions.json")
23− && (file /= "ref.json")
24− && (file /= "refRemote.json")
28+-- Recursively return the contents of a directory
29+-- (or return itself if given a file as an argument).
30+--
31+-- Return paths are relative to that directory.
32+listFiles :: FilePath -> IO [FilePath]
33+listFiles path = fmap ( fromMaybe "stripPrefix failed"
34+ . stripPrefix (path <> "/")
35+ ) <$> listFilesFullPath path
36+
37+-- Recursively return the contents of a directory
38+-- (or return itself if given a file as an argument).
39+--
40+-- All return paths start with the 'FilePath' argument.
41+listFilesFullPath :: FilePath -> IO [FilePath]
42+listFilesFullPath path = do
43+ -- Check if it's a file or directory:
44+ res <- SD.doesFileExist path
45+ if res
46+ then pure [path]
47+ else do
48+ fs <- fmap (path </>) <$> SD.listDirectory path
49+ concat <$> traverse listFilesFullPath fs
50+
51+checkPointer :: Value -> D4.Failure -> HU.Assertion
52+checkPointer v failure =
53+ case AP.resolve (D4._failureOffendingPointer failure) v of
54+ Left e -> error ("Couldn't resolve pointer: " <> show e)
55+ Right a -> assertContains a (D4._failureOffendingData failure)
56+ where
57+ -- Some validators, such as 'additionalItems', only return a subset
58+ -- of the data incated by their '_failureOffendingPointer'.
59+ -- See the comments on 'Data.Validator.Failure.Failure' for more info.
60+ assertContains :: Value -> Value -> HU.Assertion
61+ assertContains x y
62+ | x == y = pure ()
63+ | otherwise =
64+ case (x,y) of
65+ (Array xs, Array ys) ->
66+ HU.assertBool "Pointer resolution incorrect"
67+ (V.toList ys `isInfixOf` V.toList xs)
68+ (Object xhm, Object yhm) ->
69+ HU.assertBool "Pointer resolution incorrect"
70+ (HM.toList yhm `isInfixOf` HM.toList xhm)
71+ _ -> HU.assertFailure
72+ "Pointer resolution incorrect: result mismatch"
73+
74+isHTTPTest :: String -> Bool
75+isHTTPTest file = (file == "definitions.json")
76+ || (file == "ref.json")
77+ || (file == "refRemote.json")
78+
79+-- | We may never support the @"format"@ keywords, and
80+-- are currently failing the zeroTerminatedFloats test.
81+skipOptional :: String -> Bool
82+skipOptional file = (file == "optional/format.json")
83+ || (file == "optional/zeroTerminatedFloats.json")
2584
2685data SchemaTest = SchemaTest
27− { _stDescription :: Text
28− , _stSchema :: D4.Schema
29− , _stCases :: [SchemaTestCase]
30− }
86+ { _stDescription :: Text
87+ , _stSchema :: Value
88+ , _stCases :: [SchemaTestCase]
89+ }
3190
3291data SchemaTestCase = SchemaTestCase
33− { _scDescription :: Text
34− , _scData :: Value
35− , _scValid :: Bool
36− } deriving Generic
92+ { _scDescription :: Text
93+ , _scData :: Value
94+ , _scValid :: Bool
95+ } deriving Generic
3796
3897instance FromJSON SchemaTest where
39− parseJSON = withObject "SchemaTest" $ \o -> SchemaTest
40− <$> o .: "description"
41− <*> o .: "schema"
42− <*> o .: "tests" -- Perhaps "cases" would have been a more descriptive key.
98+ parseJSON = withObject "SchemaTest" $ \o -> SchemaTest
99+ <$> o .: "description"
100+ <*> o .: "schema"
101+ <*> o .: "tests" -- Perhaps "cases" would have been
102+ -- a more descriptive key.
43103
44104instance FromJSON SchemaTestCase where
45− parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = fmap toLower . drop 3 }
105+ parseJSON = genericParseJSON
106+ defaultOptions
107+ { fieldLabelModifier = fmap toLower . drop 3 }
46108
47−readSchemaTests :: String -> [String] -> IO [SchemaTest]
48−readSchemaTests dir jsonFiles = concatMapM fileToCases jsonFiles
109+readSchemaTests
110+ :: String
111+ -- ^ The path to a directory.
112+ -> (String -> Bool)
113+ -- ^ A function to decide what we're interested in within that directory.
114+ -> IO [SchemaTest]
115+readSchemaTests dir filterFunc = do
116+ files <- filter filterFunc <$> listFiles dir
117+ concat <$> traverse fileToCases files
49118 where
50119 -- Each file contains an array of SchemaTests, not just one.
51120 fileToCases :: String -> IO [SchemaTest]
52121 fileToCases name = do
53− let fullPath = dir </> name
54− jsonBS <- LBS.readFile fullPath
55− case eitherDecode jsonBS of
56− Left e -> fail $ "couldn't parse file '" <> fullPath <> "': " <> e
57− Right schemaTests -> pure $ prependFileName name <$> schemaTests
122+ let fullPath = dir </> name
123+ jsonBS <- LBS.readFile fullPath
124+ case eitherDecode jsonBS of
125+ Left e -> fail $ "couldn't parse file '" <> fullPath <> "': " <> e
126+ Right schemaTests -> pure $ prependFileName name <$> schemaTests
58127
59128 prependFileName :: String -> SchemaTest -> SchemaTest
60129 prependFileName fileName s = s
61− { _stDescription = T.pack fileName <> ": " <> _stDescription s
62− }
63−
64− concatMapM :: (Monad m) => (a -> m [b]) -> [a] -> m [b]
65− concatMapM f xs = liftM concat (mapM f xs)
66−
67−toTest :: SchemaTest -> TestTree
68−toTest st =
69− HU.testCase (T.unpack (_stDescription st)) $ do
70− forM_ (_stCases st) $ \sc -> do
71− g <- assertRight =<< D4.referencesViaHTTP (D4.SchemaWithURI (_stSchema st) Nothing)
72− validate <- assertRight . D4.checkSchema g $ D4.SchemaWithURI (_stSchema st) Nothing
73− let res = validate (_scData sc)
74− if _scValid sc
75− then assertValid sc res
76− else assertInvalid sc res
77−
78−assertValid :: SchemaTestCase -> [D4.Invalid] -> HU.Assertion
130+ { _stDescription = T.pack fileName <> ": " <> _stDescription s
131+ }
132+
133+toTest
134+ :: forall schema. FromJSON schema
135+ => (schema -> SchemaTestCase -> HU.Assertion)
136+ -> SchemaTest
137+ -> TestTree
138+toTest validate st =
139+ HU.testCase (T.unpack (_stDescription st)) $
140+ forM_ (_stCases st) (validate schema)
141+ where
142+ schema :: schema
143+ schema = case fromJSON (_stSchema st) of
144+ Error e -> error ("Couldn't parse schema: " <> show e)
145+ Success a -> a
146+
147+assertResult :: SchemaTestCase -> [D4.Failure] -> HU.Assertion
148+assertResult sc failures
149+ | _scValid sc = assertValid sc failures
150+ | otherwise = assertInvalid sc failures
151+
152+assertValid :: SchemaTestCase -> [D4.Failure] -> HU.Assertion
79153assertValid _ [] = pure ()
80−assertValid sc errs =
81− HU.assertFailure $ unlines
82− [ " Failed to validate data"
83− , " Description: " <> T.unpack (_scDescription sc)
84− , " Data: " <> show (_scData sc)
85− , " Validation failures: " <> show errs
86− ]
87−
88−assertInvalid :: SchemaTestCase -> [D4.Invalid] -> HU.Assertion
154+assertValid sc failures =
155+ HU.assertFailure $ unlines
156+ [ " Failed to validate data"
157+ , " Description: " <> T.unpack (_scDescription sc)
158+ , " Data: " <> show (_scData sc)
159+ , " Validation failures: " <> show failures
160+ ]
161+
162+assertInvalid :: SchemaTestCase -> [D4.Failure] -> HU.Assertion
89163assertInvalid sc [] =
90− HU.assertFailure $ unlines
91− [ " Validated invalid data"
92− , " Description: " <> T.unpack (_scDescription sc)
93− , " Data: " <> show (_scData sc)
94− ]
164+ HU.assertFailure $ unlines
165+ [ " Validated invalid data"
166+ , " Description: " <> T.unpack (_scDescription sc)
167+ , " Data: " <> show (_scData sc)
168+ ]
95169assertInvalid _ _ = pure ()
96−
97−assertRight :: (Show a) => Either a b -> IO b
98−assertRight (Left e) = HU.assertFailure (show e) >> fail "assertRight failed"
99−assertRight (Right b) = pure b
test/supplement/recurse.jsonnew file+17
@@ -0,0 +1,17 @@
1+[
2+ {
3+ "description": "recursive ref",
4+ "schema": {
5+ "allOf": [
6+ {"$ref": "#"}
7+ ]
8+ },
9+ "tests": [
10+ {
11+ "description": "arbitrary data",
12+ "data": {},
13+ "valid": false
14+ }
15+ ]
16+ }
17+]