diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b36c7f..9c380084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [2.1.0] - XXXX-XX-XX + +### Bug Fixes + +- Limit anyOf/oneOf discriminator to listed refs: https://github.com/davishmcclurg/json_schemer/pull/145 +- Require discriminator `propertyName` property: https://github.com/davishmcclurg/json_schemer/pull/145 +- Support `Schema#ref` in subschemas: https://github.com/davishmcclurg/json_schemer/pull/145 +- Resolve JSON pointer refs using correct base URI: https://github.com/davishmcclurg/json_schemer/pull/147 +- `date` format in OpenAPI 3.0: https://github.com/davishmcclurg/json_schemer/commit/69fe7a815ecf0cfb1c40ac402bf46a789c05e972 + +### Features + +- Custom error messages with `x-error` keyword and I18n: https://github.com/davishmcclurg/json_schemer/pull/149 +- Custom content encodings and media types: https://github.com/davishmcclurg/json_schemer/pull/148 + +[2.1.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.1.0 + ## [2.0.0] - 2023-08-20 For 2.0.0, much of the codebase was rewritten to simplify support for the two new JSON Schema draft versions (2019-09 and 2020-12). The major change is moving each keyword into its own class and organizing them into vocabularies. [Output formats](https://json-schema.org/draft/2020-12/json-schema-core.html#section-12) and [annotations](https://json-schema.org/draft/2020-12/json-schema-core.html#section-7.7) from the new drafts are also supported. The known breaking changes are listed below, but there may be others that haven't been identified. diff --git a/Gemfile.lock b/Gemfile.lock index 8e3e063e..c5b256da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - json_schemer (2.0.0) + json_schemer (2.1.0) hana (~> 1.3) regexp_parser (~> 2.0) simpleidn (~> 0.2) @@ -9,11 +9,16 @@ PATH GEM remote: https://rubygems.org/ specs: + concurrent-ruby (1.2.2) docile (1.4.0) hana (1.3.7) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + i18n-debug (1.2.0) + i18n (< 2) minitest (5.15.0) rake (13.0.6) - regexp_parser (2.8.1) + regexp_parser (2.8.2) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -25,7 +30,7 @@ GEM unf (0.1.4) unf_ext unf (0.1.4-java) - unf_ext (0.0.8.2) + unf_ext (0.0.9) PLATFORMS java @@ -33,6 +38,8 @@ PLATFORMS DEPENDENCIES bundler (~> 2.0) + i18n + i18n-debug json_schemer! minitest (~> 5.0) rake (~> 13.0) diff --git a/JSON-Schema-Test-Suite/README.md b/JSON-Schema-Test-Suite/README.md index cdd5dc8a..f638315c 100644 --- a/JSON-Schema-Test-Suite/README.md +++ b/JSON-Schema-Test-Suite/README.md @@ -313,7 +313,7 @@ Node-specific support is maintained in a [separate repository](https://github.co * [fastjsonschema](https://github.com/seznam/python-fastjsonschema) * [hypothesis-jsonschema](https://github.com/Zac-HD/hypothesis-jsonschema) * [jschon](https://github.com/marksparkza/jschon) -* [python-experimental, OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/python-experimental.md) +* [OpenAPI JSON Schema Generator](https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) ### Ruby diff --git a/JSON-Schema-Test-Suite/bin/jsonschema_suite b/JSON-Schema-Test-Suite/bin/jsonschema_suite index 9fee8d7b..c83e7cb2 100755 --- a/JSON-Schema-Test-Suite/bin/jsonschema_suite +++ b/JSON-Schema-Test-Suite/bin/jsonschema_suite @@ -14,9 +14,11 @@ import warnings try: import jsonschema.validators except ImportError: - jsonschema = None + jsonschema = Unresolvable = None VALIDATORS = {} else: + from referencing.exceptions import Unresolvable + VALIDATORS = { "draft3": jsonschema.validators.Draft3Validator, "draft4": jsonschema.validators.Draft4Validator, @@ -587,7 +589,7 @@ class SanityTests(unittest.TestCase): with self.subTest(case=case, version=version.name): try: Validator(case["schema"]).is_valid(12) - except jsonschema.exceptions.RefResolutionError: + except Unresolvable: pass @unittest.skipIf(jsonschema is None, "Validation library not present!") @@ -615,9 +617,6 @@ class SanityTests(unittest.TestCase): with self.subTest(path=path): try: validator.validate(cases) - except jsonschema.exceptions.RefResolutionError: - # python-jsonschema/jsonschema#884 - pass except jsonschema.ValidationError as error: self.fail(str(error)) diff --git a/JSON-Schema-Test-Suite/output-tests/draft2019-09/content/type.json b/JSON-Schema-Test-Suite/output-tests/draft2019-09/content/type.json index cff77a74..21118fd5 100644 --- a/JSON-Schema-Test-Suite/output-tests/draft2019-09/content/type.json +++ b/JSON-Schema-Test-Suite/output-tests/draft2019-09/content/type.json @@ -31,32 +31,6 @@ "required": ["errors"] } } - }, - { - "description": "correct type yields an output unit", - "data": "a string", - "output": { - "basic": { - "$id": "https://json-schema.org/tests/content/draft2019-09/type/0/tests/1/basic", - "$ref": "/draft/2019-09/output/schema", - "properties": { - "annotations": { - "contains": { - "properties": { - "valid": {"const": true}, - "keywordLocation": {"const": "/type"}, - "absoluteKeywordLocation": {"const": "https://json-schema.org/tests/content/draft2019-09/type/0#/type"}, - "instanceLocation": {"const": ""}, - "annotation": false, - "error": false - }, - "required": ["keywordLocation", "instanceLocation"] - } - } - }, - "required": ["annotations"] - } - } } ] } diff --git a/JSON-Schema-Test-Suite/output-tests/draft2020-12/content/type.json b/JSON-Schema-Test-Suite/output-tests/draft2020-12/content/type.json index 710475b2..2949a605 100644 --- a/JSON-Schema-Test-Suite/output-tests/draft2020-12/content/type.json +++ b/JSON-Schema-Test-Suite/output-tests/draft2020-12/content/type.json @@ -31,32 +31,6 @@ "required": ["errors"] } } - }, - { - "description": "correct type yields an output unit", - "data": "a string", - "output": { - "basic": { - "$id": "https://json-schema.org/tests/content/draft2020-12/type/0/tests/1/basic", - "$ref": "/draft/2020-12/output/schema", - "properties": { - "annotations": { - "contains": { - "properties": { - "valid": {"const": true}, - "keywordLocation": {"const": "/type"}, - "absoluteKeywordLocation": {"const": "https://json-schema.org/tests/content/draft2020-12/type/0#/type"}, - "instanceLocation": {"const": ""}, - "annotation": false, - "error": false - }, - "required": ["keywordLocation", "instanceLocation"] - } - } - }, - "required": ["annotations"] - } - } } ] } diff --git a/JSON-Schema-Test-Suite/remotes/draft-next/subSchemas-defs.json b/JSON-Schema-Test-Suite/remotes/draft-next/subSchemas-defs.json deleted file mode 100644 index 75b7583c..00000000 --- a/JSON-Schema-Test-Suite/remotes/draft-next/subSchemas-defs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/JSON-Schema-Test-Suite/remotes/draft-next/subSchemas.json b/JSON-Schema-Test-Suite/remotes/draft-next/subSchemas.json index 575dd00c..75b7583c 100644 --- a/JSON-Schema-Test-Suite/remotes/draft-next/subSchemas.json +++ b/JSON-Schema-Test-Suite/remotes/draft-next/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/next/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/JSON-Schema-Test-Suite/remotes/draft2019-09/subSchemas-defs.json b/JSON-Schema-Test-Suite/remotes/draft2019-09/subSchemas-defs.json deleted file mode 100644 index fdfee68d..00000000 --- a/JSON-Schema-Test-Suite/remotes/draft2019-09/subSchemas-defs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/JSON-Schema-Test-Suite/remotes/draft2019-09/subSchemas.json b/JSON-Schema-Test-Suite/remotes/draft2019-09/subSchemas.json index 6dea2252..fdfee68d 100644 --- a/JSON-Schema-Test-Suite/remotes/draft2019-09/subSchemas.json +++ b/JSON-Schema-Test-Suite/remotes/draft2019-09/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/JSON-Schema-Test-Suite/remotes/draft2020-12/subSchemas-defs.json b/JSON-Schema-Test-Suite/remotes/draft2020-12/subSchemas-defs.json deleted file mode 100644 index 1bb4846d..00000000 --- a/JSON-Schema-Test-Suite/remotes/draft2020-12/subSchemas-defs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/JSON-Schema-Test-Suite/remotes/draft2020-12/subSchemas.json b/JSON-Schema-Test-Suite/remotes/draft2020-12/subSchemas.json index 5fca21d8..1bb4846d 100644 --- a/JSON-Schema-Test-Suite/remotes/draft2020-12/subSchemas.json +++ b/JSON-Schema-Test-Suite/remotes/draft2020-12/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/JSON-Schema-Test-Suite/remotes/subSchemas-defs.json b/JSON-Schema-Test-Suite/remotes/subSchemas-defs.json deleted file mode 100644 index 50b7b6dc..00000000 --- a/JSON-Schema-Test-Suite/remotes/subSchemas-defs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/JSON-Schema-Test-Suite/remotes/subSchemas.json b/JSON-Schema-Test-Suite/remotes/subSchemas.json index 9f8030bc..6e9b3de3 100644 --- a/JSON-Schema-Test-Suite/remotes/subSchemas.json +++ b/JSON-Schema-Test-Suite/remotes/subSchemas.json @@ -1,8 +1,10 @@ { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } } diff --git a/JSON-Schema-Test-Suite/tests/draft-next/dependentSchemas.json b/JSON-Schema-Test-Suite/tests/draft-next/dependentSchemas.json index 8a847759..86079c34 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/dependentSchemas.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "properties": { "foo": {} }, diff --git a/JSON-Schema-Test-Suite/tests/draft-next/items.json b/JSON-Schema-Test-Suite/tests/draft-next/items.json index 459943be..dfb79af2 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/items.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft-next/optional/format/hostname.json b/JSON-Schema-Test-Suite/tests/draft-next/optional/format/hostname.json index 96784865..bfb30636 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/optional/format/hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft-next/optional/format/idn-hostname.json b/JSON-Schema-Test-Suite/tests/draft-next/optional/format/idn-hostname.json index ee2e792f..109bf73c 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/optional/format/idn-hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft-next/optional/format/ipv4.json b/JSON-Schema-Test-Suite/tests/draft-next/optional/format/ipv4.json index e3e94401..2a4bc2b2 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/optional/format/ipv4.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft-next/optional/refOfUnknownKeyword.json b/JSON-Schema-Test-Suite/tests/draft-next/optional/refOfUnknownKeyword.json index 489701cd..c832e09f 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/optional/refOfUnknownKeyword.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/JSON-Schema-Test-Suite/tests/draft-next/ref.json b/JSON-Schema-Test-Suite/tests/draft-next/ref.json index 1d5f2561..8417ce29 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/ref.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$defs": { "": { "$defs": { diff --git a/JSON-Schema-Test-Suite/tests/draft-next/refRemote.json b/JSON-Schema-Test-Suite/tests/draft-next/refRemote.json index 9befceb2..647fb9f1 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/refRemote.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -314,6 +323,7 @@ { "description": "$ref to $ref finds detached $anchor", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://localhost:1234/draft-next/detached-ref.json#/$defs/foo" }, "tests": [ diff --git a/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedItems.json b/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedItems.json index 7379afb4..8dda001f 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedItems.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedItems.json @@ -461,6 +461,48 @@ } ] }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedProperties.json b/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedProperties.json index 69fe8a00..4fe7986d 100644 --- a/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedProperties.json +++ b/JSON-Schema-Test-Suite/tests/draft-next/unevaluatedProperties.json @@ -715,6 +715,54 @@ } ] }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/additionalItems.json b/JSON-Schema-Test-Suite/tests/draft2019-09/additionalItems.json index aa44bcb7..9a7ae4f8 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/additionalItems.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/additionalItems.json @@ -182,6 +182,26 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/dependentSchemas.json b/JSON-Schema-Test-Suite/tests/draft2019-09/dependentSchemas.json index 3577efdf..c5b8ea05 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/dependentSchemas.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": {} }, diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/hostname.json b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/hostname.json index eac8cac6..f3b7181c 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/idn-hostname.json b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/idn-hostname.json index 72f17975..072a6b08 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/idn-hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/ipv4.json b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/ipv4.json index ac1e14c6..efe42471 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/ipv4.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/refOfUnknownKeyword.json b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/refOfUnknownKeyword.json index eee1c33e..e9a75dd1 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/optional/refOfUnknownKeyword.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/ref.json b/JSON-Schema-Test-Suite/tests/draft2019-09/ref.json index 7d850414..ea569908 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/ref.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$defs": { "": { "$defs": { diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/refRemote.json b/JSON-Schema-Test-Suite/tests/draft2019-09/refRemote.json index 0bacbfc2..072894cf 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/refRemote.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -314,6 +323,7 @@ { "description": "$ref to $ref finds detached $anchor", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://localhost:1234/draft2019-09/detached-ref.json#/$defs/foo" }, "tests": [ diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedItems.json b/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedItems.json index 53565a0b..9c115ab3 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedItems.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedItems.json @@ -480,6 +480,51 @@ } ] }, + { + "description": "unevaluatedItems with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "/tree", + "items": [ + true, + true, + { "type": "string" } + ], + + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + + "type": "array", + "items": [ + { "type": "number" }, + { + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "$recursiveRef": "#" + } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": [1, [2, [], "b"], "a"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": [1, [2, [], "b", "too many"], "a"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedProperties.json b/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedProperties.json index a6cce8bb..4e0d3ec8 100644 --- a/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedProperties.json +++ b/JSON-Schema-Test-Suite/tests/draft2019-09/unevaluatedProperties.json @@ -715,6 +715,64 @@ } ] }, + { + "description": "unevaluatedProperties with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "/tree", + "properties": { + "name": { "type": "string" } + }, + + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + + "type": "object", + "properties": { + "node": true, + "branches": { + "$comment": "unevaluatedProperties comes first so it's more likely to bugs errors with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "$recursiveRef": "#" + } + }, + "required": ["node"] + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "name": "b", + "node": 2 + } + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "foo": "b", + "node": 2 + } + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/dependentSchemas.json b/JSON-Schema-Test-Suite/tests/draft2020-12/dependentSchemas.json index 66ac0eb4..1c5f0574 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/dependentSchemas.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": {} }, diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/items.json b/JSON-Schema-Test-Suite/tests/draft2020-12/items.json index 1ef18bdd..6a3e1cf2 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/items.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/hostname.json b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/hostname.json index c8db9770..41418dd4 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/idn-hostname.json b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/idn-hostname.json index 5549c055..bc7d92f6 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/idn-hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/ipv4.json b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/ipv4.json index c72b6fc2..86d27bdb 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/ipv4.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/refOfUnknownKeyword.json b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/refOfUnknownKeyword.json index f91c1888..c2b080a1 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/optional/refOfUnknownKeyword.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/ref.json b/JSON-Schema-Test-Suite/tests/draft2020-12/ref.json index 5f6be8c2..8d15fa43 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/ref.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { "": { "$defs": { diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/refRemote.json b/JSON-Schema-Test-Suite/tests/draft2020-12/refRemote.json index ea4177f0..047ac74c 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/refRemote.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -314,6 +323,7 @@ { "description": "$ref to $ref finds detached $anchor", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://localhost:1234/draft2020-12/detached-ref.json#/$defs/foo" }, "tests": [ diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedItems.json b/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedItems.json index 2615c4c4..ddc35da2 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedItems.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedItems.json @@ -461,6 +461,55 @@ } ] }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedProperties.json b/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedProperties.json index f7fb420f..023e84a5 100644 --- a/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedProperties.json +++ b/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedProperties.json @@ -715,6 +715,61 @@ } ] }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft3/additionalItems.json b/JSON-Schema-Test-Suite/tests/draft3/additionalItems.json index 0cb66870..ab44a2eb 100644 --- a/JSON-Schema-Test-Suite/tests/draft3/additionalItems.json +++ b/JSON-Schema-Test-Suite/tests/draft3/additionalItems.json @@ -110,6 +110,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft3/refRemote.json b/JSON-Schema-Test-Suite/tests/draft3/refRemote.json index de0cb43a..0e4ab53e 100644 --- a/JSON-Schema-Test-Suite/tests/draft3/refRemote.json +++ b/JSON-Schema-Test-Suite/tests/draft3/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json b/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json index deb44fd3..c9e68154 100644 --- a/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json +++ b/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json @@ -146,6 +146,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft4/optional/format/hostname.json b/JSON-Schema-Test-Suite/tests/draft4/optional/format/hostname.json index 8a67fda8..a8ecd194 100644 --- a/JSON-Schema-Test-Suite/tests/draft4/optional/format/hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft4/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft4/optional/format/ipv4.json b/JSON-Schema-Test-Suite/tests/draft4/optional/format/ipv4.json index 4706581f..9680fe62 100644 --- a/JSON-Schema-Test-Suite/tests/draft4/optional/format/ipv4.json +++ b/JSON-Schema-Test-Suite/tests/draft4/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft4/refRemote.json b/JSON-Schema-Test-Suite/tests/draft4/refRemote.json index 412c9ff8..64a618b8 100644 --- a/JSON-Schema-Test-Suite/tests/draft4/refRemote.json +++ b/JSON-Schema-Test-Suite/tests/draft4/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/JSON-Schema-Test-Suite/tests/draft6/additionalItems.json b/JSON-Schema-Test-Suite/tests/draft6/additionalItems.json index cae72361..2c7d1558 100644 --- a/JSON-Schema-Test-Suite/tests/draft6/additionalItems.json +++ b/JSON-Schema-Test-Suite/tests/draft6/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft6/optional/format/hostname.json b/JSON-Schema-Test-Suite/tests/draft6/optional/format/hostname.json index 8a67fda8..a8ecd194 100644 --- a/JSON-Schema-Test-Suite/tests/draft6/optional/format/hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft6/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft6/optional/format/ipv4.json b/JSON-Schema-Test-Suite/tests/draft6/optional/format/ipv4.json index 4706581f..9680fe62 100644 --- a/JSON-Schema-Test-Suite/tests/draft6/optional/format/ipv4.json +++ b/JSON-Schema-Test-Suite/tests/draft6/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft6/refRemote.json b/JSON-Schema-Test-Suite/tests/draft6/refRemote.json index 5d60fae1..28459c4a 100644 --- a/JSON-Schema-Test-Suite/tests/draft6/refRemote.json +++ b/JSON-Schema-Test-Suite/tests/draft6/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json b/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json index cae72361..2c7d1558 100644 --- a/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json +++ b/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json b/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json index 8a67fda8..a8ecd194 100644 --- a/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json b/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json index 6c8f86a3..dc47f7b5 100644 --- a/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json +++ b/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json @@ -298,6 +298,26 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json b/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json index 4706581f..9680fe62 100644 --- a/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json +++ b/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/JSON-Schema-Test-Suite/tests/draft7/refRemote.json b/JSON-Schema-Test-Suite/tests/draft7/refRemote.json index 115e12e7..22185d67 100644 --- a/JSON-Schema-Test-Suite/tests/draft7/refRemote.json +++ b/JSON-Schema-Test-Suite/tests/draft7/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/JSON-Schema-Test-Suite/tox.ini b/JSON-Schema-Test-Suite/tox.ini index dcc0dce6..a5ded970 100644 --- a/JSON-Schema-Test-Suite/tox.ini +++ b/JSON-Schema-Test-Suite/tox.ini @@ -5,5 +5,5 @@ skipsdist = True [testenv:sanity] # used just for validating the structure of the test case files themselves -deps = jsonschema==4.18.0a4 +deps = jsonschema==4.19.0 commands = {envpython} bin/jsonschema_suite check diff --git a/README.md b/README.md index b119fa6a..fca52664 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,37 @@ JSONSchemer.schema( # default: true format: true, + # custom formats + formats: { + 'int32' => proc do |instance, _format| + instance.is_a?(Integer) && instance.bit_length <= 32 + end, + # disable specific format + 'email' => false + }, + + # custom content encodings + # only `base64` is available by default + content_encodings: { + # return [success, annotation] tuple + 'urlsafe_base64' => proc do |instance| + [true, Base64.urlsafe_decode64(instance)] + rescue + [false, nil] + end + }, + + # custom content media types + # only `application/json` is available by default + content_media_types: { + # return [success, annotation] tuple + 'text/csv' => proc do |instance| + [true, CSV.parse(instance)] + rescue + [false, nil] + end + }, + # insert default property values during validation # true/false # default: false @@ -225,6 +256,166 @@ JSONSchemer.schema( ) ``` +## Custom Error Messages + +Error messages can be customized using the `x-error` keyword and/or [I18n](https://github.com/ruby-i18n/i18n) translations. `x-error` takes precedence if both are defined. + +### `x-error` Keyword + +```ruby +# override all errors for a schema +schemer = JSONSchemer.schema({ + 'type' => 'string', + 'x-error' => 'custom error for schema and all keywords' +}) + +schemer.validate(1).first +# => {"data"=>1, +# "data_pointer"=>"", +# "schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"}, +# "schema_pointer"=>"", +# "root_schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"}, +# "type"=>"string", +# "error"=>"custom error for schema and all keywords", +# "x-error"=>true} + +schemer.validate(1, :output_format => 'basic') +# => {"valid"=>false, +# "keywordLocation"=>"", +# "absoluteKeywordLocation"=>"json-schemer://schema#", +# "instanceLocation"=>"", +# "error"=>"custom error for schema and all keywords", +# "x-error"=>true, +# "errors"=>#} + +# keyword-specific errors +schemer = JSONSchemer.schema({ + 'type' => 'string', + 'minLength' => 10, + 'x-error' => { + 'type' => 'custom error for `type` keyword', + # special `^` keyword for schema-level error + '^' => 'custom error for schema', + # same behavior as when `x-error` is a string + '*' => 'fallback error for schema and all keywords' + } +}) + +schemer.validate(1).map { _1.fetch('error') } +# => ["custom error for `type` keyword"] + +schemer.validate('1').map { _1.fetch('error') } +# => ["custom error for schema and all keywords"] + +schemer.validate(1, :output_format => 'basic').fetch('error') +# => "custom error for schema" + +# variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation) +schemer = JSONSchemer.schema({ + '$id' => 'https://example.com/schema', + 'properties' => { + 'abc' => { + 'type' => 'string', + 'x-error' => <<~ERROR + instance: %{instance} + instance location: %{instanceLocation} + keyword location: %{keywordLocation} + absolute keyword location: %{absoluteKeywordLocation} + ERROR + } + } +}) + +puts schemer.validate({ 'abc' => 1 }).first.fetch('error') +# instance: 1 +# instance location: /abc +# keyword location: /properties/abc/type +# absolute keyword location: https://example.com/schema#/properties/abc/type +``` + +### I18n + +When the [I18n gem](https://github.com/ruby-i18n/i18n) is loaded, custom error messages are looked up under the `json_schemer` key. It may be necessary to restart your application after adding the root key because the existence check is cached for performance reasons. + +Translation keys are looked up in this order: + +1. `$LOCALE.json_schemer.errors.$ABSOLUTE_KEYWORD_LOCATION` +2. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD_LOCATION` +3. `$LOCALE.json_schemer.errors.$KEYWORD_LOCATION` +4. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD` +5. `$LOCALE.json_schemer.errors.$SCHEMA_ID.*` +6. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.$KEYWORD` +7. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.*` +8. `$LOCALE.json_schemer.errors.$KEYWORD` +9. `$LOCALE.json_schemer.errors.*` + +Example translations file: + +```yaml +en: + json_schemer: + errors: + 'https://example.com/schema#/properties/abc/type': custom error for absolute keyword location + 'https://example.com/schema': + '#/properties/abc/type': custom error for keyword location, nested under schema $id + 'type': custom error for `type` keyword, nested under schema $id + '^': custom error for schema, nested under schema $id + '*': fallback error for schema and all keywords, nested under schema $id + '#/properties/abc/type': custom error for keyword location + 'http://json-schema.org/draft-07/schema#': + 'type': custom error for `type` keyword, nested under meta-schema $id ($schema) + '^': custom error for schema, nested under meta-schema $id + '*': fallback error for schema and all keywords, nested under meta-schema $id ($schema) + 'type': custom error for `type` keyword + '^': custom error for schema + # variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation) + '*': | + fallback error for schema and all keywords + instance: %{instance} + instance location: %{instanceLocation} + keyword location: %{keywordLocation} + absolute keyword location: %{absoluteKeywordLocation} +``` + +And output: + +```ruby +require 'i18n' +I18n.locale = :en # $LOCALE=en + +schemer = JSONSchemer.schema({ + '$id' => 'https://example.com/schema', # $SCHEMA_ID=https://example.com/schema + '$schema' => 'http://json-schema.org/draft-07/schema#', # $META_SCHEMA_ID=http://json-schema.org/draft-07/schema# + 'properties' => { + 'abc' => { + 'type' => 'integer' # $KEYWORD=type + } # $KEYWORD_LOCATION=#/properties/abc/type + } # $ABSOLUTE_KEYWORD_LOCATION=https://example.com/schema#/properties/abc/type +}) + +schemer.validate({ 'abc' => 'not-an-integer' }).first +# => {"data"=>"not-an-integer", +# "data_pointer"=>"/abc", +# "schema"=>{"type"=>"integer"}, +# "schema_pointer"=>"/properties/abc", +# "root_schema"=>{"$id"=>"https://example.com/schema", "$schema"=>"http://json-schema.org/draft-07/schema#", "properties"=>{"abc"=>{"type"=>"integer"}}}, +# "type"=>"integer", +# "error"=>"custom error for absolute keyword location", +# "i18n"=>true +``` + +In the example above, custom error messsages are looked up using the following keys (in order until one is found): + +1. `en.json_schemer.errors.'https://example.com/schema#/properties/abc/type'` +2. `en.json_schemer.errors.'https://example.com/schema'.'#/properties/abc/type'` +3. `en.json_schemer.errors.'#/properties/abc/type'` +4. `en.json_schemer.errors.'https://example.com/schema'.type` +5. `en.json_schemer.errors.'https://example.com/schema'.*` +6. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.type` +7. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.*` +8. `en.json_schemer.errors.type` +9. `en.json_schemer.errors.*` + ## OpenAPI ```ruby diff --git a/json_schemer.gemspec b/json_schemer.gemspec index be40b9d0..122a2dc6 100644 --- a/json_schemer.gemspec +++ b/json_schemer.gemspec @@ -26,6 +26,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "simplecov", "~> 0.22" + spec.add_development_dependency "i18n" + spec.add_development_dependency "i18n-debug" spec.add_runtime_dependency "hana", "~> 1.3" spec.add_runtime_dependency "regexp_parser", "~> 2.0" diff --git a/lib/json_schemer.rb b/lib/json_schemer.rb index f2c2817b..49ad1449 100644 --- a/lib/json_schemer.rb +++ b/lib/json_schemer.rb @@ -20,6 +20,7 @@ require 'json_schemer/format/uri_template' require 'json_schemer/format/email' require 'json_schemer/format' +require 'json_schemer/content' require 'json_schemer/errors' require 'json_schemer/cached_resolver' require 'json_schemer/ecma_regexp' @@ -145,6 +146,9 @@ def draft202012 @draft202012 ||= Schema.new( Draft202012::SCHEMA, :base_uri => Draft202012::BASE_URI, + :formats => Draft202012::FORMATS, + :content_encodings => Draft202012::CONTENT_ENCODINGS, + :content_media_types => Draft202012::CONTENT_MEDIA_TYPES, :ref_resolver => Draft202012::Meta::SCHEMAS.to_proc, :regexp_resolver => 'ecma' ) @@ -154,6 +158,9 @@ def draft201909 @draft201909 ||= Schema.new( Draft201909::SCHEMA, :base_uri => Draft201909::BASE_URI, + :formats => Draft201909::FORMATS, + :content_encodings => Draft201909::CONTENT_ENCODINGS, + :content_media_types => Draft201909::CONTENT_MEDIA_TYPES, :ref_resolver => Draft201909::Meta::SCHEMAS.to_proc, :regexp_resolver => 'ecma' ) @@ -164,6 +171,9 @@ def draft7 Draft7::SCHEMA, :vocabulary => { 'json-schemer://draft7' => true }, :base_uri => Draft7::BASE_URI, + :formats => Draft7::FORMATS, + :content_encodings => Draft7::CONTENT_ENCODINGS, + :content_media_types => Draft7::CONTENT_MEDIA_TYPES, :regexp_resolver => 'ecma' ) end @@ -173,6 +183,9 @@ def draft6 Draft6::SCHEMA, :vocabulary => { 'json-schemer://draft6' => true }, :base_uri => Draft6::BASE_URI, + :formats => Draft6::FORMATS, + :content_encodings => Draft6::CONTENT_ENCODINGS, + :content_media_types => Draft6::CONTENT_MEDIA_TYPES, :regexp_resolver => 'ecma' ) end @@ -182,6 +195,9 @@ def draft4 Draft4::SCHEMA, :vocabulary => { 'json-schemer://draft4' => true }, :base_uri => Draft4::BASE_URI, + :formats => Draft4::FORMATS, + :content_encodings => Draft4::CONTENT_ENCODINGS, + :content_media_types => Draft4::CONTENT_MEDIA_TYPES, :regexp_resolver => 'ecma' ) end @@ -190,16 +206,9 @@ def openapi31 @openapi31 ||= Schema.new( OpenAPI31::SCHEMA, :base_uri => OpenAPI31::BASE_URI, + :formats => OpenAPI31::FORMATS, :ref_resolver => OpenAPI31::Meta::SCHEMAS.to_proc, - :regexp_resolver => 'ecma', - # https://spec.openapis.org/oas/latest.html#data-types - :formats => { - 'int32' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 32 }, - 'int64' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 64 }, - 'float' => proc { |instance, _value| instance.is_a?(Float) }, - 'double' => proc { |instance, _value| instance.is_a?(Float) }, - 'password' => proc { |_instance, _value| true } - } + :regexp_resolver => 'ecma' ) end @@ -211,17 +220,9 @@ def openapi30 'json-schemer://openapi30' => true }, :base_uri => OpenAPI30::BASE_URI, + :formats => OpenAPI30::FORMATS, :ref_resolver => OpenAPI30::Meta::SCHEMAS.to_proc, - :regexp_resolver => 'ecma', - :formats => { - 'int32' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 32 }, - 'int64' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 64 }, - 'float' => proc { |instance, _value| instance.is_a?(Float) }, - 'double' => proc { |instance, _value| instance.is_a?(Float) }, - 'byte' => proc { |instance, _value| Format.decode_content_encoding(instance, 'base64').first }, - 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT }, - 'password' => proc { |_instance, _value| true } - } + :regexp_resolver => 'ecma' ) end diff --git a/lib/json_schemer/content.rb b/lib/json_schemer/content.rb new file mode 100644 index 00000000..40ac3282 --- /dev/null +++ b/lib/json_schemer/content.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module JSONSchemer + module ContentEncoding + BASE64 = proc do |instance| + [true, Base64.strict_decode64(instance)] + rescue + [false, nil] + end + end + + module ContentMediaType + JSON = proc do |instance| + [true, ::JSON.parse(instance)] + rescue + [false, nil] + end + end +end diff --git a/lib/json_schemer/draft201909/meta.rb b/lib/json_schemer/draft201909/meta.rb index ac307a70..54c6b12e 100644 --- a/lib/json_schemer/draft201909/meta.rb +++ b/lib/json_schemer/draft201909/meta.rb @@ -2,6 +2,9 @@ module JSONSchemer module Draft201909 BASE_URI = URI('https://json-schema.org/draft/2019-09/schema') + FORMATS = Draft202012::FORMATS + CONTENT_ENCODINGS = Draft202012::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft202012::CONTENT_MEDIA_TYPES SCHEMA = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/schema', @@ -48,9 +51,6 @@ module Meta CORE = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/core', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/core' => true - }, '$recursiveAnchor' => true, 'title' => 'Core vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -105,9 +105,6 @@ module Meta APPLICATOR = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/applicator', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/applicator' => true - }, '$recursiveAnchor' => true, 'title' => 'Applicator vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -161,9 +158,6 @@ module Meta VALIDATION = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/validation', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/validation' => true - }, '$recursiveAnchor' => true, 'title' => 'Validation vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -259,9 +253,6 @@ module Meta META_DATA = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/meta-data', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/meta-data' => true - }, '$recursiveAnchor' => true, 'title' => 'Meta-data vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -295,9 +286,6 @@ module Meta FORMAT = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/format', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/format' => true - }, '$recursiveAnchor' => true, 'title' => 'Format vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -309,9 +297,6 @@ module Meta CONTENT = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/content', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/content' => true - }, '$recursiveAnchor' => true, 'title' => 'Content vocabulary meta-schema', 'type' => ['object', 'boolean'], diff --git a/lib/json_schemer/draft202012/meta.rb b/lib/json_schemer/draft202012/meta.rb index e31d7625..3b1729f6 100644 --- a/lib/json_schemer/draft202012/meta.rb +++ b/lib/json_schemer/draft202012/meta.rb @@ -2,6 +2,33 @@ module JSONSchemer module Draft202012 BASE_URI = URI('https://json-schema.org/draft/2020-12/schema') + FORMATS = { + 'date-time' => Format::DATE_TIME, + 'date' => Format::DATE, + 'time' => Format::TIME, + 'duration' => Format::DURATION, + 'email' => Format::EMAIL, + 'idn-email' => Format::IDN_EMAIL, + 'hostname' => Format::HOSTNAME, + 'idn-hostname' => Format::IDN_HOSTNAME, + 'ipv4' => Format::IPV4, + 'ipv6' => Format::IPV6, + 'uri' => Format::URI, + 'uri-reference' => Format::URI_REFERENCE, + 'iri' => Format::IRI, + 'iri-reference' => Format::IRI_REFERENCE, + 'uuid' => Format::UUID, + 'uri-template' => Format::URI_TEMPLATE, + 'json-pointer' => Format::JSON_POINTER, + 'relative-json-pointer' => Format::RELATIVE_JSON_POINTER, + 'regex' => Format::REGEX + } + CONTENT_ENCODINGS = { + 'base64' => ContentEncoding::BASE64 + } + CONTENT_MEDIA_TYPES = { + 'application/json' => ContentMediaType::JSON + } SCHEMA = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/schema', @@ -64,9 +91,6 @@ module Meta CORE = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/core', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/core' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Core vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -114,9 +138,6 @@ module Meta APPLICATOR = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/applicator', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/applicator' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Applicator vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -161,9 +182,6 @@ module Meta UNEVALUATED = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/unevaluated', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/unevaluated' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Unevaluated applicator vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -175,9 +193,6 @@ module Meta VALIDATION = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/validation', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/validation' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Validation vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -272,9 +287,6 @@ module Meta META_DATA = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/meta-data', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/meta-data' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Meta-data vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -307,9 +319,6 @@ module Meta FORMAT_ANNOTATION = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/format-annotation', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Format vocabulary meta-schema for annotation results', 'type' => ['object', 'boolean'], @@ -320,9 +329,6 @@ module Meta FORMAT_ASSERTION = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/format-assertion', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/format-assertion' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Format vocabulary meta-schema for assertion results', 'type' => ['object', 'boolean'], @@ -333,9 +339,6 @@ module Meta CONTENT = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/content', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/content' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Content vocabulary meta-schema', 'type' => ['object', 'boolean'], diff --git a/lib/json_schemer/draft202012/vocab.rb b/lib/json_schemer/draft202012/vocab.rb index 7eaafc51..a77b70b0 100644 --- a/lib/json_schemer/draft202012/vocab.rb +++ b/lib/json_schemer/draft202012/vocab.rb @@ -16,7 +16,9 @@ module Vocab '$defs' => Core::Defs, 'definitions' => Core::Defs, # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01#section-8.3 - '$comment' => Core::Comment + '$comment' => Core::Comment, + # https://github.com/orgs/json-schema-org/discussions/329 + 'x-error' => Core::XError } # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01#section-10 APPLICATOR = { diff --git a/lib/json_schemer/draft202012/vocab/content.rb b/lib/json_schemer/draft202012/vocab/content.rb index abdefd95..8bfbbd75 100644 --- a/lib/json_schemer/draft202012/vocab/content.rb +++ b/lib/json_schemer/draft202012/vocab/content.rb @@ -4,21 +4,29 @@ module Draft202012 module Vocab module Content class ContentEncoding < Keyword + def parse + root.fetch_content_encoding(value) { raise UnknownContentEncoding, value } + end + def validate(instance, instance_location, keyword_location, _context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) - _valid, annotation = Format.decode_content_encoding(instance, value) + _valid, annotation = parsed.call(instance) result(instance, instance_location, keyword_location, true, :annotation => annotation) end end class ContentMediaType < Keyword + def parse + root.fetch_content_media_type(value) { raise UnknownContentMediaType, value } + end + def validate(instance, instance_location, keyword_location, context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance - _valid, annotation = Format.parse_content_media_type(decoded_instance, value) + _valid, annotation = parsed.call(decoded_instance) result(instance, instance_location, keyword_location, true, :annotation => annotation) end diff --git a/lib/json_schemer/draft202012/vocab/core.rb b/lib/json_schemer/draft202012/vocab/core.rb index 8d288d06..66fea2c1 100644 --- a/lib/json_schemer/draft202012/vocab/core.rb +++ b/lib/json_schemer/draft202012/vocab/core.rb @@ -119,6 +119,12 @@ def parse class Comment < Keyword; end + class XError < Keyword + def message(error_key) + value.is_a?(Hash) ? (value[error_key] || value[CATCHALL]) : value + end + end + class UnknownKeyword < Keyword def parse if value.is_a?(Hash) diff --git a/lib/json_schemer/draft202012/vocab/format_annotation.rb b/lib/json_schemer/draft202012/vocab/format_annotation.rb index 34c3b05b..9bc59cf7 100644 --- a/lib/json_schemer/draft202012/vocab/format_annotation.rb +++ b/lib/json_schemer/draft202012/vocab/format_annotation.rb @@ -4,20 +4,12 @@ module Draft202012 module Vocab module FormatAnnotation class Format < Keyword - extend JSONSchemer::Format - - DEFAULT_FORMAT = proc do |instance, value| - !instance.is_a?(String) || valid_spec_format?(instance, value) - rescue UnknownFormat - true - end - def error(formatted_instance_location:, **) "value at #{formatted_instance_location} does not match format: #{value}" end def parse - root.format && root.formats.fetch(value) { root.meta_schema.formats.fetch(value, DEFAULT_FORMAT) } + root.format && root.fetch_format(value, false) end def validate(instance, instance_location, keyword_location, _context) diff --git a/lib/json_schemer/draft202012/vocab/format_assertion.rb b/lib/json_schemer/draft202012/vocab/format_assertion.rb index 2ff6117c..05b5da16 100644 --- a/lib/json_schemer/draft202012/vocab/format_assertion.rb +++ b/lib/json_schemer/draft202012/vocab/format_assertion.rb @@ -4,18 +4,12 @@ module Draft202012 module Vocab module FormatAssertion class Format < Keyword - extend JSONSchemer::Format - - DEFAULT_FORMAT = proc do |instance, value| - !instance.is_a?(String) || valid_spec_format?(instance, value) - end - def error(formatted_instance_location:, **) "value at #{formatted_instance_location} does not match format: #{value}" end def parse - root.format && root.formats.fetch(value) { root.meta_schema.formats.fetch(value, DEFAULT_FORMAT) } + root.format && root.fetch_format(value) { raise UnknownFormat, value } end def validate(instance, instance_location, keyword_location, _context) diff --git a/lib/json_schemer/draft4/meta.rb b/lib/json_schemer/draft4/meta.rb index ed3421c8..14d9c27b 100644 --- a/lib/json_schemer/draft4/meta.rb +++ b/lib/json_schemer/draft4/meta.rb @@ -2,6 +2,12 @@ module JSONSchemer module Draft4 BASE_URI = URI('http://json-schema.org/draft-04/schema#') + FORMATS = Draft6::FORMATS.dup + FORMATS.delete('uri-reference') + FORMATS.delete('uri-template') + FORMATS.delete('json-pointer') + CONTENT_ENCODINGS = Draft6::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft6::CONTENT_MEDIA_TYPES SCHEMA = { 'id' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#', diff --git a/lib/json_schemer/draft6/meta.rb b/lib/json_schemer/draft6/meta.rb index 9ffa8842..502a0268 100644 --- a/lib/json_schemer/draft6/meta.rb +++ b/lib/json_schemer/draft6/meta.rb @@ -2,6 +2,17 @@ module JSONSchemer module Draft6 BASE_URI = URI('http://json-schema.org/draft-06/schema#') + FORMATS = Draft7::FORMATS.dup + FORMATS.delete('date') + FORMATS.delete('time') + FORMATS.delete('idn-email') + FORMATS.delete('idn-hostname') + FORMATS.delete('iri') + FORMATS.delete('iri-reference') + FORMATS.delete('relative-json-pointer') + FORMATS.delete('regex') + CONTENT_ENCODINGS = Draft7::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft7::CONTENT_MEDIA_TYPES SCHEMA = { '$schema' => 'http://json-schema.org/draft-06/schema#', '$id' => 'http://json-schema.org/draft-06/schema#', diff --git a/lib/json_schemer/draft7/meta.rb b/lib/json_schemer/draft7/meta.rb index 3405404a..832aa857 100644 --- a/lib/json_schemer/draft7/meta.rb +++ b/lib/json_schemer/draft7/meta.rb @@ -2,6 +2,11 @@ module JSONSchemer module Draft7 BASE_URI = URI('http://json-schema.org/draft-07/schema#') + FORMATS = Draft201909::FORMATS.dup + FORMATS.delete('duration') + FORMATS.delete('uuid') + CONTENT_ENCODINGS = Draft201909::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft201909::CONTENT_MEDIA_TYPES SCHEMA = { '$schema' => 'http://json-schema.org/draft-07/schema#', '$id' => 'http://json-schema.org/draft-07/schema#', diff --git a/lib/json_schemer/draft7/vocab/validation.rb b/lib/json_schemer/draft7/vocab/validation.rb index c7df92ef..f38f0810 100644 --- a/lib/json_schemer/draft7/vocab/validation.rb +++ b/lib/json_schemer/draft7/vocab/validation.rb @@ -35,7 +35,7 @@ def validate(instance, instance_location, keyword_location, context) end end - class ContentEncoding < Keyword + class ContentEncoding < Draft202012::Vocab::Content::ContentEncoding def error(formatted_instance_location:, **) "string at #{formatted_instance_location} could not be decoded using encoding: #{value}" end @@ -43,13 +43,13 @@ def error(formatted_instance_location:, **) def validate(instance, instance_location, keyword_location, _context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) - valid, annotation = Format.decode_content_encoding(instance, value) + valid, annotation = parsed.call(instance) result(instance, instance_location, keyword_location, valid, :annotation => annotation) end end - class ContentMediaType < Keyword + class ContentMediaType < Draft202012::Vocab::Content::ContentMediaType def error(formatted_instance_location:, **) "string at #{formatted_instance_location} could not be parsed using media type: #{value}" end @@ -58,7 +58,7 @@ def validate(instance, instance_location, keyword_location, context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance - valid, annotation = Format.parse_content_media_type(decoded_instance, value) + valid, annotation = parsed.call(decoded_instance) result(instance, instance_location, keyword_location, valid, :annotation => annotation) end diff --git a/lib/json_schemer/format.rb b/lib/json_schemer/format.rb index fcf239ff..570afe4b 100644 --- a/lib/json_schemer/format.rb +++ b/lib/json_schemer/format.rb @@ -1,15 +1,76 @@ # frozen_string_literal: true module JSONSchemer module Format - include Duration - include Email - include Hostname - include JSONPointer - include URITemplate + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3 + DATE_TIME = proc do |instance, _format| + !instance.is_a?(String) || valid_date_time?(instance) + end + DATE = proc do |instance, _format| + !instance.is_a?(String) || valid_date_time?("#{instance}T04:05:06.123456789+07:00") + end + TIME = proc do |instance, _format| + !instance.is_a?(String) || valid_date_time?("2001-02-03T#{instance}") + end + DURATION = proc do |instance, _format| + !instance.is_a?(String) || valid_duration?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.2 + EMAIL = proc do |instance, _format| + !instance.is_a?(String) || instance.ascii_only? && valid_email?(instance) + end + IDN_EMAIL = proc do |instance, _format| + !instance.is_a?(String) || valid_email?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.3 + HOSTNAME = proc do |instance, _format| + !instance.is_a?(String) || instance.ascii_only? && valid_hostname?(instance) + end + IDN_HOSTNAME = proc do |instance, _format| + !instance.is_a?(String) || valid_hostname?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.4 + IPV4 = proc do |instance, _format| + !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET) + end + IPV6 = proc do |instance, _format| + !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET6) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.5 + URI = proc do |instance, _format| + !instance.is_a?(String) || valid_uri?(instance) + end + URI_REFERENCE = proc do |instance, _format| + !instance.is_a?(String) || valid_uri_reference?(instance) + end + IRI = proc do |instance, _format| + !instance.is_a?(String) || valid_uri?(iri_escape(instance)) + end + IRI_REFERENCE = proc do |instance, _format| + !instance.is_a?(String) || valid_uri_reference?(iri_escape(instance)) + end + UUID = proc do |instance, _format| + !instance.is_a?(String) || valid_uuid?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.6 + URI_TEMPLATE = proc do |instance, _format| + !instance.is_a?(String) || valid_uri_template?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.7 + JSON_POINTER = proc do |instance, _format| + !instance.is_a?(String) || valid_json_pointer?(instance) + end + RELATIVE_JSON_POINTER = proc do |instance, _format| + !instance.is_a?(String) || valid_relative_json_pointer?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.8 + REGEX = proc do |instance, _format| + !instance.is_a?(String) || valid_regex?(instance) + end DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze - HOUR_24_REGEX = /T24/.freeze - LEAP_SECOND_REGEX = /T\d{2}:\d{2}:6/.freeze + DATE_TIME_SEPARATOR_CHARACTER_CLASS = '[Tt\s]' + HOUR_24_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}24:/.freeze + LEAP_SECOND_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}\d{2}:\d{2}:6/.freeze IP_REGEX = /\A[\h:.]+\z/.freeze INVALID_QUERY_REGEX = /\s/.freeze IRI_ESCAPE_REGEX = /[^[:ascii:]]/ @@ -20,6 +81,12 @@ module Format end.freeze class << self + include Duration + include Email + include Hostname + include JSONPointer + include URITemplate + def percent_encode(data, regexp) data = data.dup data.force_encoding(Encoding::ASCII_8BIT) @@ -27,126 +94,55 @@ def percent_encode(data, regexp) data.force_encoding(Encoding::US_ASCII) end - def decode_content_encoding(data, content_encoding) - case content_encoding - when 'base64' - begin - [true, Base64.strict_decode64(data)] - rescue - [false, nil] - end - else - raise UnknownContentEncoding, content_encoding - end + def valid_date_time?(data) + return false if HOUR_24_REGEX.match?(data) + datetime = DateTime.rfc3339(data) + return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59' + DATE_TIME_OFFSET_REGEX.match?(data) + rescue ArgumentError + false end - def parse_content_media_type(data, content_media_type) - case content_media_type - when 'application/json' - begin - [true, JSON.parse(data)] - rescue - [false, nil] - end - else - raise UnknownContentMediaType, content_media_type - end + def valid_ip?(data, family) + IPAddr.new(data, family) + IP_REGEX.match?(data) + rescue IPAddr::Error + false end - end - def valid_spec_format?(data, format) - case format - when 'date-time' - valid_date_time?(data) - when 'date' - valid_date_time?("#{data}T04:05:06.123456789+07:00") - when 'time' - valid_date_time?("2001-02-03T#{data}") - when 'email' - data.ascii_only? && valid_email?(data) - when 'idn-email' - valid_email?(data) - when 'hostname' - data.ascii_only? && valid_hostname?(data) - when 'idn-hostname' - valid_hostname?(data) - when 'ipv4' - valid_ip?(data, Socket::AF_INET) - when 'ipv6' - valid_ip?(data, Socket::AF_INET6) - when 'uri' - valid_uri?(data) - when 'uri-reference' - valid_uri_reference?(data) - when 'iri' - valid_uri?(iri_escape(data)) - when 'iri-reference' - valid_uri_reference?(iri_escape(data)) - when 'uri-template' - valid_uri_template?(data) - when 'json-pointer' - valid_json_pointer?(data) - when 'relative-json-pointer' - valid_relative_json_pointer?(data) - when 'regex' - valid_regex?(data) - when 'duration' - valid_duration?(data) - when 'uuid' - valid_uuid?(data) - else - raise UnknownFormat, format + def parse_uri_scheme(data) + scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = ::URI::RFC3986_PARSER.split(data) + # ::URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should + raise ::URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque) + scheme end - end - - def valid_date_time?(data) - return false if HOUR_24_REGEX.match?(data) - datetime = DateTime.rfc3339(data) - return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59' - DATE_TIME_OFFSET_REGEX.match?(data) - rescue ArgumentError - false - end - - def valid_ip?(data, family) - IPAddr.new(data, family) - IP_REGEX.match?(data) - rescue IPAddr::Error - false - end - - def parse_uri_scheme(data) - scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = URI::RFC3986_PARSER.split(data) - # URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should - raise URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque) - scheme - end - def valid_uri?(data) - !!parse_uri_scheme(data) - rescue URI::InvalidURIError - false - end + def valid_uri?(data) + !!parse_uri_scheme(data) + rescue ::URI::InvalidURIError + false + end - def valid_uri_reference?(data) - parse_uri_scheme(data) - true - rescue URI::InvalidURIError - false - end + def valid_uri_reference?(data) + parse_uri_scheme(data) + true + rescue ::URI::InvalidURIError + false + end - def iri_escape(data) - Format.percent_encode(data, IRI_ESCAPE_REGEX) - end + def iri_escape(data) + Format.percent_encode(data, IRI_ESCAPE_REGEX) + end - def valid_regex?(data) - !!EcmaRegexp.ruby_equivalent(data) - rescue InvalidEcmaRegexp - false - end + def valid_regex?(data) + !!EcmaRegexp.ruby_equivalent(data) + rescue InvalidEcmaRegexp + false + end - def valid_uuid?(data) - UUID_REGEX.match?(data) || NIL_UUID == data + def valid_uuid?(data) + UUID_REGEX.match?(data) || NIL_UUID == data + end end end end diff --git a/lib/json_schemer/keyword.rb b/lib/json_schemer/keyword.rb index 5b8a5cc1..091d7d8e 100644 --- a/lib/json_schemer/keyword.rb +++ b/lib/json_schemer/keyword.rb @@ -26,6 +26,10 @@ def schema_pointer @schema_pointer ||= "#{parent.schema_pointer}/#{escaped_keyword}" end + def error_key + keyword + end + private def parse diff --git a/lib/json_schemer/openapi30/meta.rb b/lib/json_schemer/openapi30/meta.rb index 4deee431..4b62eb86 100644 --- a/lib/json_schemer/openapi30/meta.rb +++ b/lib/json_schemer/openapi30/meta.rb @@ -2,6 +2,12 @@ module JSONSchemer module OpenAPI30 BASE_URI = URI('json-schemer://openapi30/schema') + # https://spec.openapis.org/oas/v3.0.3#data-types + FORMATS = OpenAPI31::FORMATS.merge( + 'byte' => proc { |instance, _value| ContentEncoding::BASE64.call(instance).first }, + 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT }, + 'date' => Format::DATE + ) SCHEMA = { 'id' => 'json-schemer://openapi30/schema', '$schema' => 'http://json-schema.org/draft-04/schema#', diff --git a/lib/json_schemer/openapi31/meta.rb b/lib/json_schemer/openapi31/meta.rb index 85e7c11e..e9bb7ac9 100644 --- a/lib/json_schemer/openapi31/meta.rb +++ b/lib/json_schemer/openapi31/meta.rb @@ -2,6 +2,14 @@ module JSONSchemer module OpenAPI31 BASE_URI = URI('https://spec.openapis.org/oas/3.1/dialect/base') + # https://spec.openapis.org/oas/v3.1.0#data-types + FORMATS = { + 'int32' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 32 }, + 'int64' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 64 }, + 'float' => proc { |instance, _format| instance.is_a?(Float) }, + 'double' => proc { |instance, _format| instance.is_a?(Float) }, + 'password' => proc { |_instance, _format| true } + } SCHEMA = { '$id' => 'https://spec.openapis.org/oas/3.1/dialect/base', '$schema' => 'https://json-schema.org/draft/2020-12/schema', diff --git a/lib/json_schemer/openapi31/vocab/base.rb b/lib/json_schemer/openapi31/vocab/base.rb index 937fbcfc..7a59c5b3 100644 --- a/lib/json_schemer/openapi31/vocab/base.rb +++ b/lib/json_schemer/openapi31/vocab/base.rb @@ -34,7 +34,8 @@ def validate(*) end class Discriminator < Keyword - include Format::JSONPointer + # https://spec.openapis.org/oas/v3.1.0#components-object + FIXED_FIELD_REGEX = /\A[a-zA-Z0-9\.\-_]+$\z/ attr_accessor :skip_ref_once @@ -42,43 +43,80 @@ def error(formatted_instance_location:, **) "value at #{formatted_instance_location} does not match `discriminator` schema" end - def validate(instance, instance_location, keyword_location, context) - property_name = value.fetch('propertyName') - mapping = value['mapping'] || {} + def mapping + @mapping ||= value['mapping'] || {} + end + + def subschemas_by_property_value + @subschemas_by_property_value ||= if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf') + subschemas = schema.parsed['anyOf']&.parsed || [] + subschemas += schema.parsed['oneOf']&.parsed || [] - return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) && instance.key?(property_name) + subschemas_by_ref = {} + subschemas_by_schema_name = {} - property = instance.fetch(property_name) - ref = mapping.fetch(property, property) + subschemas.each do |subschema| + subschema_ref = subschema.parsed.fetch('$ref').parsed + subschemas_by_ref[subschema_ref] = subschema - ref_schema = nil - unless ref.start_with?('#') && valid_json_pointer?(ref.delete_prefix('#')) - ref_schema = begin - root.resolve_ref(URI.join(schema.base_uri, "#/components/schemas/#{ref}")) - rescue InvalidRefPointer - nil + if subschema_ref.start_with?('#/components/schemas/') + schema_name = subschema_ref.delete_prefix('#/components/schemas/') + subschemas_by_schema_name[schema_name] = subschema if FIXED_FIELD_REGEX.match?(schema_name) + end end - end - ref_schema ||= root.resolve_ref(URI.join(schema.base_uri, ref)) - return if skip_ref_once == ref_schema.absolute_keyword_location + explicit_mapping = mapping.transform_values do |schema_name_or_ref| + subschemas_by_schema_name.fetch(schema_name_or_ref) { subschemas_by_ref.fetch(schema_name_or_ref) } + end - nested = [] + implicit_mapping = subschemas_by_schema_name.reject do |_schema_name, subschema| + explicit_mapping.value?(subschema) + end - if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf') - subschemas = schema.parsed['anyOf']&.parsed || [] - subschemas += schema.parsed['oneOf']&.parsed || [] - subschemas.each do |subschema| - if subschema.parsed.fetch('$ref').ref_schema.absolute_keyword_location == ref_schema.absolute_keyword_location - nested << subschema.validate_instance(instance, instance_location, keyword_location, context) + implicit_mapping.merge(explicit_mapping) + else + Hash.new do |hash, property_value| + schema_name_or_ref = mapping.fetch(property_value, property_value) + + subschema = nil + + if FIXED_FIELD_REGEX.match?(schema_name_or_ref) + subschema = begin + schema.ref("#/components/schemas/#{schema_name_or_ref}") + rescue InvalidRefPointer + nil + end + end + + subschema ||= begin + schema.ref(schema_name_or_ref) + rescue InvalidRefResolution, UnknownRef + nil end + + hash[property_value] = subschema end - else - ref_schema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location - nested << ref_schema.validate_instance(instance, instance_location, keyword_location, context) end + end + + def validate(instance, instance_location, keyword_location, context) + return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) + + property_name = value.fetch('propertyName') + + return result(instance, instance_location, keyword_location, false) unless instance.key?(property_name) + + property_value = instance.fetch(property_name) + subschema = subschemas_by_property_value[property_value] + + return result(instance, instance_location, keyword_location, false) unless subschema + + return if skip_ref_once == subschema.absolute_keyword_location + subschema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location + + subschema_result = subschema.validate_instance(instance, instance_location, keyword_location, context) - result(instance, instance_location, keyword_location, (nested.any? && nested.all?(&:valid)), nested) + result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested) ensure self.skip_ref_once = nil end diff --git a/lib/json_schemer/output.rb b/lib/json_schemer/output.rb index 3d5c92c8..74c55ec6 100644 --- a/lib/json_schemer/output.rb +++ b/lib/json_schemer/output.rb @@ -5,14 +5,15 @@ module Output attr_reader :keyword, :schema + def x_error + return @x_error if defined?(@x_error) + @x_error = schema.parsed['x-error']&.message(error_key) + end + private def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false) - if valid - Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'annotations') - else - Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'errors') - end + Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, valid ? 'annotations' : 'errors') end def escaped_keyword diff --git a/lib/json_schemer/result.rb b/lib/json_schemer/result.rb index 3612c892..52b05d95 100644 --- a/lib/json_schemer/result.rb +++ b/lib/json_schemer/result.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true module JSONSchemer - Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do - CLASSIC_ERROR_TYPES = Hash.new do |hash, klass| - hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase) - end + CATCHALL = '*' + I18N_SEPARATOR = "\x1F" # unit separator + I18N_SCOPE = 'json_schemer' + I18N_ERRORS_SCOPE = "#{I18N_SCOPE}#{I18N_SEPARATOR}errors" + X_ERROR_REGEX = /%\{(instance|instanceLocation|keywordLocation|absoluteKeywordLocation)\}/ + CLASSIC_ERROR_TYPES = Hash.new do |hash, klass| + hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase) + end + Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do def output(output_format) case output_format when 'classic' @@ -24,10 +29,59 @@ def output(output_format) def error return @error if defined?(@error) - resolved_instance_location = Location.resolve(instance_location) - @error = source.error( - :formatted_instance_location => resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`", - :details => details + if source.x_error + # not using sprintf because it warns: "too many arguments for format string" + @error = source.x_error.gsub( + X_ERROR_REGEX, + '%{instance}' => instance, + '%{instanceLocation}' => Location.resolve(instance_location), + '%{keywordLocation}' => Location.resolve(keyword_location), + '%{absoluteKeywordLocation}' => source.absolute_keyword_location + ) + @x_error = true + else + resolved_instance_location = Location.resolve(instance_location) + formatted_instance_location = resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`" + @error = source.error(:formatted_instance_location => formatted_instance_location, :details => details) + if i18n? + begin + @error = i18n! + @i18n = true + rescue I18n::MissingTranslationData + end + end + end + @error + end + + def i18n? + return @@i18n if defined?(@@i18n) + @@i18n = defined?(I18n) && I18n.exists?(I18N_SCOPE) + end + + def i18n! + base_uri_str = source.schema.base_uri.to_s + meta_schema_base_uri_str = source.schema.meta_schema.base_uri.to_s + resolved_keyword_location = Location.resolve(keyword_location) + error_key = source.error_key + I18n.translate!( + source.absolute_keyword_location, + :default => [ + "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}", + "##{resolved_keyword_location}", + "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + error_key, + CATCHALL + ].map!(&:to_sym), + :separator => I18N_SEPARATOR, + :scope => I18N_ERRORS_SCOPE, + :instance => instance, + :instanceLocation => Location.resolve(instance_location), + :keywordLocation => resolved_keyword_location, + :absoluteKeywordLocation => source.absolute_keyword_location ) end @@ -38,8 +92,13 @@ def to_output_unit 'absoluteKeywordLocation' => source.absolute_keyword_location, 'instanceLocation' => Location.resolve(instance_location) } - out['error'] = error unless valid - out['annotation'] = annotation if valid && annotation + if valid + out['annotation'] = annotation if annotation + else + out['error'] = error + out['x-error'] = true if @x_error + out['i18n'] = true if @i18n + end out end @@ -54,6 +113,8 @@ def to_classic 'type' => type || CLASSIC_ERROR_TYPES[source.class] } out['error'] = error + out['x-error'] = true if @x_error + out['i18n'] = true if @i18n out['details'] = details if details out end diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index db68a4af..d77bfa72 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -10,7 +10,6 @@ def original_instance(instance_location) end include Output - include Format::JSONPointer DEFAULT_SCHEMA = Draft202012::BASE_URI.to_s.freeze SCHEMA_KEYWORD_CLASS = Draft202012::Vocab::Core::Schema @@ -21,6 +20,8 @@ def original_instance(instance_location) PROPERTIES_KEYWORD_CLASS = Draft202012::Vocab::Applicator::Properties DEFAULT_BASE_URI = URI('json-schemer://schema').freeze DEFAULT_FORMATS = {}.freeze + DEFAULT_CONTENT_ENCODINGS = {}.freeze + DEFAULT_CONTENT_MEDIA_TYPES = {}.freeze DEFAULT_KEYWORDS = {}.freeze DEFAULT_BEFORE_PROPERTY_VALIDATION = [].freeze DEFAULT_AFTER_PROPERTY_VALIDATION = [].freeze @@ -42,7 +43,7 @@ def original_instance(instance_location) attr_accessor :base_uri, :meta_schema, :keywords, :keyword_order attr_reader :value, :parent, :root, :parsed - attr_reader :vocabulary, :format, :formats, :custom_keywords, :before_property_validation, :after_property_validation, :insert_property_defaults, :property_default_resolver + attr_reader :vocabulary, :format, :formats, :content_encodings, :content_media_types, :custom_keywords, :before_property_validation, :after_property_validation, :insert_property_defaults, :property_default_resolver def initialize( value, @@ -54,6 +55,8 @@ def initialize( vocabulary: nil, format: true, formats: DEFAULT_FORMATS, + content_encodings: DEFAULT_CONTENT_ENCODINGS, + content_media_types: DEFAULT_CONTENT_MEDIA_TYPES, keywords: DEFAULT_KEYWORDS, before_property_validation: DEFAULT_BEFORE_PROPERTY_VALIDATION, after_property_validation: DEFAULT_AFTER_PROPERTY_VALIDATION, @@ -75,6 +78,8 @@ def initialize( @vocabulary = vocabulary @format = format @formats = formats + @content_encodings = content_encodings + @content_media_types = content_media_types @custom_keywords = keywords @before_property_validation = Array(before_property_validation) @after_property_validation = Array(after_property_validation) @@ -113,7 +118,7 @@ def validate_schema end def ref(value) - resolve_ref(URI.join(base_uri, value)) + root.resolve_ref(URI.join(base_uri, value)) end def validate_instance(instance, instance_location, keyword_location, context) @@ -160,7 +165,7 @@ def validate_instance(instance, instance_location, keyword_location, context) def resolve_ref(uri) pointer = '' - if valid_json_pointer?(uri.fragment) + if Format.valid_json_pointer?(uri.fragment) pointer = URI.decode_www_form_component(uri.fragment) uri.fragment = nil end @@ -183,6 +188,8 @@ def resolve_ref(uri) :meta_schema => meta_schema, :format => format, :formats => formats, + :content_encodings => content_encodings, + :content_media_types => content_media_types, :keywords => custom_keywords, :before_property_validation => before_property_validation, :after_property_validation => after_property_validation, @@ -288,6 +295,34 @@ def schema_pointer end end + def error_key + '^' + end + + def fetch_format(format, *args, &block) + if meta_schema == self + formats.fetch(format, *args, &block) + else + formats.fetch(format) { meta_schema.fetch_format(format, *args, &block) } + end + end + + def fetch_content_encoding(content_encoding, *args, &block) + if meta_schema == self + content_encodings.fetch(content_encoding, *args, &block) + else + content_encodings.fetch(content_encoding) { meta_schema.fetch_content_encoding(content_encoding, *args, &block) } + end + end + + def fetch_content_media_type(content_media_type, *args, &block) + if meta_schema == self + content_media_types.fetch(content_media_type, *args, &block) + else + content_media_types.fetch(content_media_type) { meta_schema.fetch_content_media_type(content_media_type, *args, &block) } + end + end + def id_keyword @id_keyword ||= (keywords.key?('$id') ? '$id' : 'id') end @@ -329,28 +364,27 @@ def parse VOCABULARY_KEYWORD_CLASS.new(vocabulary, self, '$vocabulary') end - if root == self && (!value.is_a?(Hash) || !value.key?(meta_schema.id_keyword)) + keywords = meta_schema.keywords + exclusive_ref = value.is_a?(Hash) && value.key?('$ref') && keywords.fetch('$ref').exclusive? + + if root == self && (!value.is_a?(Hash) || !value.key?(meta_schema.id_keyword) || exclusive_ref) ID_KEYWORD_CLASS.new(base_uri, self, meta_schema.id_keyword) end - if value.is_a?(Hash) - keywords = meta_schema.keywords - - if value.key?('$ref') && keywords.fetch('$ref').exclusive? - @parsed['$ref'] = keywords.fetch('$ref').new(value.fetch('$ref'), self, '$ref') - defs_keyword = meta_schema.defs_keyword - if value.key?(defs_keyword) && keywords.key?(defs_keyword) - @parsed[defs_keyword] = keywords.fetch(defs_keyword).new(value.fetch(defs_keyword), self, defs_keyword) - end - else - keyword_order = meta_schema.keyword_order - last = keywords.size - - value.sort do |(keyword_a, _value_a), (keyword_b, _value_b)| - keyword_order.fetch(keyword_a, last) <=> keyword_order.fetch(keyword_b, last) - end.each do |keyword, value| - @parsed[keyword] ||= keywords.fetch(keyword, UNKNOWN_KEYWORD_CLASS).new(value, self, keyword) - end + if exclusive_ref + @parsed['$ref'] = keywords.fetch('$ref').new(value.fetch('$ref'), self, '$ref') + defs_keyword = meta_schema.defs_keyword + if value.key?(defs_keyword) && keywords.key?(defs_keyword) + @parsed[defs_keyword] = keywords.fetch(defs_keyword).new(value.fetch(defs_keyword), self, defs_keyword) + end + elsif value.is_a?(Hash) + keyword_order = meta_schema.keyword_order + last = keywords.size + + value.sort do |(keyword_a, _value_a), (keyword_b, _value_b)| + keyword_order.fetch(keyword_a, last) <=> keyword_order.fetch(keyword_b, last) + end.each do |keyword, value| + @parsed[keyword] ||= keywords.fetch(keyword, UNKNOWN_KEYWORD_CLASS).new(value, self, keyword) end end diff --git a/lib/json_schemer/version.rb b/lib/json_schemer/version.rb index 8834a4c7..e1062643 100644 --- a/lib/json_schemer/version.rb +++ b/lib/json_schemer/version.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true module JSONSchemer - VERSION = '2.0.0' + VERSION = '2.1.0' end diff --git a/test/errors_test.rb b/test/errors_test.rb new file mode 100644 index 00000000..6a57bd9c --- /dev/null +++ b/test/errors_test.rb @@ -0,0 +1,272 @@ +require 'test_helper' + +class ErrorsTest < Minitest::Test + def test_x_error + schema = { + 'oneOf' => [ + { + 'x-error' => 'properties a and b were provided, however only one or the other may be specified', + 'required' => ['a'], + 'not' => { 'required' => ['b'] } + }, + { + 'x-error' => { + 'not' => '%{instance} `%{instanceLocation}` %{keywordLocation} %{absoluteKeywordLocation}' + }, + 'required' => ['b'], + 'not' => { 'required' => ['a'] } + } + ], + 'x-error' => { + '*' => 'schema error', + 'oneOf' => 'oneOf error' + } + } + data = { + 'a' => 'foo', + 'b' => 'bar' + } + assert_equal( + [ + 'properties a and b were provided, however only one or the other may be specified', + '{"a"=>"foo", "b"=>"bar"} `` /oneOf/1/not json-schemer://schema#/oneOf/1/not' + ].sort, + JSONSchemer.schema(schema).validate(data).map { |error| error.fetch('error') }.sort + ) + + assert_equal('schema error', JSONSchemer.schema(schema).validate(data, :output_format => 'basic').fetch('error')) + assert_equal('oneOf error', JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').fetch('error')) + + assert_equal(true, JSONSchemer.schema(schema).validate(data, :output_format => 'basic').fetch('x-error')) + assert_equal(true, JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').fetch('x-error')) + assert_equal([true, true], JSONSchemer.schema(schema).validate(data).map { |error| error.fetch('x-error') }) + + refute(JSONSchemer.schema(schema).validate(data, :output_format => 'basic').key?('i18n')) + refute(JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').key?('i18n')) + assert_equal([false, false], JSONSchemer.schema(schema).validate(data).map { |error| error.key?('i18n') }) + end + + def test_x_error_override + schema = { + 'required' => ['a'], + 'minProperties' => 2 + } + assert_equal( + ['object at root is missing required properties: a', 'object size at root is less than: 2'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => 'schema error') + assert_equal( + ['schema error', 'schema error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { 'required' => 'required error' }) + assert_equal( + ['required error', 'object size at root is less than: 2'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => { 'required' => 'required error', 'minProperties' => 'minProperties error' }) + assert_equal( + ['required error', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => { '*' => 'catchall', 'minProperties' => 'minProperties error' }) + assert_equal( + ['catchall', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'catchall', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { '^' => 'schema error', 'minProperties' => 'minProperties error' }) + assert_equal( + ['object at root is missing required properties: a', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { '^' => 'schema error', '*' => 'catchall' }) + assert_equal( + ['catchall', 'catchall'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + end + + def test_x_error_precedence + schema = { + '$id' => 'https://example.com/schema', + 'required' => ['a'] + } + x_error_schema = schema.merge( + 'x-error' => { + 'required' => 'x error' + } + ) + + assert_equal('x error', JSONSchemer.schema(x_error_schema).validate({}).first.fetch('error')) + assert_equal('object at root is missing required properties: a', JSONSchemer.schema(schema).validate({}).first.fetch('error')) + + i18n({ 'https://example.com/schema#/required' => 'i18n error' }) do + assert_equal('x error', JSONSchemer.schema(x_error_schema).validate({}).first.fetch('error')) + assert_equal('i18n error', JSONSchemer.schema(schema).validate({}).first.fetch('error')) + end + end + + def test_i18n_error + schema = { + '$id' => 'https://example.com/schema', + '$schema' => 'https://json-schema.org/draft/2019-09/schema', + 'properties' => { + 'yah' => { + 'type' => 'string' + } + } + } + schemer = JSONSchemer.schema(schema) + data = { 'yah' => 1 } + + errors = { + 'https://example.com/schema#' => 'A', + 'https://example.com/schema#/properties/yah/type' => '1', + 'https://example.com/schema' => { + '#' => 'B', + '#/properties/yah/type' => '2', + '^' => 'D', + 'type' => '4', + '*' => 'E/5' + }, + '#/properties/yah/type' => '3', + '#' => 'C', + 'https://json-schema.org/draft/2019-09/schema' => { + '^' => 'F', + 'type' => '6', + '*' => 'G/7' + }, + '^' => 'H', + 'type' => '8', + '*' => 'I/9: %{instance} `%{instanceLocation}` %{keywordLocation} %{absoluteKeywordLocation}', + + 'https://example.com/differentschema#/properties/yah/type' => '?', + 'https://example.com/differentschema' => { + '#/properties/yah/type' => '?', + 'type' => '?', + '*' => '?' + }, + '?' => '?' + } + + i18n(errors) do + assert_equal('A', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('1', schemer.validate(data).first.fetch('error')) + end + + errors.delete('https://example.com/schema#') + assert_equal('B', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('https://example.com/schema#/properties/yah/type') + assert_equal('2', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('#') + assert_equal('C', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://example.com/schema').delete('#/properties/yah/type') + assert_equal('3', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.delete('#') + assert_equal('D', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('#/properties/yah/type') + assert_equal('4', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('^') + assert_equal('E/5', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://example.com/schema').delete('type') + assert_equal('E/5', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('*') + i18n(errors) do + assert_equal('F', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('6', schemer.validate(data).first.fetch('error')) + end + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('^') + assert_equal('G/7', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('type') + assert_equal('G/7', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('*') + i18n(errors) do + assert_equal('H', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('8', schemer.validate(data).first.fetch('error')) + end + + errors.delete('^') + assert_equal('I/9: {"yah"=>1} `` https://example.com/schema#', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('type') + assert_equal('I/9: 1 `/yah` /properties/yah/type https://example.com/schema#/properties/yah/type', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + i18n(errors) do + assert_equal(true, schemer.validate(data).first.fetch('i18n')) + refute(schemer.validate(data).first.key?('x-error')) + assert_equal(true, schemer.validate(data, :output_format => 'basic').fetch('i18n')) + refute(schemer.validate(data, :output_format => 'basic').key?('x-error')) + end + + errors.delete('*') + i18n(errors) do + assert_equal('value at root does not match schema', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('value at `/yah` is not a string', schemer.validate(data).first.fetch('error')) + + refute(schemer.validate(data).first.key?('i18n')) + refute(schemer.validate(data).first.key?('x-error')) + refute(schemer.validate(data, :output_format => 'basic').key?('i18n')) + refute(schemer.validate(data, :output_format => 'basic').key?('x-error')) + end + end + +private + + def i18n(errors) + require 'yaml' + require 'i18n' + # require 'i18n/debug' + + JSONSchemer.remove_class_variable(:@@i18n) if JSONSchemer.class_variable_defined?(:@@i18n) + # @on_lookup ||= I18n::Debug.on_lookup + # I18n::Debug.on_lookup(&@on_lookup) + + Tempfile.create(['translations', '.yml']) do |file| + file.write(YAML.dump({ 'en' => { 'json_schemer' => { 'errors' => errors } } })) + file.flush + + I18n.load_path += [file.path] + + yield + ensure + I18n.load_path -= [file.path] + end + ensure + JSONSchemer.remove_class_variable(:@@i18n) if JSONSchemer.class_variable_defined?(:@@i18n) + # I18n::Debug.on_lookup {} + end +end diff --git a/test/exe_test.rb b/test/exe_test.rb index 8f37a1c1..9b73a386 100644 --- a/test/exe_test.rb +++ b/test/exe_test.rb @@ -175,7 +175,9 @@ def test_stdin def exe(*args, **kwargs) Open3.capture3('bundle', 'exec', 'json_schemer', *args, **kwargs).tap do |_stdout, stderr, _status| + # :nocov: stderr.gsub!(RUBY_2_5_WARNING_REGEX, '') if RUBY_ENGINE == 'ruby' && RUBY_VERSION.match?(/\A2\.5\.\d+\z/) + # :nocov: end end diff --git a/test/fixtures/draft2019-09.json b/test/fixtures/draft2019-09.json index bf9baff6..da884b6e 100644 --- a/test/fixtures/draft2019-09.json +++ b/test/fixtures/draft2019-09.json @@ -190,6 +190,43 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -1691,9 +1728,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -1765,9 +1799,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -1839,9 +1870,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -3459,9 +3487,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/validation": true - }, "$recursiveAnchor": true, "title": "Validation vocabulary meta-schema", "type": [ @@ -3597,9 +3622,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/validation": true - }, "$recursiveAnchor": true, "title": "Validation vocabulary meta-schema", "type": [ @@ -4225,6 +4247,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": { } @@ -4252,6 +4275,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": { } @@ -5316,9 +5340,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5392,9 +5413,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5468,9 +5486,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5544,9 +5559,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5620,9 +5632,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5696,9 +5705,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5772,9 +5778,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -12057,6 +12060,21 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -12651,6 +12669,21 @@ ], [ + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -12791,6 +12824,22 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -15019,6 +15068,32 @@ "type": "integer" } ] + ], + [ + [ + + ], + [ + { + "data": 42, + "data_pointer": "", + "schema": { + "type": "string" + }, + "schema_pointer": "/examples/0", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "/base", + "examples": [ + { + "type": "string" + } + ], + "$ref": "#/examples/0" + }, + "type": "string" + } + ] ] ], "JSON-Schema-Test-Suite/tests/draft2019-09/pattern.json": [ @@ -18218,9 +18293,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/validation": true - }, "$recursiveAnchor": true, "title": "Validation vocabulary meta-schema", "type": [ @@ -19023,9 +19095,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -19161,6 +19230,7 @@ }, "schema_pointer": "/$defs/foo/$defs/bar", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -19189,6 +19259,7 @@ }, "schema_pointer": "/if", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -19213,6 +19284,7 @@ }, "schema_pointer": "/then", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -19237,6 +19309,7 @@ }, "schema_pointer": "/else", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -19264,6 +19337,7 @@ }, "schema_pointer": "/$defs/b", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -19294,6 +19368,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -19319,6 +19394,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -19344,6 +19420,7 @@ }, "schema_pointer": "/$defs//$defs/", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$defs": { "": { "$defs": { @@ -21697,6 +21774,50 @@ } ] ], + [ + [ + + ], + [ + { + "data": "too many", + "data_pointer": "/1/3", + "schema": false, + "schema_pointer": "/$defs/tree/items/1/unevaluatedItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + "$recursiveAnchor": true, + "$ref": "/tree", + "items": [ + true, + true, + { + "type": "string" + } + ], + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + "type": "array", + "items": [ + { + "type": "number" + }, + { + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "$recursiveRef": "#" + } + ] + } + } + }, + "type": "schema" + } + ] + ], [ [ { @@ -22667,6 +22788,49 @@ } ] ], + [ + [ + + ], + [ + { + "data": "b", + "data_pointer": "/branches/foo", + "schema": false, + "schema_pointer": "/$defs/tree/properties/branches/unevaluatedProperties", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + "$recursiveAnchor": true, + "$ref": "/tree", + "properties": { + "name": { + "type": "string" + } + }, + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + "type": "object", + "properties": { + "node": true, + "branches": { + "$comment": "unevaluatedProperties comes first so it's more likely to bugs errors with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "$recursiveRef": "#" + } + }, + "required": [ + "node" + ] + } + } + }, + "type": "schema" + } + ] + ], [ [ { diff --git a/test/fixtures/draft2020-12.json b/test/fixtures/draft2020-12.json index b57ca081..7ffbab6a 100644 --- a/test/fixtures/draft2020-12.json +++ b/test/fixtures/draft2020-12.json @@ -1494,9 +1494,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -1573,9 +1570,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -1652,9 +1646,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -3277,9 +3268,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -3415,9 +3403,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -4043,6 +4028,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": { } @@ -4070,6 +4056,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": { } @@ -6353,9 +6340,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6433,9 +6417,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6513,9 +6494,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6593,9 +6571,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6673,9 +6648,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6753,9 +6725,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6833,9 +6802,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -7969,6 +7935,43 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/items", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + } + ], + "items": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/items", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + } + ], + "items": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -11697,9 +11700,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -13288,6 +13288,21 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -13882,6 +13897,21 @@ ], [ + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -14022,6 +14052,22 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -16298,6 +16344,32 @@ "type": "integer" } ] + ], + [ + [ + + ], + [ + { + "data": 42, + "data_pointer": "", + "schema": { + "type": "string" + }, + "schema_pointer": "/examples/0", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/base", + "examples": [ + { + "type": "string" + } + ], + "$ref": "#/examples/0" + }, + "type": "string" + } + ] ] ], "JSON-Schema-Test-Suite/tests/draft2020-12/pattern.json": [ @@ -17691,9 +17763,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -18495,9 +18564,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -18638,6 +18704,7 @@ }, "schema_pointer": "/$defs/foo/$defs/bar", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -18666,6 +18733,7 @@ }, "schema_pointer": "/if", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -18690,6 +18758,7 @@ }, "schema_pointer": "/then", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -18714,6 +18783,7 @@ }, "schema_pointer": "/else", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -18741,6 +18811,7 @@ }, "schema_pointer": "/$defs/b", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -18771,6 +18842,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -18796,6 +18868,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -18821,6 +18894,7 @@ }, "schema_pointer": "/$defs//$defs/", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { "": { "$defs": { @@ -21181,6 +21255,54 @@ } ] ], + [ + [ + + ], + [ + { + "data": "baz", + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/$defs/baseSchema/unevaluatedItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + "$ref": "/baseSchema", + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { + "type": "string" + } + ] + }, + "baseSchema": { + "$id": "/baseSchema", + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { + "type": "string" + } + ], + "$dynamicRef": "#addons", + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "type": "schema" + } + ] + ], [ [ { @@ -22639,6 +22761,53 @@ } ] ], + [ + [ + + ], + [ + { + "data": "baz", + "data_pointer": "/baz", + "schema": false, + "schema_pointer": "/$defs/baseSchema/unevaluatedProperties", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + "$ref": "/baseSchema", + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { + "type": "string" + } + } + }, + "baseSchema": { + "$id": "/baseSchema", + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "$dynamicRef": "#addons", + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "type": "schema" + } + ] + ], [ [ { diff --git a/test/fixtures/draft4.json b/test/fixtures/draft4.json index 4eb61ce1..9850ed7e 100644 --- a/test/fixtures/draft4.json +++ b/test/fixtures/draft4.json @@ -143,6 +143,41 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -6451,6 +6486,18 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -6577,6 +6624,20 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -9020,13 +9081,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" @@ -9044,13 +9107,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" diff --git a/test/fixtures/draft6.json b/test/fixtures/draft6.json index 1ad115bf..4ae2b503 100644 --- a/test/fixtures/draft6.json +++ b/test/fixtures/draft6.json @@ -184,6 +184,41 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -8117,6 +8152,18 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -8243,6 +8290,20 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -11664,13 +11725,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" @@ -11688,13 +11751,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" diff --git a/test/fixtures/draft7.json b/test/fixtures/draft7.json index 522d49af..6c8092e8 100644 --- a/test/fixtures/draft7.json +++ b/test/fixtures/draft7.json @@ -184,6 +184,41 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -9052,6 +9087,18 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -9584,6 +9631,18 @@ ], [ + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -9710,6 +9769,20 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -14049,13 +14122,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" @@ -14073,13 +14148,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" diff --git a/test/format_test.rb b/test/format_test.rb index b0599279..613e5240 100644 --- a/test/format_test.rb +++ b/test/format_test.rb @@ -21,22 +21,28 @@ def test_it_ignores_unknown_format schemer = JSONSchemer.schema({ 'type' => 'string', 'format' => 'unknown' }) assert(schemer.valid?('1')) refute(schemer.valid?(1)) + schemer = JSONSchemer.schema({ 'maximum' => 1, 'format' => 'unknown' }) + assert(schemer.valid?(1)) + refute(schemer.valid?(2)) end def test_format_assertion_raises_unknown_format - meta = { + annotation = { + '$vocabulary' => { + 'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true + } + } + assertion = { '$vocabulary' => { 'https://json-schema.org/draft/2020-12/vocab/format-assertion' => true } } - schemer = JSONSchemer.schema( - { - '$schema' => 'http://example.com', - 'format' => 'unknown' - }, - :ref_resolver => proc { meta } - ) - assert_raises(JSONSchemer::UnknownFormat) { schemer.validate('anything') } + schema = { + '$schema' => 'http://example.com', + 'format' => 'unknown' + } + assert(JSONSchemer.schema(schema, :ref_resolver => proc { annotation }).valid?('x')) + assert_raises(JSONSchemer::UnknownFormat) { JSONSchemer.schema(schema, :ref_resolver => proc { assertion }) } end def test_it_validates_spaces_in_uri_format @@ -107,4 +113,632 @@ def test_email_format assert_equal(valid, schema.valid?(email)) end end + + def test_rfc3339_vs_iso8601 + # generated via: https://ijmacd.github.io/rfc3339-iso8601/ + rfc3339_dates = Set[ + '2023-11-01' + ] + rfc3339_times = Set[ + '11:24:25-07:00', + '11:24:25.5-07:00', + '11:24:25.50-07:00', + '11:24:25.500-07:00', + '11:24:25.500623-07:00', + '18:24:25Z', + '18:24:25.5Z', + '18:24:25.50Z', + '18:24:25.500Z', + '18:24:25.500623Z', + '18:24:25+00:00', + '18:24:25.5+00:00', + '18:24:25.500+00:00', + '18:24:25.500623+00:00', + '18:24:25-00:00', + '18:24:25.5-00:00', + '18:24:25.500-00:00', + '18:24:25.500623-00:00' + ] + rfc3339_date_times = Set[ + '2023-11-01T18:24:25Z', + '2023-11-01T18:24:25.5Z', + '2023-11-01T18:24:25.50Z', + '2023-11-01T18:24:25.500Z', + '2023-11-01T18:24:25.500623Z', + '2023-11-01t18:24:25z', + '2023-11-01t18:24:25.500z', + '2023-11-01T11:24:25-07:00', + '2023-11-01T11:24:25.5-07:00', + '2023-11-01T11:24:25.50-07:00', + '2023-11-01T11:24:25.500-07:00', + '2023-11-01T11:24:25.500623-07:00', + '2023-11-01 11:24:25-07:00', + '2023-11-01 11:24:25.5-07:00', + '2023-11-01 11:24:25.50-07:00', + '2023-11-01 11:24:25.500-07:00', + '2023-11-01 11:24:25.500623-07:00', + '2023-11-01 18:24:25Z', + # '2023-11-01_18:24:25Z', + '2023-11-01 18:24:25z', + # '2023-11-01_18:24:25z', + '2023-11-01 18:24:25.5Z', + '2023-11-01 18:24:25.50Z', + '2023-11-01 18:24:25.500Z', + # '2023-11-01_18:24:25.500Z', + '2023-11-01 18:24:25.500623Z', + # '2023-11-01_18:24:25.500623Z', + '2023-11-01 18:24:25.500z', + # '2023-11-01_18:24:25.500z', + '2023-11-01 18:24:25.500623z', + # '2023-11-01_18:24:25.500623z', + '2023-11-01 18:24:25-00:00', + '2023-11-01 18:24:25.500-00:00', + '2023-11-01T18:24:25-00:00', + '2023-11-01T18:24:25.500-00:00', + '2023-11-02T03:09:25+08:45', + '2023-11-01T18:24:25+00:00', + '2023-11-01T18:24:25.500+00:00' + ] + iso8601_dates = Set[ + '2023-11-01', + '20', + '202', + '2023', + '2023-11', + '2023-305', + '2023-W44', + '2023-W44-3', + '20231101', + '2023305', + '2023W44', + '2023W443' + ] + iso8601_times = Set[ + '11:24:25-07:00', + '11:24:25.5-07:00', + '11:24:25.50-07:00', + '11:24:25.500-07:00', + '11:24:25.500623-07:00', + '18:24:25Z', + '18:24:25.5Z', + '18:24:25.50Z', + '18:24:25.500Z', + '18:24:25.500623Z', + '18:24:25+00:00', + '18:24:25.5+00:00', + '18:24:25.500+00:00', + '18:24:25.500623+00:00', + '11', + '11,4', + '11.4', + '11:24', + '11:24,4', + '11:24.4', + '11:24:25', + '11:24:25.5', + '11:24:25.50', + '11:24:25,500', + '11:24:25.500', + '11:24:25,500623', + '11:24:25.500623', + '18Z', + '18,4Z', + '18.4Z', + '18:24Z', + '18:24,4Z', + '18:24.4Z', + '18:24:25,500Z', + '18:24:25,500623Z', + '11-07', + '11,4-07', + '11.4-07', + '11:24-07', + '11:24,4-07', + '11:24.4-07', + '11:24:25-07', + '11:24:25.5-07', + '11:24:25.50-07', + '11:24:25,500-07', + '11:24:25.500-07', + '11:24:25,500623-07', + '11:24:25.500623-07', + '11-07:00', + '11,4-07:00', + '11.4-07:00', + '11:24-07:00', + '11:24,4-07:00', + '11:24.4-07:00', + '11:24:25,500-07:00', + '11:24:25,500623-07:00', + 'T11', + 'T11,4', + 'T11.4', + 'T11:24', + 'T11:24,4', + 'T11:24.4', + 'T11:24:25', + 'T11:24:25.5', + 'T11:24:25.50', + 'T11:24:25,500', + 'T11:24:25.500', + 'T11:24:25,500623', + 'T11:24:25.500623', + 'T18Z', + 'T18,4Z', + 'T18.4Z', + 'T18:24Z', + 'T18:24,4Z', + 'T18:24.4Z', + 'T18:24:25Z', + 'T18:24:25.5Z', + 'T18:24:25.50Z', + 'T18:24:25,500Z', + 'T18:24:25.500Z', + 'T18:24:25,500623Z', + 'T18:24:25.500623Z', + 'T11-07', + 'T11,4-07', + 'T11.4-07', + 'T11:24-07', + 'T11:24,4-07', + 'T11:24.4-07', + 'T11:24:25-07', + 'T11:24:25.5-07', + 'T11:24:25.50-07', + 'T11:24:25,500-07', + 'T11:24:25.500-07', + 'T11:24:25,500623-07', + 'T11:24:25.500623-07', + 'T11-07:00', + 'T11,4-07:00', + 'T11.4-07:00', + 'T11:24-07:00', + 'T11:24,4-07:00', + 'T11:24.4-07:00', + 'T11:24:25-07:00', + 'T11:24:25.5-07:00', + 'T11:24:25.50-07:00', + 'T11:24:25,500-07:00', + 'T11:24:25.500-07:00', + 'T11:24:25,500623-07:00', + 'T11:24:25.500623-07:00', + '1124', + '1124,4', + '1124.4', + '112425', + '112425.5', + '112425.50', + '112425,500', + '112425.500', + '112425,500623', + '112425.500623', + '1824Z', + '1824,4Z', + '1824.4Z', + '182425Z', + '182425.5Z', + '182425.50Z', + '182425,500Z', + '182425.500Z', + '182425,500623Z', + '182425.500623Z', + '1124-07', + '1124,4-07', + '1124.4-07', + '112425-07', + '112425.5-07', + '112425.50-07', + '112425,500-07', + '112425.500-07', + '112425,500623-07', + '112425.500623-07', + '11-0700', + '11,4-0700', + '11.4-0700', + '1124-0700', + '1124,4-0700', + '1124.4-0700', + '112425-0700', + '112425.5-0700', + '112425.50-0700', + '112425,500-0700', + '112425.500-0700', + '112425,500623-0700', + '112425.500623-0700', + 'T1124', + 'T1124,4', + 'T1124.4', + 'T112425', + 'T112425.5', + 'T112425.50', + 'T112425,500', + 'T112425.500', + 'T112425,500623', + 'T112425.500623', + 'T1824Z', + 'T1824,4Z', + 'T1824.4Z', + 'T182425Z', + 'T182425.5Z', + 'T182425.50Z', + 'T182425,500Z', + 'T182425.500Z', + 'T182425,500623Z', + 'T182425.500623Z', + 'T1124-07', + 'T1124,4-07', + 'T1124.4-07', + 'T112425-07', + 'T112425.5-07', + 'T112425.50-07', + 'T112425,500-07', + 'T112425.500-07', + 'T112425,500623-07', + 'T112425.500623-07', + 'T11-0700', + 'T11,4-0700', + 'T11.4-0700', + 'T1124-0700', + 'T1124,4-0700', + 'T1124.4-0700', + 'T112425-0700', + 'T112425.5-0700', + 'T112425.50-0700', + 'T112425,500-0700', + 'T112425.500-0700', + 'T112425,500623-0700', + 'T112425.500623-0700' + ] + iso8601_date_times = Set[ + '2023-11-01T18:24:25Z', + '2023-11-01T18:24:25.5Z', + '2023-11-01T18:24:25.50Z', + '2023-11-01T18:24:25.500Z', + '2023-11-01T18:24:25.500623Z', + '2023-11-01T11:24:25-07:00', + '2023-11-01T11:24:25.500-07:00', + '2023-11-01T11:24:25.500623-07:00', + '2023-11-02T03:09:25+08:45', + '2023-11-01T18:24:25+00:00', + '2023-11-01T18:24:25.500+00:00', + '2023-11-01T11', + '2023-11-01T11,4', + '2023-11-01T11.4', + '2023-11-01T11:24', + '2023-11-01T11:24,4', + '2023-11-01T11:24.4', + '2023-11-01T11:24:25', + '2023-11-01T11:24:25.5', + '2023-11-01T11:24:25.50', + '2023-11-01T11:24:25,500', + '2023-11-01T11:24:25.500', + '2023-11-01T11:24:25,500623', + '2023-11-01T11:24:25.500623', + '2023-11-01T18Z', + '2023-11-01T18,4Z', + '2023-11-01T18.4Z', + '2023-11-01T18:24Z', + '2023-11-01T18:24,4Z', + '2023-11-01T18:24.4Z', + '2023-11-01T18:24:25,500Z', + '2023-11-01T18:24:25,500623Z', + '2023-11-01T11-07', + '2023-11-01T11,4-07', + '2023-11-01T11.4-07', + '2023-11-01T11:24-07', + '2023-11-01T11:24,4-07', + '2023-11-01T11:24.4-07', + '2023-11-01T11:24:25-07', + '2023-11-01T11:24:25.5-07', + '2023-11-01T11:24:25.50-07', + '2023-11-01T11:24:25,500-07', + '2023-11-01T11:24:25.500-07', + '2023-11-01T11:24:25,500623-07', + '2023-11-01T11:24:25.500623-07', + '2023-11-01T11-07:00', + '2023-11-01T11,4-07:00', + '2023-11-01T11.4-07:00', + '2023-11-01T11:24-07:00', + '2023-11-01T11:24,4-07:00', + '2023-11-01T11:24.4-07:00', + '2023-11-01T11:24:25.5-07:00', + '2023-11-01T11:24:25.50-07:00', + '2023-11-01T11:24:25,500-07:00', + '2023-11-01T11:24:25,500623-07:00', + '2023-W44-3T11', + '2023-W44-3T11,4', + '2023-W44-3T11.4', + '2023-W44-3T11:24', + '2023-W44-3T11:24,4', + '2023-W44-3T11:24.4', + '2023-W44-3T11:24:25', + '2023-W44-3T11:24:25.5', + '2023-W44-3T11:24:25.50', + '2023-W44-3T11:24:25,500', + '2023-W44-3T11:24:25.500', + '2023-W44-3T11:24:25,500623', + '2023-W44-3T11:24:25.500623', + '2023-W44-3T18Z', + '2023-W44-3T18,4Z', + '2023-W44-3T18.4Z', + '2023-W44-3T18:24Z', + '2023-W44-3T18:24,4Z', + '2023-W44-3T18:24.4Z', + '2023-W44-3T18:24:25Z', + '2023-W44-3T18:24:25.5Z', + '2023-W44-3T18:24:25.50Z', + '2023-W44-3T18:24:25,500Z', + '2023-W44-3T18:24:25.500Z', + '2023-W44-3T18:24:25,500623Z', + '2023-W44-3T18:24:25.500623Z', + '2023-W44-3T11-07', + '2023-W44-3T11,4-07', + '2023-W44-3T11.4-07', + '2023-W44-3T11:24-07', + '2023-W44-3T11:24,4-07', + '2023-W44-3T11:24.4-07', + '2023-W44-3T11:24:25-07', + '2023-W44-3T11:24:25.5-07', + '2023-W44-3T11:24:25.50-07', + '2023-W44-3T11:24:25,500-07', + '2023-W44-3T11:24:25.500-07', + '2023-W44-3T11:24:25,500623-07', + '2023-W44-3T11:24:25.500623-07', + '2023-W44-3T11-07:00', + '2023-W44-3T11,4-07:00', + '2023-W44-3T11.4-07:00', + '2023-W44-3T11:24-07:00', + '2023-W44-3T11:24,4-07:00', + '2023-W44-3T11:24.4-07:00', + '2023-W44-3T11:24:25-07:00', + '2023-W44-3T11:24:25.5-07:00', + '2023-W44-3T11:24:25.50-07:00', + '2023-W44-3T11:24:25,500-07:00', + '2023-W44-3T11:24:25.500-07:00', + '2023-W44-3T11:24:25,500623-07:00', + '2023-W44-3T11:24:25.500623-07:00', + '2023-305T11', + '2023-305T11,4', + '2023-305T11.4', + '2023-305T11:24', + '2023-305T11:24,4', + '2023-305T11:24.4', + '2023-305T11:24:25', + '2023-305T11:24:25.5', + '2023-305T11:24:25.50', + '2023-305T11:24:25,500', + '2023-305T11:24:25.500', + '2023-305T11:24:25,500623', + '2023-305T11:24:25.500623', + '2023-305T18Z', + '2023-305T18,4Z', + '2023-305T18.4Z', + '2023-305T18:24Z', + '2023-305T18:24,4Z', + '2023-305T18:24.4Z', + '2023-305T18:24:25Z', + '2023-305T18:24:25.5Z', + '2023-305T18:24:25.50Z', + '2023-305T18:24:25,500Z', + '2023-305T18:24:25.500Z', + '2023-305T18:24:25,500623Z', + '2023-305T18:24:25.500623Z', + '2023-305T11-07', + '2023-305T11,4-07', + '2023-305T11.4-07', + '2023-305T11:24-07', + '2023-305T11:24,4-07', + '2023-305T11:24.4-07', + '2023-305T11:24:25-07', + '2023-305T11:24:25.5-07', + '2023-305T11:24:25.50-07', + '2023-305T11:24:25,500-07', + '2023-305T11:24:25.500-07', + '2023-305T11:24:25,500623-07', + '2023-305T11:24:25.500623-07', + '2023-305T11-07:00', + '2023-305T11,4-07:00', + '2023-305T11.4-07:00', + '2023-305T11:24-07:00', + '2023-305T11:24,4-07:00', + '2023-305T11:24.4-07:00', + '2023-305T11:24:25-07:00', + '2023-305T11:24:25.5-07:00', + '2023-305T11:24:25.50-07:00', + '2023-305T11:24:25,500-07:00', + '2023-305T11:24:25.500-07:00', + '2023-305T11:24:25,500623-07:00', + '2023-305T11:24:25.500623-07:00', + '20231101T11', + '20231101T11,4', + '20231101T11.4', + '20231101T1124', + '20231101T1124,4', + '20231101T1124.4', + '20231101T112425', + '20231101T112425.5', + '20231101T112425.50', + '20231101T112425,500', + '20231101T112425.500', + '20231101T112425,500623', + '20231101T112425.500623', + '20231101T18Z', + '20231101T18,4Z', + '20231101T18.4Z', + '20231101T1824Z', + '20231101T1824,4Z', + '20231101T1824.4Z', + '20231101T182425Z', + '20231101T182425.5Z', + '20231101T182425.50Z', + '20231101T182425,500Z', + '20231101T182425.500Z', + '20231101T182425,500623Z', + '20231101T182425.500623Z', + '20231101T11-07', + '20231101T11,4-07', + '20231101T11.4-07', + '20231101T1124-07', + '20231101T1124,4-07', + '20231101T1124.4-07', + '20231101T112425-07', + '20231101T112425.5-07', + '20231101T112425.50-07', + '20231101T112425,500-07', + '20231101T112425.500-07', + '20231101T112425,500623-07', + '20231101T112425.500623-07', + '20231101T11-0700', + '20231101T11,4-0700', + '20231101T11.4-0700', + '20231101T1124-0700', + '20231101T1124,4-0700', + '20231101T1124.4-0700', + '20231101T112425-0700', + '20231101T112425.5-0700', + '20231101T112425.50-0700', + '20231101T112425,500-0700', + '20231101T112425.500-0700', + '20231101T112425,500623-0700', + '20231101T112425.500623-0700', + '2023W443T11', + '2023W443T11,4', + '2023W443T11.4', + '2023W443T1124', + '2023W443T1124,4', + '2023W443T1124.4', + '2023W443T112425', + '2023W443T112425.5', + '2023W443T112425.50', + '2023W443T112425,500', + '2023W443T112425.500', + '2023W443T112425,500623', + '2023W443T112425.500623', + '2023W443T18Z', + '2023W443T18,4Z', + '2023W443T18.4Z', + '2023W443T1824Z', + '2023W443T1824,4Z', + '2023W443T1824.4Z', + '2023W443T182425Z', + '2023W443T182425.5Z', + '2023W443T182425.50Z', + '2023W443T182425,500Z', + '2023W443T182425.500Z', + '2023W443T182425,500623Z', + '2023W443T182425.500623Z', + '2023W443T11-07', + '2023W443T11,4-07', + '2023W443T11.4-07', + '2023W443T1124-07', + '2023W443T1124,4-07', + '2023W443T1124.4-07', + '2023W443T112425-07', + '2023W443T112425.5-07', + '2023W443T112425.50-07', + '2023W443T112425,500-07', + '2023W443T112425.500-07', + '2023W443T112425,500623-07', + '2023W443T112425.500623-07', + '2023W443T11-0700', + '2023W443T11,4-0700', + '2023W443T11.4-0700', + '2023W443T1124-0700', + '2023W443T1124,4-0700', + '2023W443T1124.4-0700', + '2023W443T112425-0700', + '2023W443T112425.5-0700', + '2023W443T112425.50-0700', + '2023W443T112425,500-0700', + '2023W443T112425.500-0700', + '2023W443T112425,500623-0700', + '2023W443T112425.500623-0700', + '2023305T11', + '2023305T11,4', + '2023305T11.4', + '2023305T1124', + '2023305T1124,4', + '2023305T1124.4', + '2023305T112425', + '2023305T112425.5', + '2023305T112425.50', + '2023305T112425,500', + '2023305T112425.500', + '2023305T112425,500623', + '2023305T112425.500623', + '2023305T18Z', + '2023305T18,4Z', + '2023305T18.4Z', + '2023305T1824Z', + '2023305T1824,4Z', + '2023305T1824.4Z', + '2023305T182425Z', + '2023305T182425.5Z', + '2023305T182425.50Z', + '2023305T182425,500Z', + '2023305T182425.500Z', + '2023305T182425,500623Z', + '2023305T182425.500623Z', + '2023305T11-07', + '2023305T11,4-07', + '2023305T11.4-07', + '2023305T1124-07', + '2023305T1124,4-07', + '2023305T1124.4-07', + '2023305T112425-07', + '2023305T112425.5-07', + '2023305T112425.50-07', + '2023305T112425,500-07', + '2023305T112425.500-07', + '2023305T112425,500623-07', + '2023305T112425.500623-07', + '2023305T11-0700', + '2023305T11,4-0700', + '2023305T11.4-0700', + '2023305T1124-0700', + '2023305T1124,4-0700', + '2023305T1124.4-0700', + '2023305T112425-0700', + '2023305T112425.5-0700', + '2023305T112425.50-0700', + '2023305T112425,500-0700', + '2023305T112425.500-0700', + '2023305T112425,500623-0700', + '2023305T112425.500623-0700', + '2023-11-02T02:24:25+08', + '2023-11-01T06-12', + '2023-11-01T06-12:00', + '2023-11-01T06:24-12', + '2023-11-01T06:24-12:00', + ] + + rfc3339_dates.each do |date| + assert(JSONSchemer.schema({ 'format' => 'date' }).valid?(date)) + end + rfc3339_times.each do |time| + assert(JSONSchemer.schema({ 'format' => 'time' }).valid?(time)) + end + rfc3339_date_times.each do |date_time| + assert(JSONSchemer.schema({ 'format' => 'date-time' }).valid?(date_time)) + end + + (iso8601_dates - rfc3339_dates).each do |date| + refute(JSONSchemer.schema({ 'format' => 'date' }).valid?(date)) + end + (iso8601_times - rfc3339_times).each do |time| + refute(JSONSchemer.schema({ 'format' => 'time' }).valid?(time)) + end + (iso8601_date_times - rfc3339_date_times).each do |date_time| + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?(date_time)) + end + end + + def test_date_time_space_separator + assert(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01T23:00:00Z')) + assert(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01 23:00:00Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01T24:00:00Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01 24:00:00Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('1998-12-31T23:58:60Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('1998-12-31 23:58:60Z')) + end end diff --git a/test/json_schema_test_suite_test.rb b/test/json_schema_test_suite_test.rb index 26dedbef..f68d1a60 100644 --- a/test/json_schema_test_suite_test.rb +++ b/test/json_schema_test_suite_test.rb @@ -45,7 +45,7 @@ class JSONSchemaTestSuiteTest < Minitest::Test path = Pathname.new(__dir__).join('..', 'JSON-Schema-Test-Suite', 'remotes', uri.path.gsub(/\A\//, '')) JSON.parse(path.read) else - JSON.parse(Net::HTTP.get(uri)) + JSON.parse(fetch(uri)) end end diff --git a/test/json_schemer_test.rb b/test/json_schemer_test.rb index 1bde86f3..346ba11c 100644 --- a/test/json_schemer_test.rb +++ b/test/json_schemer_test.rb @@ -1,4 +1,5 @@ require 'test_helper' +require 'csv' class JSONSchemerTest < Minitest::Test def test_that_it_has_a_version_number @@ -237,11 +238,11 @@ def test_it_ignores_invalid_types end def test_it_raises_for_unsupported_content_encoding - assert_raises(JSONSchemer::UnknownContentEncoding) { JSONSchemer.schema({ 'contentEncoding' => '7bit' }).valid?('') } + assert_raises(JSONSchemer::UnknownContentEncoding) { JSONSchemer.schema({ 'contentEncoding' => '7bit' }) } end def test_it_raises_for_unsupported_content_media_type - assert_raises(JSONSchemer::UnknownContentMediaType) { JSONSchemer.schema({ 'contentMediaType' => 'application/xml' }).valid?('') } + assert_raises(JSONSchemer::UnknownContentMediaType) { JSONSchemer.schema({ 'contentMediaType' => 'application/xml' }) } end def test_it_raises_for_required_unknown_vocabulary @@ -380,6 +381,12 @@ def test_schema_ref 'type' => 'integer', '$defs' => { 'foo' => { + '$id' => 'subschemer', + '$defs' => { + 'bar' => { + 'required' => ['z'] + } + }, 'type' => 'object', 'required' => ['x', 'y'], 'properties' => { @@ -401,10 +408,20 @@ def test_schema_ref refute(subschemer.valid?(1)) assert_equal( - [["/x", "/$defs/foo/properties/x", "string"], ["", "/$defs/foo", "required"]], + [['/x', '/$defs/foo/properties/x', 'string'], ['', '/$defs/foo', 'required']], subschemer.validate({ 'x' => 1 }).map { |error| error.values_at('data_pointer', 'schema_pointer', 'type') } ) assert(subschemer.valid?({ 'x' => '1', 'y' => 1 })) + + subsubschemer = subschemer.ref('#/$defs/bar') + refute(subsubschemer.valid?({ 'x' => 1 })) + assert_equal( + [['', '/$defs/foo/$defs/bar', 'required']], + subsubschemer.validate({ 'x' => 1 }).map { |error| error.values_at('data_pointer', 'schema_pointer', 'type') } + ) + + assert_equal(subschemer, subschemer.ref('#')) + assert_equal(subschemer, subsubschemer.ref('#')) end def test_published_meta_schemas @@ -422,7 +439,7 @@ def test_published_meta_schemas JSONSchemer::OpenAPI30::Document::SCHEMA ].each do |meta_schema| id = meta_schema.key?('$id') ? meta_schema.fetch('$id') : meta_schema.fetch('id') - assert_equal(meta_schema, JSON.parse(Net::HTTP.get(URI(id)))) + assert_equal(meta_schema, JSON.parse(fetch(id))) end end @@ -531,4 +548,55 @@ def test_bundle_exclusive_ref assert(bundle.valid?('yah')) refute(bundle.valid?('nah')) end + + def test_custom_content_encodings_and_media_types + data = '😊' + instance = Base64.urlsafe_encode64(data) + schema = { + 'contentEncoding' => 'urlsafe_base64', + 'contentMediaType' => 'text/csv' + } + content_encodings = { + 'urlsafe_base64' => proc do |instance| + [true, Base64.urlsafe_decode64(instance).force_encoding('utf-8')] + rescue + [false, nil] + end + } + content_media_types = { + 'text/csv' => proc do |instance| + [true, CSV.parse(instance)] + rescue + [false, nil] + end + } + + refute(JSONSchemer.schema({ 'contentEncoding' => 'base64' }).validate(instance, :output_format => 'basic').fetch('annotations').first.key?('annotation')) + + schemer = JSONSchemer.schema(schema, :content_encodings => content_encodings, :content_media_types => content_media_types) + + assert_nil(annotation(schemer.validate('invalid', :output_format => 'basic'), '/contentEncoding')) + assert_nil(annotation(schemer.validate(Base64.urlsafe_encode64("#{data}\""), :output_format => 'basic'), '/contentMediaType')) + + result = schemer.validate(instance, :output_format => 'basic') + assert_equal(data, annotation(result, '/contentEncoding')) + assert_equal([[data]], annotation(result, '/contentMediaType')) + + draft7_schemer = JSONSchemer.schema( + schema, + :meta_schema => JSONSchemer.draft7, + :content_encodings => content_encodings, + :content_media_types => content_media_types + ) + + assert(draft7_schemer.valid?(instance)) + refute(draft7_schemer.valid?('invalid')) + refute(draft7_schemer.valid?(Base64.urlsafe_encode64("#{data}\""))) + end + +private + + def annotation(result, keyword_location) + result.fetch('annotations').find { |annotation| annotation.fetch('keywordLocation') == keyword_location }['annotation'] + end end diff --git a/test/open_api_test.rb b/test/open_api_test.rb index 14423441..5b472fa5 100644 --- a/test/open_api_test.rb +++ b/test/open_api_test.rb @@ -205,9 +205,9 @@ def test_discriminator_specification_example assert_equal([['enum', '/components/schemas/Cat/allOf/1/properties/huntingSkill']], schemer.validate(invalid_hunting_skill).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['required', '/components/schemas/Dog/allOf/1']], schemer.validate(missing_pack_size).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['format', '/components/schemas/Dog/allOf/1/properties/packSize']], schemer.validate(invalid_pack_size).map { |error| error.values_at('type', 'schema_pointer') }) - assert_equal([['required', '/components/schemas/Pet']], schemer.validate(missing_pet_type).map { |error| error.values_at('type', 'schema_pointer') }) + assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet']], schemer.validate(missing_pet_type).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['required', '/components/schemas/Pet'], ['required', '/components/schemas/Cat/allOf/1']], schemer.validate(missing_name).map { |error| error.values_at('type', 'schema_pointer') }) - assert_raises(JSONSchemer::UnknownRef) { schemer.validate(invalid_pet_type) } + assert_equal([['discriminator', '/components/schemas/Pet']], schemer.validate(invalid_pet_type).map { |error| error.values_at('type', 'schema_pointer') }) end def test_all_of_discriminator @@ -404,7 +404,7 @@ def test_all_of_discriminator_subclass_schemas_work_on_their_own assert(schemer.valid?(CAT)) assert(schemer.valid?(MISTY)) assert_equal(['/components/schemas/Cat/allOf/1/properties/name'], schemer.validate(INVALID_CAT).map { |error| error.fetch('schema_pointer') }) - assert_equal([['required', '/components/schemas/Pet']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) + assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) end def test_all_of_discriminator_with_non_discriminator_ref @@ -444,7 +444,52 @@ def test_all_of_discriminator_with_non_discriminator_ref refute(schemer.valid?(CAT)) assert(schemer.valid?(CAT.merge('other' => 'y'))) assert_equal(['/components/schemas/Other', '/components/schemas/Cat/allOf/2/properties/name'], schemer.validate(INVALID_CAT).map { |error| error.fetch('schema_pointer') }) - assert_equal([['required', '/components/schemas/Pet'], ['required', '/components/schemas/Other']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) + assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet'], ['required', '/components/schemas/Other']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) + end + + def test_all_of_discriminator_with_remote_ref + schema = { + '$id' => 'http://example.com/schema', + 'discriminator' => { + 'propertyName' => 'petType', + 'mapping' => { + 'Dog' => 'http://example.com/dog' + } + } + } + schemer = JSONSchemer.schema( + schema, + :meta_schema => JSONSchemer.openapi31, + :ref_resolver => { + URI('http://example.com/schema') => schema, + URI('http://example.com/cat') => { + 'allOf' => [ + { '$ref' => 'http://example.com/schema' }, + CAT_SCHEMA + ] + }, + URI('http://example.com/dog') => { + 'allOf' => [ + { '$ref' => 'http://example.com/schema' }, + DOG_SCHEMA + ] + } + }.to_proc + ) + + assert(schemer.valid_schema?) + refute(schemer.valid?(CAT)) + assert(schemer.valid?(CAT.merge('petType' => 'http://example.com/cat'))) + assert(schemer.valid?(DOG)) + + invalid_cat = INVALID_CAT.merge('petType' => 'http://example.com/cat') + invalid_cat_result = schemer.validate(invalid_cat, output_format: 'basic', resolve_enumerators: true) + assert_equal('/discriminator/allOf/1/properties/name/type', invalid_cat_result.dig('errors', 0, 'keywordLocation')) + assert_equal('http://example.com/cat#/allOf/1/properties/name/type', invalid_cat_result.dig('errors', 0, 'absoluteKeywordLocation')) + + invalid_dog_result = schemer.validate(INVALID_DOG, output_format: 'basic', resolve_enumerators: true) + assert_equal('/discriminator/allOf/1/properties/bark/type', invalid_dog_result.dig('errors', 0, 'keywordLocation')) + assert_equal('http://example.com/dog#/allOf/1/properties/bark/type', invalid_dog_result.dig('errors', 0, 'absoluteKeywordLocation')) end def test_any_of_discriminator_without_matching_schema @@ -513,6 +558,60 @@ def test_one_of_discriminator_without_matching_schema assert_equal([['discriminator', '/components/schemas/MyResponseType']], schemer.validate(INVALID_LIZARD).map { |error| error.values_at('type', 'schema_pointer') }) end + def test_any_of_discriminator_ignores_nested_schemas + openapi = { + 'openapi' => '3.1.0', + 'components' => { + 'schemas' => { + 'MyResponseType' => { + 'anyOf' => [ + { '$ref' => '#/components/schemas/Cat' }, + { '$ref' => '#/components/schemas/Cat/$defs/nah' } + ], + 'discriminator' => { + 'propertyName' => 'petType' + } + }, + 'Cat' => CAT_SCHEMA.merge('$defs' => { 'nah' => {} }) + } + } + } + + schemer = JSONSchemer.openapi(openapi).schema('MyResponseType') + + assert(schemer.valid_schema?) + assert(schemer.valid?(CAT)) + refute(schemer.valid?(CAT.merge('petType' => 'nah'))) + refute(schemer.valid?(CAT.merge('petType' => 'Cat/$defs/nah'))) + end + + def test_one_of_discriminator_ignores_nested_schemas + openapi = { + 'openapi' => '3.1.0', + 'components' => { + 'schemas' => { + 'MyResponseType' => { + 'oneOf' => [ + { '$ref' => '#/components/schemas/Cat' }, + { '$ref' => '#/components/schemas/Cat/$defs/nah' } + ], + 'discriminator' => { + 'propertyName' => 'petType' + } + }, + 'Cat' => CAT_SCHEMA.merge('$defs' => { 'nah' => {} }) + } + } + } + + schemer = JSONSchemer.openapi(openapi).schema('MyResponseType') + + assert(schemer.valid_schema?) + assert(schemer.valid?(CAT)) + refute(schemer.valid?(CAT.merge('petType' => 'nah'))) + refute(schemer.valid?(CAT.merge('petType' => 'Cat/$defs/nah'))) + end + def test_discrimator_mapping openapi = { 'openapi' => '3.1.0', @@ -542,7 +641,7 @@ def test_discrimator_mapping assert(schemer.valid_schema?) assert(schemer.valid?(CAT.merge('petType' => 'c'))) - assert(schemer.valid?(MISTY.merge('petType' => 'Cat'))) + refute(schemer.valid?(MISTY.merge('petType' => 'Cat'))) assert_equal(['/components/schemas/Cat/properties/name'], schemer.validate(INVALID_CAT.merge('petType' => 'c')).map { |error| error.fetch('schema_pointer') }) assert(schemer.valid?(DOG.merge('petType' => 'd'))) assert_equal(['/components/schemas/Dog/properties/bark'], schemer.validate(INVALID_DOG.merge('petType' => 'dog')).map { |error| error.fetch('schema_pointer') }) @@ -585,8 +684,9 @@ def test_non_json_pointer_discriminator assert(schemer.valid?(CAT)) assert(schemer.valid?(MISTY)) assert_equal(['/components/schemas/Cat/properties/name'], schemer.validate(INVALID_CAT).map { |error| error.fetch('schema_pointer') }) - assert(schemer.valid?(DOG)) - assert_equal(['/components/schemas/Dog/properties/bark'], schemer.validate(INVALID_DOG).map { |error| error.fetch('schema_pointer') }) + refute(schemer.valid?(DOG)) + assert_equal(['/components/schemas/MyResponseType'], schemer.validate(INVALID_DOG).map { |error| error.fetch('schema_pointer') }) + assert_equal(['/components/schemas/Dog/properties/bark'], schemer.validate(INVALID_DOG.merge('petType' => 'dog')).map { |error| error.fetch('schema_pointer') }) assert(schemer.valid?(LIZARD)) assert_equal(['/components/schemas/Lizard/properties/lovesRocks'], schemer.validate(INVALID_LIZARD).map { |error| error.fetch('schema_pointer') }) assert(schemer.valid?(MONSTER)) @@ -597,7 +697,7 @@ def test_non_json_pointer_discriminator def test_discriminator_non_object_and_missing_property_name schemer = JSONSchemer.schema({ 'discriminator' => { 'propertyName' => 'x' } }, :meta_schema => JSONSchemer.openapi31) assert(schemer.valid?(1)) - assert(schemer.valid?({ 'y' => 'z' })) + refute(schemer.valid?({ 'y' => 'z' })) end def test_openapi31_formats @@ -635,7 +735,9 @@ def test_openapi30_formats 'd' => { 'format' => 'double' }, 'e' => { 'format' => 'password' }, 'f' => { 'format' => 'byte' }, - 'g' => { 'format' => 'binary' } + 'g' => { 'format' => 'binary' }, + 'h' => { 'format' => 'date' }, + 'i' => { 'format' => 'date-time' } } } @@ -656,6 +758,10 @@ def test_openapi30_formats assert(schemer.valid?({ 'f' => 'IQ==' })) refute(schemer.valid?({ 'g' => '!' })) assert(schemer.valid?({ 'g' => '!'.b })) + refute(schemer.valid?({ 'h' => '2001-02-03T04:05:06.123456789+07:00' })) + assert(schemer.valid?({ 'h' => '2001-02-03' })) + refute(schemer.valid?({ 'i' => '2001-02-03' })) + assert(schemer.valid?({ 'i' => '2001-02-03T04:05:06.123456789+07:00' })) end def test_unsupported_openapi_version diff --git a/test/ref_test.rb b/test/ref_test.rb index d9f812a5..1cf8edef 100644 --- a/test/ref_test.rb +++ b/test/ref_test.rb @@ -379,4 +379,20 @@ def test_exclusive_ref_supports_definitions assert(schema.valid?(1)) refute(schema.valid?('1')) end + + def test_exclusive_ref_supports_definitions_with_id_and_json_pointer + schema = JSONSchemer.schema({ + '$schema' => 'http://json-schema.org/draft-07/schema#', + '$id' => 'https://example.com/schema', + '$ref' => '#/definitions/yah', + 'definitions' => { + 'yah' => { + '$id' => '#yah', + 'type' => 'integer' + } + } + }) + assert(schema.valid?(1)) + refute(schema.valid?('1')) + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 60544faa..8a576cb2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,3 +12,17 @@ require "json_schemer" require "minitest/autorun" + +def fetch(location, limit = 10) + raise if limit.zero? + uri = URI(location) + response = Net::HTTP.get_response(uri) + case response + when Net::HTTPSuccess + response.body + when Net::HTTPRedirection + fetch(URI.join(uri, response['Location']), limit - 1) + else + response.value.body + end +end