JSON-Schema-Modern-0.627 000755 000766 000024 0 15114374332 14320 5 ustar 00ether staff 000000 000000 README 100644 000766 000024 611 15114374332 15237 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 This archive contains the distribution JSON-Schema-Modern,
version 0.627:
Validate data against a schema using a JSON Schema
This software is copyright (c) 2020 by Karen Etheridge.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
This README file was generated by Dist::Zilla::Plugin::Readme v6.036.
Changes 100640 000766 000024 101650 15114374332 15733 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 Revision history for JSON-Schema-Modern
0.627 2025-12-04 21:10:22Z
- improved efficiency of "short_circuit" mode, disabling it
in fewer and more tightly constrained conditions while allowing
other functionality to still work (e.g. "strict" mode to detect
unknown keywords, or when unevaluatedProperties or
unevaluatedItems are present)
0.626 2025-12-02 00:29:17Z
- now correctly disallowing booleans in draft4 subschemas
0.625 2025-11-28 22:34:54Z
- introduce a global schema document cache that can be used by other
distributions (such as OpenAPI::Modern)
0.624 2025-11-26 20:57:48Z
- fix strict (unknown keyword checks) validation for draft4 schemas
0.623 2025-11-17 00:14:31Z
- fix error when navigating a $dynamicRef after initial evaluation
at a subschema
- fix incorrect $dynamicRef target following changing scopes into a
subschema of a schema resource (one with an $id)
0.622 2025-11-08 22:22:19Z
- allow export of JSON::Schema::Modern::Utilities::is_bool
- adjust acceptance tests to handle new failure cases in the test
suite for the hostname format
0.621 2025-10-30 17:56:52Z
- adjust acceptance tests to handle new failure cases in the test
suite for hostname, idn-hostname formats
0.620 2025-10-15 23:36:52Z
- new "clone" method for Error objects
- work around an issue with using an object overload as a constant
on perl 5.20
0.619 2025-09-28 22:28:52Z
- internal changes only (breaking changes for dependencies like
OpenAPI::Modern)
0.618 2025-09-07 20:27:21Z
- the json-schema-eval executable now also handles YAML-encoded
schemas and data, via both files and STDIN, via string
autodetection
0.617 2025-08-16 20:50:11Z
- various internal changes for performance improvement
0.616 2025-07-26 23:15:28Z
- documentation notes added for which definitions can be overridden
(formats, media-types and encodings can; vocabularies cannot).
- "validate" in JSON::Schema::Modern::Document must now be provided
with a JSON::Schema::Modern object if a custom metaschema is used
by the schema (this is a forward-looking change in preparation for
removing the "evaluator" attribute from the JSMD object in a
subsequent release).
0.615 2025-07-12 16:12:36Z
- do not allow the use of builtin bools when Storable is too old to
know how to serialize them
- avoid experimental warnings in tests from perl versions 5.36.0 to
5.40.0
0.614 2025-06-28 18:51:12Z
- revert to using JSON::PP booleans for now
0.613 2025-06-28 18:07:20Z
- now checking for keyword collisions in custom vocabulary classes
- now using builtin booleans when available (requires perl 5.36 and
Cpanel::JSON::XS 4.38); reverted in 0.614
0.612 2025-06-03 22:50:17Z
- additional diagnostics to track down some edge case failures
0.611 2025-05-31 03:38:55Z
- fix integer vs number distinction in draft4 for x.0 values. This
comes at the expense of properly identifying other numbers as
integers in draft4 instead of numbers, e.g. 2e1 as a json literal,
as these all decode to Math::BigFloat objects.
- some fixes to id/$id validity checks across all drafts
0.610 2025-05-16 20:58:28Z
- evaluate() and evaluate_json_string() can no longer be called with
a JSON::Schema::Modern::Document object; now you should only pass
in the desired URI directly (or the raw schema), as before.
- fixed some issues when evaluating a schema that had a base URI
added to it after the document was created, including selecting
the proper base URI to use in error messages when there are
multiple choices
0.609 2025-04-20 23:40:09Z
- validate_schema() (and the json-schema-eval --validate-schema
option) now detect more errors
0.608 2025-04-11 20:57:16Z
- documentation now makes a note of the special licence for JSON
Schema schema files
- documentation fixes for JSM and JSM::Document regarding handling
of uris
- introduction of 'original_uri' for JSON::Schema::Modern::Document,
which document subclasses may wish to use for logic of their own
after changing the canonical uri of the document
0.607 2025-04-01 19:35:50Z
- now performing stricter email address validation for the 'email' and
'idn-email' formats
- some improvements to the handling of unimplemented formats (only
missing core formats are checked for at traverse time; custom
formats can be added at any time and errors can be avoided with
'short-circuit')
0.606 2025-03-23 00:16:35Z
- bump required version for optional module Data::Validate::Domain
(used by the 'hostname' and 'idn-hostname' formats)
0.605 2025-03-09 16:31:05Z
- update 'hostname' and 'idn-hostname' format validation to be more
lax with respect to domain names; fixes some acceptance tests
0.604 2025-03-08 23:23:05Z
- mark some failing format tests as todo that will be added in
Test::JSON::Schema::Acceptance 1.028
0.603 2025-02-28 23:35:43Z
- fix Sereal serialization hooks
- various performance improvements when dealing with base URIs
0.602 2025-02-21 00:11:32Z
- more checks at traverse and evaluation time for inconsistent
results
- fix Sereal deserialization hooks
0.601 2025-02-02 00:23:38Z
- check for builtin::Backport at install time
0.600 2025-01-31 23:23:21Z
BREAKING CHANGES: OpenAPI::Modern must be updated to 0.079 after
installing this version.
- the specification version can now be overridden when constructing
Documents directly (rather than via the evaluator), if not
specified by the "$schema" keyword
- some fixes for $id handling in earlier drafts
- rework of document management and URI resolution:
$jsm->add_schema($uri, $schema) no longer sets the $uri in the
document object, allowing the document to be added to the index
under multiple URIs. Instead, we resolve the document's indexed
resources against the base URI and only store the resolved form in
the evaluator itself. This allows the same document to be reused
with different base URIs if it only contains relative $ids
internally. add_document($uri, $document) now resolves all URI
resources against the provided URI, rather than adding document
resources as-is and adding $uri as one more resource URI.
- further restructuring of internal resource indexes, anchor
management, and traversal of subschemas from embedded non-JSON
Schema documents (e.g. OpenAPI documents)
0.599 2025-01-26 22:54:02Z
- the boolean overload in JSON::Schema::Modern::Result is now
deprecated: it will warn on use, and will be removed no sooner
than 2026-02-01.
0.598 2025-01-19 18:30:32Z
- fix format acceptance tests that would fail when some optional
modules are not installed
- fix some acceptance tests that used the wrong validate_formats
mode (since v0.592)
0.597 2024-12-07 23:30:38Z
- report installed version of builtin
0.596 2024-11-24 00:57:13Z
- properly handle changing of dialects (including swapping out
vocabularies and keywords) when evaluating a local subschema
(without a $ref)
- now collecting identifiers within contentSchema keywords as we do
for any other subschema
0.595 2024-11-07 18:57:16Z
- bump required version of builtin::compat
0.594 2024-11-03 17:01:42Z
- improved handling of numeric type checking in draft4
- numeric type checking is relaxed, now allowing for dualvars
arising from the simple case of using a number in string context
or a string in numeric context (only a problem in perls <5.36)
0.593 2024-10-14 18:43:23Z
- fix new integer type tests that fail on perls < 5.35.9
0.592 2024-10-13 19:43:20Z
- support added for the draft4, draft6 specification versions
0.591 2024-10-06 21:06:15Z
- new --dump-identifiers option in json-schema-eval executable
- the document form of 'add_schema' has been extracted out into
'add_document', with the old form deprecated
- is_equal() utility function, and the const and enum keywords, now
provide more detailed error diagnostics
- fix "strict" mode for draft7 documents
0.590 2024-09-07 00:48:50Z
- "strict" mode is now recognized by validate_schema(), to report on
any unrecognized keywords used in the schema being validated
0.589 2024-07-06 21:07:44Z
- skip unpassable tests when ivsize < 8
0.588 2024-06-28 17:14:22Z
- bump required version of Math::BigInt for bdiv, bmod fixes
0.587 2024-06-26 22:55:11Z
- fix multipleOf test that fails on some peculiar architectures
- document the use of Sereal for caching large evaluator objects
0.586 2024-06-23 18:46:01Z
- simplification of calculation for "multipleOf" keyword
0.585 2024-06-19 04:03:36Z
- make use of core bool functionality where it exists (perl 5.36+)
0.584 2024-05-18 21:06:35Z
- add 'get_entity_locations' helper sub to ::Document
0.583 2024-03-30 17:56:17Z
- further optimization of error and annotation construction, which
should significantly improve evaluation performance of large
documents which heavily use the 'unevaluated*' keywords (such as a
schema evaluated against its metaschema, or an OpenAPI document
evaluated against its schema)
0.582 2024-01-23 03:18:31Z
- change the status of some format tests that rely on optional
modules so they are not reported as specification failures
- fix forking test that failed on MSWin32
0.581 2024-01-18 05:35:16Z
- make automated tests much quieter when some optional modules (e.g.
used for format tests) are not installed
0.580 2024-01-18 04:38:33Z
- update a format test to be more amenable to 32-bit architectures
0.579 2024-01-15 03:32:27Z
- improve performance by checking for duplicates by comparing the
checksum of a schema rather than the content itself
- fix error occurring when using the FormatAssertion vocabulary in
an evaluator object that was loaded from a serialized object
- fixed custom format definitions to not allow 'integer' types, as
per the specification
- support custom format definitions that operate on more than one
core data type
- fixed handling of unrecognized formats in draft2020-12 and later
(but only when the FormatAssertion vocabulary is explicitly
requested, not with validate_formats=1)
- the format keyword now respects the "stringy_numbers" option
0.578 2023-12-29 23:13:44Z
- remove use of JSON::MaybeXS, to avoid potential use of JSON::XS;
now we use Cpanel::JSON::XS or JSON::PP directly, using the same
environment variables as in Mojo::JSON for customization.
- new helper interface, get_document()
0.577 2023-12-19 05:27:04Z
- new attribute on Error and Result objects: "recommended_response",
for use when validating HTTP requests
0.576 2023-12-10 06:10:57Z
- the "stringy_numbers" feature now also applies to the "enum",
"const", and "uniqueItems" keywords
0.575 2023-11-26 05:11:10Z
- properly handle some edge cases where the "$schema" keyword can
change the dialect to a different specification version with
different Core vocabulary keywords
- evaluation at non-schema locations is now prohibited
0.574 2023-11-13 00:50:55Z
- better detection of schema locations, for use by Document
subclasses
- fixed vocabulary ordering (from v0.567)
- properly handle "$dynamicRef", "$recursiveRef" and "$schema"
referencing a boolean schema
- bundled metaschemas have been updated to their latest versions
from https://json-schema.org
0.573 2023-10-21 23:49:03Z
- fix construction of default values of some attributes e.g.
media_types, encodings
- fix list context of has_errors, error_count, annotation_count
methods
0.572 2023-10-14 22:04:52Z
- boost runtime performance by removing uses of MooX::HandlesVia
0.571 2023-09-17 01:14:47Z
- removed duration and uuid formats for draft7 (they were not
defined until the next spec version)
- properly default format validation to true in draft7
0.570 2023-09-02 20:42:03Z
- small performance improvements to 'date-time' and 'date' format
validation
- new stringy_numbers option, for validating numbers more loosely
0.569 2023-07-08 23:36:06Z
- fixed some edge cases with ipv6 format validation
0.568 2023-06-17 07:16:54Z
- add media-type support for application/x-ndjson and
application/x-www-form-urlencoded
0.567 2023-06-03 22:11:23Z
- vocabularies are now evaluated in a different order: Validation
and Format vocabularies now come before Applicator, in order to
allow faster short circuiting when errors are encountered.
0.566 2023-05-11 03:34:59Z
- treat ambiguous types as a normal error, rather than an exception
which may provide incorrect location data
0.565 2023-03-12 21:19:27Z
- traverse and evaluate callbacks can now produce errors,
which are incorporated into the overall evaluation results
- fix bad handling of empty patterns in "pattern",
"patternProperties" keywords
0.564 2023-03-04 00:43:42Z
- further tweak performance by short-circuiting inside some
subschemas (but not when annotations must be collected for
"unevaluated" keywords)
- added support for 'base64url' encoding
0.563 2023-02-04 23:38:13Z
- documentation update: improve language around data types and
JSON decoder recommendations
0.562 2023-01-22 00:49:07Z
- bump a test prereq to fix a mismatched exception message
0.561 2023-01-07 20:57:37Z
- further tweak performance by only collecting annotations when
explicitly requested, or needed by the current evaluation scope
0.560 2022-12-20 20:00:05Z
- fix a test that depended on optional prereqs
0.559 2022-12-16 04:29:55Z
- fix regression where formats do not validate beyond a $ref (since
v0.556)
0.558 2022-11-26 02:43:15Z
- add fallback media type handling for text/*
- performance is (hopefully) improved by delaying some calculations
in annotations until they are needed
0.557 2022-10-30 21:59:04Z
- improvements to processing of keywords in the Content vocabulary
(contentEncoding, contentMediaType, contentSchema)
- LICENSE now provided with bundled metaschema files
0.556 2022-09-18 22:41:50Z
- some performance optimizations for schema traversal and evaluation
0.555 2022-09-10 21:43:11Z
- the "iri-reference" format is now supported, sort of (all strings
will be accepted as valid)
- "enum" no longer incorrectly errors if elements are not unique
- new experimental output format: "data_only", which encapsulates
what is produced when a JSON::Schema::Modern::Result object is
stringified
0.554 2022-07-24 00:08:54Z
- use new Slurpy type in Types::Standard
0.553 2022-06-25 03:27:14Z
- expanded on the documentation for serializing results.
- updated IETF URI references from draft-bhutton-json-schema-00
to draft-bhutton-json-schema-01, to reflect the updates to
specification draft2020-12 (implementation updates have already
been reflected in earlier releases, notably 0.548 and 0.550)
0.552 2022-05-03 03:31:59Z
- fix result serialization from exceptions (broken in 0.550)
0.551 2022-05-01 01:29:51Z
- the "specification_version" configuration option now accepts
values without "draft" in the name, to facilitate a new naming
convention used for future specification versions
- new "formatted_annotations" option for JSON::Schema::Modern::Result,
to allow for omitting annotations from result output
0.550 2022-04-14 04:32:32Z
- added 'dump' method to Result, Error and Annotation objects, for
easier debugging and generating test output
- adjusted syntax checks for $vocabulary keyword to allow for
bundled metaschemas
- new validate_schema() method, for easily validating a schema
against its metaschema
0.549 2022-03-22 03:55:15Z
- properly detect the metaschema in json-schema-eval
--validate-schema
0.548 2022-03-09 06:27:10Z
- "annotate_unknown_keywords" option removed; behaviour is now on
for draft2020-12 and off otherwise
- annotation behaviour for applicator keywords is fixed per the
spec, resulting in fewer redundant errors from unevaluatedItems,
unevaluatedProperties keywords
0.547 2022-03-03 06:08:51Z
- improved error stringification on document error
0.546 2022-02-23 01:33:03Z
- avoid use of newly-experimental signature syntax on 5.35.9
0.545 2022-02-22 04:30:36Z
- avoid new experimental warning on 5.35.9
- skip unresolvable identifiers for future drafts in acceptance
tests (added in TJSA 1.016)
0.544 2022-02-16 05:53:08Z
- add_schema() now has more consistent exception handling
0.543 2022-02-11 04:11:55Z
- now allowing runtime overriding of the "strict" configuration
- add "effective_base_uri", for adjusting the locations of errors
and annotations against a dynamic base (removed in version 0.620)
0.542 2022-01-23 08:17:08Z
- new "strict" option (and --strict flag to json-schema-eval), for
disallowing unknown keywords
0.541 2022-01-17 23:57:35Z
- add --add-schema option to json-schema-eval to allow for the
reference of additional schemas during evaluation
0.540 2022-01-17 18:58:36Z
- make "unimplemented format" errors more visible
- fixed serializing of results in acceptance tests
- add --validate-schema option to json-schema-eval to provide an
easy way to validate a schema against its meta-schema
0.539 2022-01-06 04:07:20Z
- updated error message for the "type" keyword to include the actual
type, as well as the expected type(s)
0.538 2021-12-31 21:18:21Z
- remove no-longer-needed TODO in tests that caused warnings in
small-int systems
0.537 2021-12-30 22:49:02Z
- fix number/integer differentiation on small-int systems
- improve division calculations if either argument is a non-integer
0.536 2021-12-30 05:32:44Z
- very large/small numbers are now properly accomodated in all
cases, including from JSON-serialized data
0.535 2021-12-28 06:38:37Z
- mark more tests TODO (temporarily!) for small-int systems
0.534 2021-12-27 21:05:34Z
- clarify exit statuses for 'json-schema-eval'
- fix numeric tests for architectures with small int size
- fix handling of unsigned ints that cannot be represented in a
signed int
0.533 2021-12-23 19:45:11Z
- fix exit statuses in json-schema-eval
0.532 2021-12-22 19:14:06Z
- add media_type decoders for application/schema+json,
application/schema-instance+json
- add 'json-schema-eval' executable, for ad-hoc evaluation
0.531 2021-12-06 05:39:53Z
- add method to add format implementations after construction
- treat media_type names case-insensitively, as per RFC6838,
and lookups support wildcards (get_media_type('text/plain')
will match an entry for 'text/*' or '*/*')
0.530 2021-12-03 16:44:23Z
- fix hash slice syntax that is not available before perl 5.28
0.529 2021-12-03 04:00:26Z
- added FREEZE and THAW callbacks to assist with serialization
0.528 2021-11-30 06:17:29Z
- evaluation callback sub signature has changed, to add $data
- minor performance improvement during evaluation
0.527 2021-11-26 00:53:42Z
- fixes to base64 and json decoders used in the Content vocabulary
0.526 2021-11-23 05:11:46Z
- fix evaluate() callbacks for keywords that have no
runtime-specific actions
- optional support for contentEncoding, contentMediaType and
contentSchema with the validate_content_schemas option and
"encoding", "media_type" handler registries
- a boolean flag "unknown" has been added to Annotation objects to
indicate they correspond to an unknown keyword in the schema
0.525 2021-11-17 05:35:24Z
- minimum Perl version raised to 5.20
- dropped bundling hyper-schema files, as they are not validatable
without vocabulary support (see issue #44)
- & overload added to JSON::Schema::Modern::Result, for combining
results
- add callbacks to evaluate(), to enable finding certain positions
of interest in a document or schema
0.524 2021-11-10 04:36:49Z
- some refactoring of vocabulary and document methods, to faciliate
re-use
- new utility functions: is_uri_reference, assert_keyword_exists
- allow specifying the metaschema_uri for a schema document
- add "validate" method to Document object
- updated draft2019-09 and added draft2020-12 hyperschema schemas
0.523 2021-10-24 05:58:27Z
- fix tests that were relying on an optional prereq
- properly gate experimental features by version
0.522 2021-10-22 22:26:29Z
- 'date-time' format now properly handles leap seconds
0.521 2021-10-04 05:36:10Z
- fix issues when referencing a schema in a metaschema:
whose vocabulary is not known, but its keywords can still be
validated, or which does not use the $vocabulary keyword at all
0.520 2021-09-27 05:56:11Z
- support arbitrary metaschemas in the "$schema" keyword
- support custom vocabulary classes, for use in metaschemas that use
the "$vocabulary" keyword
0.519 2021-09-21 03:56:13Z
- fix tests that were relying on an optional prereq
0.518 2021-09-18 22:00:42Z
- skip some regex tests when Unicode library is too old for those
character classes being tested
- specification versions can now change, via the $schema keyword,
within schema resources in a single document or via $ref to another
resource
0.517 2021-08-28 04:34:17Z
- restore some optional modules used for format validation to the
prereq list that were mistakenly dropped in version 0.515
- date-time, date and time formats no longer match non-ascii digits
0.516 2021-08-14 19:53:16Z
- fix email format tests on older prereqs
- avoid errors when enabling validate_formats before evaluating but
after loading a schema
0.515 2021-08-03 04:07:02Z
- no longer calling a keyword's callback during traverse() if that
keyword has an error
- better handling of blessed data types and other references
- new config option scalarref_booleans, which will treat \0, \1 in
data as json booleans
- support for the most recent specification version, draft2020-12
0.514 2021-07-22 05:17:08Z
- add_schema() now dies with the errors themselves, rather than an
object that serializes to an unhelpful value in uncaught die()s.
0.513 2021-06-26 19:45:51Z
- skip acceptance test for integer overflow when nvsize is too
large to produce the expected error
- support for specification version draft7, through the "$schema"
keyword and the new 'specification_version' constructor option
0.512 2021-06-09 02:29:23Z
- distribution has been renamed from JSON-Schema-Draft201909 to
JSON-Schema-Modern. JSON::Schema::Draft201909 lives on as a
compatibility wrapper.
0.028 2021-06-08 02:48:07Z
- fix validation regex for the $anchor keyword
- unevaluatedItems and unevaluatedProperties keywords are now
applied after all other keywords in the applicator vocabulary
(true/false results are not affected, but the order of annotations
and errors will change)
- calculate the canonical uri correctly after navigating a $ref:
using the closest resource identifier to the destination, not the
one that was used in the $ref
0.027 2021-05-15 18:13:21Z
- fixed error strings used for failing "dependentRequired"
- in terse output format, do not discard non-summary errors from
schema-form of items
- keywords in the applicator vocabulary are now applied before the
keywords in the validation vocabulary (true/false results are not
affected, but the order of annotations and errors will change)
- improved validation of the "date-time", "date" and "time" formats
0.026 2021-04-08 20:13:27Z
- fix scoping of annotations from uncle keywords (siblings of the
schema's parent) that were improperly visible to unevaluatedItems,
unevaluatedProperties
- 'result' attribute in JSON::Schema::Draft201909::Result has been
renamed to 'valid', to better match what it represents (a boolean)
0.025 2021-03-30 05:36:14Z
- minor changes to error strings to distinguish between issues that
can be determined from static inspection of schema(s), and those
that only arise during runtime evaluation (such as URIs that
map to missing schema documents, or inconsistent configuration
values).
- more validity checks at traversal time of $ref, $schema,
$vocabulary values
- update ipv4 format validation to reject leading zeroes in octets,
helping avoid a newly-discovered vulnerability in netmasks
0.024 2021-03-23 21:53:42Z
- the default value for "validate_formats" is once again false (it
became true in v0.020), to properly conform to the specification
0.023 2021-02-21 18:36:32Z
- fix "try/catch is experimental" warnings in perl 5.33.7
0.022 2021-02-07 17:33:14Z
- fix erroneous use of postfix dereference (closes #42).
0.021 2021-02-06 18:50:42Z
- [Pp]roperties$ keywords now always produce annotations when
evaluating successfully, even if there were no matching properties
- added the "strict_basic" output format, for strict (but incorrect)
adherence to the draft 2019-09 specification
0.020 2021-01-02 17:12:09Z
- the default value for "validate_formats" is now true, to reflect
the most typical usecase.
- gracefully handle the erroneous schema { "type": null }
- fixes to relative-json-pointer format validation
- new "annotate_unknown_keywords" config option
0.019 2020-12-08 18:40:10Z
- further improvements to the "terse" output format
- add_schema will now die with a Result object rather than a
listref of Error objects, when the document contains errors.
0.018 2020-12-07 18:22:07Z
- now can correctly evaluate schemas containing unevaluatedItems,
unevaluatedProperties keywords without the user having to
explicitly set collect_annotations => 1 in the constructor
- fix error in "terse" output formatting that mistakenly dropped
some unevaluatedProperties errors
0.017 2020-11-24 19:15:18Z
- refactor keyword implementations into separate vocabulary classes,
to faciliate future support for custom vocabularies
- traverse the schema before evaluation, for more correct
and complete extraction of identifiers and invalid syntax
- add callbacks to traverse(), to easily find keywords of interest
0.016 2020-11-18 18:18:40Z
- further fixes to infinite loop detection
- fix dereference error when evaluating "definitions",
"dependencies"
- when adding two schema documents with no canonical uri, preserve
knowledge of other identifiers found in the first document
- add_schema() no longer adds additional URIs to the document
object, only the evaluator
0.015 2020-10-20 03:08:36Z
- fixed infinity/overflow checks for older perls
0.014 2020-10-16 19:21:17Z
- ensure "enum" value is an array
- do not evaluate non-arrays against "unevaluatedItems"
- fix detection of bad $recursiveAnchor
- fix canonical uri calculation for $schema, $recursiveAnchor, and
infinite loop detection
- for output_format=terse, do not omit the important errors for
unevaluated* when annotation collection is disabled
0.013 2020-09-15 19:14:53Z
- detect more cases of resource identifier collisions
- fix resolution of relative $ids
- new "terse" output format
0.012 2020-08-13 20:23:21Z
- now using unicode semantics for pattern matching
0.011 2020-08-04 22:16:46Z
- better normalization of uris in errors
- now detecting infinite loops separately from deep traversal
- optionally collect annotations
- support for the "unevaluatedItems" and "unevaluatedProperties"
keywords
0.010 2020-07-23 16:50:18Z
- fixed error generation for validator keywords with numeric
arguments (e.g. minimum, multipleOf)
- new "get" method for fetching the schema found at a URI
- improved "ipv6" format validation
0.009 2020-07-07 19:54:44Z
- no longer allowing adding another schema document with a duplicate
uri but different schema content (some collision checks were too
lax).
- fix behaviour of $recursiveRef without an $recursiveAnchor in the
initial target scope
0.008 2020-06-22 04:24:06Z
- fix bad syntax used in a test
0.007 2020-06-21 21:20:33Z
- raise some inadequate prereq declarations
- fix incorrect canonical uri when evaluating a (sub)schema using a
non-canonical uri
0.006 2020-06-19 20:54:40Z
- add support for evaluation against a uri
- add "add_schema" interface for using additional schema documents
within the implementation
- support using the "format" keyword as an assertion, with the
"validate_formats" option
0.005 2020-06-09 01:54:05Z
- fix some edge cases with usage of $recursiveAnchor, $recursiveRef
- fixed several issues with resource identification within schema
documents
0.004 2020-06-02 19:14:32Z
- add support for $recursiveAnchor and $recursiveRef
- support use of "$ref":"https://json-schema.org/draft/2019-09/schema"
by loading common metaschemas from a local cache
0.003 2020-05-31 20:10:02Z
- add infinite recursion detection
- process properties in sorted order, for consistent ordering of
results
- mark a numeric comparison test as TODO on 32-bit machines (see
GHI #10)
0.002 2020-05-27 22:28:15Z
- fix incorrect prereq needed for tests
- add support for $id and $anchor in single schema documents
0.001 2020-05-21 15:51:00Z
- Initial release [as JSON-Schema-Draft201909].
t 000755 000766 000024 0 15114374332 14504 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 ref.t 100640 000766 000024 125710 15114374332 15647 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Storable 'dclone';
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new;
subtest 'local JSON pointer' => sub {
cmp_result(
$js->evaluate(true, { '$defs' => { true => true }, '$ref' => '#/$defs/true' })->TO_JSON,
{ valid => true },
'can follow local $ref to a true schema',
);
cmp_result(
$js->evaluate(true, { '$defs' => { false => false }, '$ref' => '#/$defs/false' })->TO_JSON,
superhashof({ valid => false }),
'can follow local $ref to a false schema',
);
ok(
lives {
my $result = $js->evaluate(true, { '$ref' => '#/$defs/nowhere' });
like(
(($result->errors)[0])->error,
qr{^EXCEPTION: unable to find resource "\#/\$defs/nowhere"},
'got error for unresolvable ref',
);
},
'no exception',
);
};
subtest 'fragment with URI-escaped and JSON Pointer-escaped characters' => sub {
cmp_result(
$js->evaluate(
1,
{
'$defs' => { 'foo-bar-tilde~-slash/-braces{}-def' => true },
'$ref' => '#/$defs/foo-bar-tilde~0-slash~1-braces%7B%7D-def',
},
)->TO_JSON,
{ valid => true },
'can follow $ref with escaped components',
);
};
subtest 'local anchor' => sub {
cmp_result(
$js->evaluate(
true,
{
'$defs' => {
true => {
'$anchor' => 'true',
},
},
'$ref' => '#true',
},
)->TO_JSON,
{ valid => true },
'can follow local $ref to an $anchor to a true schema',
);
cmp_result(
$js->evaluate(
true,
{
'$defs' => {
false => {
'$anchor' => 'false',
not => true,
},
},
'$ref' => '#false',
},
)->TO_JSON,
superhashof({ valid => false }),
'can follow local $ref to an $anchor to a false schema',
);
is(
dies {
my $result = $js->evaluate(true, { '$ref' => '#nowhere' });
like(
(($result->errors)[0])->error,
qr{^EXCEPTION: unable to find resource "\#nowhere"},
'got error for unresolvable ref',
);
},
undef,
'no exception',
);
};
subtest '$id with an empty fragment' => sub {
my $js = JSON::Schema::Modern->new(max_traversal_depth => 2);
cmp_result(
$js->evaluate(
1,
{
'$defs' => {
foo => {
'$id' => 'http://localhost:4242/my_foo#',
type => 'string',
},
reference_to_foo => {
'$ref' => 'http://localhost:4242/my_foo',
},
},
allOf => [
{ '$ref' => 'http://localhost:4242/my_foo' },
{ '$ref' => '#/$defs/reference_to_foo' },
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/type',
absoluteKeywordLocation => 'http://localhost:4242/my_foo#/type',
error => 'got integer, not string',
},
{
absoluteKeywordLocation => 'http://localhost:4242/my_foo',
error => 'EXCEPTION: maximum evaluation depth (2) exceeded',
instanceLocation => '',
keywordLocation => "/allOf/1/\$ref/\$ref",
},
],
},
'$id with empty fragment can be found by $ref that did not include it; fragment not included in error either',
);
};
$js = JSON::Schema::Modern->new(specification_version => 'draft2019-09');
subtest '$recursiveRef without nesting behaves like $ref' => sub {
cmp_result(
$js->evaluate(
{ foo => { bar => 'hello', baz => 1 } },
{
'$id' => 'http://localhost:4242',
'$recursiveAnchor' => true,
anyOf => [
{ type => 'string' },
{
type => 'object',
additionalProperties => { '$recursiveRef' => '#' },
},
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/anyOf/0/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/0/type',
error => 'got object, not string',
},
# /anyOf/1 with ''
# /anyOf/1/additionalProperties/$recursiveRef with '/foo'
# /anyOf/1/additionalProperties/$recursiveRef/anyOf/0 - wrong type
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$recursiveRef/anyOf/0/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/0/type',
error => 'got object, not string',
},
# /anyOf/1/additionalProperties/$recursiveRef/anyOf/1 with /foo
# additionalProperties: consider /foo/bar
# /anyOf/1/additionalProperties/$recursiveRef/anyOf/1/additionalProperties/$recursiveRef
# /anyOf/1/additionalProperties/$recursiveRef/anyOf/1/additionalProperties/$recursiveRef/anyOf/0 - is string, so no error
# /anyOf/1/additionalProperties/$recursiveRef/anyOf/1/additionalProperties/$recursiveRef/anyOf/1 - is object
# additionalProperties: consider /foo/baz
# is neither string or object
{
instanceLocation => '/foo/baz',
keywordLocation => '/anyOf/1/additionalProperties/$recursiveRef/anyOf/1/additionalProperties/$recursiveRef/anyOf/0/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/0/type',
error => 'got integer, not string',
},
{
instanceLocation => '/foo/baz',
keywordLocation => '/anyOf/1/additionalProperties/$recursiveRef/anyOf/1/additionalProperties/$recursiveRef/anyOf/1/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/1/type',
error => 'got integer, not object',
},
{
instanceLocation => '/foo/baz',
keywordLocation => '/anyOf/1/additionalProperties/$recursiveRef/anyOf/1/additionalProperties/$recursiveRef/anyOf',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf',
error => 'no subschemas are valid',
},
# and now we start to unwind.
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$recursiveRef/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$recursiveRef/anyOf',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf',
error => 'no subschemas are valid',
},
],
},
'$recursiveRef without nested $recursiveAnchor behaves like $ref',
);
};
subtest '$recursiveRef without $recursiveAnchor behaves like $ref' => sub {
cmp_result(
$js->evaluate(
{ foo => { bar => 1 } },
{
properties => { foo => { '$recursiveRef' => '#' } },
additionalProperties => false,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/foo/bar',
keywordLocation => '/properties/foo/$recursiveRef/additionalProperties',
absoluteKeywordLocation => '#/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/$recursiveRef/additionalProperties',
absoluteKeywordLocation => '#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'$recursiveRef without $recursiveAnchor behaves like $ref',
);
};
subtest '$recursiveAnchor must be at a schema resource root' => sub {
my $schema = {
'$defs' => {
myobject => {
'$recursiveAnchor' => true,
anyOf => [
{ type => 'integer' },
{
type => 'object',
additionalProperties => { '$recursiveRef' => '#' },
},
],
},
},
anyOf => [
{ type => 'integer' },
{ '$ref' => '#/$defs/myobject' },
],
};
cmp_result(
$js->evaluate({ foo => 1 }, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$defs/myobject/$recursiveAnchor',
error => '"$recursiveAnchor" keyword used without "$id"',
},
],
},
'$recursiveAnchor can only appear at a schema resource root',
);
$schema = dclone($schema);
$schema->{'$defs'}{myobject}{'$id'} = 'myobject.json';
cmp_result(
$js->evaluate({ foo => 1 }, $schema)->TO_JSON,
{
valid => true,
},
'schema now valid when an $id is added',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$defs' => {
inner => {
'$recursiveAnchor' => true, # this is illegal - canonical uri has a fragment
type => [ qw(integer object) ],
additionalProperties => { '$recursiveRef' => '#/$defs/inner' },
},
},
type => 'object',
additionalProperties => { '$recursiveRef' => '#/$defs/inner' },
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$defs/inner/$recursiveAnchor',
error => '"$recursiveAnchor" keyword used without "$id"',
},
],
},
'$recursiveAnchor can only appear at a schema resource root',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
allOf => [
{
'$recursiveAnchor' => true,
type => [ qw(integer object) ],
additionalProperties => { '$recursiveRef' => '#/allOf/1' },
},
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/$recursiveAnchor',
error => '"$recursiveAnchor" keyword used without "$id"',
},
],
},
'properly detecting a bad $recursiveAnchor even before passing through a $ref',
);
};
subtest '$recursiveAnchor and $recursiveRef - standard usecases' => sub {
my $schema = {
'$id' => 'https://base.com',
#'$recursiveAnchor' => true, # the presence of this keyword changes everything
type => [ 'object', 'integer' ],
additionalProperties => {
'$id' => 'https://innerbase.com',
#'$recursiveAnchor' => true, # the presence of this keyword changes everything
type => [ 'object', 'boolean' ],
additionalProperties => {
'$ref' => '#', # if this was a $recursiveRef and there are $recursiveAnchors, we will go to base.
},
},
};
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '/foo/bar',
keywordLocation => '/additionalProperties/additionalProperties/$ref/type',
absoluteKeywordLocation => 'https://innerbase.com#/type',
error => 'got integer, not one of object, boolean',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/additionalProperties',
absoluteKeywordLocation => 'https://innerbase.com#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://base.com#/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'validation requires the override that is not in scope',
);
# now make the ref a recursiveRef, but still won't recurse to base because no recursiveanchor.
$js->{_resource_index} = {};
$schema->{additionalProperties}{additionalProperties}{'$recursiveRef'} =
delete $schema->{additionalProperties}{additionalProperties}{'$ref'};
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => [
+{
$errors->[0]->%*,
keywordLocation => ($errors->[0]{keywordLocation} =~ s/ref/recursiveRef/r),
},
$errors->@[1..2],
],
},
'$recursiveRef requires a $recursiveAnchor that does not exist',
);
# now we will recurse to the base.
$js->{_resource_index} = {};
$schema->{'$recursiveAnchor'} = true;
cmp_result(
$js->evaluate({ foo => true }, $schema)->TO_JSON,
{
valid => true,
},
'$recursiveRef with both $recursiveAnchors in scope',
);
};
subtest '$recursiveRef without $recursiveAnchor' => sub {
my $schema = {
'$id' => 'strings_only',
'$defs' => {
allow_ints => {
'$id' => 'allow_ints',
anyOf => [
{ type => 'integer' },
{ type => 'object', additionalProperties => { '$ref' => '#' } },
],
},
},
anyOf => [
{ type => 'string' },
{ type => 'object', additionalProperties => { '$ref' => '#' } },
],
};
cmp_result(
$js->evaluate(
{ foo => 1 },
$schema,
)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '',
keywordLocation => '/anyOf/0/type',
absoluteKeywordLocation => 'strings_only#/anyOf/0/type',
error => 'got object, not string',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$ref/anyOf/0/type',
absoluteKeywordLocation => 'strings_only#/anyOf/0/type',
error => 'got integer, not string',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$ref/anyOf/1/type',
absoluteKeywordLocation => 'strings_only#/anyOf/1/type',
error => 'got integer, not object',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$ref/anyOf',
absoluteKeywordLocation => 'strings_only#/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'strings_only#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
absoluteKeywordLocation => 'strings_only#/anyOf',
error => 'no subschemas are valid',
},
],
},
'$ref - one level recursion',
);
$js->{_resource_index} = {};
cmp_result(
$js->evaluate(
{ foo => 1 },
$js->_json_decoder->decode($js->_json_decoder->encode($schema) =~ s/\$ref/\$recursiveRef/gr),
)->TO_JSON,
{
valid => false,
errors => $js->_json_decoder->decode($js->_json_decoder->encode($errors) =~ s/\$ref/\$recursiveRef/gr),
},
'$recursiveRef with no $recursiveAnchor in scope has the same outcome',
);
};
subtest '$recursiveAnchor in our dynamic scope, but not in the target schema' => sub {
my $schema = {
'$id' => 'base',
'$recursiveAnchor' => true,
anyOf => [
{ type => 'boolean' },
{
type => 'object',
additionalProperties => {
'$id' => 'inner',
# note: no $recursiveAnchor here! so we do NOT recurse to the base.
anyOf => [
{ type => 'integer' },
{ type => 'object', additionalProperties => { '$recursiveRef' => '#' } },
],
},
},
],
};
cmp_result(
$js->evaluate(
{ foo => { bar => 1 } },
$schema,
)->TO_JSON,
{
valid => true,
},
'$recursiveAnchor does not exist in the target schema - local recursion only, so integers match',
);
cmp_result(
$js->evaluate(
{ foo => true },
'base',
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/anyOf/0/type',
absoluteKeywordLocation => 'base#/anyOf/0/type',
error => 'got object, not boolean',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/anyOf/0/type',
absoluteKeywordLocation => 'inner#/anyOf/0/type',
error => 'got boolean, not integer',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/anyOf/1/type',
absoluteKeywordLocation => 'inner#/anyOf/1/type',
error => 'got boolean, not object',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/anyOf',
absoluteKeywordLocation => 'inner#/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'base#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
absoluteKeywordLocation => 'base#/anyOf',
error => 'no subschemas are valid',
},
],
},
'$recursiveAnchor does not exist in the target schema - no recursion',
);
cmp_result(
$js->evaluate(
{ foo => { bar => true } },
'base',
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/anyOf/0/type',
absoluteKeywordLocation => 'base#/anyOf/0/type',
error => 'got object, not boolean',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/anyOf/0/type',
absoluteKeywordLocation => 'inner#/anyOf/0/type',
error => 'got object, not integer',
},
{
instanceLocation => '/foo/bar',
keywordLocation => '/anyOf/1/additionalProperties/anyOf/1/additionalProperties/$recursiveRef/anyOf/0/type',
absoluteKeywordLocation => 'inner#/anyOf/0/type',
error => 'got boolean, not integer',
},
{
instanceLocation => '/foo/bar',
keywordLocation => '/anyOf/1/additionalProperties/anyOf/1/additionalProperties/$recursiveRef/anyOf/1/type',
absoluteKeywordLocation => 'inner#/anyOf/1/type',
error => 'got boolean, not object',
},
{
instanceLocation => '/foo/bar',
keywordLocation => '/anyOf/1/additionalProperties/anyOf/1/additionalProperties/$recursiveRef/anyOf',
absoluteKeywordLocation => 'inner#/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'inner#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/anyOf',
absoluteKeywordLocation => 'inner#/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'base#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
absoluteKeywordLocation => 'base#/anyOf',
error => 'no subschemas are valid',
},
],
},
'$recursiveAnchor does not exist in the target schema - local recursion only',
);
};
$js = JSON::Schema::Modern->new;
subtest '$dynamicRef without nesting behaves like $ref' => sub {
cmp_result(
$js->evaluate(
{ foo => { bar => 'hello', baz => 1 } },
{
'$id' => 'http://localhost:4242',
'$dynamicAnchor' => 'hi',
anyOf => [
{ type => 'string' },
{
type => 'object',
additionalProperties => { '$dynamicRef' => '#hi' },
},
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/anyOf/0/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/0/type',
error => 'got object, not string',
},
# /anyOf/1 with ''
# /anyOf/1/additionalProperties/$dynamicRef with '/foo'
# /anyOf/1/additionalProperties/$dynamicRef/anyOf/0 - wrong type
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$dynamicRef/anyOf/0/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/0/type',
error => 'got object, not string',
},
# /anyOf/1/additionalProperties/$dynamicRef/anyOf/1 with /foo
# additionalProperties: consider /foo/bar
# /anyOf/1/additionalProperties/$dynamicRef/anyOf/1/additionalProperties/$dynamicRef
# /anyOf/1/additionalProperties/$dynamicRef/anyOf/1/additionalProperties/$dynamicRef/anyOf/0 - is string, so no error
# /anyOf/1/additionalProperties/$dynamicRef/anyOf/1/additionalProperties/$dynamicRef/anyOf/1 - is object
# additionalProperties: consider /foo/baz
# is neither string or object
{
instanceLocation => '/foo/baz',
keywordLocation => '/anyOf/1/additionalProperties/$dynamicRef/anyOf/1/additionalProperties/$dynamicRef/anyOf/0/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/0/type',
error => 'got integer, not string',
},
{
instanceLocation => '/foo/baz',
keywordLocation => '/anyOf/1/additionalProperties/$dynamicRef/anyOf/1/additionalProperties/$dynamicRef/anyOf/1/type',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/1/type',
error => 'got integer, not object',
},
{
instanceLocation => '/foo/baz',
keywordLocation => '/anyOf/1/additionalProperties/$dynamicRef/anyOf/1/additionalProperties/$dynamicRef/anyOf',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf',
error => 'no subschemas are valid',
},
# and now we start to unwind.
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$dynamicRef/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '/foo',
keywordLocation => '/anyOf/1/additionalProperties/$dynamicRef/anyOf',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/additionalProperties',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf/1/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
absoluteKeywordLocation => 'http://localhost:4242#/anyOf',
error => 'no subschemas are valid',
},
],
},
'$dynamicRef without nested $dynamicAnchor behaves like $ref',
);
};
subtest '$recursiveRef without $dynamicAnchor behaves like $ref' => sub {
cmp_result(
$js->evaluate(
{ foo => { bar => 1 } },
{
properties => { foo => { '$dynamicRef' => '#' } },
additionalProperties => false,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/foo/bar',
keywordLocation => '/properties/foo/$dynamicRef/additionalProperties',
absoluteKeywordLocation => '#/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/$dynamicRef/additionalProperties',
absoluteKeywordLocation => '#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'$dynamicRef without $dynamicAnchor behaves like $ref',
);
};
subtest '$dynamicAnchor and $dynamicRef - standard usecases' => sub {
my $schema = {
'$id' => 'https://base.com',
#'$dynamicAnchor' => 'thingy', # adding this, and changing the $ref, will make us recurse here.
type => [ 'object', 'integer' ],
additionalProperties => {
'$id' => 'https://innerbase.com', # without $dynamicRef, we will recurse here.
#'$dynamicAnchor' => 'thingy',
type => [ 'object', 'boolean' ],
additionalProperties => {
'$ref' => '#', # if this was a $dynamicRef and there are $dynamicAnchors, we will go to base.
},
},
};
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '/foo/bar',
keywordLocation => '/additionalProperties/additionalProperties/$ref/type',
absoluteKeywordLocation => 'https://innerbase.com#/type',
error => 'got integer, not one of object, boolean',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/additionalProperties',
absoluteKeywordLocation => 'https://innerbase.com#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://base.com#/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'validation requires the override that is not in scope',
);
# now make the ref a dynamicRef, but still won't recurse to base because no dynamicanchor.
$js->{_resource_index} = {};
$schema->{additionalProperties}{additionalProperties}{'$dynamicRef'} =
delete $schema->{additionalProperties}{additionalProperties}{'$ref'}; # '#'
$errors->[0]{keywordLocation} =~ s/ref/dynamicRef/;
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'$dynamicRef requires a $dynamicAnchor that does not exist',
);
# we still won't recurse to the base because $dynamicRef doesn't use the anchor URI.
$js->{_resource_index} = {};
$schema->{additionalProperties}{'$dynamicAnchor'} = 'thingy';
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'$dynamicRef must use a URI containing the dynamic anchor fragment',
);
# use the anchor URI for $dynamicRef, but we still won't recurse to the base because there is no
# outer $dynamicAnchor.
$js->{_resource_index} = {};
$schema->{additionalProperties}{additionalProperties}{'$dynamicRef'} = '#thingy';
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'there is no outer $dynamicAnchor in scope to recurse to',
);
# change $dynamicRef back to $ref, but use the fragment uri.
$js->{_resource_index} = {};
$schema->{additionalProperties}{additionalProperties}{'$ref'} =
delete $schema->{additionalProperties}{additionalProperties}{'$dynamicRef'}; # '#thingy'
$errors->[0]{keywordLocation} =~ s/dynamicRef/ref/;
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'we have an outer $dynamicAnchor, and are using the fragment URI, but we used $ref rather than $dynamicRef',
);
# now add a $dynamicAnchor to base, but we still won't recurse to the base because $dynamicRef
# doesn't use the anchor.
$js->{_resource_index} = {};
delete $schema->{additionalProperties}{additionalProperties}{'$ref'};
$schema->{additionalProperties}{additionalProperties}{'$dynamicRef'} = '#';
$schema->{'$dynamicAnchor'} = 'thingy';
$errors->[0]{keywordLocation} =~ s/ref/dynamicRef/;
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'there is an outer $dynamicAnchor in scope to recurse to, but $dynamicRef must use a URI containing the dynamic anchor fragment',
);
$js->{_resource_index} = {};
$schema->{additionalProperties}{additionalProperties}{'$dynamicRef'} = '#thingy';
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => true,
},
'now everything is in place to recurse to the base',
);
$js->{_resource_index} = {};
delete $schema->{additionalProperties}{'$dynamicAnchor'};
$schema->{additionalProperties}{additionalProperties}{'$dynamicRef'} = '#';
cmp_result(
$js->evaluate({ foo => { bar => 1 } }, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'there is no $dynamicAnchor at the original target, and no anchor used in the target URI',
);
};
subtest '$dynamicRef to $dynamicAnchor not directly in the evaluation path' => sub {
$js->{_resource_index} = {};
my $schema = {
'$id' => 'base',
'$defs' => {
override => {
# this is in base uri 'base'
#'$dynamicAnchor' => 'thingy',
type => 'number',
},
start => {
'$id' => 'start',
'$defs' => {
main => {
# start#thingy
'$dynamicAnchor' => 'thingy',
type => 'string',
},
},
'$dynamicRef' => '#thingy', # -> start#thingy ( -> base#thingy ), when second anchor is in place
},
},
'$ref' => 'start',
};
cmp_result(
$js->evaluate(42, $schema)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '',
keywordLocation => '/$ref/$dynamicRef/type',
absoluteKeywordLocation => 'start#/$defs/main/type',
error => 'got integer, not string',
},
],
},
'second dynamic anchor is not in the evaluation path, but we found it via dynamic scope - type does not match',
);
$js->{_resource_index} = {};
$schema->{'$defs'}{override}{'$anchor'} = 'thingy';
cmp_result(
$js->evaluate(42, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'regular $anchor in dynamic scope should not be used by $dynamicRef',
);
$js->{_resource_index} = {};
delete $schema->{'$defs'}{override}{'$anchor'};
$schema->{'$defs'}{override}{'$dynamicAnchor'} = 'some_other_thingy';
cmp_result(
$js->evaluate(42, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'some other $dynamicAnchor in dynamic scope should not be used by $dynamicRef',
);
$js->{_resource_index} = {};
$schema->{'$defs'}{override}{'$dynamicAnchor'} = 'thingy';
cmp_result(
$js->evaluate(42, $schema)->TO_JSON,
{
valid => true,
},
'second dynamic anchor is not in the evaluation path, but we found it via dynamic scope - type matches',
);
$js->{_resource_index} = {};
my $canonical_uri = delete $schema->{'$id'};
$js->add_schema($canonical_uri => $schema);
cmp_result(
$js->evaluate(42, $canonical_uri)->TO_JSON,
{
valid => true,
},
'the first dynamic scope is set by document uri, not just the $id keyword',
);
};
subtest 'multiple layers in the dynamic scope' => sub {
$js->{_resource_index} = {};
my $schema = {
# We $ref from base -> first#/$defs/stuff -> second#/$defs/stuff -> third#/$defs/stuff
# and then follow a $dynamicRef to #length.
# At no point do we ever actually evaluate at the root schema for each scope.
# The dynamic scope is [ base, first, second, third ] and we check the scopes in order,
# therefore the first scope we find with a dynamic anchor "length" is "second".
'$id' => 'base',
'$ref' => 'first#/$defs/stuff',
'$defs' => {
first => {
'$id' => 'first',
'$defs' => {
stuff => { # first#/$defs/stuff
'$ref' => 'second#/$defs/stuff',
},
length => { # first#length
# no $dynamicAnchor here!
maxLength => 1,
},
},
},
second => {
'$id' => 'second',
'$defs' => {
stuff => { # second#/$defs/stuff
'$ref' => 'third#/$defs/stuff',
},
length => { # second#length
'$dynamicAnchor' => 'length',
maxLength => 2, # <-- this is the scope that we should find and evaluate
},
},
},
third => {
'$id' => 'third',
'$defs' => {
stuff => { # third#/$defs/stuff
'$dynamicRef' => '#length',
},
length => { # third#length
'$dynamicAnchor' => 'length',
maxLength => 3, # this should never get evaluated
}
},
},
},
};
cmp_result(
$js->evaluate('hello', $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/$ref/$dynamicRef/maxLength',
absoluteKeywordLocation => 'second#/$defs/length/maxLength',
error => 'length is greater than 2',
},
],
},
'dynamic scopes are pushed onto the stack even when its root resource (and $id keyword) are not directly evaluated',
);
};
subtest 'after leaving a dynamic scope, it should not be used by a $dynamicRef' => sub {
$js->{_resource_index} = {};
my $schema = {
'$id' => 'main',
if => {
'$id' => 'first_scope',
'$defs' => {
thingy => {
# this is first_scope#thingy
'$dynamicAnchor' => 'thingy',
type => 'number',
},
},
},
'then' => {
'$id' => 'second_scope',
'$ref' => 'start',
'$defs' => {
'thingy' => {
# this is second_scope#thingy, the final destination of the $dynamicRef
'$dynamicAnchor' => 'thingy',
type => 'null',
},
},
},
'$defs' => {
start => {
# this is the landing spot from $ref
'$id' => 'start',
'$dynamicRef' => 'inner_scope#thingy',
},
'thingy' => {
# this is the first stop by the $dynamicRef
'$id' => 'inner_scope',
'$dynamicAnchor' => 'thingy',
type => 'string',
}
}
};
cmp_result(
$js->evaluate(undef, $schema)->TO_JSON,
{
valid => true,
},
'first_scope is no longer in scope, so it is not used by $dynamicRef',
);
};
subtest 'anchors do not match' => sub {
$js->{_resource_index} = {};
my $schema = {
'$defs' => {
enhanced => {
'$dynamicAnchor' => 'thingy', # change this to $anchor and watch what happens
minimum => 2,
},
orig => {
'$id' => 'orig',
'$dynamicAnchor' => 'thingy',
minimum => 10,
},
},
'$dynamicRef' => 'orig#thingy',
};
cmp_result(
$js->evaluate(1, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$dynamicRef/minimum',
absoluteKeywordLocation => '#/$defs/enhanced/minimum',
error => 'value is less than 2',
},
],
},
'$dynamicRef goes to enhanced schema',
);
$js->{_resource_index} = {};
$schema->{'$defs'}{enhanced}{'$anchor'} = delete $schema->{'$defs'}{enhanced}{'$dynamicAnchor'};
$schema->{'$defs'}{enhanced}{'$dynamicAnchor'} = 'somethingelse';
cmp_result(
$js->evaluate(1, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$dynamicRef/minimum',
absoluteKeywordLocation => 'orig#/minimum',
error => 'value is less than 10',
},
],
},
'$dynamicRef -> $dynamicAnchor -> $anchor is a no go: we stay at the original schema',
);
};
subtest 'reference to a non-schema location' => sub {
$js->{_resource_index} = {};
my $schema = {
example => { not_a_schema => true },
'$defs' => {
anchor => {
'$dynamicAnchor' => 'my_anchor',
'$dynamicRef' => '#/example/not_a_schema',
},
},
type => 'object',
properties => {
'$ref' => {
'$ref' => '#/example/not_a_schema',
},
'$dynamicRef' => {
'$dynamicRef' => '#/example/not_a_schema',
},
},
};
cmp_result(
$js->evaluate({ '$ref' => 1 }, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/$ref',
keywordLocation => '/properties/$ref/$ref',
error => 'EXCEPTION: bad reference to "#/example/not_a_schema": not a schema',
},
],
},
'$ref to a non-schema is not permitted',
);
cmp_result(
$js->evaluate({ '$dynamicRef' => 1 }, '')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/$dynamicRef',
keywordLocation => '/properties/$dynamicRef/$dynamicRef',
error => 'EXCEPTION: bad reference to "#/example/not_a_schema": not a schema',
},
],
},
'$dynamicRef to a non-schema is not permitted',
);
$js->{_resource_index} = {};
$schema = {
'$id' => '/foo',
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
'$recursiveAnchor' => true,
example => { not_a_schema => true },
type => 'object',
properties => {
'$recursiveRef' => {
'$recursiveRef' => '#/example/not_a_schema',
},
},
};
cmp_result(
$js->evaluate({ '$recursiveRef' => 1 }, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/$recursiveRef',
keywordLocation => '/properties/$recursiveRef/$recursiveRef',
absoluteKeywordLocation => '/foo#/properties/$recursiveRef/$recursiveRef',
error => 'EXCEPTION: bad reference to "/foo#/example/not_a_schema": not a schema',
},
],
},
'$recursiveRef to a non-schema is not permitted',
);
package MyDocument {
use strict; use warnings;
use Moo;
extends 'JSON::Schema::Modern::Document';
use experimental 'signatures';
sub traverse ($self, @) {
return {
initial_schema_uri => $self->canonical_uri,
errors => [],
specification_version => 'draft2020-12',
vocabularies => [],
identifiers => {},
subschemas => [],
};
}
};
$js->{_resource_index} = {};
my $doc = MyDocument->new(
schema => [ 'not a json schema' ],
canonical_uri => 'https://my_non_schema',
);
$js->add_document($doc);
$schema = {
'$id' => '/foo',
'$schema' => 'https://my_non_schema',
};
cmp_result(
$js->evaluate(1, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema',
# we haven't processed $id yet, so we don't know the absolute location
error => 'EXCEPTION: bad reference to $schema "https://my_non_schema": not a schema',
},
],
},
'$schema to a non-schema is not permitted',
);
};
subtest 'evaluate at a non-schema location' => sub {
$js->{_resource_index} = {};
$js->add_schema('http://my_schema', { example => { not_a_schema => true } });
cmp_result(
$js->evaluate(1, 'http://my_schema#/example/not_a_schema')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'EXCEPTION: "http://my_schema#/example/not_a_schema" is not a schema',
},
],
},
'evaluating at a non-schema location is not permitted',
);
};
subtest 'evaluate at a subschema, with $dynamicRef' => sub {
# from a real bug I encountered while writing a t/parameters.t test in OpenAPI-Modern!
# if we evaluate in the middle of a document, and a $dynamicRef is involved, mayhem ensues.
$js->{_resource_index} = {};
$js->add_schema({
'$id' => 'http://strict_metaschema',
'$defs' => {
schema => {
'$dynamicAnchor' => 'meta',
type => 'object', # disallows boolean
},
parameter => {
'$ref' => 'http://loose_metaschema#/$defs/parameter',
},
},
'$ref' => 'http://loose_metaschema#/intentionally/bad/reference',
});
$js->add_schema({
'$id' => 'http://loose_metaschema',
'$defs' => {
schema => {
'$dynamicAnchor' => 'meta',
type => [ 'object', 'boolean' ],
},
parameter => {
type => 'object',
properties => {
name => { type => 'string' },
schema => { '$dynamicRef' => '#meta' },
},
},
},
});
cmp_result(
$js->evaluate(
{ name => 'hi', schema => false },
'http://strict_metaschema#/$defs/parameter',
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/schema',
keywordLocation => '/$ref/properties/schema/$dynamicRef/type',
absoluteKeywordLocation => 'http://strict_metaschema#/$defs/schema/type',
error => 'got boolean, not object',
},
{
instanceLocation => '',
keywordLocation => '/$ref/properties',
absoluteKeywordLocation => 'http://loose_metaschema#/$defs/parameter/properties',
error => 'not all properties are valid',
},
],
},
'correctly navigated a $dynamicRef while evaluating in the middle of a document',
);
};
done_testing;
LICENCE 100644 000766 000024 46312 15114374332 15414 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 This software is copyright (c) 2020 by Karen Etheridge.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
Terms of the Perl programming language system itself
a) the GNU General Public License as published by the Free
Software Foundation; either version 1, or (at your option) any
later version, or
b) the "Artistic License"
--- The GNU General Public License, Version 1, February 1989 ---
This software is Copyright (c) 2020 by Karen Etheridge.
This is free software, licensed under:
The GNU General Public License, Version 1, February 1989
GNU GENERAL PUBLIC LICENSE
Version 1, February 1989
Copyright (C) 1989 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The license agreements of most software companies try to keep users
at the mercy of those companies. By contrast, our General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. The
General Public License applies to the Free Software Foundation's
software and to any other program whose authors commit to using it.
You can use it for your programs, too.
When we speak of free software, we are referring to freedom, not
price. Specifically, the General Public License is designed to make
sure that you have the freedom to give away or sell copies of free
software, that you receive source code or can get it if you want it,
that you can change the software or use pieces of it in new free
programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of a such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must tell them their rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any program or other work which
contains a notice placed by the copyright holder saying it may be
distributed under the terms of this General Public License. The
"Program", below, refers to any such program or work, and a "work based
on the Program" means either the Program or any work containing the
Program or a portion of it, either verbatim or with modifications. Each
licensee is addressed as "you".
1. You may copy and distribute verbatim copies of the Program's source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and
disclaimer of warranty; keep intact all the notices that refer to this
General Public License and to the absence of any warranty; and give any
other recipients of the Program a copy of this General Public License
along with the Program. You may charge a fee for the physical act of
transferring a copy.
2. You may modify your copy or copies of the Program or any portion of
it, and copy and distribute such modifications under the terms of Paragraph
1 above, provided that you also do the following:
a) cause the modified files to carry prominent notices stating that
you changed the files and the date of any change; and
b) cause the whole of any work that you distribute or publish, that
in whole or in part contains the Program or any part thereof, either
with or without modifications, to be licensed at no charge to all
third parties under the terms of this General Public License (except
that you may choose to grant warranty protection to some or all
third parties, at your option).
c) If the modified program normally reads commands interactively when
run, you must cause it, when started running for such interactive use
in the simplest and most usual way, to print or display an
announcement including an appropriate copyright notice and a notice
that there is no warranty (or else, saying that you provide a
warranty) and that users may redistribute the program under these
conditions, and telling the user how to view a copy of this General
Public License.
d) You may charge a fee for the physical act of transferring a
copy, and you may at your option offer warranty protection in
exchange for a fee.
Mere aggregation of another independent work with the Program (or its
derivative) on a volume of a storage or distribution medium does not bring
the other work under the scope of these terms.
3. You may copy and distribute the Program (or a portion or derivative of
it, under Paragraph 2) in object code or executable form under the terms of
Paragraphs 1 and 2 above provided that you also do one of the following:
a) accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of
Paragraphs 1 and 2 above; or,
b) accompany it with a written offer, valid for at least three
years, to give any third party free (except for a nominal charge
for the cost of distribution) a complete machine-readable copy of the
corresponding source code, to be distributed under the terms of
Paragraphs 1 and 2 above; or,
c) accompany it with the information you received as to where the
corresponding source code may be obtained. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form alone.)
Source code for a work means the preferred form of the work for making
modifications to it. For an executable file, complete source code means
all the source code for all modules it contains; but, as a special
exception, it need not include source code for modules which are standard
libraries that accompany the operating system on which the executable
file runs, or for standard header files or definitions files that
accompany that operating system.
4. You may not copy, modify, sublicense, distribute or transfer the
Program except as expressly provided under this General Public License.
Any attempt otherwise to copy, modify, sublicense, distribute or transfer
the Program is void, and will automatically terminate your rights to use
the Program under this License. However, parties who have received
copies, or rights to use copies, from you under this General Public
License will not have their licenses terminated so long as such parties
remain in full compliance.
5. By copying, distributing or modifying the Program (or any work based
on the Program) you indicate your acceptance of this license to do so,
and all its terms and conditions.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the original
licensor to copy, distribute or modify the Program subject to these
terms and conditions. You may not impose any further restrictions on the
recipients' exercise of the rights granted herein.
7. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of the license which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
the license, you may choose any version ever published by the Free Software
Foundation.
8. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
Appendix: How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to humanity, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
Copyright (C) 19yy
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 1, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) 19xx name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the
appropriate parts of the General Public License. Of course, the
commands you use may be called something other than `show w' and `show
c'; they could even be mouse-clicks or menu items--whatever suits your
program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
program `Gnomovision' (a program to direct compilers to make passes
at assemblers) written by James Hacker.
, 1 April 1989
Moe Ghoul, President of Vice
That's all there is to it!
--- The Perl Artistic License 1.0 ---
This software is Copyright (c) 2020 by Karen Etheridge.
This is free software, licensed under:
The Perl Artistic License 1.0
The "Artistic License"
Preamble
The intent of this document is to state the conditions under which a
Package may be copied, such that the Copyright Holder maintains some
semblance of artistic control over the development of the package,
while giving the users of the package the right to use and distribute
the Package in a more-or-less customary fashion, plus the right to make
reasonable modifications.
Definitions:
"Package" refers to the collection of files distributed by the
Copyright Holder, and derivatives of that collection of files
created through textual modification.
"Standard Version" refers to such a Package if it has not been
modified, or has been modified in accordance with the wishes
of the Copyright Holder as specified below.
"Copyright Holder" is whoever is named in the copyright or
copyrights for the package.
"You" is you, if you're thinking about copying or distributing
this Package.
"Reasonable copying fee" is whatever you can justify on the
basis of media cost, duplication charges, time of people involved,
and so on. (You will not be required to justify it to the
Copyright Holder, but only to the computing community at large
as a market that must bear the fee.)
"Freely Available" means that no fee is charged for the item
itself, though there may be fees involved in handling the item.
It also means that recipients of the item may redistribute it
under the same conditions they received it.
1. You may make and give away verbatim copies of the source form of the
Standard Version of this Package without restriction, provided that you
duplicate all of the original copyright notices and associated disclaimers.
2. You may apply bug fixes, portability fixes and other modifications
derived from the Public Domain or from the Copyright Holder. A Package
modified in such a way shall still be considered the Standard Version.
3. You may otherwise modify your copy of this Package in any way, provided
that you insert a prominent notice in each changed file stating how and
when you changed that file, and provided that you do at least ONE of the
following:
a) place your modifications in the Public Domain or otherwise make them
Freely Available, such as by posting said modifications to Usenet or
an equivalent medium, or placing the modifications on a major archive
site such as uunet.uu.net, or by allowing the Copyright Holder to include
your modifications in the Standard Version of the Package.
b) use the modified Package only within your corporation or organization.
c) rename any non-standard executables so the names do not conflict
with standard executables, which must also be provided, and provide
a separate manual page for each non-standard executable that clearly
documents how it differs from the Standard Version.
d) make other distribution arrangements with the Copyright Holder.
4. You may distribute the programs of this Package in object code or
executable form, provided that you do at least ONE of the following:
a) distribute a Standard Version of the executables and library files,
together with instructions (in the manual page or equivalent) on where
to get the Standard Version.
b) accompany the distribution with the machine-readable source of
the Package with your modifications.
c) give non-standard executables non-standard names, and clearly
document the differences in manual pages (or equivalent), together
with instructions on where to get the Standard Version.
d) make other distribution arrangements with the Copyright Holder.
5. You may charge a reasonable copying fee for any distribution of this
Package. You may charge any fee you choose for support of this
Package. You may not charge a fee for this Package itself. However,
you may distribute this Package in aggregate with other (possibly
commercial) programs as part of a larger (possibly commercial) software
distribution provided that you do not advertise this Package as a
product of your own. You may embed this Package's interpreter within
an executable of yours (by linking); this shall be construed as a mere
form of aggregation, provided that the complete Standard Version of the
interpreter is so embedded.
6. The scripts and library files supplied as input to or produced as
output from the programs of this Package do not automatically fall
under the copyright of this Package, but belong to whoever generated
them, and may be sold commercially, and may be aggregated with this
Package. If such scripts or library files are aggregated with this
Package via the so-called "undump" or "unexec" methods of producing a
binary executable image, then distribution of such an image shall
neither be construed as a distribution of this Package nor shall it
fall under the restrictions of Paragraphs 3 and 4, provided that you do
not represent such an executable image as a Standard Version of this
Package.
7. C subroutines (or comparably compiled subroutines in other
languages) supplied by you and linked into this Package in order to
emulate subroutines and variables of the language defined by this
Package shall not be considered part of this Package, but are the
equivalent of input as in Paragraph 6, provided these subroutines do
not change the language in any way that would cause it to fail the
regression tests for the language.
8. Aggregation of this Package with a commercial distribution is always
permitted provided that the use of this Package is embedded; that is,
when no overt attempt is made to make this Package's interfaces visible
to the end user of the commercial distribution. Such use shall not be
construed as a distribution of this Package.
9. The name of the Copyright Holder may not be used to endorse or promote
products derived from this software without specific prior written permission.
10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
The End
INSTALL 100644 000766 000024 4631 15114374332 15436 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 This is the Perl distribution JSON-Schema-Modern.
Installing JSON-Schema-Modern is straightforward.
## Installation with cpanm
If you have cpanm, you only need one line:
% cpanm JSON::Schema::Modern
If it does not have permission to install modules to the current perl, cpanm
will automatically set up and install to a local::lib in your home directory.
See the local::lib documentation (https://metacpan.org/pod/local::lib) for
details on enabling it in your environment.
## Installing with the CPAN shell
Alternatively, if your CPAN shell is set up, you should just be able to do:
% cpan JSON::Schema::Modern
## Manual installation
As a last resort, you can manually install it. If you have not already
downloaded the release tarball, you can find the download link on the module's
MetaCPAN page: https://metacpan.org/pod/JSON::Schema::Modern
Untar the tarball, install configure prerequisites (see below), then build it:
% perl Makefile.PL
% make && make test
Then install it:
% make install
On Windows platforms, you should use `dmake` or `nmake`, instead of `make`.
If your perl is system-managed, you can create a local::lib in your home
directory to install modules to. For details, see the local::lib documentation:
https://metacpan.org/pod/local::lib
The prerequisites of this distribution will also have to be installed manually. The
prerequisites are listed in one of the files: `MYMETA.yml` or `MYMETA.json` generated
by running the manual build process described above.
## Configure Prerequisites
This distribution requires other modules to be installed before this
distribution's installer can be run. They can be found under the
"configure_requires" key of META.yml or the
"{prereqs}{configure}{requires}" key of META.json.
## Other Prerequisites
This distribution may require additional modules to be installed after running
Makefile.PL.
Look for prerequisites in the following phases:
* to run make, PHASE = build
* to use the module code itself, PHASE = runtime
* to run tests, PHASE = test
They can all be found in the "PHASE_requires" key of MYMETA.yml or the
"{prereqs}{PHASE}{requires}" key of MYMETA.json.
## Documentation
JSON-Schema-Modern documentation is available as POD.
You can run `perldoc` from a shell to read the documentation:
% perldoc JSON::Schema::Modern
For more information on installing Perl modules via CPAN, please see:
https://www.cpan.org/modules/INSTALL.html
dist.ini 100640 000766 000024 5712 15114374332 16046 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 name = JSON-Schema-Modern
author = Karen Etheridge
copyright_holder = Karen Etheridge
copyright_year = 2020
license = Perl_5
; ATTENTION DISTRO REPACKAGERS: do NOT use fresh copies of these files
; from their source; it is important to include the original versions
; of the files as they were packaged with this cpan distribution, or
; surprising behaviour may occur.
[Run::BeforeRelease]
eval = do './update-schemas'; die $@ || $! if $@ || $!
[@Author::ETHER]
:version = 0.154
bugtracker = github
installer = MakeMaker
Test::MinimumVersion.max_target_perl = 5.020 ; may go higher later on
Git::GatherDir.exclude_filename = pull_request_template.md
Test::ReportPrereqs.include[0] = JSON::PP
Test::ReportPrereqs.include[1] = Cpanel::JSON::XS
Test::ReportPrereqs.include[2] = JSON::XS
Test::ReportPrereqs.include[3] = Mojolicious
Test::ReportPrereqs.include[4] = Sereal::Encoder
Test::ReportPrereqs.include[5] = Sereal::Decoder
Test::ReportPrereqs.include[6] = Math::BigInt
Test::ReportPrereqs.include[7] = Math::BigFloat
Test::ReportPrereqs.include[8] = builtin
Test::ReportPrereqs.include[9] = builtin::Backport
-remove = Test::Pod::No404s ; some vocabulary class URIs now return 403 Forbidden
StaticInstall.mode = off
[=inc::CheckConflicts]
[ShareDir]
dir = share
[Prereqs / RuntimeRequires]
Mojolicious = 7.87 ; Mojo::JSON::JSON_XS
Math::BigInt = 1.999701 ; bdiv and bmod fixes
Email::Address::XS = 1.04 ; softened later
Sereal = 0 ; softened later
JSON::PP = 4.11 ; softened later
Cpanel::JSON::XS = 4.38 ; softened later
builtin::compat = 0.003003
[Prereqs / RuntimeSuggests]
Class::XSAccessor = 0
Type::Tiny::XS = 0
[Prereqs::Soften]
to_relationship = suggests
copy_to = develop.requires
module = Time::Moment ; required for format 'date-time', 'date'
module = DateTime::Format::RFC3339 ; required for edge cases for format 'date-time'
module = Data::Validate::Domain ; required for format 'hostname', 'idn-hostname'
module = Email::Address::XS ; required for format 'email', 'idn-email'
module = Net::IDN::Encode ; required for format 'idn-hostname'
module = Sereal ; required for serialization support
module = JSON::PP ; support for core bools
module = Cpanel::JSON::XS ; support for core bools
[DynamicPrereqs]
-delimiter = |
-body = |if (!want_pp() && can_xs()) {
-body = | requires('Cpanel::JSON::XS', '4.38');
-body = |}
-body = |else {
-body = | requires('JSON::PP', '4.11');
-body = |}
[Breaks]
JSON::Schema::Modern::Vocabulary::OpenAPI = < 0.080 ; discriminator traversal error status
JSON::Schema::Modern::Document::OpenAPI = < 0.097 ; $state key renaming
OpenAPI::Modern = < 0.077 ; ::Result boolean overload
Mojolicious::Plugin::OpenAPI::Modern = < 0.014 ; ::Result boolean overload
Test::Mojo::Role::OpenAPI::Modern = < 0.007 ; ::Result boolean overload
[Test::CheckBreaks]
type.t 100640 000766 000024 30424 15114374332 16031 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use utf8;
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Scalar::Util qw(isdual dualvar);
use Math::BigInt;
use Math::BigFloat;
use JSON::PP ();
use lib 't/lib';
use Helper;
use JSON::Schema::Modern::Utilities qw(is_type get_type);
use constant HAVE_BUILTIN => "$]" >= 5.035010;
use if HAVE_BUILTIN, experimental => 'builtin';
my %inflated_data = (
null => [ undef ],
boolean => [ false, true, JSON::PP::false, JSON::PP::true, HAVE_BUILTIN ? (builtin::true, builtin::false) : () ],
object => [ {}, { a => 1 } ],
array => [ [], [ 1 ] ],
number => [ 3.1, 1.23456789012e10, Math::BigFloat->new('0.123'), Math::BigFloat->new('12345123451234512345.2') ],
integer => [ 0, -1, 2, 2.0, 2**31-1, 2**31, 2**63-1, 2**63, 2**64, 2**65, 1000000000000000,
Math::BigInt->new('1e100'), Math::BigInt->new('1'), Math::BigInt->new('1.0'),
Math::BigInt->new('12345123451234512345.0'), Math::BigFloat->new('12345123451234512345.0'),
Math::BigFloat->new('1e100'), Math::BigFloat->new('20000000000000.0'),
Math::BigFloat->new('2e1'), Math::BigFloat->new('1'), Math::BigFloat->new('1.0') ],
string => [ '', '0', '-1', '2', '2.0', '3.1', 'école', 'ಠ_ಠ' ],
);
my %json_data = (
null => [ 'null' ],
boolean => [ 'false', 'true' ],
object => [ '{}', '{"a":1}' ],
array => [ '[]', '[1]' ],
number => [ '3.1', '1.23456789012e10', '0.123', '12345123451234512345.2' ],
integer => [ '0', '-1', '2.0', (map $_.'', 2**31-1, 2**31, 2**63-1, 2**63, 2**64, 2**65), '1000000000000000', '2e1', '1e100', '12345123451234512345.0' ],
string => [ '""', '"0"', '"-1"', '"2.0"', '"3.1"',
qq{"\x{c3}\x{a9}cole"}, qq{"\x{e0}\x{b2}\x{a0}_\x{e0}\x{b2}\x{a0}"} ],
);
foreach my $type (sort keys %inflated_data) {
subtest 'inflated data, type: '.$type => sub {
foreach my $value ($inflated_data{$type}->@*) {
my $value_copy = $value;
ok(is_type($type, $value), json_sprintf(('is_type("'.$type.'", %s) is true'), $value_copy ));
ok(is_type('number', $value), json_sprintf(('is_type("number", %s) is true'), $value_copy ))
if $type eq 'integer';
is(get_type($value), $type, json_sprintf(('get_type(%s) = '.$type), $value_copy));
foreach my $other_type (sort keys %inflated_data) {
next if $other_type eq $type;
next if $type eq 'integer' and $other_type eq 'number';
ok(!is_type($other_type, $value),
json_sprintf('is_type("'.$other_type.'", %s) is false', $value));
}
ok(!isdual($value), 'data is not tampered with while it is tested (not dualvar)')
if not (HAVE_BUILTIN and builtin::is_bool($value));
}
};
}
my $decoder = JSON::Schema::Modern::_JSON_BACKEND()->new
->allow_nonref(1)
->canonical(1)
->utf8(1)
->allow_bignum(1);
foreach my $type (sort keys %json_data) {
subtest 'JSON-encoded data, type: '.$type => sub {
foreach my $value ($json_data{$type}->@*) {
$value = $decoder->decode($value);
my $value_copy = $value;
ok(is_type($type, $value), json_sprintf(('is_type("'.$type.'", %s) is true'), $value_copy ));
ok(is_type('number', $value), json_sprintf(('is_type("number", %s) is true'), $value_copy ))
if $type eq 'integer';
is(get_type($value), $type, json_sprintf(('get_type(%s) = '.$type), $value_copy));
foreach my $other_type (sort keys %json_data) {
next if $other_type eq $type;
next if $type eq 'integer' and $other_type eq 'number';
ok(!is_type($other_type, $value),
json_sprintf('is_type("'.$other_type.'", %s) is false', $value));
}
ok(!isdual($value), 'data is not tampered with while it is tested (not dualvar)')
if not (HAVE_BUILTIN and builtin::is_bool($value));
}
};
}
subtest 'integers and numbers in draft4' => sub {
subtest 'pre-inflated data' => sub {
my %draft4_inflated_data = (
number => [ 3.1, 2.0, 1.23456789012e10, Math::BigFloat->new('0.123'), Math::BigFloat->new('2.0') ],
integer => [ 0, -1, 2, Math::BigInt->new('2'), Math::BigInt->new('1.0') ],
);
foreach my $type (sort keys %draft4_inflated_data) {
foreach my $value ($draft4_inflated_data{$type}->@*) {
my $value_copy = $value;
ok(is_type($type, $value, { legacy_ints => 1 }), json_sprintf(('is_type("'.$type.'", %s) is true'), $value_copy ));
ok(is_type('number', $value, { legacy_ints => 1 }), json_sprintf(('is_type("number", %s) is true'), $value_copy ))
if $type eq 'integer';
is(get_type($value, { legacy_ints => 1 }), $type, json_sprintf(('get_type(%s) = '.$type), $value_copy));
foreach my $other_type (qw(null boolean object array string), sort keys %draft4_inflated_data) {
next if $other_type eq $type;
next if $type eq 'integer' and $other_type eq 'number';
ok(!is_type($other_type, $value, { legacy_ints => 1 }),
json_sprintf('is_type("'.$other_type.'", %s) is false', $value));
}
ok(!isdual($value), 'data is not tampered with while it is tested (not dualvar)')
if not (HAVE_BUILTIN and builtin::is_bool($value));
}
}
};
subtest 'data from encoded json' => sub {
my %draft4_json_data = (
number => [ '3.1', '1.23456789012e10', '0.123', '2.0' ],
integer => [ '0', '-1', '1000000000000000' ],
# these are actually integers, but we are unable to verify that, as they inflate to
# Math::BigFloat objects where is_int is true:
# (map $_.'', 2**31-1, 2**31, 2**63-1, 2**63, 2**64, 2**65), '2e1', '1e100'
);
foreach my $type (sort keys %draft4_json_data) {
foreach my $value ($draft4_json_data{$type}->@*) {
$value = $decoder->decode($value);
my $value_copy = $value;
ok(is_type($type, $value, { legacy_ints => 1 }), json_sprintf(('is_type("'.$type.'", %s) is true'), $value_copy ));
ok(is_type('number', $value, { legacy_ints => 1 }), json_sprintf(('is_type("number", %s) is true'), $value_copy ))
if $type eq 'integer';
is(get_type($value, { legacy_ints => 1 }), $type, json_sprintf(('get_type(%s) = '.$type), $value_copy));
foreach my $other_type (qw(null boolean object array string), sort keys %draft4_json_data) {
next if $other_type eq $type;
next if $type eq 'integer' and $other_type eq 'number';
ok(!is_type($other_type, $value, { legacy_ints => 1 }),
json_sprintf('is_type("'.$other_type.'", %s) is false', $value));
}
ok(!isdual($value), 'data is not tampered with while it is tested (not dualvar)')
if not (HAVE_BUILTIN and builtin::is_bool($value));
}
}
};
};
ok(!is_type('foo', 'wharbarbl'), 'non-existent type does not result in exception');
subtest 'ambiguous types' => sub {
subtest 'integers' => sub {
my $integer = dualvar(5, 'five');
is(get_type($integer), 'ambiguous type', 'dualvar integers with different values are ambiguous');
ok(!is_type($_, $integer), "dualvar integers with different values are not ${_}s") foreach qw(integer number string);
$integer = 5;
()= sprintf('%s', $integer);
# legacy behaviour (this only happens for IVs, not NVs)
SKIP: {
skip 'on perls < 5.35.9, reading the string form of an integer value sets the flag SVf_POK', 1
if "$]" >= 5.035009;
is(get_type($integer), 'string', 'older perls only: integer that is later used as a string is now identified as a string');
ok(is_type('integer', $integer), 'integer that is later used as a string is still an integer');
ok(is_type('number', $integer), 'integer that is later used as a string is still a number');
ok(is_type('string', $integer), 'older perls only: integer that is later used as a string is now a string');
}
# modern behaviour
SKIP: {
skip 'on perls < 5.35.9, reading the string form of an integer value sets the flag SVf_POK', 1
if "$]" < 5.035009;
is(get_type($integer), 'integer', 'integer that is later used as a string is still identified as a integer');
ok(is_type('integer', $integer), 'integer that is later used as a string is still an integer');
ok(is_type('number', $integer), 'integer that is later used as a string is still a number');
ok(!is_type('string', $integer), 'integer that is later used as a string is not a string');
}
};
subtest 'numbers' => sub {
my $number = dualvar(5.1, 'five');
is(get_type($number), 'ambiguous type', 'dualvar numbers are ambiguous in get_type');
ok(!is_type($_, $number), "dualvar numbers are not ${_}s") foreach qw(integer number string);
$number = 5.1;
()= sprintf('%s', $number);
is(get_type($number), 'number', 'number that is later used as a string is still identified as a number');
ok(!is_type('integer', $number), 'number that is later used as a string is not an integer');
ok(is_type('number', $number), 'number that is later used as a string is still a number');
ok(!is_type('string', $number), 'number that is later used as a string is not a string');
};
subtest 'strings' => sub {
my $string = dualvar(5.1, 'five');
is(get_type($string), 'ambiguous type', 'dualvar strings are ambiguous in get_type');
ok(!is_type($_, $string), "dualvar strings are not ${_}s") foreach qw(integer number string);
$string = '5';
()= 0+$string;
is(get_type($string), 'string', 'string that is later used as an integer is still identified as a string');
# legacy behaviour
SKIP: {
skip 'on perls < 5.35.9, reading the string form of an integer value sets the flag SVf_POK', 1
if "$]" >= 5.035009;
ok(is_type('integer', $string), 'older perls only: string that is later used as an integer becomes an integer');
ok(is_type('number', $string), 'older perls only: string that is later used as an integer becomes a number');
}
# modern behaviour
SKIP: {
skip 'on perls < 5.35.9, reading the integer form of a string value sets the flag SVf_IOK', 1
if "$]" < 5.035009;
ok(!is_type('integer', $string), 'string that is later used as an integer is not an integer');
ok(!is_type('number', $string), 'string that is later used as an integer is not a number');
}
ok(is_type('string', $string), 'string that is later used as an integer is still a string');
$string = '5.1';
()= 0+$string;
is(get_type($string), 'string', 'string that is later used as a number is still identified as a string');
ok(!is_type('integer', $string), 'string that is later used as a number is not an integer');
# legacy behaviour
SKIP: {
skip 'on perls < 5.35.9, reading the string form of an integer value sets the flag SVf_POK', 1
if "$]" >= 5.035009;
ok(is_type('number', $string), 'older perls only: string that is later used as a number becomes a number');
}
# modern behaviour
SKIP: {
skip 'on perls < 5.35.9, reading the numeric form of a string value sets the flag SVf_NOK', 1
if "$]" < 5.035009;
ok(!is_type('number', $string), 'string that is later used as a number is not a number');
}
ok(is_type('string', $string), 'string that is later used as a number is still a string');
};
};
subtest 'is_type and get_type for references' => sub {
foreach my $test (
[ \1, 'reference to SCALAR' ],
[ \\2, 'reference to REF' ],
[ sub { 1 }, 'reference to CODE' ],
[ \*stdout, 'reference to GLOB' ],
[ \substr('a', '1'), 'reference to LVALUE' ],
[ \v1.2.3, 'reference to VSTRING' ],
[ qr/foo/, 'Regexp' ],
[ *STDIN{IO}, 'IO::File' ],
[ bless({}, 'Foo'), 'Foo' ],
[ bless({}, '0'), '0' ],
) {
is(get_type($test->[0]), $test->[1], $test->[1].' type is reported without exception');
ok(is_type($test->[1], $test->[0]), 'value is a '.$test->[1]);
foreach my $type (qw(null object array boolean string number integer)) {
ok(!is_type($type, $test->[0]), 'value is not a '.$type);
}
}
};
done_testing;
META.yml 100644 000766 000024 76514 15114374332 15707 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 ---
abstract: 'Validate data against a schema using a JSON Schema'
author:
- 'Karen Etheridge '
build_requires:
CPAN::Meta::Check: '0.011'
CPAN::Meta::Requirements: '0'
Data::Dumper: '0'
ExtUtils::MakeMaker: '0'
File::Spec: '0'
Math::BigInt: '1.999701'
Term::ANSIColor: '0'
Test2::API: '0'
Test2::V0: '0'
Test2::Warnings: '0.038'
Test::Deep: '0'
Test::Deep::UnorderedPairs: '0'
Test::File::ShareDir: '0'
Test::JSON::Schema::Acceptance: '1.035'
Test::Memory::Cycle: '0'
Test::More: '0'
Test::Needs: '0'
Test::Without::Module: '0.19'
lib: '0'
perl: v5.20.0
utf8: '0'
configure_requires:
ExtUtils::MakeMaker: '0'
File::ShareDir::Install: '0.06'
Text::ParseWords: '0'
perl: '5.020'
dynamic_config: 1
generated_by: 'Dist::Zilla version 6.036, CPAN::Meta::Converter version 2.150010'
keywords:
- JSON
- Schema
- validator
- data
- validation
- structure
- specification
license: perl
meta-spec:
url: http://module-build.sourceforge.net/META-spec-v1.4.html
version: '1.4'
name: JSON-Schema-Modern
no_index:
directory:
- inc
- share
- t
- xt
provides:
JSON::Schema::Modern:
file: lib/JSON/Schema/Modern.pm
version: '0.627'
JSON::Schema::Modern::Annotation:
file: lib/JSON/Schema/Modern/Annotation.pm
version: '0.627'
JSON::Schema::Modern::Document:
file: lib/JSON/Schema/Modern/Document.pm
version: '0.627'
JSON::Schema::Modern::Error:
file: lib/JSON/Schema/Modern/Error.pm
version: '0.627'
JSON::Schema::Modern::Result:
file: lib/JSON/Schema/Modern/Result.pm
version: '0.627'
JSON::Schema::Modern::ResultNode:
file: lib/JSON/Schema/Modern/ResultNode.pm
version: '0.627'
JSON::Schema::Modern::Utilities:
file: lib/JSON/Schema/Modern/Utilities.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary:
file: lib/JSON/Schema/Modern/Vocabulary.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::Applicator:
file: lib/JSON/Schema/Modern/Vocabulary/Applicator.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::Content:
file: lib/JSON/Schema/Modern/Vocabulary/Content.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::Core:
file: lib/JSON/Schema/Modern/Vocabulary/Core.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::FormatAnnotation:
file: lib/JSON/Schema/Modern/Vocabulary/FormatAnnotation.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::FormatAssertion:
file: lib/JSON/Schema/Modern/Vocabulary/FormatAssertion.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::MetaData:
file: lib/JSON/Schema/Modern/Vocabulary/MetaData.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::Unevaluated:
file: lib/JSON/Schema/Modern/Vocabulary/Unevaluated.pm
version: '0.627'
JSON::Schema::Modern::Vocabulary::Validation:
file: lib/JSON/Schema/Modern/Vocabulary/Validation.pm
version: '0.627'
requires:
B: '0'
Carp: '0'
Digest::MD5: '0'
Exporter: '0'
Feature::Compat::Try: '0'
File::ShareDir: '0'
Getopt::Long::Descriptive: '0'
List::Util: '1.55'
MIME::Base64: '0'
Math::BigFloat: '0'
Math::BigInt: '1.999701'
Mojo::File: '0'
Mojo::JSON: '0'
Mojo::JSON::Pointer: '0'
Mojo::Message::Response: '0'
Mojo::URL: '0'
Mojolicious: '7.87'
Moo: '0'
Moo::Role: '0'
MooX::TypeTiny: '0.002002'
Safe::Isa: '1.000008'
Scalar::Util: '0'
Storable: '0'
Types::Common::Numeric: '0'
Types::Standard: '1.016003'
YAML::PP: '0'
autovivification: '0'
builtin::compat: '0.003003'
constant: '0'
experimental: '0.026'
feature: '0'
if: '0'
namespace::clean: '0'
open: '0'
overload: '0'
perl: v5.20.0
stable: '0.031'
strict: '0'
strictures: '2'
warnings: '0'
resources:
bugtracker: https://github.com/karenetheridge/JSON-Schema-Modern/issues
homepage: https://github.com/karenetheridge/JSON-Schema-Modern
repository: https://github.com/karenetheridge/JSON-Schema-Modern.git
version: '0.627'
x_Dist_Zilla:
perl:
version: '5.043005'
plugins:
-
class: Dist::Zilla::Plugin::Run::BeforeRelease
config:
Dist::Zilla::Plugin::Run::Role::Runner:
eval:
- "do './update-schemas'; die $@ || $! if $@ || $!"
fatal_errors: 1
quiet: 0
version: '0.050'
name: Run::BeforeRelease
version: '0.050'
-
class: Dist::Zilla::Plugin::Prereqs
config:
Dist::Zilla::Plugin::Prereqs:
phase: develop
type: recommends
name: '@Author::ETHER/pluginbundle version'
version: '6.036'
-
class: Dist::Zilla::Plugin::PromptIfStale
config:
Dist::Zilla::Plugin::PromptIfStale:
check_all_plugins: 0
check_all_prereqs: 0
modules:
- Dist::Zilla::PluginBundle::Author::ETHER
phase: build
run_under_travis: 0
skip: []
name: '@Author::ETHER/stale modules, build'
version: '0.060'
-
class: Dist::Zilla::Plugin::ExecDir
name: '@Author::ETHER/ExecDir'
version: '6.036'
-
class: Dist::Zilla::Plugin::FileFinder::ByName
name: '@Author::ETHER/Examples'
version: '6.036'
-
class: Dist::Zilla::Plugin::Git::GatherDir
config:
Dist::Zilla::Plugin::GatherDir:
exclude_filename:
- CONTRIBUTING
- INSTALL
- LICENCE
- README.pod
- TODO
- pull_request_template.md
exclude_match: []
include_dotfiles: 0
prefix: ''
prune_directory: []
root: .
Dist::Zilla::Plugin::Git::GatherDir:
include_untracked: 0
name: '@Author::ETHER/Git::GatherDir'
version: '2.052'
-
class: Dist::Zilla::Plugin::MetaYAML
name: '@Author::ETHER/MetaYAML'
version: '6.036'
-
class: Dist::Zilla::Plugin::MetaJSON
name: '@Author::ETHER/MetaJSON'
version: '6.036'
-
class: Dist::Zilla::Plugin::Readme
name: '@Author::ETHER/Readme'
version: '6.036'
-
class: Dist::Zilla::Plugin::Manifest
name: '@Author::ETHER/Manifest'
version: '6.036'
-
class: Dist::Zilla::Plugin::License
name: '@Author::ETHER/License'
version: '6.036'
-
class: Dist::Zilla::Plugin::GenerateFile::FromShareDir
config:
Dist::Zilla::Plugin::GenerateFile::FromShareDir:
destination_filename: CONTRIBUTING
dist: Dist-Zilla-PluginBundle-Author-ETHER
encoding: UTF-8
has_xs: 0
location: build
source_filename: CONTRIBUTING
Dist::Zilla::Role::RepoFileInjector:
allow_overwrite: 1
repo_root: .
version: '0.009'
name: '@Author::ETHER/generate CONTRIBUTING'
version: '0.015'
-
class: Dist::Zilla::Plugin::InstallGuide
config:
Dist::Zilla::Role::ModuleMetadata:
Module::Metadata: '1.000038'
version: '0.006'
name: '@Author::ETHER/InstallGuide'
version: '1.200014'
-
class: Dist::Zilla::Plugin::Test::Compile
config:
Dist::Zilla::Plugin::Test::Compile:
bail_out_on_fail: 1
fail_on_warning: author
fake_home: 0
filename: xt/author/00-compile.t
module_finder:
- ':InstallModules'
needs_display: 0
phase: develop
script_finder:
- ':PerlExecFiles'
- '@Author::ETHER/Examples'
skips: []
switch: []
name: '@Author::ETHER/Test::Compile'
version: '2.058'
-
class: Dist::Zilla::Plugin::Test::NoTabs
config:
Dist::Zilla::Plugin::Test::NoTabs:
filename: xt/author/no-tabs.t
finder:
- ':InstallModules'
- ':ExecFiles'
- '@Author::ETHER/Examples'
- ':TestFiles'
- ':ExtraTestFiles'
name: '@Author::ETHER/Test::NoTabs'
version: '0.15'
-
class: Dist::Zilla::Plugin::Test::EOL
config:
Dist::Zilla::Plugin::Test::EOL:
filename: xt/author/eol.t
finder:
- ':ExecFiles'
- ':ExtraTestFiles'
- ':InstallModules'
- ':TestFiles'
- '@Author::ETHER/Examples'
trailing_whitespace: 1
name: '@Author::ETHER/Test::EOL'
version: '0.19'
-
class: Dist::Zilla::Plugin::MetaTests
name: '@Author::ETHER/MetaTests'
version: '6.036'
-
class: Dist::Zilla::Plugin::Test::CPAN::Changes
config:
Dist::Zilla::Plugin::Test::CPAN::Changes:
changelog: Changes
filename: xt/release/cpan-changes.t
name: '@Author::ETHER/Test::CPAN::Changes'
version: '0.013'
-
class: Dist::Zilla::Plugin::Test::ChangesHasContent
name: '@Author::ETHER/Test::ChangesHasContent'
version: '0.011'
-
class: Dist::Zilla::Plugin::Test::MinimumVersion
config:
Dist::Zilla::Plugin::Test::MinimumVersion:
max_target_perl: '5.020'
name: '@Author::ETHER/Test::MinimumVersion'
version: '2.000011'
-
class: Dist::Zilla::Plugin::PodSyntaxTests
name: '@Author::ETHER/PodSyntaxTests'
version: '6.036'
-
class: Dist::Zilla::Plugin::Test::Pod::Coverage::TrustMe
config:
Dist::Zilla::Plugin::Test::Pod::Coverage::TrustMe:
finder:
- ':InstallModules'
name: '@Author::ETHER/Test::Pod::Coverage::TrustMe'
version: v1.0.1
-
class: Dist::Zilla::Plugin::Test::PodSpelling
config:
Dist::Zilla::Plugin::Test::PodSpelling:
directories:
- examples
- lib
- script
- t
- xt
spell_cmd: ''
stopwords:
- irc
wordlist: Pod::Wordlist
name: '@Author::ETHER/Test::PodSpelling'
version: '2.007006'
-
class: Dist::Zilla::Plugin::Test::Kwalitee
config:
Dist::Zilla::Plugin::Test::Kwalitee:
filename: xt/author/kwalitee.t
skiptest: []
name: '@Author::ETHER/Test::Kwalitee'
version: '2.12'
-
class: Dist::Zilla::Plugin::MojibakeTests
name: '@Author::ETHER/MojibakeTests'
version: '0.8'
-
class: Dist::Zilla::Plugin::Test::ReportPrereqs
name: '@Author::ETHER/Test::ReportPrereqs'
version: '0.029'
-
class: Dist::Zilla::Plugin::Test::Portability
config:
Dist::Zilla::Plugin::Test::Portability:
options: ''
name: '@Author::ETHER/Test::Portability'
version: '2.001003'
-
class: Dist::Zilla::Plugin::Test::CleanNamespaces
config:
Dist::Zilla::Plugin::Test::CleanNamespaces:
filename: xt/author/clean-namespaces.t
skips: []
name: '@Author::ETHER/Test::CleanNamespaces'
version: '0.006'
-
class: Dist::Zilla::Plugin::Git::Describe
name: '@Author::ETHER/Git::Describe'
version: '0.008'
-
class: Dist::Zilla::Plugin::PodWeaver
config:
Dist::Zilla::Plugin::PodWeaver:
finder:
- ':InstallModules'
- ':PerlExecFiles'
plugins:
-
class: Pod::Weaver::Section::GenerateSection
name: SUPPORT
version: '4.020'
-
class: Pod::Weaver::Section::GenerateSection
name: 'COPYRIGHT AND LICENCE'
version: '4.020'
-
class: Pod::Weaver::Plugin::EnsurePod5
name: '@Author::ETHER/EnsurePod5'
version: '4.020'
-
class: Pod::Weaver::Plugin::H1Nester
name: '@Author::ETHER/H1Nester'
version: '4.020'
-
class: Pod::Weaver::Plugin::SingleEncoding
name: '@Author::ETHER/SingleEncoding'
version: '4.020'
-
class: Pod::Weaver::Plugin::Transformer
name: '@Author::ETHER/List'
version: '4.020'
-
class: Pod::Weaver::Plugin::Transformer
name: '@Author::ETHER/Verbatim'
version: '4.020'
-
class: Pod::Weaver::Section::Region
name: '@Author::ETHER/header'
version: '4.020'
-
class: Pod::Weaver::Section::Name
name: '@Author::ETHER/Name'
version: '4.020'
-
class: Pod::Weaver::Section::Version
name: '@Author::ETHER/Version'
version: '4.020'
-
class: Pod::Weaver::Section::Region
name: '@Author::ETHER/prelude'
version: '4.020'
-
class: Pod::Weaver::Section::Generic
name: SYNOPSIS
version: '4.020'
-
class: Pod::Weaver::Section::Generic
name: DESCRIPTION
version: '4.020'
-
class: Pod::Weaver::Section::Generic
name: OVERVIEW
version: '4.020'
-
class: Pod::Weaver::Section::Collect
name: ATTRIBUTES
version: '4.020'
-
class: Pod::Weaver::Section::Collect
name: METHODS
version: '4.020'
-
class: Pod::Weaver::Section::Collect
name: FUNCTIONS
version: '4.020'
-
class: Pod::Weaver::Section::Collect
name: TYPES
version: '4.020'
-
class: Pod::Weaver::Section::Leftovers
name: '@Author::ETHER/Leftovers'
version: '4.020'
-
class: Pod::Weaver::Section::Region
name: '@Author::ETHER/postlude'
version: '4.020'
-
class: Pod::Weaver::Section::GenerateSection
name: '@Author::ETHER/generate GIVING THANKS'
version: '4.020'
-
class: Pod::Weaver::Section::GenerateSection
name: '@Author::ETHER/generate SUPPORT'
version: '4.020'
-
class: Pod::Weaver::Section::Authors
name: '@Author::ETHER/Authors'
version: '4.020'
-
class: Pod::Weaver::Section::AllowOverride
name: '@Author::ETHER/allow override AUTHOR'
version: '0.05'
-
class: Pod::Weaver::Section::Contributors
name: '@Author::ETHER/Contributors'
version: '0.009'
-
class: Pod::Weaver::Section::Legal
name: '@Author::ETHER/Legal'
version: '4.020'
-
class: Pod::Weaver::Section::Region
name: '@Author::ETHER/footer'
version: '4.020'
-
class: inc::AppendSection
name: AppendSupport
version: ~
-
class: inc::AppendSection
name: AppendCopyright
version: ~
name: '@Author::ETHER/PodWeaver'
version: '4.010'
-
class: Dist::Zilla::Plugin::GithubMeta
name: '@Author::ETHER/GithubMeta'
version: '0.58'
-
class: Dist::Zilla::Plugin::AutoMetaResources
name: '@Author::ETHER/AutoMetaResources'
version: '1.21'
-
class: Dist::Zilla::Plugin::Authority
name: '@Author::ETHER/Authority'
version: '1.009'
-
class: Dist::Zilla::Plugin::MetaNoIndex
name: '@Author::ETHER/MetaNoIndex'
version: '6.036'
-
class: Dist::Zilla::Plugin::MetaProvides::Package
config:
Dist::Zilla::Plugin::MetaProvides::Package:
finder:
- ':InstallModules'
finder_objects:
-
class: Dist::Zilla::Plugin::FinderCode
name: ':InstallModules'
version: '6.036'
include_underscores: 0
Dist::Zilla::Role::MetaProvider::Provider:
$Dist::Zilla::Role::MetaProvider::Provider::VERSION: '2.002004'
inherit_missing: 0
inherit_version: 0
meta_noindex: 1
Dist::Zilla::Role::ModuleMetadata:
Module::Metadata: '1.000038'
version: '0.006'
name: '@Author::ETHER/MetaProvides::Package'
version: '2.004003'
-
class: Dist::Zilla::Plugin::MetaConfig
name: '@Author::ETHER/MetaConfig'
version: '6.036'
-
class: Dist::Zilla::Plugin::Keywords
config:
Dist::Zilla::Plugin::Keywords:
keywords:
- JSON
- Schema
- validator
- data
- validation
- structure
- specification
name: '@Author::ETHER/Keywords'
version: '0.007'
-
class: Dist::Zilla::Plugin::UseUnsafeInc
config:
Dist::Zilla::Plugin::UseUnsafeInc:
dot_in_INC: 0
name: '@Author::ETHER/UseUnsafeInc'
version: '0.002'
-
class: Dist::Zilla::Plugin::AutoPrereqs
name: '@Author::ETHER/AutoPrereqs'
version: '6.036'
-
class: Dist::Zilla::Plugin::Prereqs::AuthorDeps
name: '@Author::ETHER/Prereqs::AuthorDeps'
version: '0.007'
-
class: Dist::Zilla::Plugin::MinimumPerl
name: '@Author::ETHER/MinimumPerl'
version: '1.006'
-
class: Dist::Zilla::Plugin::MakeMaker
config:
Dist::Zilla::Role::TestRunner:
default_jobs: 9
name: '@Author::ETHER/MakeMaker'
version: '6.036'
-
class: Dist::Zilla::Plugin::Git::Contributors
config:
Dist::Zilla::Plugin::Git::Contributors:
git_version: 2.50.1
include_authors: 0
include_releaser: 1
order_by: commits
paths: []
name: '@Author::ETHER/Git::Contributors'
version: '0.038'
-
class: Dist::Zilla::Plugin::StaticInstall
config:
Dist::Zilla::Plugin::StaticInstall:
dry_run: 0
mode: off
name: '@Author::ETHER/StaticInstall'
version: '0.012'
-
class: Dist::Zilla::Plugin::RunExtraTests
config:
Dist::Zilla::Role::TestRunner:
default_jobs: 9
name: '@Author::ETHER/RunExtraTests'
version: '0.029'
-
class: Dist::Zilla::Plugin::CheckSelfDependency
config:
Dist::Zilla::Plugin::CheckSelfDependency:
finder:
- ':InstallModules'
Dist::Zilla::Role::ModuleMetadata:
Module::Metadata: '1.000038'
version: '0.006'
name: '@Author::ETHER/CheckSelfDependency'
version: '0.011'
-
class: Dist::Zilla::Plugin::Run::AfterBuild
config:
Dist::Zilla::Plugin::Run::Role::Runner:
fatal_errors: 1
quiet: 1
run:
- "bash -c \"test -e .ackrc && grep -q -- '--ignore-dir=.latest' .ackrc || echo '--ignore-dir=.latest' >> .ackrc; if [[ `dirname '%d'` != .build ]]; then test -e .ackrc && grep -q -- '--ignore-dir=%d' .ackrc || echo '--ignore-dir=%d' >> .ackrc; fi\""
version: '0.050'
name: '@Author::ETHER/.ackrc'
version: '0.050'
-
class: Dist::Zilla::Plugin::Run::AfterBuild
config:
Dist::Zilla::Plugin::Run::Role::Runner:
eval:
- "if ('%d' =~ /^%n-[.[:xdigit:]]+$/) { unlink '.latest'; symlink '%d', '.latest'; }"
fatal_errors: 0
quiet: 1
version: '0.050'
name: '@Author::ETHER/.latest'
version: '0.050'
-
class: Dist::Zilla::Plugin::CheckStrictVersion
name: '@Author::ETHER/CheckStrictVersion'
version: '0.001'
-
class: Dist::Zilla::Plugin::CheckMetaResources
name: '@Author::ETHER/CheckMetaResources'
version: '0.001'
-
class: Dist::Zilla::Plugin::EnsureLatestPerl
config:
Dist::Zilla::Plugin::EnsureLatestPerl:
Module::CoreList: '5.20251120'
name: '@Author::ETHER/EnsureLatestPerl'
version: '0.010'
-
class: Dist::Zilla::Plugin::PromptIfStale
config:
Dist::Zilla::Plugin::PromptIfStale:
check_all_plugins: 1
check_all_prereqs: 1
modules: []
phase: release
run_under_travis: 0
skip: []
name: '@Author::ETHER/stale modules, release'
version: '0.060'
-
class: Dist::Zilla::Plugin::Git::Check
config:
Dist::Zilla::Plugin::Git::Check:
untracked_files: die
Dist::Zilla::Role::Git::DirtyFiles:
allow_dirty: []
allow_dirty_match: []
changelog: Changes
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
name: '@Author::ETHER/initial check'
version: '2.052'
-
class: Dist::Zilla::Plugin::Git::CheckFor::MergeConflicts
config:
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
name: '@Author::ETHER/Git::CheckFor::MergeConflicts'
version: '0.014'
-
class: Dist::Zilla::Plugin::Git::CheckFor::CorrectBranch
config:
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
name: '@Author::ETHER/Git::CheckFor::CorrectBranch'
version: '0.014'
-
class: Dist::Zilla::Plugin::Git::Remote::Check
name: '@Author::ETHER/Git::Remote::Check'
version: 0.1.2
-
class: Dist::Zilla::Plugin::CheckPrereqsIndexed
name: '@Author::ETHER/CheckPrereqsIndexed'
version: '0.022'
-
class: Dist::Zilla::Plugin::TestRelease
name: '@Author::ETHER/TestRelease'
version: '6.036'
-
class: Dist::Zilla::Plugin::Git::Check
config:
Dist::Zilla::Plugin::Git::Check:
untracked_files: die
Dist::Zilla::Role::Git::DirtyFiles:
allow_dirty: []
allow_dirty_match: []
changelog: Changes
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
name: '@Author::ETHER/after tests'
version: '2.052'
-
class: Dist::Zilla::Plugin::CheckIssues
name: '@Author::ETHER/CheckIssues'
version: '0.011'
-
class: Dist::Zilla::Plugin::UploadToCPAN
name: '@Author::ETHER/UploadToCPAN'
version: '6.036'
-
class: Dist::Zilla::Plugin::CopyFilesFromRelease
config:
Dist::Zilla::Plugin::CopyFilesFromRelease:
filename:
- CONTRIBUTING
- INSTALL
- LICENCE
- LICENSE
- ppport.h
match: []
name: '@Author::ETHER/copy generated files'
version: '0.007'
-
class: Dist::Zilla::Plugin::ReadmeAnyFromPod
config:
Dist::Zilla::Role::FileWatcher:
version: '0.006'
name: '@Author::ETHER/ReadmeAnyFromPod'
version: '0.163250'
-
class: Dist::Zilla::Plugin::Prereqs
config:
Dist::Zilla::Plugin::Prereqs:
phase: develop
type: recommends
name: '@Author::ETHER/@Git::VersionManager/pluginbundle version'
version: '6.036'
-
class: Dist::Zilla::Plugin::RewriteVersion::Transitional
config:
Dist::Zilla::Plugin::RewriteVersion:
add_tarball_name: 0
finders:
- ':ExecFiles'
- ':InstallModules'
global: 1
skip_version_provider: 0
Dist::Zilla::Plugin::RewriteVersion::Transitional: {}
name: '@Author::ETHER/@Git::VersionManager/RewriteVersion::Transitional'
version: '0.009'
-
class: Dist::Zilla::Plugin::MetaProvides::Update
name: '@Author::ETHER/@Git::VersionManager/MetaProvides::Update'
version: '0.007'
-
class: Dist::Zilla::Plugin::CopyFilesFromRelease
config:
Dist::Zilla::Plugin::CopyFilesFromRelease:
filename:
- Changes
match: []
name: '@Author::ETHER/@Git::VersionManager/CopyFilesFromRelease'
version: '0.007'
-
class: Dist::Zilla::Plugin::Git::Commit
config:
Dist::Zilla::Plugin::Git::Commit:
add_files_in:
- .
commit_msg: '%N-%v%t%n%n%c'
signoff: 0
Dist::Zilla::Role::Git::DirtyFiles:
allow_dirty:
- CONTRIBUTING
- Changes
- INSTALL
- LICENCE
- README.pod
allow_dirty_match: []
changelog: Changes
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
Dist::Zilla::Role::Git::StringFormatter:
time_zone: local
name: '@Author::ETHER/@Git::VersionManager/release snapshot'
version: '2.052'
-
class: Dist::Zilla::Plugin::Git::Tag
config:
Dist::Zilla::Plugin::Git::Tag:
branch: ~
changelog: Changes
signed: 0
tag: v0.627
tag_format: v%V
tag_message: v%v%t
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
Dist::Zilla::Role::Git::StringFormatter:
time_zone: local
name: '@Author::ETHER/@Git::VersionManager/Git::Tag'
version: '2.052'
-
class: Dist::Zilla::Plugin::BumpVersionAfterRelease::Transitional
config:
Dist::Zilla::Plugin::BumpVersionAfterRelease:
finders:
- ':InstallModules'
global: 1
munge_makefile_pl: 1
Dist::Zilla::Plugin::BumpVersionAfterRelease::Transitional: {}
name: '@Author::ETHER/@Git::VersionManager/BumpVersionAfterRelease::Transitional'
version: '0.009'
-
class: Dist::Zilla::Plugin::NextRelease
name: '@Author::ETHER/@Git::VersionManager/NextRelease'
version: '6.036'
-
class: Dist::Zilla::Plugin::Git::Commit
config:
Dist::Zilla::Plugin::Git::Commit:
add_files_in: []
commit_msg: 'increment $VERSION after %v release'
signoff: 0
Dist::Zilla::Role::Git::DirtyFiles:
allow_dirty:
- Build.PL
- Changes
- Makefile.PL
allow_dirty_match:
- (?^:^lib/.*\.pm$)
changelog: Changes
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
Dist::Zilla::Role::Git::StringFormatter:
time_zone: local
name: '@Author::ETHER/@Git::VersionManager/post-release commit'
version: '2.052'
-
class: Dist::Zilla::Plugin::Prereqs
config:
Dist::Zilla::Plugin::Prereqs:
phase: x_Dist_Zilla
type: requires
name: '@Author::ETHER/@Git::VersionManager/prereqs for @Git::VersionManager'
version: '6.036'
-
class: Dist::Zilla::Plugin::Git::Push
config:
Dist::Zilla::Plugin::Git::Push:
push_to:
- origin
remotes_must_exist: 1
Dist::Zilla::Role::Git::Repo:
git_version: 2.50.1
repo_root: .
name: '@Author::ETHER/Git::Push'
version: '2.052'
-
class: Dist::Zilla::Plugin::GitHub::Update
config:
Dist::Zilla::Plugin::GitHub::Update:
metacpan: 1
name: '@Author::ETHER/GitHub::Update'
version: '0.49'
-
class: Dist::Zilla::Plugin::Run::AfterRelease
config:
Dist::Zilla::Plugin::Run::Role::Runner:
fatal_errors: 0
quiet: 0
run:
- REDACTED
version: '0.050'
name: '@Author::ETHER/install release'
version: '0.050'
-
class: Dist::Zilla::Plugin::Run::AfterRelease
config:
Dist::Zilla::Plugin::Run::Role::Runner:
eval:
- 'print "release complete!\xa"'
fatal_errors: 1
quiet: 1
version: '0.050'
name: '@Author::ETHER/release complete'
version: '0.050'
-
class: Dist::Zilla::Plugin::ConfirmRelease
name: '@Author::ETHER/ConfirmRelease'
version: '6.036'
-
class: Dist::Zilla::Plugin::Prereqs
config:
Dist::Zilla::Plugin::Prereqs:
phase: x_Dist_Zilla
type: requires
name: '@Author::ETHER/prereqs for @Author::ETHER'
version: '6.036'
-
class: inc::CheckConflicts
name: =inc::CheckConflicts
version: ~
-
class: Dist::Zilla::Plugin::ShareDir
name: ShareDir
version: '6.036'
-
class: Dist::Zilla::Plugin::Prereqs
config:
Dist::Zilla::Plugin::Prereqs:
phase: runtime
type: requires
name: RuntimeRequires
version: '6.036'
-
class: Dist::Zilla::Plugin::Prereqs
config:
Dist::Zilla::Plugin::Prereqs:
phase: runtime
type: suggests
name: RuntimeSuggests
version: '6.036'
-
class: Dist::Zilla::Plugin::Prereqs::Soften
config:
Dist::Zilla::Plugin::Prereqs::Soften:
copy_to:
- develop.requires
modules:
- Time::Moment
- DateTime::Format::RFC3339
- Data::Validate::Domain
- Email::Address::XS
- Net::IDN::Encode
- Sereal
- JSON::PP
- Cpanel::JSON::XS
modules_from_features: ~
to_relationship: suggests
name: Prereqs::Soften
version: '0.006003'
-
class: Dist::Zilla::Plugin::DynamicPrereqs
config:
Dist::Zilla::Role::ModuleMetadata:
Module::Metadata: '1.000038'
version: '0.006'
name: DynamicPrereqs
version: '0.040'
-
class: Dist::Zilla::Plugin::Breaks
name: Breaks
version: '0.005'
-
class: Dist::Zilla::Plugin::Test::CheckBreaks
config:
Dist::Zilla::Plugin::Test::CheckBreaks:
conflicts_module: []
no_forced_deps: 0
Dist::Zilla::Role::ModuleMetadata:
Module::Metadata: '1.000038'
version: '0.006'
name: Test::CheckBreaks
version: '0.020'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':InstallModules'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':IncModules'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':TestFiles'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':ExtraTestFiles'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':ExecFiles'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':PerlExecFiles'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':ShareFiles'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':MainModule'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':AllFiles'
version: '6.036'
-
class: Dist::Zilla::Plugin::FinderCode
name: ':NoFiles'
version: '6.036'
-
class: Dist::Zilla::Plugin::VerifyPhases
name: '@Author::ETHER/PHASE VERIFICATION'
version: '0.016'
zilla:
class: Dist::Zilla::Dist::Builder
config:
is_trial: 0
version: '6.036'
x_authority: cpan:ETHER
x_breaks:
JSON::Schema::Modern::Document::OpenAPI: '< 0.097'
JSON::Schema::Modern::Vocabulary::OpenAPI: '< 0.080'
Mojolicious::Plugin::OpenAPI::Modern: '< 0.014'
OpenAPI::Modern: '< 0.077'
Test::Mojo::Role::OpenAPI::Modern: '< 0.007'
x_generated_by_perl: v5.43.5
x_serialization_backend: 'YAML::Tiny version 1.76'
x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later'
x_static_install: 0
x_use_unsafe_inc: 0
MANIFEST 100644 000766 000024 17055 15114374332 15562 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 # This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.036.
CONTRIBUTING
Changes
INSTALL
LICENCE
MANIFEST
META.json
META.yml
Makefile.PL
README
dist.ini
inc/AppendSection.pm
inc/CheckConflicts.pm
inc/ExtUtils/HasCompiler.pm
lib/JSON/Schema/Modern.pm
lib/JSON/Schema/Modern/Annotation.pm
lib/JSON/Schema/Modern/Document.pm
lib/JSON/Schema/Modern/Error.pm
lib/JSON/Schema/Modern/Result.pm
lib/JSON/Schema/Modern/ResultNode.pm
lib/JSON/Schema/Modern/Utilities.pm
lib/JSON/Schema/Modern/Vocabulary.pm
lib/JSON/Schema/Modern/Vocabulary/Applicator.pm
lib/JSON/Schema/Modern/Vocabulary/Content.pm
lib/JSON/Schema/Modern/Vocabulary/Core.pm
lib/JSON/Schema/Modern/Vocabulary/FormatAnnotation.pm
lib/JSON/Schema/Modern/Vocabulary/FormatAssertion.pm
lib/JSON/Schema/Modern/Vocabulary/MetaData.pm
lib/JSON/Schema/Modern/Vocabulary/Unevaluated.pm
lib/JSON/Schema/Modern/Vocabulary/Validation.pm
script/json-schema-eval
share/LICENSE
share/draft2019-09/meta/applicator.json
share/draft2019-09/meta/content.json
share/draft2019-09/meta/core.json
share/draft2019-09/meta/format.json
share/draft2019-09/meta/meta-data.json
share/draft2019-09/meta/validation.json
share/draft2019-09/output/schema.json
share/draft2019-09/schema.json
share/draft2020-12/meta/applicator.json
share/draft2020-12/meta/content.json
share/draft2020-12/meta/core.json
share/draft2020-12/meta/format-annotation.json
share/draft2020-12/meta/format-assertion.json
share/draft2020-12/meta/meta-data.json
share/draft2020-12/meta/unevaluated.json
share/draft2020-12/meta/validation.json
share/draft2020-12/output/schema.json
share/draft2020-12/schema.json
share/draft4/schema.json
share/draft6/schema.json
share/draft7/schema.json
t/00-report-prereqs.dd
t/00-report-prereqs.t
t/add-schema.t
t/additional-tests-draft2019-09.t
t/additional-tests-draft2019-09/README
t/additional-tests-draft2019-09/anchor.json
t/additional-tests-draft2019-09/annotation-collection.json
t/additional-tests-draft2019-09/badRef.json
t/additional-tests-draft2019-09/faux-buggy-schemas.json
t/additional-tests-draft2019-09/format-date-time.json
t/additional-tests-draft2019-09/format-date.json
t/additional-tests-draft2019-09/format-duration.json
t/additional-tests-draft2019-09/format-ipv4.json
t/additional-tests-draft2019-09/format-ipv6.json
t/additional-tests-draft2019-09/format-relative-json-pointer.json
t/additional-tests-draft2019-09/format-time.json
t/additional-tests-draft2019-09/formats.json
t/additional-tests-draft2019-09/id.json
t/additional-tests-draft2019-09/integers.json
t/additional-tests-draft2019-09/keyword-independence.json
t/additional-tests-draft2019-09/loose-types-const-enum.json
t/additional-tests-draft2019-09/not.json
t/additional-tests-draft2019-09/recursive-dynamic.json
t/additional-tests-draft2019-09/ref-and-id.json
t/additional-tests-draft2019-09/ref.json
t/additional-tests-draft2019-09/short-circuit.json
t/additional-tests-draft2019-09/unknownKeyword.json
t/additional-tests-draft2019-09/vocabulary.json
t/additional-tests-draft2020-12.t
t/additional-tests-draft2020-12/README
t/additional-tests-draft2020-12/anchor.json
t/additional-tests-draft2020-12/annotation-collection.json
t/additional-tests-draft2020-12/badRef.json
t/additional-tests-draft2020-12/dynamicRef.json
t/additional-tests-draft2020-12/faux-buggy-schemas.json
t/additional-tests-draft2020-12/format-date-time.json
t/additional-tests-draft2020-12/format-date.json
t/additional-tests-draft2020-12/format-duration.json
t/additional-tests-draft2020-12/format-ipv4.json
t/additional-tests-draft2020-12/format-ipv6.json
t/additional-tests-draft2020-12/format-relative-json-pointer.json
t/additional-tests-draft2020-12/format-time.json
t/additional-tests-draft2020-12/formats.json
t/additional-tests-draft2020-12/id.json
t/additional-tests-draft2020-12/integers.json
t/additional-tests-draft2020-12/keyword-independence.json
t/additional-tests-draft2020-12/loose-types-const-enum.json
t/additional-tests-draft2020-12/not.json
t/additional-tests-draft2020-12/recursive-dynamic.json
t/additional-tests-draft2020-12/ref-and-id.json
t/additional-tests-draft2020-12/ref.json
t/additional-tests-draft2020-12/short-circuit.json
t/additional-tests-draft2020-12/unknownKeyword.json
t/additional-tests-draft2020-12/vocabulary.json
t/additional-tests-draft4.t
t/additional-tests-draft4/format-date-time.json
t/additional-tests-draft4/format-ipv4.json
t/additional-tests-draft4/format-ipv6.json
t/additional-tests-draft4/id.json
t/additional-tests-draft4/integers.json
t/additional-tests-draft4/type.json
t/additional-tests-draft7.t
t/additional-tests-draft7/README
t/additional-tests-draft7/badRef.json
t/additional-tests-draft7/faux-buggy-schemas.json
t/additional-tests-draft7/format-date-time.json
t/additional-tests-draft7/format-date.json
t/additional-tests-draft7/format-ipv4.json
t/additional-tests-draft7/format-relative-json-pointer.json
t/additional-tests-draft7/format-time.json
t/additional-tests-draft7/id.json
t/additional-tests-draft7/integers.json
t/additional-tests-draft7/keyword-independence.json
t/additional-tests-draft7/loose-types-const-enum.json
t/additional-tests-draft7/not-an-anchor.json
t/additional-tests-draft7/not-an-id.json
t/additional-tests-draft7/ref-and-id.json
t/additional-tests-draft7/ref.json
t/additional-tests-draft7/short-circuit.json
t/additional-tests-draft7/unknownKeyword.json
t/additional-tests-draft7/vocabulary.json
t/annotations.t
t/boolean-data.t
t/boolean-schemas.t
t/cached-metaschemas.t
t/callbacks.t
t/checksums.t
t/content-encoding.t
t/dialects.t
t/document.t
t/equality.t
t/errors.t
t/evaluate_json_string.t
t/find-identifiers.t
t/formats.t
t/invalid-schemas.t
t/invalid-schemas/invalid-input.json
t/invalid-schemas/ref.json
t/invalid-schemas/vocabulary.json
t/lib/Acceptance.pm
t/lib/Helper.pm
t/lib/MyVocabulary/BadEvaluationOrder.pm
t/lib/MyVocabulary/BadVocabularySub1.pm
t/lib/MyVocabulary/BadVocabularySub2.pm
t/lib/MyVocabulary/BadVocabularySub3.pm
t/lib/MyVocabulary/ConflictingKeyword.pm
t/lib/MyVocabulary/MissingRole.pm
t/lib/MyVocabulary/MissingSub.pm
t/lib/MyVocabulary/ReservedKeyword.pm
t/lib/MyVocabulary/StringComparison.pm
t/max_traversal_depth.t
t/multipleOf.t
t/pattern.t
t/read_serialized_file
t/ref.t
t/result-object.t
t/results/draft2019-09-acceptance-format.txt
t/results/draft2019-09-acceptance.txt
t/results/draft2019-09-additional-tests.txt
t/results/draft2019-09-invalid-schemas.txt
t/results/draft2020-12-acceptance-format.txt
t/results/draft2020-12-acceptance.txt
t/results/draft2020-12-additional-tests.txt
t/results/draft2020-12-invalid-schemas.txt
t/results/draft4-acceptance-format.txt
t/results/draft4-acceptance.txt
t/results/draft4-additional-tests.txt
t/results/draft6-acceptance-format.txt
t/results/draft6-acceptance.txt
t/results/draft7-acceptance-format.txt
t/results/draft7-acceptance.txt
t/results/draft7-additional-tests.txt
t/serialization.t
t/specification_version.t
t/strict.t
t/stringy-numbers.t
t/traverse.t
t/type.t
t/unsupported-keywords.t
t/validate-schema.t
t/vocabularies.t
t/zzz-acceptance-draft2019-09-format.t
t/zzz-acceptance-draft2019-09.t
t/zzz-acceptance-draft2020-12-format.t
t/zzz-acceptance-draft2020-12.t
t/zzz-acceptance-draft4-format.t
t/zzz-acceptance-draft4.t
t/zzz-acceptance-draft6-format.t
t/zzz-acceptance-draft6.t
t/zzz-acceptance-draft7-format.t
t/zzz-acceptance-draft7.t
t/zzz-check-breaks.t
update-schemas
weaver.ini
xt/author/00-compile.t
xt/author/clean-namespaces.t
xt/author/distmeta.t
xt/author/eol.t
xt/author/kwalitee.t
xt/author/minimum-version.t
xt/author/mojibake.t
xt/author/no-tabs.t
xt/author/pod-coverage.t
xt/author/pod-spell.t
xt/author/pod-syntax.t
xt/author/portability.t
xt/release/changes_has_content.t
xt/release/cpan-changes.t
META.json 100644 000766 000024 153422 15114374332 16071 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 {
"abstract" : "Validate data against a schema using a JSON Schema",
"author" : [
"Karen Etheridge "
],
"dynamic_config" : 1,
"generated_by" : "Dist::Zilla version 6.036, CPAN::Meta::Converter version 2.150010",
"keywords" : [
"JSON",
"Schema",
"validator",
"data",
"validation",
"structure",
"specification"
],
"license" : [
"perl_5"
],
"meta-spec" : {
"url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
"version" : 2
},
"name" : "JSON-Schema-Modern",
"no_index" : {
"directory" : [
"inc",
"share",
"t",
"xt"
]
},
"prereqs" : {
"configure" : {
"requires" : {
"ExtUtils::MakeMaker" : "0",
"File::ShareDir::Install" : "0.06",
"Text::ParseWords" : "0",
"perl" : "5.020"
}
},
"develop" : {
"recommends" : {
"Dist::Zilla::PluginBundle::Author::ETHER" : "0.170",
"Dist::Zilla::PluginBundle::Git::VersionManager" : "0.007"
},
"requires" : {
"Cpanel::JSON::XS" : "4.38",
"Data::Validate::Domain" : "0.13",
"DateTime::Format::RFC3339" : "0",
"Email::Address::XS" : "1.04",
"Encode" : "0",
"ExtUtils::HasCompiler" : "0.014",
"File::Spec" : "0",
"IO::Handle" : "0",
"IPC::Open3" : "0",
"JSON::PP" : "4.11",
"Net::IDN::Encode" : "0",
"Pod::Wordlist" : "0",
"Sereal" : "0",
"Test::CPAN::Changes" : "0.19",
"Test::CPAN::Meta" : "0",
"Test::CleanNamespaces" : "0.15",
"Test::EOL" : "0",
"Test::Kwalitee" : "1.21",
"Test::MinimumVersion" : "0",
"Test::Mojibake" : "0",
"Test::More" : "0.96",
"Test::NoTabs" : "0",
"Test::Pod" : "1.41",
"Test::Pod::Coverage::TrustMe" : "0.002001",
"Test::Portability::Files" : "0",
"Test::Spelling" : "0.17",
"Time::Moment" : "0"
}
},
"runtime" : {
"requires" : {
"B" : "0",
"Carp" : "0",
"Digest::MD5" : "0",
"Exporter" : "0",
"Feature::Compat::Try" : "0",
"File::ShareDir" : "0",
"Getopt::Long::Descriptive" : "0",
"List::Util" : "1.55",
"MIME::Base64" : "0",
"Math::BigFloat" : "0",
"Math::BigInt" : "1.999701",
"Mojo::File" : "0",
"Mojo::JSON" : "0",
"Mojo::JSON::Pointer" : "0",
"Mojo::Message::Response" : "0",
"Mojo::URL" : "0",
"Mojolicious" : "7.87",
"Moo" : "0",
"Moo::Role" : "0",
"MooX::TypeTiny" : "0.002002",
"Safe::Isa" : "1.000008",
"Scalar::Util" : "0",
"Storable" : "0",
"Types::Common::Numeric" : "0",
"Types::Standard" : "1.016003",
"YAML::PP" : "0",
"autovivification" : "0",
"builtin::compat" : "0.003003",
"constant" : "0",
"experimental" : "0.026",
"feature" : "0",
"if" : "0",
"namespace::clean" : "0",
"open" : "0",
"overload" : "0",
"perl" : "v5.20.0",
"stable" : "0.031",
"strict" : "0",
"strictures" : "2",
"warnings" : "0"
},
"suggests" : {
"Class::XSAccessor" : "0",
"Cpanel::JSON::XS" : "4.38",
"Data::Validate::Domain" : "0.13",
"DateTime::Format::RFC3339" : "0",
"Email::Address::XS" : "1.04",
"JSON::PP" : "4.11",
"Net::IDN::Encode" : "0",
"Sereal" : "0",
"Time::Moment" : "0",
"Type::Tiny::XS" : "0"
}
},
"test" : {
"recommends" : {
"CPAN::Meta" : "2.120900"
},
"requires" : {
"CPAN::Meta::Check" : "0.011",
"CPAN::Meta::Requirements" : "0",
"Data::Dumper" : "0",
"ExtUtils::MakeMaker" : "0",
"File::Spec" : "0",
"Math::BigInt" : "1.999701",
"Term::ANSIColor" : "0",
"Test2::API" : "0",
"Test2::V0" : "0",
"Test2::Warnings" : "0.038",
"Test::Deep" : "0",
"Test::Deep::UnorderedPairs" : "0",
"Test::File::ShareDir" : "0",
"Test::JSON::Schema::Acceptance" : "1.035",
"Test::Memory::Cycle" : "0",
"Test::More" : "0",
"Test::Needs" : "0",
"Test::Without::Module" : "0.19",
"lib" : "0",
"perl" : "v5.20.0",
"utf8" : "0"
}
},
"x_Dist_Zilla" : {
"requires" : {
"Dist::Zilla" : "5",
"Dist::Zilla::Plugin::Authority" : "1.009",
"Dist::Zilla::Plugin::AutoMetaResources" : "0",
"Dist::Zilla::Plugin::AutoPrereqs" : "5.038",
"Dist::Zilla::Plugin::Breaks" : "0",
"Dist::Zilla::Plugin::BumpVersionAfterRelease::Transitional" : "0.004",
"Dist::Zilla::Plugin::CheckIssues" : "0",
"Dist::Zilla::Plugin::CheckMetaResources" : "0",
"Dist::Zilla::Plugin::CheckPrereqsIndexed" : "0.019",
"Dist::Zilla::Plugin::CheckSelfDependency" : "0",
"Dist::Zilla::Plugin::CheckStrictVersion" : "0",
"Dist::Zilla::Plugin::ConfirmRelease" : "0",
"Dist::Zilla::Plugin::CopyFilesFromRelease" : "0",
"Dist::Zilla::Plugin::DynamicPrereqs" : "0",
"Dist::Zilla::Plugin::EnsureLatestPerl" : "0",
"Dist::Zilla::Plugin::ExecDir" : "0",
"Dist::Zilla::Plugin::FileFinder::ByName" : "0",
"Dist::Zilla::Plugin::GenerateFile::FromShareDir" : "0",
"Dist::Zilla::Plugin::Git::Check" : "0",
"Dist::Zilla::Plugin::Git::CheckFor::CorrectBranch" : "0.004",
"Dist::Zilla::Plugin::Git::CheckFor::MergeConflicts" : "0",
"Dist::Zilla::Plugin::Git::Commit" : "2.020",
"Dist::Zilla::Plugin::Git::Contributors" : "0.029",
"Dist::Zilla::Plugin::Git::Describe" : "0.004",
"Dist::Zilla::Plugin::Git::GatherDir" : "2.016",
"Dist::Zilla::Plugin::Git::Push" : "0",
"Dist::Zilla::Plugin::Git::Remote::Check" : "0",
"Dist::Zilla::Plugin::Git::Tag" : "0",
"Dist::Zilla::Plugin::GitHub::Update" : "0.40",
"Dist::Zilla::Plugin::GithubMeta" : "0.54",
"Dist::Zilla::Plugin::InstallGuide" : "1.200005",
"Dist::Zilla::Plugin::Keywords" : "0.004",
"Dist::Zilla::Plugin::License" : "5.038",
"Dist::Zilla::Plugin::MakeMaker" : "0",
"Dist::Zilla::Plugin::Manifest" : "0",
"Dist::Zilla::Plugin::MetaConfig" : "0",
"Dist::Zilla::Plugin::MetaJSON" : "0",
"Dist::Zilla::Plugin::MetaNoIndex" : "0",
"Dist::Zilla::Plugin::MetaProvides::Package" : "1.15000002",
"Dist::Zilla::Plugin::MetaTests" : "0",
"Dist::Zilla::Plugin::MetaYAML" : "0",
"Dist::Zilla::Plugin::MinimumPerl" : "1.006",
"Dist::Zilla::Plugin::MojibakeTests" : "0.8",
"Dist::Zilla::Plugin::NextRelease" : "5.033",
"Dist::Zilla::Plugin::PodSyntaxTests" : "5.040",
"Dist::Zilla::Plugin::PodWeaver" : "4.008",
"Dist::Zilla::Plugin::Prereqs" : "0",
"Dist::Zilla::Plugin::Prereqs::AuthorDeps" : "0.006",
"Dist::Zilla::Plugin::Prereqs::Soften" : "0",
"Dist::Zilla::Plugin::PromptIfStale" : "0",
"Dist::Zilla::Plugin::Readme" : "0",
"Dist::Zilla::Plugin::ReadmeAnyFromPod" : "0.142180",
"Dist::Zilla::Plugin::RewriteVersion::Transitional" : "0.006",
"Dist::Zilla::Plugin::Run::AfterBuild" : "0.041",
"Dist::Zilla::Plugin::Run::AfterRelease" : "0.038",
"Dist::Zilla::Plugin::Run::BeforeRelease" : "0",
"Dist::Zilla::Plugin::RunExtraTests" : "0.024",
"Dist::Zilla::Plugin::ShareDir" : "0",
"Dist::Zilla::Plugin::StaticInstall" : "0.005",
"Dist::Zilla::Plugin::Test::CPAN::Changes" : "0.012",
"Dist::Zilla::Plugin::Test::ChangesHasContent" : "0",
"Dist::Zilla::Plugin::Test::CheckBreaks" : "0",
"Dist::Zilla::Plugin::Test::CleanNamespaces" : "0.006",
"Dist::Zilla::Plugin::Test::Compile" : "2.039",
"Dist::Zilla::Plugin::Test::EOL" : "0.17",
"Dist::Zilla::Plugin::Test::Kwalitee" : "2.10",
"Dist::Zilla::Plugin::Test::MinimumVersion" : "2.000010",
"Dist::Zilla::Plugin::Test::NoTabs" : "0.08",
"Dist::Zilla::Plugin::Test::Pod::Coverage::TrustMe" : "0",
"Dist::Zilla::Plugin::Test::PodSpelling" : "2.006003",
"Dist::Zilla::Plugin::Test::Portability" : "2.000007",
"Dist::Zilla::Plugin::Test::ReportPrereqs" : "0.022",
"Dist::Zilla::Plugin::TestRelease" : "0",
"Dist::Zilla::Plugin::UploadToCPAN" : "0",
"Dist::Zilla::Plugin::UseUnsafeInc" : "0",
"Dist::Zilla::PluginBundle::Author::ETHER" : "0.154",
"Dist::Zilla::PluginBundle::Git::VersionManager" : "0.007",
"Software::License::Perl_5" : "0"
}
}
},
"provides" : {
"JSON::Schema::Modern" : {
"file" : "lib/JSON/Schema/Modern.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Annotation" : {
"file" : "lib/JSON/Schema/Modern/Annotation.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Document" : {
"file" : "lib/JSON/Schema/Modern/Document.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Error" : {
"file" : "lib/JSON/Schema/Modern/Error.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Result" : {
"file" : "lib/JSON/Schema/Modern/Result.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::ResultNode" : {
"file" : "lib/JSON/Schema/Modern/ResultNode.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Utilities" : {
"file" : "lib/JSON/Schema/Modern/Utilities.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::Applicator" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/Applicator.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::Content" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/Content.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::Core" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/Core.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::FormatAnnotation" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/FormatAnnotation.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::FormatAssertion" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/FormatAssertion.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::MetaData" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/MetaData.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::Unevaluated" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/Unevaluated.pm",
"version" : "0.627"
},
"JSON::Schema::Modern::Vocabulary::Validation" : {
"file" : "lib/JSON/Schema/Modern/Vocabulary/Validation.pm",
"version" : "0.627"
}
},
"release_status" : "stable",
"resources" : {
"bugtracker" : {
"web" : "https://github.com/karenetheridge/JSON-Schema-Modern/issues"
},
"homepage" : "https://github.com/karenetheridge/JSON-Schema-Modern",
"repository" : {
"type" : "git",
"url" : "https://github.com/karenetheridge/JSON-Schema-Modern.git",
"web" : "https://github.com/karenetheridge/JSON-Schema-Modern"
}
},
"version" : "0.627",
"x_Dist_Zilla" : {
"perl" : {
"version" : "5.043005"
},
"plugins" : [
{
"class" : "Dist::Zilla::Plugin::Run::BeforeRelease",
"config" : {
"Dist::Zilla::Plugin::Run::Role::Runner" : {
"eval" : [
"do './update-schemas'; die $@ || $! if $@ || $!"
],
"fatal_errors" : 1,
"quiet" : 0,
"version" : "0.050"
}
},
"name" : "Run::BeforeRelease",
"version" : "0.050"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs",
"config" : {
"Dist::Zilla::Plugin::Prereqs" : {
"phase" : "develop",
"type" : "recommends"
}
},
"name" : "@Author::ETHER/pluginbundle version",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::PromptIfStale",
"config" : {
"Dist::Zilla::Plugin::PromptIfStale" : {
"check_all_plugins" : 0,
"check_all_prereqs" : 0,
"modules" : [
"Dist::Zilla::PluginBundle::Author::ETHER"
],
"phase" : "build",
"run_under_travis" : 0,
"skip" : []
}
},
"name" : "@Author::ETHER/stale modules, build",
"version" : "0.060"
},
{
"class" : "Dist::Zilla::Plugin::ExecDir",
"name" : "@Author::ETHER/ExecDir",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FileFinder::ByName",
"name" : "@Author::ETHER/Examples",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Git::GatherDir",
"config" : {
"Dist::Zilla::Plugin::GatherDir" : {
"exclude_filename" : [
"CONTRIBUTING",
"INSTALL",
"LICENCE",
"README.pod",
"TODO",
"pull_request_template.md"
],
"exclude_match" : [],
"include_dotfiles" : 0,
"prefix" : "",
"prune_directory" : [],
"root" : "."
},
"Dist::Zilla::Plugin::Git::GatherDir" : {
"include_untracked" : 0
}
},
"name" : "@Author::ETHER/Git::GatherDir",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::MetaYAML",
"name" : "@Author::ETHER/MetaYAML",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::MetaJSON",
"name" : "@Author::ETHER/MetaJSON",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Readme",
"name" : "@Author::ETHER/Readme",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Manifest",
"name" : "@Author::ETHER/Manifest",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::License",
"name" : "@Author::ETHER/License",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::GenerateFile::FromShareDir",
"config" : {
"Dist::Zilla::Plugin::GenerateFile::FromShareDir" : {
"destination_filename" : "CONTRIBUTING",
"dist" : "Dist-Zilla-PluginBundle-Author-ETHER",
"encoding" : "UTF-8",
"has_xs" : 0,
"location" : "build",
"source_filename" : "CONTRIBUTING"
},
"Dist::Zilla::Role::RepoFileInjector" : {
"allow_overwrite" : 1,
"repo_root" : ".",
"version" : "0.009"
}
},
"name" : "@Author::ETHER/generate CONTRIBUTING",
"version" : "0.015"
},
{
"class" : "Dist::Zilla::Plugin::InstallGuide",
"config" : {
"Dist::Zilla::Role::ModuleMetadata" : {
"Module::Metadata" : "1.000038",
"version" : "0.006"
}
},
"name" : "@Author::ETHER/InstallGuide",
"version" : "1.200014"
},
{
"class" : "Dist::Zilla::Plugin::Test::Compile",
"config" : {
"Dist::Zilla::Plugin::Test::Compile" : {
"bail_out_on_fail" : 1,
"fail_on_warning" : "author",
"fake_home" : 0,
"filename" : "xt/author/00-compile.t",
"module_finder" : [
":InstallModules"
],
"needs_display" : 0,
"phase" : "develop",
"script_finder" : [
":PerlExecFiles",
"@Author::ETHER/Examples"
],
"skips" : [],
"switch" : []
}
},
"name" : "@Author::ETHER/Test::Compile",
"version" : "2.058"
},
{
"class" : "Dist::Zilla::Plugin::Test::NoTabs",
"config" : {
"Dist::Zilla::Plugin::Test::NoTabs" : {
"filename" : "xt/author/no-tabs.t",
"finder" : [
":InstallModules",
":ExecFiles",
"@Author::ETHER/Examples",
":TestFiles",
":ExtraTestFiles"
]
}
},
"name" : "@Author::ETHER/Test::NoTabs",
"version" : "0.15"
},
{
"class" : "Dist::Zilla::Plugin::Test::EOL",
"config" : {
"Dist::Zilla::Plugin::Test::EOL" : {
"filename" : "xt/author/eol.t",
"finder" : [
":ExecFiles",
":ExtraTestFiles",
":InstallModules",
":TestFiles",
"@Author::ETHER/Examples"
],
"trailing_whitespace" : 1
}
},
"name" : "@Author::ETHER/Test::EOL",
"version" : "0.19"
},
{
"class" : "Dist::Zilla::Plugin::MetaTests",
"name" : "@Author::ETHER/MetaTests",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Test::CPAN::Changes",
"config" : {
"Dist::Zilla::Plugin::Test::CPAN::Changes" : {
"changelog" : "Changes",
"filename" : "xt/release/cpan-changes.t"
}
},
"name" : "@Author::ETHER/Test::CPAN::Changes",
"version" : "0.013"
},
{
"class" : "Dist::Zilla::Plugin::Test::ChangesHasContent",
"name" : "@Author::ETHER/Test::ChangesHasContent",
"version" : "0.011"
},
{
"class" : "Dist::Zilla::Plugin::Test::MinimumVersion",
"config" : {
"Dist::Zilla::Plugin::Test::MinimumVersion" : {
"max_target_perl" : "5.020"
}
},
"name" : "@Author::ETHER/Test::MinimumVersion",
"version" : "2.000011"
},
{
"class" : "Dist::Zilla::Plugin::PodSyntaxTests",
"name" : "@Author::ETHER/PodSyntaxTests",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Test::Pod::Coverage::TrustMe",
"config" : {
"Dist::Zilla::Plugin::Test::Pod::Coverage::TrustMe" : {
"finder" : [
":InstallModules"
]
}
},
"name" : "@Author::ETHER/Test::Pod::Coverage::TrustMe",
"version" : "v1.0.1"
},
{
"class" : "Dist::Zilla::Plugin::Test::PodSpelling",
"config" : {
"Dist::Zilla::Plugin::Test::PodSpelling" : {
"directories" : [
"examples",
"lib",
"script",
"t",
"xt"
],
"spell_cmd" : "",
"stopwords" : [
"irc"
],
"wordlist" : "Pod::Wordlist"
}
},
"name" : "@Author::ETHER/Test::PodSpelling",
"version" : "2.007006"
},
{
"class" : "Dist::Zilla::Plugin::Test::Kwalitee",
"config" : {
"Dist::Zilla::Plugin::Test::Kwalitee" : {
"filename" : "xt/author/kwalitee.t",
"skiptest" : []
}
},
"name" : "@Author::ETHER/Test::Kwalitee",
"version" : "2.12"
},
{
"class" : "Dist::Zilla::Plugin::MojibakeTests",
"name" : "@Author::ETHER/MojibakeTests",
"version" : "0.8"
},
{
"class" : "Dist::Zilla::Plugin::Test::ReportPrereqs",
"name" : "@Author::ETHER/Test::ReportPrereqs",
"version" : "0.029"
},
{
"class" : "Dist::Zilla::Plugin::Test::Portability",
"config" : {
"Dist::Zilla::Plugin::Test::Portability" : {
"options" : ""
}
},
"name" : "@Author::ETHER/Test::Portability",
"version" : "2.001003"
},
{
"class" : "Dist::Zilla::Plugin::Test::CleanNamespaces",
"config" : {
"Dist::Zilla::Plugin::Test::CleanNamespaces" : {
"filename" : "xt/author/clean-namespaces.t",
"skips" : []
}
},
"name" : "@Author::ETHER/Test::CleanNamespaces",
"version" : "0.006"
},
{
"class" : "Dist::Zilla::Plugin::Git::Describe",
"name" : "@Author::ETHER/Git::Describe",
"version" : "0.008"
},
{
"class" : "Dist::Zilla::Plugin::PodWeaver",
"config" : {
"Dist::Zilla::Plugin::PodWeaver" : {
"finder" : [
":InstallModules",
":PerlExecFiles"
],
"plugins" : [
{
"class" : "Pod::Weaver::Section::GenerateSection",
"name" : "SUPPORT",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::GenerateSection",
"name" : "COPYRIGHT AND LICENCE",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Plugin::EnsurePod5",
"name" : "@Author::ETHER/EnsurePod5",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Plugin::H1Nester",
"name" : "@Author::ETHER/H1Nester",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Plugin::SingleEncoding",
"name" : "@Author::ETHER/SingleEncoding",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Plugin::Transformer",
"name" : "@Author::ETHER/List",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Plugin::Transformer",
"name" : "@Author::ETHER/Verbatim",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Region",
"name" : "@Author::ETHER/header",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Name",
"name" : "@Author::ETHER/Name",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Version",
"name" : "@Author::ETHER/Version",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Region",
"name" : "@Author::ETHER/prelude",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Generic",
"name" : "SYNOPSIS",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Generic",
"name" : "DESCRIPTION",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Generic",
"name" : "OVERVIEW",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Collect",
"name" : "ATTRIBUTES",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Collect",
"name" : "METHODS",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Collect",
"name" : "FUNCTIONS",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Collect",
"name" : "TYPES",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Leftovers",
"name" : "@Author::ETHER/Leftovers",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Region",
"name" : "@Author::ETHER/postlude",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::GenerateSection",
"name" : "@Author::ETHER/generate GIVING THANKS",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::GenerateSection",
"name" : "@Author::ETHER/generate SUPPORT",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Authors",
"name" : "@Author::ETHER/Authors",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::AllowOverride",
"name" : "@Author::ETHER/allow override AUTHOR",
"version" : "0.05"
},
{
"class" : "Pod::Weaver::Section::Contributors",
"name" : "@Author::ETHER/Contributors",
"version" : "0.009"
},
{
"class" : "Pod::Weaver::Section::Legal",
"name" : "@Author::ETHER/Legal",
"version" : "4.020"
},
{
"class" : "Pod::Weaver::Section::Region",
"name" : "@Author::ETHER/footer",
"version" : "4.020"
},
{
"class" : "inc::AppendSection",
"name" : "AppendSupport",
"version" : null
},
{
"class" : "inc::AppendSection",
"name" : "AppendCopyright",
"version" : null
}
]
}
},
"name" : "@Author::ETHER/PodWeaver",
"version" : "4.010"
},
{
"class" : "Dist::Zilla::Plugin::GithubMeta",
"name" : "@Author::ETHER/GithubMeta",
"version" : "0.58"
},
{
"class" : "Dist::Zilla::Plugin::AutoMetaResources",
"name" : "@Author::ETHER/AutoMetaResources",
"version" : "1.21"
},
{
"class" : "Dist::Zilla::Plugin::Authority",
"name" : "@Author::ETHER/Authority",
"version" : "1.009"
},
{
"class" : "Dist::Zilla::Plugin::MetaNoIndex",
"name" : "@Author::ETHER/MetaNoIndex",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::MetaProvides::Package",
"config" : {
"Dist::Zilla::Plugin::MetaProvides::Package" : {
"finder" : [
":InstallModules"
],
"finder_objects" : [
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":InstallModules",
"version" : "6.036"
}
],
"include_underscores" : 0
},
"Dist::Zilla::Role::MetaProvider::Provider" : {
"$Dist::Zilla::Role::MetaProvider::Provider::VERSION" : "2.002004",
"inherit_missing" : 0,
"inherit_version" : 0,
"meta_noindex" : 1
},
"Dist::Zilla::Role::ModuleMetadata" : {
"Module::Metadata" : "1.000038",
"version" : "0.006"
}
},
"name" : "@Author::ETHER/MetaProvides::Package",
"version" : "2.004003"
},
{
"class" : "Dist::Zilla::Plugin::MetaConfig",
"name" : "@Author::ETHER/MetaConfig",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Keywords",
"config" : {
"Dist::Zilla::Plugin::Keywords" : {
"keywords" : [
"JSON",
"Schema",
"validator",
"data",
"validation",
"structure",
"specification"
]
}
},
"name" : "@Author::ETHER/Keywords",
"version" : "0.007"
},
{
"class" : "Dist::Zilla::Plugin::UseUnsafeInc",
"config" : {
"Dist::Zilla::Plugin::UseUnsafeInc" : {
"dot_in_INC" : 0
}
},
"name" : "@Author::ETHER/UseUnsafeInc",
"version" : "0.002"
},
{
"class" : "Dist::Zilla::Plugin::AutoPrereqs",
"name" : "@Author::ETHER/AutoPrereqs",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs::AuthorDeps",
"name" : "@Author::ETHER/Prereqs::AuthorDeps",
"version" : "0.007"
},
{
"class" : "Dist::Zilla::Plugin::MinimumPerl",
"name" : "@Author::ETHER/MinimumPerl",
"version" : "1.006"
},
{
"class" : "Dist::Zilla::Plugin::MakeMaker",
"config" : {
"Dist::Zilla::Role::TestRunner" : {
"default_jobs" : 9
}
},
"name" : "@Author::ETHER/MakeMaker",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Git::Contributors",
"config" : {
"Dist::Zilla::Plugin::Git::Contributors" : {
"git_version" : "2.50.1",
"include_authors" : 0,
"include_releaser" : 1,
"order_by" : "commits",
"paths" : []
}
},
"name" : "@Author::ETHER/Git::Contributors",
"version" : "0.038"
},
{
"class" : "Dist::Zilla::Plugin::StaticInstall",
"config" : {
"Dist::Zilla::Plugin::StaticInstall" : {
"dry_run" : 0,
"mode" : "off"
}
},
"name" : "@Author::ETHER/StaticInstall",
"version" : "0.012"
},
{
"class" : "Dist::Zilla::Plugin::RunExtraTests",
"config" : {
"Dist::Zilla::Role::TestRunner" : {
"default_jobs" : 9
}
},
"name" : "@Author::ETHER/RunExtraTests",
"version" : "0.029"
},
{
"class" : "Dist::Zilla::Plugin::CheckSelfDependency",
"config" : {
"Dist::Zilla::Plugin::CheckSelfDependency" : {
"finder" : [
":InstallModules"
]
},
"Dist::Zilla::Role::ModuleMetadata" : {
"Module::Metadata" : "1.000038",
"version" : "0.006"
}
},
"name" : "@Author::ETHER/CheckSelfDependency",
"version" : "0.011"
},
{
"class" : "Dist::Zilla::Plugin::Run::AfterBuild",
"config" : {
"Dist::Zilla::Plugin::Run::Role::Runner" : {
"fatal_errors" : 1,
"quiet" : 1,
"run" : [
"bash -c \"test -e .ackrc && grep -q -- '--ignore-dir=.latest' .ackrc || echo '--ignore-dir=.latest' >> .ackrc; if [[ `dirname '%d'` != .build ]]; then test -e .ackrc && grep -q -- '--ignore-dir=%d' .ackrc || echo '--ignore-dir=%d' >> .ackrc; fi\""
],
"version" : "0.050"
}
},
"name" : "@Author::ETHER/.ackrc",
"version" : "0.050"
},
{
"class" : "Dist::Zilla::Plugin::Run::AfterBuild",
"config" : {
"Dist::Zilla::Plugin::Run::Role::Runner" : {
"eval" : [
"if ('%d' =~ /^%n-[.[:xdigit:]]+$/) { unlink '.latest'; symlink '%d', '.latest'; }"
],
"fatal_errors" : 0,
"quiet" : 1,
"version" : "0.050"
}
},
"name" : "@Author::ETHER/.latest",
"version" : "0.050"
},
{
"class" : "Dist::Zilla::Plugin::CheckStrictVersion",
"name" : "@Author::ETHER/CheckStrictVersion",
"version" : "0.001"
},
{
"class" : "Dist::Zilla::Plugin::CheckMetaResources",
"name" : "@Author::ETHER/CheckMetaResources",
"version" : "0.001"
},
{
"class" : "Dist::Zilla::Plugin::EnsureLatestPerl",
"config" : {
"Dist::Zilla::Plugin::EnsureLatestPerl" : {
"Module::CoreList" : "5.20251120"
}
},
"name" : "@Author::ETHER/EnsureLatestPerl",
"version" : "0.010"
},
{
"class" : "Dist::Zilla::Plugin::PromptIfStale",
"config" : {
"Dist::Zilla::Plugin::PromptIfStale" : {
"check_all_plugins" : 1,
"check_all_prereqs" : 1,
"modules" : [],
"phase" : "release",
"run_under_travis" : 0,
"skip" : []
}
},
"name" : "@Author::ETHER/stale modules, release",
"version" : "0.060"
},
{
"class" : "Dist::Zilla::Plugin::Git::Check",
"config" : {
"Dist::Zilla::Plugin::Git::Check" : {
"untracked_files" : "die"
},
"Dist::Zilla::Role::Git::DirtyFiles" : {
"allow_dirty" : [],
"allow_dirty_match" : [],
"changelog" : "Changes"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
}
},
"name" : "@Author::ETHER/initial check",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::Git::CheckFor::MergeConflicts",
"config" : {
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
}
},
"name" : "@Author::ETHER/Git::CheckFor::MergeConflicts",
"version" : "0.014"
},
{
"class" : "Dist::Zilla::Plugin::Git::CheckFor::CorrectBranch",
"config" : {
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
}
},
"name" : "@Author::ETHER/Git::CheckFor::CorrectBranch",
"version" : "0.014"
},
{
"class" : "Dist::Zilla::Plugin::Git::Remote::Check",
"name" : "@Author::ETHER/Git::Remote::Check",
"version" : "0.1.2"
},
{
"class" : "Dist::Zilla::Plugin::CheckPrereqsIndexed",
"name" : "@Author::ETHER/CheckPrereqsIndexed",
"version" : "0.022"
},
{
"class" : "Dist::Zilla::Plugin::TestRelease",
"name" : "@Author::ETHER/TestRelease",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Git::Check",
"config" : {
"Dist::Zilla::Plugin::Git::Check" : {
"untracked_files" : "die"
},
"Dist::Zilla::Role::Git::DirtyFiles" : {
"allow_dirty" : [],
"allow_dirty_match" : [],
"changelog" : "Changes"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
}
},
"name" : "@Author::ETHER/after tests",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::CheckIssues",
"name" : "@Author::ETHER/CheckIssues",
"version" : "0.011"
},
{
"class" : "Dist::Zilla::Plugin::UploadToCPAN",
"name" : "@Author::ETHER/UploadToCPAN",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::CopyFilesFromRelease",
"config" : {
"Dist::Zilla::Plugin::CopyFilesFromRelease" : {
"filename" : [
"CONTRIBUTING",
"INSTALL",
"LICENCE",
"LICENSE",
"ppport.h"
],
"match" : []
}
},
"name" : "@Author::ETHER/copy generated files",
"version" : "0.007"
},
{
"class" : "Dist::Zilla::Plugin::ReadmeAnyFromPod",
"config" : {
"Dist::Zilla::Role::FileWatcher" : {
"version" : "0.006"
}
},
"name" : "@Author::ETHER/ReadmeAnyFromPod",
"version" : "0.163250"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs",
"config" : {
"Dist::Zilla::Plugin::Prereqs" : {
"phase" : "develop",
"type" : "recommends"
}
},
"name" : "@Author::ETHER/@Git::VersionManager/pluginbundle version",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::RewriteVersion::Transitional",
"config" : {
"Dist::Zilla::Plugin::RewriteVersion" : {
"add_tarball_name" : 0,
"finders" : [
":ExecFiles",
":InstallModules"
],
"global" : 1,
"skip_version_provider" : 0
},
"Dist::Zilla::Plugin::RewriteVersion::Transitional" : {}
},
"name" : "@Author::ETHER/@Git::VersionManager/RewriteVersion::Transitional",
"version" : "0.009"
},
{
"class" : "Dist::Zilla::Plugin::MetaProvides::Update",
"name" : "@Author::ETHER/@Git::VersionManager/MetaProvides::Update",
"version" : "0.007"
},
{
"class" : "Dist::Zilla::Plugin::CopyFilesFromRelease",
"config" : {
"Dist::Zilla::Plugin::CopyFilesFromRelease" : {
"filename" : [
"Changes"
],
"match" : []
}
},
"name" : "@Author::ETHER/@Git::VersionManager/CopyFilesFromRelease",
"version" : "0.007"
},
{
"class" : "Dist::Zilla::Plugin::Git::Commit",
"config" : {
"Dist::Zilla::Plugin::Git::Commit" : {
"add_files_in" : [
"."
],
"commit_msg" : "%N-%v%t%n%n%c",
"signoff" : 0
},
"Dist::Zilla::Role::Git::DirtyFiles" : {
"allow_dirty" : [
"CONTRIBUTING",
"Changes",
"INSTALL",
"LICENCE",
"README.pod"
],
"allow_dirty_match" : [],
"changelog" : "Changes"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
},
"Dist::Zilla::Role::Git::StringFormatter" : {
"time_zone" : "local"
}
},
"name" : "@Author::ETHER/@Git::VersionManager/release snapshot",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::Git::Tag",
"config" : {
"Dist::Zilla::Plugin::Git::Tag" : {
"branch" : null,
"changelog" : "Changes",
"signed" : 0,
"tag" : "v0.627",
"tag_format" : "v%V",
"tag_message" : "v%v%t"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
},
"Dist::Zilla::Role::Git::StringFormatter" : {
"time_zone" : "local"
}
},
"name" : "@Author::ETHER/@Git::VersionManager/Git::Tag",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::BumpVersionAfterRelease::Transitional",
"config" : {
"Dist::Zilla::Plugin::BumpVersionAfterRelease" : {
"finders" : [
":InstallModules"
],
"global" : 1,
"munge_makefile_pl" : 1
},
"Dist::Zilla::Plugin::BumpVersionAfterRelease::Transitional" : {}
},
"name" : "@Author::ETHER/@Git::VersionManager/BumpVersionAfterRelease::Transitional",
"version" : "0.009"
},
{
"class" : "Dist::Zilla::Plugin::NextRelease",
"name" : "@Author::ETHER/@Git::VersionManager/NextRelease",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Git::Commit",
"config" : {
"Dist::Zilla::Plugin::Git::Commit" : {
"add_files_in" : [],
"commit_msg" : "increment $VERSION after %v release",
"signoff" : 0
},
"Dist::Zilla::Role::Git::DirtyFiles" : {
"allow_dirty" : [
"Build.PL",
"Changes",
"Makefile.PL"
],
"allow_dirty_match" : [
"(?^:^lib/.*\\.pm$)"
],
"changelog" : "Changes"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
},
"Dist::Zilla::Role::Git::StringFormatter" : {
"time_zone" : "local"
}
},
"name" : "@Author::ETHER/@Git::VersionManager/post-release commit",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs",
"config" : {
"Dist::Zilla::Plugin::Prereqs" : {
"phase" : "x_Dist_Zilla",
"type" : "requires"
}
},
"name" : "@Author::ETHER/@Git::VersionManager/prereqs for @Git::VersionManager",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Git::Push",
"config" : {
"Dist::Zilla::Plugin::Git::Push" : {
"push_to" : [
"origin"
],
"remotes_must_exist" : 1
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.50.1",
"repo_root" : "."
}
},
"name" : "@Author::ETHER/Git::Push",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::GitHub::Update",
"config" : {
"Dist::Zilla::Plugin::GitHub::Update" : {
"metacpan" : 1
}
},
"name" : "@Author::ETHER/GitHub::Update",
"version" : "0.49"
},
{
"class" : "Dist::Zilla::Plugin::Run::AfterRelease",
"config" : {
"Dist::Zilla::Plugin::Run::Role::Runner" : {
"fatal_errors" : 0,
"quiet" : 0,
"run" : [
"REDACTED"
],
"version" : "0.050"
}
},
"name" : "@Author::ETHER/install release",
"version" : "0.050"
},
{
"class" : "Dist::Zilla::Plugin::Run::AfterRelease",
"config" : {
"Dist::Zilla::Plugin::Run::Role::Runner" : {
"eval" : [
"print \"release complete!\\xa\""
],
"fatal_errors" : 1,
"quiet" : 1,
"version" : "0.050"
}
},
"name" : "@Author::ETHER/release complete",
"version" : "0.050"
},
{
"class" : "Dist::Zilla::Plugin::ConfirmRelease",
"name" : "@Author::ETHER/ConfirmRelease",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs",
"config" : {
"Dist::Zilla::Plugin::Prereqs" : {
"phase" : "x_Dist_Zilla",
"type" : "requires"
}
},
"name" : "@Author::ETHER/prereqs for @Author::ETHER",
"version" : "6.036"
},
{
"class" : "inc::CheckConflicts",
"name" : "=inc::CheckConflicts",
"version" : null
},
{
"class" : "Dist::Zilla::Plugin::ShareDir",
"name" : "ShareDir",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs",
"config" : {
"Dist::Zilla::Plugin::Prereqs" : {
"phase" : "runtime",
"type" : "requires"
}
},
"name" : "RuntimeRequires",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs",
"config" : {
"Dist::Zilla::Plugin::Prereqs" : {
"phase" : "runtime",
"type" : "suggests"
}
},
"name" : "RuntimeSuggests",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::Prereqs::Soften",
"config" : {
"Dist::Zilla::Plugin::Prereqs::Soften" : {
"copy_to" : [
"develop.requires"
],
"modules" : [
"Time::Moment",
"DateTime::Format::RFC3339",
"Data::Validate::Domain",
"Email::Address::XS",
"Net::IDN::Encode",
"Sereal",
"JSON::PP",
"Cpanel::JSON::XS"
],
"modules_from_features" : null,
"to_relationship" : "suggests"
}
},
"name" : "Prereqs::Soften",
"version" : "0.006003"
},
{
"class" : "Dist::Zilla::Plugin::DynamicPrereqs",
"config" : {
"Dist::Zilla::Role::ModuleMetadata" : {
"Module::Metadata" : "1.000038",
"version" : "0.006"
}
},
"name" : "DynamicPrereqs",
"version" : "0.040"
},
{
"class" : "Dist::Zilla::Plugin::Breaks",
"name" : "Breaks",
"version" : "0.005"
},
{
"class" : "Dist::Zilla::Plugin::Test::CheckBreaks",
"config" : {
"Dist::Zilla::Plugin::Test::CheckBreaks" : {
"conflicts_module" : [],
"no_forced_deps" : 0
},
"Dist::Zilla::Role::ModuleMetadata" : {
"Module::Metadata" : "1.000038",
"version" : "0.006"
}
},
"name" : "Test::CheckBreaks",
"version" : "0.020"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":InstallModules",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":IncModules",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":TestFiles",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":ExtraTestFiles",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":ExecFiles",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":PerlExecFiles",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":ShareFiles",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":MainModule",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":AllFiles",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::FinderCode",
"name" : ":NoFiles",
"version" : "6.036"
},
{
"class" : "Dist::Zilla::Plugin::VerifyPhases",
"name" : "@Author::ETHER/PHASE VERIFICATION",
"version" : "0.016"
}
],
"zilla" : {
"class" : "Dist::Zilla::Dist::Builder",
"config" : {
"is_trial" : 0
},
"version" : "6.036"
}
},
"x_authority" : "cpan:ETHER",
"x_breaks" : {
"JSON::Schema::Modern::Document::OpenAPI" : "< 0.097",
"JSON::Schema::Modern::Vocabulary::OpenAPI" : "< 0.080",
"Mojolicious::Plugin::OpenAPI::Modern" : "< 0.014",
"OpenAPI::Modern" : "< 0.077",
"Test::Mojo::Role::OpenAPI::Modern" : "< 0.007"
},
"x_generated_by_perl" : "v5.43.5",
"x_serialization_backend" : "Cpanel::JSON::XS version 4.40",
"x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later",
"x_static_install" : 0,
"x_use_unsafe_inc" : 0
}
errors.t 100640 000766 000024 140220 15114374332 16400 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new(short_circuit => 0);
my $js_short = JSON::Schema::Modern->new(short_circuit => 1);
subtest 'multiple types' => sub {
my $result = $js->evaluate(true, { type => ['string','number'] });
ok(!$result->valid, 'type returned false');
is($result->error_count, 1, 'got error count');
cmp_result(
[ $result->errors ],
[
all(
isa('JSON::Schema::Modern::Error'),
methods(
instance_location => '',
keyword_location => '/type',
absolute_keyword_location => undef,
error => 'got boolean, not one of string, number',
),
),
],
'correct error generated from type',
);
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/type',
error => 'got boolean, not one of string, number',
},
],
},
'result object serializes correctly',
);
cmp_result(
my $e = ($result->errors)[0]->clone(error => 'oh noes'),
methods(
instance_location => '',
keyword_location => '/type',
absolute_keyword_location => undef,
error => 'oh noes',
),
'cloning leaves absolute_keyword_location as-is',
);
};
subtest 'multipleOf' => sub {
cmp_result(
$js->evaluate(3, { multipleOf => 2 })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/multipleOf',
error => 'value is not a multiple of 2',
},
],
},
'correct error generated from multipleOf',
);
};
subtest 'uniqueItems' => sub {
cmp_result(
$js->evaluate([qw(a b c d c)], { uniqueItems => true })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/uniqueItems',
error => 'items at indices 2 and 4 are not unique',
},
],
},
'correct error generated from uniqueItems',
);
};
subtest 'allOf, not, and false schema' => sub {
cmp_result(
$js->evaluate(
my $data = 1,
my $schema = { allOf => [ true, false, { not => { not => false } } ] },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/1',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/allOf/2/not',
error => 'subschema is valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschemas 1, 2 are not valid',
},
],
},
'correct errors with locations; did not collect errors inside "not"',
);
cmp_result(
$js_short->evaluate($data, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/1',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschema 1 is not valid',
},
],
},
'short-circuited results contain fewer errors',
);
};
subtest 'anyOf keeps all errors for false paths when invalid, discards errors for false paths when valid' => sub {
cmp_result(
$js->evaluate(
my $data = 1,
my $schema = { anyOf => [ false, false ] },
)->TO_JSON,
my $result = {
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/anyOf/0',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
error => 'no subschemas are valid',
},
],
},
'correct errors with locations; did not collect errors inside "not"',
);
cmp_result(
$js_short->evaluate($data, $schema)->TO_JSON,
$result,
'short-circuited results contain the same errors (short-circuiting not possible)',
);
cmp_result(
$result = $js->evaluate(1, { anyOf => [ false, true ], not => true })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/not',
error => 'subschema is true',
},
],
},
'did not collect errors from failure paths from successful anyOf',
);
cmp_result(
$js->evaluate(1, { anyOf => [ false, true ] })->TO_JSON,
{ valid => true },
'no errors collected for true validation',
);
};
subtest 'applicators with non-boolean subschemas, discarding intermediary errors - items' => sub {
my $result = $js->evaluate(
my $data = [ 1, 2 ],
my $schema = {
items => {
anyOf => [
{ minimum => 2 },
{ allOf => [ { maximum => -1 }, { maximum => 0 } ] },
]
},
},
);
# - evaluate /items on instance ''
# - evaluate /items on instance /0
# - evaluate /items/anyOf on instance /0
# - evaluate /items/anyOf/0 on instance /0
# - evaluate /items/anyOf/0/minimum on instance /0 FAIL
# /items/anyOf/0 FAILS
# - evaluate /items/anyOf/1 on instance /0
# - evaluate /items/anyOf/1/allOf on instance /0
# - evaluate /items/anyOf/1/allOf/0 on instance /0
# - evaluate /items/anyOf/1/allOf/0/maximum on instance /0 FAIL
# - evaluate /items/anyOf/1/allOf/1 on instance /0
# - evaluate /items/anyOf/1/allOf/1/maximum on instance /0 FAIL
# /items/anyOf/1/allOf FAILS
# /items/anyOf/1 FAILS (no message)
# /items/anyOf FAILS
# /items FAILS on instance /0 (no message)
# - evaluate /items on instance /1
# - evaluate /items/anyOf on instance /1
# - evaluate /items/anyOf/0 on instance /1
# - evaluate /items/anyOf/0/minimum on instance /1 PASS
# /items/anyOf/0 PASSES
# - evaluate /items/anyOf/1 on instance /1
# - evaluate /items/anyOf/1/allOf on instance /1
# - evaluate /items/anyOf/1/allOf/0 on instance /1
# - evaluate /items/anyOf/1/allOf/0/maximum on instance /1 FAIL
# - evaluate /items/anyOf/1/allOf/1 on instance /1
# - evaluate /items/anyOf/1/allOf/1/maximum on instance /1 FAIL
# /items/anyOf/1/allOf FAILS
# /items/anyOf/1 FAILS (no message)
# /items/anyOf PASSES -- all failures above are discarded
# /items PASSES on instance /1
# /items FAILS (across all instances)
# entire schema FAILS
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf/0/minimum',
error => 'value is less than 2',
},
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf/1/allOf/0/maximum',
error => 'value is greater than -1',
},
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf/1/allOf/1/maximum',
error => 'value is greater than 0',
},
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf/1/allOf',
error => 'subschemas 0, 1 are not valid',
},
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf',
error => 'no subschemas are valid',
},
# these errors are discarded because /items/anyOf passes on instance /1
#{
# instanceLocation => '/1',
# keywordLocation => '/items/anyOf/1/allOf/0/maximum',
# error => 'value is greater than -1',
#},
#{
# instanceLocation => '/1',
# keywordLocation => '/items/anyOf/1/allOf/1/maximum',
# error => 'value is greater than 0',
#},
#{
# instanceLocation => '/1',
# keywordLocation => '/items/anyOf/1/allOf',
# error => 'subschemas 0, 1 are not valid',
#},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'collected all errors from subschemas for failing branches only (passing branches discard errors)',
);
cmp_result(
$js_short->evaluate($data, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf/0/minimum',
error => 'value is less than 2'
},
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf/1/allOf/0/maximum',
error => 'value is greater than -1',
},
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf/1/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '/0',
keywordLocation => '/items/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'short-circuited results contain fewer errors',
);
};
subtest 'applicators with non-boolean subschemas, discarding intermediary errors - contains' => sub {
my $result = $js->evaluate(
my $data = [
{ foo => 1 },
{ bar => 2 },
],
my $schema = {
not => true,
contains => {
properties => {
foo => false, # if 'foo' is present, then we fail
},
},
},
);
# - evaluate /not on instance ''
# - evaluate subschema "true" - PASS
# /not FAILS.
# - evaluate /contains on instance ''
# - evaluate /contains on instance /0
# - evaluate /contains/properties on instance /0
# - evaluate /contains/properties/foo on instance /0/foo
# schema is FALSE.
# /contains/properties FAILS
# /contains does not match on instance /0
# - evaluate /contains on instance /1
# - evaluate /contains/properties on instance /1
# - evaluate /contains/properties/foo on instance /1/foo - does not exist.
# /contains/properties/foo PASSES
# /contains/properties PASSES
# /contains matches on instance /1
# /contains has at least 1 match; it PASSES
# entire schema FAILS
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/not',
error => 'subschema is true',
},
# these errors are discarded because /contains passes on instance /1
#{
# instanceLocation => '/0/foo',
# keywordLocation => '/contains/properties/foo',
# error => 'subschema is false',
#},
#{
# instanceLocation => '/0',
# keywordLocation => '/contains/properties',
# error => 'not all properties are valid',
#},
],
},
'collected all errors from subschemas for failing branches only (passing branches discard errors)',
);
cmp_result(
$js_short->evaluate($data, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/not',
error => 'subschema is true',
},
],
},
'short-circuited results contain the same errors',
);
};
subtest 'errors with $refs' => sub {
my $result = $js->evaluate(
[ { x => 1 }, { x => 2 }, { x => 3 } ],
{
'$defs' => {
mydef => {
type => 'integer',
minimum => 5,
'$ref' => '#/$defs/myint',
},
myint => {
multipleOf => 5,
},
},
items => {
properties => {
x => {
'$ref' => '#/$defs/mydef',
maximum => 2,
},
},
}
},
);
# evaluation order:
# /items/properties/x/$ref (mydef) /$ref (myint) /multipleOf
# /items/properties/x/$ref (mydef) /type
# /items/properties/x/$ref (mydef) /minimum
# /items/properties/x/maximum
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0/x',
keywordLocation => '/items/properties/x/$ref/$ref/multipleOf',
absoluteKeywordLocation => '#/$defs/myint/multipleOf',
error => 'value is not a multiple of 5',
},
{
instanceLocation => '/0/x',
keywordLocation => '/items/properties/x/$ref/minimum',
absoluteKeywordLocation => '#/$defs/mydef/minimum',
error => 'value is less than 5',
},
{
instanceLocation => '/0',
keywordLocation => '/items/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '/1/x',
keywordLocation => '/items/properties/x/$ref/$ref/multipleOf',
absoluteKeywordLocation => '#/$defs/myint/multipleOf',
error => 'value is not a multiple of 5',
},
{
instanceLocation => '/1/x',
keywordLocation => '/items/properties/x/$ref/minimum',
absoluteKeywordLocation => '#/$defs/mydef/minimum',
error => 'value is less than 5',
},
{
instanceLocation => '/1',
keywordLocation => '/items/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '/2/x',
keywordLocation => '/items/properties/x/$ref/$ref/multipleOf',
absoluteKeywordLocation => '#/$defs/myint/multipleOf',
error => 'value is not a multiple of 5',
},
{
instanceLocation => '/2/x',
keywordLocation => '/items/properties/x/$ref/minimum',
absoluteKeywordLocation => '#/$defs/mydef/minimum',
error => 'value is less than 5',
},
{
instanceLocation => '/2/x',
keywordLocation => '/items/properties/x/maximum',
error => 'value is greater than 2',
},
{
instanceLocation => '/2',
keywordLocation => '/items/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'errors have correct absolute keyword location via $ref',
);
};
subtest 'const and enum' => sub {
cmp_result(
$js->evaluate(
{ foo => { a => { b => { c => { d => 1 } } } } },
{
properties => {
foo => {
allOf => [
{ const => { a => { b => { c => { d => 2 } } } } },
{ enum => [ 0, 'whargarbl', { a => { b => { c => { d => 2 } } } } ] },
],
}
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/allOf/0/const',
error => 'value does not match (at \'/a/b/c/d\': integers not equal)',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/allOf/1/enum',
error => 'value does not match (from enum 0 at \'\': wrong type: object vs integer; from enum 1 at \'\': wrong type: object vs string; from enum 2 at \'/a/b/c/d\': integers not equal)',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/allOf',
error => 'subschemas 0, 1 are not valid',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'got details about object differences in errors from const and enum',
);
};
subtest 'exceptions' => sub {
cmp_result(
(my $result = $js->evaluate_json_string('[ 1, 2, 3, whargarbl ]', true))->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => re(qr/malformed JSON string/),
},
],
},
'attempting to evaluate a json string returns the exception as an error',
);
ok($result->exception, 'exception flag is true on the result');
cmp_result(
($result = $js->evaluate(
{ x => 'hello' },
{
allOf => [
{ properties => { x => 1 } },
{ properties => { x => 'hi' } },
],
}
))->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/properties/x',
error => 'invalid schema type: integer',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/properties/x',
error => 'invalid schema type: string',
},
],
},
'a subschema of an invalid type returns an error at the right position, and traversal continues',
);
ok($result->exception, 'exception flag is true on the result');
cmp_result(
($result = $js->evaluate(
1,
{
allOf => [
{ type => 'whargarbl' },
{ type => 'whoops' },
],
}
))->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
error => 'unrecognized type "whargarbl"',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/type',
error => 'unrecognized type "whoops"',
},
],
},
'invalid argument to "type" returns an error at the right position, and evaluation continues',
);
ok($result->exception, 'exception flag is true on the result');
};
subtest 'errors after crossing multiple $refs using $id and $anchor' => sub {
cmp_result(
$js->evaluate(
1,
{
'$id' => 'base.json',
'$defs' => {
def1 => {
'$comment' => 'canonical uri: "def1.json"',
'$id' => 'def1.json',
'$ref' => 'base.json#/$defs/myint',
type => 'integer',
maximum => -1,
minimum => 5,
},
myint => {
'$comment' => 'canonical uri: "def2.json"',
'$id' => 'def2.json',
'$ref' => 'base.json#my_not',
multipleOf => 5,
exclusiveMaximum => 1,
},
mynot => {
'$comment' => 'canonical uri: "base.json#/$defs/mynot"',
'$anchor' => 'my_not',
'$ref' => 'http://localhost:4242/object.json',
not => true,
},
myobject => {
'$id' => 'http://localhost:4242/object.json',
type => 'object',
anyOf => [ false ],
},
},
'$ref' => '#/$defs/def1',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/$ref/$ref/type',
absoluteKeywordLocation => 'http://localhost:4242/object.json#/type',
error => 'got integer, not object',
},
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/$ref/$ref/anyOf/0',
absoluteKeywordLocation => 'http://localhost:4242/object.json#/anyOf/0',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/$ref/$ref/anyOf',
absoluteKeywordLocation => 'http://localhost:4242/object.json#/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/$ref/not',
absoluteKeywordLocation => 'base.json#/$defs/mynot/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/multipleOf',
absoluteKeywordLocation => 'def2.json#/multipleOf',
error => 'value is not a multiple of 5',
},
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/exclusiveMaximum',
absoluteKeywordLocation => 'def2.json#/exclusiveMaximum',
error => 'value is greater than or equal to 1',
},
{
instanceLocation => '',
keywordLocation => '/$ref/maximum',
absoluteKeywordLocation => 'def1.json#/maximum',
error => 'value is greater than -1',
},
{
instanceLocation => '',
keywordLocation => '/$ref/minimum',
absoluteKeywordLocation => 'def1.json#/minimum',
error => 'value is less than 5',
},
],
},
'errors have correct absolute keyword location via $ref',
);
cmp_result(
$js->evaluate(
1,
{
'$id' => 'http://localhost:1234/hello',
'$defs' => {
foo => {
'$id' => 'http://localhost:1234/a/b.json',
'$defs' => {
bar => {
'$anchor' => 'my_anchor',
'$defs' => {
baz => {
'$anchor' => 'another_anchor',
not => true
},
},
},
},
},
},
'$ref' => 'http://localhost:1234/hello#/$defs/foo/$defs/bar/$defs/baz',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/not',
absoluteKeywordLocation => 'http://localhost:1234/a/b.json#/$defs/bar/$defs/baz/not',
error => 'subschema is true',
},
],
},
'absolute keyword location is correct, even when not used in the $ref',
);
};
subtest 'unresolvable $ref to a local resource' => sub {
cmp_result(
(my $result = $js->evaluate(
1,
{
'$ref' => '#/$defs/myint',
'$defs' => {
myint => {
'$ref' => '#/$defs/does-not-exist',
},
},
anyOf => [ false ],
},
))->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/$ref',
absoluteKeywordLocation => '#/$defs/myint/$ref',
error => 'EXCEPTION: unable to find resource "#/$defs/does-not-exist"',
},
],
},
'error for a bad $ref reports the correct absolute location that was referred to',
);
ok($result->exception, 'exception flag is true on the result');
};
subtest 'unresolvable $ref to a remote resource' => sub {
# new evaluator, with no resources remembered
my $js = JSON::Schema::Modern->new;
cmp_result(
(my $result = $js->evaluate(
1,
{
'$id' => 'http://localhost:4242/foo/bar/top_id.json',
'$ref' => '/baz/myint.json',
'$defs' => {
myint => {
'$id' => '/baz/myint.json',
'$ref' => 'does-not-exist.json',
},
},
anyOf => [ false ],
},
))->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/$ref',
absoluteKeywordLocation => 'http://localhost:4242/baz/myint.json#/$ref',
error => 'EXCEPTION: unable to find resource "http://localhost:4242/baz/does-not-exist.json"',
},
],
},
'error for a bad $ref reports the correct absolute location that was referred to',
);
ok($result->exception, 'exception flag is true on the result');
};
subtest 'unresolvable $ref to plain-name fragment' => sub {
cmp_result(
(my $result = $js->evaluate(1, { '$ref' => '#nowhere' }))->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref',
error => 'EXCEPTION: unable to find resource "#nowhere"',
},
],
},
'properly handled a bad $ref to an anchor',
);
ok($result->exception, 'exception flag is true on the result');
};
subtest 'abort due to a schema error' => sub {
cmp_result(
$js->evaluate(
1,
{
oneOf => [
{ type => 'number' },
{ type => 'string' },
{ type => 'whargarbl' },
],
}
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/oneOf/2/type',
error => 'unrecognized type "whargarbl"',
},
],
},
'exception inside a oneOf (where errors are localized) are still included in the result',
);
};
subtest 'sorted property names' => sub {
cmp_result(
$js->evaluate(
{ foo => 1, bar => 1, baz => 1, hello => 1 },
{
properties => {
foo => false,
bar => false,
},
additionalProperties => false,
}
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/bar',
keywordLocation => '/properties/bar',
error => 'property not permitted',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo',
error => 'property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '/baz',
keywordLocation => '/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '/hello',
keywordLocation => '/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'property names are considered in sorted order',
);
};
subtest 'bad regex in schema' => sub {
cmp_result(
$js->evaluate(
{
my_pattern => 'foo',
my_patternProperties => { foo => 1 },
},
my $schema = {
type => 'object',
properties => {
my_pattern => {
type => 'string',
pattern => '(',
},
my_patternProperties => {
type => 'object',
patternProperties => { '(' => true },
additionalProperties => false,
},
my_runtime_pattern => {
type => 'string',
pattern => '\p{main::IsFoo}', # qr/$pattern/ will not find this error, but m/$pattern/ will
},
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/properties/my_pattern/pattern',
error => re(qr/^Unmatched \( in regex/),
},
{
instanceLocation => '',
keywordLocation => '/properties/my_patternProperties/patternProperties/(',
error => re(qr/^Unmatched \( in regex/),
},
# Note no error for missing IsFoo
],
},
'bad "pattern" and "patternProperties" regexes are properly noted in error',
);
cmp_result(
$js->evaluate(
{ my_runtime_pattern => 'foo' },
$schema = {
$schema->%{type},
properties => +{ $schema->{properties}->%{my_runtime_pattern} },
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/my_runtime_pattern',
keywordLocation => '/properties/my_runtime_pattern/pattern',
# in 5.28 and earlier: Can't find Unicode property definition "IsFoo"
# in 5.30 and later: Unknown user-defined property name \p{main::IsFoo}
error => re(qr/^EXCEPTION: .*property.*IsFoo/),
},
],
},
'bad "pattern" regex is properly noted in error',
);
no warnings 'once';
*IsFoo = sub { "0066\n006F\n" }; # accepts 'f', 'o'
cmp_result(
$js->evaluate(
{ my_runtime_pattern => 'foo' },
$schema,
)->TO_JSON,
{
valid => true,
},
'"pattern" regex is now valid, due to the Unicode property becoming defined',
);
};
subtest 'JSON pointer escaping' => sub {
cmp_result(
$js->evaluate(
{ '{}' => { 'my~tilde/slash-property' => 1 } },
my $schema = {
'$defs' => {
mydef => {
properties => {
'{}' => {
properties => {
'my~tilde/slash-property' => false,
},
patternProperties => {
'/' => { minimum => 6 },
'[~/]' => { minimum => 7 },
'~' => { minimum => 5 },
'~.*/' => false,
},
},
},
},
},
'$ref' => '#/$defs/mydef',
},
)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '/{}/my~0tilde~1slash-property',
keywordLocation => '/$ref/properties/{}/properties/my~0tilde~1slash-property',
absoluteKeywordLocation => '#/$defs/mydef/properties/%7B%7D/properties/my~0tilde~1slash-property',
error => 'property not permitted',
},
{
instanceLocation => '/{}',
keywordLocation => '/$ref/properties/{}/properties',
absoluteKeywordLocation => '#/$defs/mydef/properties/%7B%7D/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '/{}/my~0tilde~1slash-property',
keywordLocation => '/$ref/properties/{}/patternProperties/~1/minimum',
absoluteKeywordLocation => '#/$defs/mydef/properties/%7B%7D/patternProperties/~1/minimum', # /
error => 'value is less than 6',
},
{
instanceLocation => '/{}/my~0tilde~1slash-property',
keywordLocation => '/$ref/properties/{}/patternProperties/[~0~1]/minimum',
absoluteKeywordLocation => '#/$defs/mydef/properties/%7B%7D/patternProperties/%5B~0~1%5D/minimum', # [~/]
error => 'value is less than 7',
},
{
instanceLocation => '/{}/my~0tilde~1slash-property',
keywordLocation => '/$ref/properties/{}/patternProperties/~0/minimum',
absoluteKeywordLocation => '#/$defs/mydef/properties/%7B%7D/patternProperties/~0/minimum', # ~
error => 'value is less than 5',
},
{
instanceLocation => '/{}/my~0tilde~1slash-property',
keywordLocation => '/$ref/properties/{}/patternProperties/~0.*~1',
absoluteKeywordLocation => '#/$defs/mydef/properties/%7B%7D/patternProperties/~0.*~1', # ~.*/
error => 'property not permitted',
},
{
instanceLocation => '/{}',
keywordLocation => '/$ref/properties/{}/patternProperties',
absoluteKeywordLocation => '#/$defs/mydef/properties/%7B%7D/patternProperties',
error => 'not all properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/$ref/properties',
absoluteKeywordLocation => '#/$defs/mydef/properties',
error => 'not all properties are valid',
},
],
},
'JSON pointers are properly escaped; URIs doubly so',
);
cmp_result(
$js->evaluate(
{ '{}' => { 'my~tilde/slash-property' => 1 } },
$schema->{'$defs'}{mydef},
)->TO_JSON,
{
valid => false,
errors => [
map +{
error => $_->{error},
instanceLocation => $_->{instanceLocation},
keywordLocation => $_->{keywordLocation} =~ s{^/\$ref}{}r,
}, @$errors
],
},
'absoluteKeywordLocation is omitted when paths are the same, not counting uri encoding',
);
cmp_result(
$js->evaluate(
{ '{}' => { 'my~tilde/slash-property' => 1 } },
{
'$defs' => {
mydef => {
properties => {
'{}' => {
patternProperties => {
'a{' => { minimum => 2 }, # this is a broken regex
},
},
},
},
},
'$ref' => '#/$defs/mydef',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$defs/mydef/properties/{}/patternProperties/a{',
error => re(qr/^Unescaped left brace in regex is (deprecated|illegal|passed through)/),
},
],
},
# all the other _keyword_path_suffix cases are tested in the earlier test case
'use of _keyword_path_suffix in a fatal error',
) if "$]" >= 5.022;
};
subtest 'absoluteKeywordLocation' => sub {
cmp_result(
JSON::Schema::Modern->new(max_traversal_depth => 1)->evaluate(
[ [ 1 ] ],
{ items => { '$ref' => '#' } },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0',
keywordLocation => '/items/$ref',
absoluteKeywordLocation => '',
error => 'EXCEPTION: maximum evaluation depth (1) exceeded',
},
],
},
'absoluteKeywordLocation is included when different from instanceLocation, even when empty',
);
cmp_result(
$js->evaluate(1, { '$ref' => '#does_not_exist' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref',
error => 'EXCEPTION: unable to find resource "#does_not_exist"',
},
],
},
'absoluteKeywordLocation is not included when the path equals keywordLocation, even if a $ref is present',
);
$js->add_schema(false);
cmp_result(
$js->evaluate(1, '#')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'subschema is false',
},
],
},
'absoluteKeywordLocation is never "#"',
);
cmp_result(
$js->evaluate(
1,
my $schema = {
'$id' => 'https://localhost:1234/bloop',
allOf => [
{
'$id' => 'foo.json',
type => 'object',
},
{
'$id' => 'bar/',
allOf => [
{
'$id' => 'alpha',
type => 'object',
},
],
},
{ type => 'object' },
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
absoluteKeywordLocation => 'https://localhost:1234/foo.json#/type',
error => 'got integer, not object',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/allOf/0/type',
absoluteKeywordLocation => 'https://localhost:1234/bar/alpha#/type',
error => 'got integer, not object',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/allOf',
absoluteKeywordLocation => 'https://localhost:1234/bar/#/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/2/type',
absoluteKeywordLocation => 'https://localhost:1234/bloop#/allOf/2/type',
error => 'got integer, not object',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://localhost:1234/bloop#/allOf',
error => 'subschemas 0, 1, 2 are not valid',
},
],
},
'absoluteKeywordLocation reflects the canonical schema uri as it changes when passing through $id',
);
$schema->{'$id'} = 'https://example.com';
$schema->{allOf}[2]{'$id'} = '#my_anchor2';
cmp_result(
JSON::Schema::Modern->new(specification_version => 'draft7')->evaluate(1, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
absoluteKeywordLocation => 'https://example.com/foo.json#/type',
error => 'got integer, not object',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/allOf/0/type',
absoluteKeywordLocation => 'https://example.com/bar/alpha#/type',
error => 'got integer, not object',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/allOf',
absoluteKeywordLocation => 'https://example.com/bar/#/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/2/type',
absoluteKeywordLocation => 'https://example.com#/allOf/2/type',
error => 'got integer, not object',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://example.com#/allOf',
error => 'subschemas 0, 1, 2 are not valid',
},
],
},
'plain-name fragment in $id does not change canonical schema uri',
);
};
subtest dependentRequired => sub {
cmp_result(
$js->evaluate(1, { dependentRequired => { foo => [ 1 ] } })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/dependentRequired/foo/0',
error => 'element is not a string',
},
],
},
'dependentRequired traversal error',
);
};
subtest 'numbers in output' => sub {
cmp_result(
$js->evaluate(
5,
{
multipleOf => 1.23456789,
maximum => 4.23456789,
minimum => 6.23456789,
exclusiveMaximum => 4.23456789,
exclusiveMinimum => 6.23456789,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/multipleOf',
error => 'value is not a multiple of 1.23456789',
},
{
instanceLocation => '',
keywordLocation => '/maximum',
error => 'value is greater than 4.23456789',
},
{
instanceLocation => '',
keywordLocation => '/exclusiveMaximum',
error => 'value is greater than or equal to 4.23456789',
},
{
instanceLocation => '',
keywordLocation => '/minimum',
error => 'value is less than 6.23456789',
},
{
instanceLocation => '',
keywordLocation => '/exclusiveMinimum',
error => 'value is less than or equal to 6.23456789',
},
],
},
'numbers in errors do not lose any digits of precision',
);
};
subtest 'overriding starting locations' => sub {
# evaluating this document from its root would do nothing, as it is only definitions
my $doc = $js->add_schema('/api', my $schema = {
'$defs' => {
alpha => {
items => {
'$ref' => '#/$defs/beta',
},
},
beta => {
not => true,
},
},
});
$js->add_document('https://example.com/api', $doc);
cmp_result(
$js->evaluate(
[ 5 ],
'https://example.com/api#/$defs/alpha',
{
data_path => '/html/body/div/div/h1/div/p', # reported data location
traversed_keyword_path => '/some/other/document/$ref', # reported keywords passed through before we start
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/html/body/div/div/h1/div/p/0',
keywordLocation => '/some/other/document/$ref/items/$ref/not',
absoluteKeywordLocation => 'https://example.com/api#/$defs/beta/not',
error => 'subschema is true',
},
{
instanceLocation => '/html/body/div/div/h1/div/p',
keywordLocation => '/some/other/document/$ref/items',
absoluteKeywordLocation => 'https://example.com/api#/$defs/alpha/items',
error => 'subschema is not valid against all items',
},
],
},
'can alter locations with data_path, traversed_keyword_path, and add_schema()',
);
};
subtest 'recommended_response' => sub {
cmp_result(
JSON::Schema::Modern::Result->new(valid => 1)->recommended_response,
undef,
'recommended_response is not defined when there are no errors',
);
my $result = $js->evaluate(
{ foo => 3 },
{
type => 'object',
properties => {
foo => {
type => 'integer',
minimum => 5,
},
},
},
);
cmp_result(
$result->recommended_response,
[ 400, q{'/foo': value is less than 5} ],
'recommended_response uses the first error in the result',
);
my $result2 = $js->evaluate(1, { '$ref' => '#/$defs/does_not_exist' });
cmp_result(
$result2->recommended_response,
[ 500, 'Internal Server Error' ],
'recommended_response indicates an exception occurred',
);
my $result3 = JSON::Schema::Modern::Result->new(
valid => 0,
errors => [
$result->errors,
# TODO: I haven't implemented authentication in OpenAPI::Modern yet, so I'm not sure how
# exactly these errors are going to look
JSON::Schema::Modern::Error->new(
depth => 0,
mode => 'evaluate',
keyword => 'authentication',
instance_location => '/request/headers/Authentication',
keyword_location => '/paths/foo/get/security',
error => 'security check failed',
recommended_response => [ 401, 'Unauthorized' ],
),
],
);
cmp_result(
$result3->recommended_response,
[ 401, 'Unauthorized' ],
'recommended_response uses the one from the error that is explicitly set',
);
cmp_result(
my $e = ($result3->errors)[-1]->clone(error => 'oh noes'),
methods(
error => 'oh noes',
recommended_response => [ 401, 'Unauthorized' ],
),
'cloning copies recommended_response',
);
};
subtest 'exclusiveMaximum, exclusiveMinimum across drafts' => sub {
cmp_result(
$js->evaluate(4, { maximum => 4, exclusiveMaximum => 4 })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/exclusiveMaximum',
error => 'value is greater than or equal to 4',
},
],
},
'later drafts; errors are produced separately from the keywords',
);
cmp_result(
$js->evaluate(5, { maximum => 4, exclusiveMaximum => 4 })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/maximum',
error => 'value is greater than 4',
},
{
instanceLocation => '',
keywordLocation => '/exclusiveMaximum',
error => 'value is greater than or equal to 4',
},
],
},
'later drafts; two errors can result',
);
my $js = JSON::Schema::Modern->new(specification_version => 'draft4');
cmp_result(
$js->evaluate(4, { maximum => 4, exclusiveMaximum => true })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/maximum',
error => 'value is greater than or equal to 4',
},
],
},
'draft4: one error comes from maximum, but includes the exclusiveMaximum check',
);
cmp_result(
$js->evaluate(5, { maximum => 4, exclusiveMaximum => true })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/maximum',
error => 'value is greater than or equal to 4',
},
],
},
'draft4: maximum + exclusiveMaximum checks are combined',
);
cmp_result(
$js->evaluate(4, { maximum => 4, exclusiveMaximum => false })->TO_JSON,
{ valid => true },
'draft4: exclusive check uses the right boundary',
);
cmp_result(
$js->evaluate(5, { maximum => 4, exclusiveMaximum => false })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/maximum',
error => 'value is greater than 4',
},
],
},
'draft4: maximum check is correct',
);
};
subtest 'boolean schemas in draft4' => sub {
my $js = JSON::Schema::Modern->new(specification_version => 'draft4', strict => 1);
cmp_result (
$js->evaluate(
1,
{
allOf => [ true ],
anyOf => [ true ],
oneOf => [ true ],
not => true,
items => true,
additionalItems => true, # ok
properties => { foo => true },
patternProperties => { foo => false },
additionalProperties => false, # ok
uniqueItems => true, # ok
},
)->TO_JSON,
{
valid => false,
errors => [
(map +{
instanceLocation => '',
keywordLocation => '/'.$_.'/0',
error => 'invalid schema type: boolean',
}, qw(allOf anyOf oneOf)),
(map +{
instanceLocation => '',
keywordLocation => '/'.$_,
error => 'invalid schema type: boolean',
}, qw(not items)),
(map +{
instanceLocation => '',
keywordLocation => '/'.$_.'/foo',
error => 'invalid schema type: boolean',
}, qw(properties patternProperties)),
],
},
'got all traverse errors from use of booleans in schemas for draft4',
);
cmp_result(
$js->evaluate(
{
array => [ 1, 1 ],
object => { foo => 1 },
},
my $schema = {
allOf => [
{
properties => {
array => {
type => 'array',
uniqueItems => true,
items => [ {} ],
additionalItems => false,
},
object => {
type => 'object',
additionalProperties => false,
},
},
},
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/array',
keywordLocation => '/allOf/0/properties/array/uniqueItems',
error => 'items at indices 0 and 1 are not unique',
},
{
instanceLocation => '/array/1',
keywordLocation => '/allOf/0/properties/array/additionalItems',
error => 'additional item not permitted',
},
{
instanceLocation => '/array',
keywordLocation => '/allOf/0/properties/array/additionalItems',
error => 'subschema is not valid against all additional items',
},
{
instanceLocation => '/object/foo',
keywordLocation => '/allOf/0/properties/object/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '/object',
keywordLocation => '/allOf/0/properties/object/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschema 0 is not valid',
},
],
},
'booleans are okay in uniqueItems, additionalItems, additionalProperties',
);
push $schema->{allOf}->@*, false;
cmp_result(
$js->evaluate(
{
array => [ 1 ],
object => { foo => 1 },
},
$schema,
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/1',
error => 'invalid schema type: boolean',
},
],
},
'boolean schemas did not exist in draft4',
);
};
done_testing;
strict.t 100640 000766 000024 17117 15114374332 16364 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new;
ok(!$js->strict, 'strict defaults to false');
my $schema = {
'$id' => 'my_loose_schema',
type => 'object',
properties => {
alpha => {
title => 'bloop', # produces an annotation for 'title' with value 'bloop'
bloop => 'hi', # unknown keyword
barf => 'no', # unknown keyword
},
beta => {
zip => 1, # unknown keyword
dah => 2, # unknown keyword
},
},
};
my $document = $js->add_schema($schema);
cmp_result(
$js->evaluate({ alpha => 1, beta => 2 }, 'my_loose_schema')->TO_JSON,
{ valid => true },
'by default, unknown keywords are allowed in evaluate()',
);
cmp_result(
$js->evaluate({ alpha => 1, beta => 2 }, 'my_loose_schema', { strict => 1 })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha',
absoluteKeywordLocation => 'my_loose_schema#/properties/alpha',
error => 'unknown keywords seen in schema: barf, bloop',
},
# beta not examined -- we immediately abort
],
},
'strict mode abort immediately on unknown keywords during evaluation via a config override',
);
cmp_result(
$js->validate_schema($schema)->TO_JSON,
{ valid => true },
'by default, unknown keywords are allowed in validate_schema()',
);
cmp_result(
$js->validate_schema($schema, { strict => 1 })->TO_JSON,
my $schema_result = {
valid => false,
errors => [
{
instanceLocation => '/properties/alpha',
keywordLocation => '',
absoluteKeywordLocation => 'https://json-schema.org/draft/2020-12/schema',
error => 'unknown keywords seen in schema: barf, bloop',
},
{
instanceLocation => '/properties/beta',
keywordLocation => '',
absoluteKeywordLocation => 'https://json-schema.org/draft/2020-12/schema',
error => 'unknown keywords seen in schema: dah, zip',
},
],
},
'strict mode finds all unknown keywords in validate_schema() via a config override',
);
$js = JSON::Schema::Modern->new(strict => 1);
cmp_result(
$js->validate_schema($schema)->TO_JSON,
$schema_result,
'strict mode disallows unknown keywords in the schema data passed to validate_schema()',
);
delete $schema->{'$id'};
cmp_result(
$js->evaluate({ alpha => 1 }, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '', # note no instance location - indicating evaluation has not started
keywordLocation => '/properties/alpha',
error => 'unknown keywords seen in schema: barf, bloop',
},
{
instanceLocation => '',
keywordLocation => '/properties/beta',
error => 'unknown keywords seen in schema: dah, zip',
},
],
},
'strict mode finds all unknown keywords during traverse',
);
my $lax_metaschema = {
'$id' => 'my_lax_metaschema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$dynamicAnchor' => 'meta',
'$ref' => 'https://json-schema.org/draft/2020-12/schema',
properties => {
bloop => true, # bloop is now a recognized property
},
};
$js->add_schema($lax_metaschema);
$schema->{'$schema'} = 'my_lax_metaschema';
cmp_result(
$js->validate_schema($schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/properties/alpha',
keywordLocation => '',
absoluteKeywordLocation => 'my_lax_metaschema',
error => 'unknown keyword seen in schema: barf',
},
{
instanceLocation => '/properties/beta',
keywordLocation => '',
absoluteKeywordLocation => 'my_lax_metaschema',
error => 'unknown keywords seen in schema: dah, zip',
},
],
},
'strict mode only detected one property in alpha this time - bloop is evaluated',
);
$schema->{'$schema'} = 'http://json-schema.org/draft-07/schema#';
cmp_result(
$js->validate_schema($schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/properties/alpha',
keywordLocation => '',
absoluteKeywordLocation => 'http://json-schema.org/draft-07/schema',
error => 'unknown keywords seen in schema: barf, bloop',
},
{
instanceLocation => '/properties/beta',
keywordLocation => '',
absoluteKeywordLocation => 'http://json-schema.org/draft-07/schema',
error => 'unknown keywords seen in schema: dah, zip',
},
],
},
'strict mode detects unknown keywords using draft7',
);
subtest 'strict and if/then/else' => sub {
cmp_result(
JSON::Schema::Modern->new(strict => 1)->evaluate(
$_,
{
if => { const => 0 },
then => true,
else => true,
},
{ strict => 1 },
)->TO_JSON,
{ valid => true },
'no unknown keywords are identified, even though "' . ($_?'else':'then').'" is never evaluated',
) foreach (0..1);
};
subtest 'strict and short-circuit' => sub {
cmp_result(
JSON::Schema::Modern->new->evaluate(
{ foo => 1, bar => 2 },
{
if => {
required => ['bar'],
properties => {
bar => { const => 1 },
},
additionalProperties => false,
bloop => 1,
},
then => false,
else => true,
},
{ strict => 1 },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/if',
error => 'unknown keyword seen in schema: bloop',
},
# no errors from additionalProperties, because we abort from within the 'if' subschema,
# and 'if' doesn't preserve its errors
],
},
'strict mode will work properly even when some keywords short-circuit',
);
cmp_result(
JSON::Schema::Modern->new->evaluate(
{ alpha => { beta => 2, gamma => 3 } },
# this is not a valid test, because we never preserve errors from 'if'.
# so we need something with a subschema that preserves errors, like properties.
my $schema = {
properties => {
alpha => {
required => ['beta'],
properties => {
beta => { const => 2 },
},
additionalProperties => false,
bloop => 1,
},
},
},
{ strict => 1, short_circuit => 1 },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/alpha/gamma',
keywordLocation => '/properties/alpha/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha',
error => 'unknown keyword seen in schema: bloop',
},
],
},
'strict mode will still work even when enabled together with short_circuit',
)
};
done_testing;
weaver.ini 100640 000766 000024 1547 15114374332 16376 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 ; this section is woven into the beginning of the document, but inc::AppendSection below later
; plucks it out of its position and appends it to the generic SUPPORT section that is subsequently
; generated by the plugin bundle.
[GenerateSection / SUPPORT]
main_module_only = 0
text = =for stopwords OpenAPI
text =
text = You can also find me on the L and L, which are also great resources for finding help.
; ditto
[GenerateSection / COPYRIGHT AND LICENCE]
main_module_only = 0
text = Some schema files have their own licence, in share/LICENSE.
[@Author::ETHER]
[=inc::AppendSection / AppendSupport]
header_re = ^SUPPORT$
match_anywhere = 1
action = append
[=inc::AppendSection / AppendCopyright]
header_re = ^COPYRIGHT
match_anywhere = 1
action = append
formats.t 100640 000766 000024 74344 15114374332 16534 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
use Test2::Warnings qw(warnings :no_end_test had_no_warnings allow_warnings);
use JSON::Schema::Modern::Utilities qw(get_type);
use Test::Without::Module 0.19 qw(
Time::Moment
DateTime::Format::RFC3339
Data::Validate::Domain
Email::Address::XS
Net::IDN::Encode
);
use constant ALL_FORMATS => [ qw(
date-time
email
hostname
ipv4
ipv6
uri
uri-reference
uri-template
json-pointer
iri
iri-reference
idn-email
idn-hostname
relative-json-pointer
regex
date
time
duration
uuid
) ];
my ($annotation_result, $validation_result);
subtest 'no validation' => sub {
cmp_result(
JSON::Schema::Modern->new(collect_annotations => 1, validate_formats => 0)
->evaluate('abc', { format => 'uuid' })->TO_JSON,
$annotation_result = {
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/format',
annotation => 'uuid',
},
],
},
'validate_formats=0 disables format assertion behaviour; annotation is still produced',
);
cmp_result(
JSON::Schema::Modern->new(collect_annotations => 1, validate_formats => 1)
->evaluate('abc', { format => 'uuid' }, { validate_formats => 0 })->TO_JSON,
$annotation_result,
'format validation can be turned off in evaluate()',
);
};
subtest 'simple validation' => sub {
my $js = JSON::Schema::Modern->new(collect_annotations => 1, validate_formats => 1);
cmp_result(
$js->evaluate(123, { format => 'uuid' })->TO_JSON,
$annotation_result,
'non-string values are valid, and produce an annotation',
);
cmp_result(
$js->evaluate(
'2eb8aa08-aa98-11ea-b4aa-73b441d16380',
{ format => 'uuid' },
)->TO_JSON,
$annotation_result,
'simple success',
);
cmp_result(
$js->evaluate('123', { format => 'uuid' })->TO_JSON,
$validation_result = {
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/format',
error => 'not a valid uuid string',
},
],
},
'simple failure',
);
$js = JSON::Schema::Modern->new(collect_annotations => 1);
ok(!$js->validate_formats, 'format_validation defaults to false');
cmp_result(
$js->evaluate('123', { format => 'uuid' }, { validate_formats => 1 })->TO_JSON,
$validation_result,
'format validation can be turned on in evaluate()',
);
ok(!$js->validate_formats, '...but the value is still false on the object');
};
subtest 'override a format sub' => sub {
like(
dies {
JSON::Schema::Modern->new(
validate_formats => 1,
format_validations => +{ uuid => 1 },
)
},
qr/Reference .* did not pass type constraint /,
'check syntax of override to existing format via constructor',
);
my $js = JSON::Schema::Modern->new(validate_formats => 1);
like(
dies { $js->add_format_validation([] => 1) },
qr/Value .* did not pass type constraint /,
'check syntax of override format name to existing format via setter',
);
like(
dies { $js->add_format_validation(uuid => 1) },
qr/Value .* did not pass type constraint /,
'check syntax of override definition value to existing format via setter',
);
like(
dies { $js->add_format_validation(uuid => { sub => sub { 0 }}) },
qr/Reference .* did not pass type constraint /,
'type is required if passing a hashref',
);
like(
dies { $js->add_format_validation(uuid => { type => 'number', sub => sub { 0 }}) },
qr/Type for override of format uuid does not match original type/,
'cannot override a core format to support a different data type',
);
$js->add_format_validation(uuid => sub { $_[0] =~ /^[a-z0-9-]+$/ });
cmp_result(
$js->evaluate(
[
0,
1,
[],
{},
'a',
'foobie!',
],
{ items => { format => 'uuid' } },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/5',
keywordLocation => '/items/format',
error => 'not a valid uuid string',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'can override a core format definition, as long as it uses the same type',
);
like(
dies {
JSON::Schema::Modern->new(
validate_formats => 1,
format_validations => +{ mult_5 => 1 },
)
},
qr/Value "1" did not pass type constraint "(Dict\[|Ref").../,
'check syntax of implementation for a new format',
);
$js = JSON::Schema::Modern->new(
collect_annotations => 1,
validate_formats => 1,
format_validations => +{
uuid => sub { $_[0] =~ /^[A-Z]+$/ },
mult_5 => +{ type => 'number', sub => sub { ($_[0] % 5) == 0 } },
},
);
like(
dies { $js->add_format_validation(uuid_bad => 1) },
qr/Value "1" did not pass type constraint "(Dict\[|Ref").../,
'check syntax of implementation when adding an override to existing format',
);
like(
dies { $js->add_format_validation(mult_5_bad => 1) },
qr/Value "1" did not pass type constraint "(Dict\[|Ref").../,
'check syntax of implementation when adding a new format',
);
cmp_result(
$js->evaluate(
[
{ uuid => '2eb8aa08-aa98-11ea-b4aa-73b441d16380', mult_5 => 3 },
{ uuid => 3, mult_5 => 'abc' },
],
{
items => {
properties => {
uuid => { format => 'uuid' },
mult_5 => { format => 'mult_5' },
},
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0/mult_5',
keywordLocation => '/items/properties/mult_5/format',
error => 'not a valid mult_5 number',
},
{
instanceLocation => '/0/uuid',
keywordLocation => '/items/properties/uuid/format',
error => 'not a valid uuid string',
},
{
instanceLocation => '/0',
keywordLocation => '/items/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'swapping out format implementation turns success into failure; wrong types are still valid',
);
# create a new format definition which uses a different type
$js->add_format_validation(keys_mult_2 => +{ type => 'object', sub => sub { keys($_[0]->%*) > 2 } });
cmp_result(
$js->evaluate(
[
{},
{ a => 1 },
{ a => 1, b => 2 },
{ a => 1, b => 2, c => 3 },
[],
'a',
],
{ items => { format => 'keys_mult_2' } },
)->TO_JSON,
{
valid => false,
errors => [
(map +{
instanceLocation => '/'.$_,
keywordLocation => '/items/format',
error => 'not a valid keys_mult_2 object',
}, 0, 1, 2),
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'can create a custom format definition to use a different type',
);
};
subtest 'toggle validate_formats after adding schema' => sub {
my $js = JSON::Schema::Modern->new;
my $document = $js->add_schema(my $uri = 'http://localhost:1234/ipv4', { format => 'ipv4' });
cmp_result(
$js->evaluate('hello', $uri)->TO_JSON,
{ valid => true },
'assertion behaviour is off initially',
);
cmp_result(
$js->evaluate('hello', $uri, { validate_formats => 1 })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/format',
absoluteKeywordLocation => 'http://localhost:1234/ipv4#/format',
error => 'not a valid ipv4 string',
},
],
},
'assertion behaviour can be enabled later with an already-loaded schema',
);
cmp_result(
$js->evaluate('127.0.0.1', $uri, { validate_formats => 1 })->TO_JSON,
{ valid => true },
'valid assertion behaviour does not die',
);
my $js2 = JSON::Schema::Modern->new(validate_formats => 1);
$js2->add_document($uri, $document);
cmp_result(
$js2->evaluate('hello', $uri)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/format',
absoluteKeywordLocation => 'http://localhost:1234/ipv4#/format',
error => 'not a valid ipv4 string',
},
],
},
'a schema document can be used with another evaluator with assertion behaviour',
);
cmp_result(
$js2->evaluate('127.0.0.1', $uri)->TO_JSON,
{ valid => true },
'valid assertion behaviour does not die',
);
};
subtest 'custom metaschemas' => sub {
my $js = JSON::Schema::Modern->new;
$js->add_schema({
'$id' => 'https://metaschema/format-assertion/false',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/format-assertion' => false,
},
});
$js->add_schema({
'$id' => 'https://metaschema/format-assertion/true',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/format-assertion' => true,
},
});
cmp_result(
$js->evaluate(
'not-an-ip',
{
'$id' => 'https://schema/ipv4/false',
'$schema' => 'https://metaschema/format-assertion/false',
type => 'string',
format => 'ipv4',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/format',
absoluteKeywordLocation => 'https://schema/ipv4/false#/format',
error => 'not a valid ipv4',
},
],
},
'custom metaschema using format-assertion=false validates formats',
);
cmp_result(
$js->evaluate(
'not-an-ip',
{
'$id' => 'https://schema/ipv4/true',
'$schema' => 'https://metaschema/format-assertion/true',
type => 'string',
format => 'ipv4',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/format',
absoluteKeywordLocation => 'https://schema/ipv4/true#/format',
error => 'not a valid ipv4',
},
],
},
'custom metaschema using format-assertion=true validates formats',
);
};
subtest 'core formats added after draft7' => sub {
my $js = JSON::Schema::Modern->new(specification_version => 'draft7', validate_formats => 1);
cmp_result(
$js->evaluate('123', { format => 'duration' })->TO_JSON,
{ valid => true },
'duration is not implemented in draft7',
);
cmp_result(
$js->evaluate('123', { format => 'uuid' })->TO_JSON,
{ valid => true },
'uuid is not implemented in draft7',
);
};
subtest 'unimplemented core formats' => sub {
# all specification versions
foreach my $spec_version (JSON::Schema::Modern::SPECIFICATION_VERSIONS_SUPPORTED->@*) {
my $js = JSON::Schema::Modern->new(specification_version => $spec_version, validate_formats => 1);
cmp_result(
my $res = $js->evaluate(
'hello',
{
format => 'uri-template',
},
)->TO_JSON,
{ valid => true },
$spec_version . ' with validate_formats = 1 and default dialect, no error when an unimplemented core format is used',
);
}
# specification version draft2020-12 and later, format-assertion vocabulary
foreach my $spec_version (JSON::Schema::Modern::SPECIFICATION_VERSIONS_SUPPORTED->@*) {
next if $spec_version =~ /^draft(?:[467]|2019-09)$/;
my $js = JSON::Schema::Modern->new(specification_version => $spec_version);
$js->add_schema({
'$id' => 'https://my_metaschema',
'$schema' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
'$vocabulary' => {
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/core}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/applicator}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/format-assertion}r => true,
},
'$ref' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
});
cmp_result(
$js->evaluate(
'hello',
{
'$schema' => 'https://my_metaschema',
format => 'uri-template',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/format',
error => 'unimplemented core format "uri-template"',
},
],
},
$spec_version . ' with Format-Assertion vocabulary: error when using a core format that is unimplemented',
);
cmp_result(
$js->evaluate(
'hello',
{
'$schema' => 'https://my_metaschema',
anyOf => [
{ minLength => 1 },
{ format => 'uri-template' },
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/anyOf/1/format',
error => 'unimplemented core format "uri-template"',
},
],
},
$spec_version . ' with Format-Assertion vocabulary: error is seen even when containing subschema would be true, and evaluation is short-circuited',
);
# add uri-template definition that allows lower-cased characters
$js->add_format_validation('uri-template' => sub { $_[0] !~ /[A-Z]/ });
cmp_result(
$js->evaluate(
'hello',
{
'$schema' => 'https://my_metaschema',
format => 'uri-template',
},
)->TO_JSON,
{ valid => true },
'unimplemented core format can have a custom definition provided',
);
}
};
subtest 'unknown custom formats' => sub {
# see https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.7.2.3
# "An implementation MUST NOT fail validation or cease processing due to an unknown format
# attribute."
foreach my $spec_version (JSON::Schema::Modern::SPECIFICATION_VERSIONS_SUPPORTED->@*) {
my $js = JSON::Schema::Modern->new(
specification_version => $spec_version,
$spec_version !~ /^draft[467]$/ ? (collect_annotations => 1) : (),
validate_formats => 1,
);
cmp_result(
$js->evaluate('hello', { format => 'whargarbl' })->TO_JSON,
{
valid => true,
$spec_version =~ /^draft[467]$/ ? () : (annotations => [
{
instanceLocation => '',
keywordLocation => '/format',
annotation => 'whargarbl',
},
]),
},
$spec_version . ': for format validation with the Format-Annotation vocabulary, unrecognized format attributes do not cause validation failure'
. ($spec_version !~ /^draft[467]$/ ? '; annotation is still produced' : ''),
);
}
# see https://json-schema.org/draft/2020-12/json-schema-validation#section-7.2.3
# "When the Format-Assertion vocabulary is specified, implementations MUST fail upon encountering
# unknown formats."
foreach my $spec_version (JSON::Schema::Modern::SPECIFICATION_VERSIONS_SUPPORTED->@*) {
next if $spec_version =~ /^draft[467]$/ or $spec_version eq 'draft2019-09';
my $js = JSON::Schema::Modern->new(specification_version => $spec_version);
$js->add_schema({
'$id' => 'https://my_metaschema',
'$schema' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
'$vocabulary' => {
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/core}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/applicator}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/validation}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/format-assertion}r => true,
},
'$ref' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
});
my $doc = $js->add_schema({
'$schema' => 'https://my_metaschema',
anyOf => [
{ minLength => 3 },
{ format => 'bloop' },
],
});
is($doc->errors, 0, $spec_version . ': for format validation with the Format-Assertion vocabulary, no errors during traversal when using an unknown custom format');
cmp_result(
$js->evaluate('hi', '')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/anyOf/1/format',
error => 'unimplemented custom format "bloop"',
},
],
},
$spec_version . ': for format validation with the Format-Assertion vocabulary, unrecognized custom formats are detected at evaluation time',
);
cmp_result(
$js->evaluate('hello', '', { short_circuit => 1 })->TO_JSON,
{ valid => true },
'...but this error can be avoided if the keyword is never evaluated',
);
}
};
subtest 'format: invalid base type(s)' => sub {
my $js = JSON::Schema::Modern->new(validate_formats => 1);
like(
dies { $js->add_format_validation(my_integer => { type => 'integer', sub => sub {} }) },
qr/Value .* did not pass type constraint /,
'integer is not a valid base type for a format validation',
);
like(
dies { $js->add_format_validation(my_integer => { type => [qw(integer string)], sub => sub {} }) },
qr/Reference .* did not pass type constraint /,
'integer, string is not a valid base type for a format validation',
);
};
subtest 'format: pure_integer' => sub {
my $js = JSON::Schema::Modern->new(
validate_formats => 1,
format_validations => +{
pure_integer => +{ type => 'number', sub => sub ($value) {
B::svref_2object(\$value)->FLAGS & B::SVf_IOK
} },
},
);
my $decoder = JSON::Schema::Modern::_JSON_BACKEND()->new->allow_nonref(1)->utf8(0);
my $int = 5;
cmp_result(
$js->evaluate(
[
(map $decoder->decode($_),
'"hello"',
'3.1',
'3.0',
'3',
),
bless(\$int, 'Local::MyInteger'),
],
{
items => {
type => 'integer',
format => 'pure_integer',
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0',
keywordLocation => '/items/type',
error => 'got string, not integer',
},
{
instanceLocation => '/1',
keywordLocation => '/items/type',
error => 'got number, not integer',
},
{
instanceLocation => '/1',
keywordLocation => '/items/format',
error => 'not a valid pure_integer number',
},
{
instanceLocation => '/2',
keywordLocation => '/items/format',
error => 'not a valid pure_integer number',
},
{
instanceLocation => '/4',
keywordLocation => '/items/type',
error => 'got Local::MyInteger, not integer',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'pure_integer format with type',
);
cmp_result(
$js->evaluate(
[
(map $decoder->decode($_),
'"hello"', # string, will not apply format
'3.1', # number, will apply format
'3.0', # ""
'3', # ""
),
bless(\$int, 'Local::MyInteger'), # blessed type, will not apply format
],
{
items => {
format => 'pure_integer',
},
},
)->TO_JSON,
{
valid => false,
errors => [
# strings are not applied to the format
{
instanceLocation => '/1',
keywordLocation => '/items/format',
error => 'not a valid pure_integer number',
},
{
instanceLocation => '/2',
keywordLocation => '/items/format',
error => 'not a valid pure_integer number',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'pure_integer format without type',
);
};
subtest 'formats supporting multiple core types' => sub {
# this is int64 from the OAI format registry: https://spec.openapis.org/registry/format/
my $js = JSON::Schema::Modern->new(
validate_formats => 1,
format_validations => +{
# a signed 64-bit integer; see https://spec.openapis.org/api/format.json
int64 => +{ type => ['number', 'string'], sub => sub ($value) {
my $type = get_type($value);
return if not grep $type eq $_, qw(integer number string);
$value = Math::BigInt->new($value) if $type eq 'string';
return if $value eq 'NaN';
# using the literal numbers rather than -2**63, 2**63 -1 to maintain precision
$value >= Math::BigInt->new('-9223372036854775808') && $value <= Math::BigInt->new('9223372036854775807');
} },
},
);
my @values = (
'{}', # object is valid
'[]', # array is valid
'true', # boolean is valid
'null', # null is valid
# string
'"-9223372036854775809"', # 4: out of bounds
'"-9223372036854775808"', # minimum value
'"-9223372036854775807"', # within bounds
'"0"',
'"9223372036854775806"', # within bounds
'"9223372036854775807"', # maximum value
'"9223372036854775808"', # out of bounds
'"Inf"',
'"NaN"',
# number
'-9223372036854775809', # 13: out of bounds
'-9223372036854775808', # minimum value; difficult to use on most architectures without Math::BigInt
'-9223372036854775807', # within bounds
'0',
'9223372036854775806', # within bounds
'9223372036854775807', # maximum value
'9223372036854775808', # 19: out of bounds
# numeric Inf and NaN are not valid JSON
);
# note: results may vary on 32-bit architectures when not using Math::BigFloat
foreach my $decoder (
JSON::Schema::Modern::_JSON_BACKEND()->new->allow_nonref(1)->utf8(0),
JSON::Schema::Modern::_JSON_BACKEND()->new->allow_nonref(1)->utf8(0)->allow_bignum(1)) {
cmp_result(
my $result = $js->evaluate(
[ map $decoder->decode($_), @values ],
{
items => {
format => 'int64',
},
},
)->TO_JSON,
{
valid => false,
errors => [
(map +{
instanceLocation => "/$_",
keywordLocation => '/items/format',
error => 'not a valid int64 number, string',
},
4, 10, 11, 12, 13, 19),
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'int64 format without type - accepts both numbers and strings',
);
}
};
subtest 'stringy numbers with a numeric format' => sub {
my $js = JSON::Schema::Modern->new(
validate_formats => 1,
stringy_numbers => 1,
format_validations => +{
mult_5 => +{ type => 'number', sub => sub { ($_[0] % 5) == 0 } },
},
);
cmp_result(
my $res = $js->evaluate(
[
3,
'3',
5,
'5',
'abc',
],
{ items => { format => 'mult_5' } },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0',
keywordLocation => '/items/format',
error => 'not a valid mult_5 number',
},
{
instanceLocation => '/1',
keywordLocation => '/items/format',
error => 'not a valid mult_5 number',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'FormatAnnotation+validate_formats: strings that look like numbers can be validated against a numeric format when stringy_numbers=1',
);
$js = JSON::Schema::Modern->new(
stringy_numbers => 1,
format_validations => +{
mult_5 => +{ type => 'number', sub => sub { ($_[0] % 5) == 0 } },
},
);
my $spec_version = $js->SPECIFICATION_VERSION_DEFAULT;
$js->add_schema({
'$id' => 'https://my_metaschema',
'$schema' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
'$vocabulary' => {
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/core}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/applicator}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/format-assertion}r => true,
},
'$ref' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
});
cmp_result(
$js->evaluate(
[
3,
'3',
5,
'5',
'abc',
],
{
'$schema' => 'https://my_metaschema',
items => { format => 'mult_5' },
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0',
keywordLocation => '/items/format',
error => 'not a valid mult_5',
},
{
instanceLocation => '/1',
keywordLocation => '/items/format',
error => 'not a valid mult_5',
},
{
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
},
],
},
'FormatAssertion: strings that look like numbers can be validated against a numeric format when stringy_numbers=1',
);
};
# we do have support for these formats, but we do not force that their dependencies be installed
# unless the formats are actually to be used.
# Therefore we will allow them to be tested against other data types (e.g. in acceptance tests)
# even without these dependencies installed, without throwing an exception.
subtest 'annotation formats using implementations that rely on optional dependencies' => sub {
cmp_result(
# relying on default format-assertion behaviour
JSON::Schema::Modern->new->evaluate(
[
undef,
true,
{},
[],
1
],
{ items => { allOf => [ map +{ format => $_ }, ALL_FORMATS->@* ] } },
)->TO_JSON,
{ valid => true },
'can annotate a non-string against formats without their optional dependencies, without dying',
);
};
subtest 'assertion formats using implementations that rely on optional dependencies' => sub {
foreach my $spec_version (JSON::Schema::Modern::SPECIFICATION_VERSIONS_SUPPORTED->@*) {
my $js = JSON::Schema::Modern->new(
specification_version => $spec_version,
validate_formats => 1,
);
cmp_result(
$js->evaluate(
[
undef,
true,
{},
[],
1
],
{
type => 'array',
items => { allOf => [ map +{ format => $_ }, ALL_FORMATS->@* ] }
},
)->TO_JSON,
{ valid => true },
$spec_version . ': for format validation with the Format-Annotation vocabulary, can assert a non-string against formats without their optional dependencies, without dying',
);
cmp_result(
$js->evaluate(
'2025-01-01T00:00:00Z',
{
type => 'string',
allOf => [
{ format => 'date-time' },
$spec_version eq 'draft4' ? {} : true,
],
},
)->TO_JSON,
{ valid => true },
$spec_version . ': for format validation with the Format-Annotation vocabulary, in assertion mode, we treat missing prereqs as the format being valid',
);
}
foreach my $spec_version (JSON::Schema::Modern::SPECIFICATION_VERSIONS_SUPPORTED->@*) {
next if $spec_version =~ /^draft[467]$/ or $spec_version eq 'draft2019-09';
my $js = JSON::Schema::Modern->new(specification_version => $spec_version);
$js->add_schema({
'$id' => 'https://my_metaschema',
'$schema' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
'$vocabulary' => {
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/core}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/applicator}r => true,
JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version} =~ s{schema$}{vocab/format-assertion}r => true,
},
'$ref' => JSON::Schema::Modern::METASCHEMA_URIS->{$spec_version},
});
my $doc;
my @warnings = warnings {
$doc = $js->add_schema({
'$schema' => 'https://my_metaschema',
allOf => [
{ format => 'date-time' },
true,
],
});
};
is($doc->errors, 0, $spec_version . ': for format validation with the Format-Assertion vocabulary, no errors during traversal when using an unknown custom format');
cmp_result(
\@warnings,
[ re(qr{Can't locate Time/Moment\.pm}) ],
'...but we do warn for the missing module',
);
cmp_result(
$js->evaluate('2025-01-01T00:00:00Z', $doc->canonical_uri)->TO_JSON,
{
valid => false,
errors => [
{
error => re(qr{^EXCEPTION: Can't locate Time/Moment\.pm}),
instanceLocation => '',
keywordLocation => '/allOf/0/format',
},
],
},
$spec_version . ': for Format-Asertion vocabulary, we immediately abort when encountering a format that throws an exception',
);
}
};
had_no_warnings() if $ENV{AUTHOR_TESTING};
done_testing;
pattern.t 100640 000766 000024 4156 15114374332 16510 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use utf8;
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new;
my $tests = sub ($char, $test_substr) {
cmp_result(
$js->evaluate($char, { pattern => '[a-z]' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/pattern',
error => 'pattern does not match',
},
],
},
$test_substr.' LATIN SMALL LETTER E WITH ACUTE does not match the ascii range [a-z]',
);
cmp_result(
$js->evaluate($char, { pattern => '\w' })->TO_JSON,
{
valid => true,
},
$test_substr.' LATIN SMALL LETTER E WITH ACUTE does match the "word" character class, because unicode semantics are used for matching',
);
};
my $letter = "é";
$tests->($letter, 'unchanged');
utf8::upgrade($letter);
$tests->($letter, 'upgraded');
utf8::downgrade($letter);
$tests->($letter, 'downgraded');
subtest 'empty pattern' => sub {
# create a "last successful match" in a containing scope
my $str = "furble" =~ s/fur/meow/r;
cmp_result(
$js->evaluate('hello', { pattern => '' })->TO_JSON,
{ valid => true },
'empty pattern in "pattern" will correctly match',
);
# create a new "last successful match"
$str = "furble" =~ s/fur/meow/r;
cmp_result(
$js->evaluate(
{ alpha => 'hello' },
{
patternProperties => { '' => true },
additionalProperties => false,
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'empty pattern in "patternProperties" will correctly match',
);
};
done_testing;
Makefile.PL 100644 000766 000024 14426 15114374332 16402 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 # This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.036.
use strict;
use warnings;
if ("$]" < 5.038) {
die "This distribution will not install where builtin::Backport exists.\n"
if eval { +require builtin::Backport; 1 };
}
use 5.020000;
use ExtUtils::MakeMaker;
use File::ShareDir::Install;
$File::ShareDir::Install::INCLUDE_DOTFILES = 1;
$File::ShareDir::Install::INCLUDE_DOTDIRS = 1;
install_share dist => "share";
my %WriteMakefileArgs = (
"ABSTRACT" => "Validate data against a schema using a JSON Schema",
"AUTHOR" => "Karen Etheridge ",
"CONFIGURE_REQUIRES" => {
"ExtUtils::MakeMaker" => 0,
"File::ShareDir::Install" => "0.06",
"Text::ParseWords" => 0
},
"DISTNAME" => "JSON-Schema-Modern",
"EXE_FILES" => [
"script/json-schema-eval"
],
"LICENSE" => "perl",
"MIN_PERL_VERSION" => "5.020000",
"NAME" => "JSON::Schema::Modern",
"PREREQ_PM" => {
"B" => 0,
"Carp" => 0,
"Digest::MD5" => 0,
"Exporter" => 0,
"Feature::Compat::Try" => 0,
"File::ShareDir" => 0,
"Getopt::Long::Descriptive" => 0,
"List::Util" => "1.55",
"MIME::Base64" => 0,
"Math::BigFloat" => 0,
"Math::BigInt" => "1.999701",
"Mojo::File" => 0,
"Mojo::JSON" => 0,
"Mojo::JSON::Pointer" => 0,
"Mojo::Message::Response" => 0,
"Mojo::URL" => 0,
"Mojolicious" => "7.87",
"Moo" => 0,
"Moo::Role" => 0,
"MooX::TypeTiny" => "0.002002",
"Safe::Isa" => "1.000008",
"Scalar::Util" => 0,
"Storable" => 0,
"Types::Common::Numeric" => 0,
"Types::Standard" => "1.016003",
"YAML::PP" => 0,
"autovivification" => 0,
"builtin::compat" => "0.003003",
"constant" => 0,
"experimental" => "0.026",
"feature" => 0,
"if" => 0,
"namespace::clean" => 0,
"open" => 0,
"overload" => 0,
"stable" => "0.031",
"strict" => 0,
"strictures" => 2,
"warnings" => 0
},
"TEST_REQUIRES" => {
"CPAN::Meta::Check" => "0.011",
"CPAN::Meta::Requirements" => 0,
"Data::Dumper" => 0,
"ExtUtils::MakeMaker" => 0,
"File::Spec" => 0,
"Math::BigInt" => "1.999701",
"Term::ANSIColor" => 0,
"Test2::API" => 0,
"Test2::V0" => 0,
"Test2::Warnings" => "0.038",
"Test::Deep" => 0,
"Test::Deep::UnorderedPairs" => 0,
"Test::File::ShareDir" => 0,
"Test::JSON::Schema::Acceptance" => "1.035",
"Test::Memory::Cycle" => 0,
"Test::More" => 0,
"Test::Needs" => 0,
"Test::Without::Module" => "0.19",
"lib" => 0,
"utf8" => 0
},
"VERSION" => "0.627",
"test" => {
"TESTS" => "t/*.t"
}
);
my %FallbackPrereqs = (
"B" => 0,
"CPAN::Meta::Check" => "0.011",
"CPAN::Meta::Requirements" => 0,
"Carp" => 0,
"Data::Dumper" => 0,
"Digest::MD5" => 0,
"Exporter" => 0,
"ExtUtils::MakeMaker" => 0,
"Feature::Compat::Try" => 0,
"File::ShareDir" => 0,
"File::Spec" => 0,
"Getopt::Long::Descriptive" => 0,
"List::Util" => "1.55",
"MIME::Base64" => 0,
"Math::BigFloat" => 0,
"Math::BigInt" => "1.999701",
"Mojo::File" => 0,
"Mojo::JSON" => 0,
"Mojo::JSON::Pointer" => 0,
"Mojo::Message::Response" => 0,
"Mojo::URL" => 0,
"Mojolicious" => "7.87",
"Moo" => 0,
"Moo::Role" => 0,
"MooX::TypeTiny" => "0.002002",
"Safe::Isa" => "1.000008",
"Scalar::Util" => 0,
"Storable" => 0,
"Term::ANSIColor" => 0,
"Test2::API" => 0,
"Test2::V0" => 0,
"Test2::Warnings" => "0.038",
"Test::Deep" => 0,
"Test::Deep::UnorderedPairs" => 0,
"Test::File::ShareDir" => 0,
"Test::JSON::Schema::Acceptance" => "1.035",
"Test::Memory::Cycle" => 0,
"Test::More" => 0,
"Test::Needs" => 0,
"Test::Without::Module" => "0.19",
"Types::Common::Numeric" => 0,
"Types::Standard" => "1.016003",
"YAML::PP" => 0,
"autovivification" => 0,
"builtin::compat" => "0.003003",
"constant" => 0,
"experimental" => "0.026",
"feature" => 0,
"if" => 0,
"lib" => 0,
"namespace::clean" => 0,
"open" => 0,
"overload" => 0,
"stable" => "0.031",
"strict" => 0,
"strictures" => 2,
"utf8" => 0,
"warnings" => 0
);
# inserted by Dist::Zilla::Plugin::DynamicPrereqs 0.040
if (!want_pp() && can_xs()) {
requires('Cpanel::JSON::XS', '4.38');
}
else {
requires('JSON::PP', '4.11');
}
unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) {
delete $WriteMakefileArgs{TEST_REQUIRES};
delete $WriteMakefileArgs{BUILD_REQUIRES};
$WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs;
}
delete $WriteMakefileArgs{CONFIGURE_REQUIRES}
unless eval { ExtUtils::MakeMaker->VERSION(6.52) };
WriteMakefile(%WriteMakefileArgs);
{
package
MY;
use File::ShareDir::Install qw(postamble);
}
# inserted by Dist::Zilla::Plugin::DynamicPrereqs 0.040
sub _add_prereq {
my ($mm_key, $module, $version_or_range) = @_;
$version_or_range ||= 0;
warn "$module already exists in $mm_key (at version $WriteMakefileArgs{$mm_key}{$module}) -- need to do a sane metamerge!"
if exists $WriteMakefileArgs{$mm_key}{$module}
and $WriteMakefileArgs{$mm_key}{$module} ne '0'
and $WriteMakefileArgs{$mm_key}{$module} ne $version_or_range;
warn "$module already exists in FallbackPrereqs (at version $FallbackPrereqs{$module}) -- need to do a sane metamerge!"
if exists $FallbackPrereqs{$module} and $FallbackPrereqs{$module} ne '0'
and $FallbackPrereqs{$module} ne $version_or_range;
$WriteMakefileArgs{$mm_key}{$module} = $FallbackPrereqs{$module} = $version_or_range;
return;
}
use lib 'inc';
use ExtUtils::HasCompiler 0.014 'can_compile_loadable_object';
{
my $can_xs;
sub can_xs {
return $can_xs if defined $can_xs;
$can_xs = can_compile_loadable_object(quiet => 1) ? 1 : 0;
}
}
{
my $parsed_args;
sub parse_args {
return $parsed_args if defined $parsed_args;
require ExtUtils::MakeMaker;
require Text::ParseWords;
ExtUtils::MakeMaker::parse_args(
my $tmp = {},
Text::ParseWords::shellwords($ENV{PERL_MM_OPT} || ''),
@ARGV,
);
$parsed_args = $tmp->{ARGS} || {};
}
}
sub requires { goto &runtime_requires }
sub runtime_requires {
my ($module, $version_or_range) = @_;
_add_prereq(PREREQ_PM => $module, $version_or_range);
}
{
my $want_pp;
sub want_pp {
return $$want_pp if defined $want_pp;
my $pp_only = parse_args()->{PUREPERL_ONLY};
$pp_only = !!$pp_only if defined $pp_only;
$want_pp = \$pp_only;
$pp_only;
}
}
dialects.t 100640 000766 000024 174467 15114374332 16700 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
use Test2::Warnings qw(warnings :no_end_test had_no_warnings allow_warnings);
my $js = JSON::Schema::Modern->new(short_circuit => 0, validate_formats => 1);
subtest 'invalid use of the $schema keyword' => sub {
cmp_result(
$js->evaluate(
1,
{
allOf => [
true,
{ '$schema' => 'https://json-schema.org/draft/2019-09/schema' },
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/1/$schema',
error => '$schema can only appear at the schema resource root',
},
],
},
'$schema can only appear at the root of a schema, when there is no canonical URI',
);
cmp_result(
$js->evaluate(
1,
{
'$id' => 'https://bloop.com',
allOf => [
true,
{ '$schema' => 'https://json-schema.org/draft/2019-09/schema' },
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/1/$schema',
absoluteKeywordLocation => 'https://bloop.com#/allOf/1/$schema',
error => '$schema can only appear at the schema resource root',
},
],
},
'$schema can only appear where the canonical URI has no fragment, when there is a canonical URI',
);
cmp_result(
$js->evaluate(
1,
{
'$id' => 'https://bloop3.com',
'$defs' => {
my_def => {
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
},
},
'$ref' => '#/$defs/my_def',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$defs/my_def/$schema',
absoluteKeywordLocation => 'https://bloop3.com#/$defs/my_def/$schema',
error => '$schema can only appear at the schema resource root',
},
],
},
'this is still not a resource root, even in a $ref target',
);
};
subtest 'defaults without a $schema keyword' => sub {
cmp_result(
$js->evaluate(1, true)->TO_JSON,
{ valid => true },
'boolean schema: no $id, no $schema',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft2020-12',
vocabularies => ignore, # for boolean schemas, vocabularies do not matter
}),
'boolean schema: defaults to draft2020-12 without a $schema keyword',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{ unevaluatedProperties => false },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/foo',
keywordLocation => '/unevaluatedProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/unevaluatedProperties',
error => 'not all additional properties are valid',
},
],
},
'object schema: no $id, no $schema',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft2020-12',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated) ],
}),
'object schema: defaults to draft2020-12 without a $schema keyword',
);
cmp_result(
$js->evaluate(
1,
{ '$defs' => { foo => { not => 'invalid subschema' } } },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$defs/foo/not',
error => 'invalid schema type: string',
},
],
},
'"not" keyword, from the Applicator vocabulary, is traversed at the root level',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'https://id-no-schema1',
unevaluatedProperties => false,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/foo',
keywordLocation => '/unevaluatedProperties',
absoluteKeywordLocation => 'https://id-no-schema1#/unevaluatedProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/unevaluatedProperties',
absoluteKeywordLocation => 'https://id-no-schema1#/unevaluatedProperties',
error => 'not all additional properties are valid',
},
],
},
'object schema: $id, no $schema',
);
cmp_result(
$js->{_resource_index}{'https://id-no-schema1'},
superhashof({
specification_version => 'draft2020-12',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated) ],
}),
'named resource defaults to draft2020-12 without a $schema keyword',
);
my $js = JSON::Schema::Modern->new(short_circuit => 0, specification_version => 'draft7');
cmp_result(
$js->evaluate(1, true)->TO_JSON,
{ valid => true },
'boolean schema: no $id, no $schema',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft7',
vocabularies => ignore, # for boolean schemas, vocabularies do not matter
}),
'boolean schema: specification_version overridden',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{ unevaluatedProperties => 'not a schema' },
)->TO_JSON,
{ valid => true },
'object schema: no $id, no $schema, specification version overridden, other keywords are ignored during traversal',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{ unevaluatedProperties => false },
)->TO_JSON,
{ valid => true },
'object schema: no $id, no $schema, specification version overridden, other keywords are ignored during evaluation',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'object schema: overridden to draft7',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'https://id-no-schema2',
unevaluatedProperties => 'not a schema',
},
)->TO_JSON,
{ valid => true },
'object schema: $id, no $schema, unrecognized+invalid keywords are ignored during traversal',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'https://id-no-schema3',
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'object schema: $id, no $schema',
);
cmp_result(
$js->{_resource_index}{'https://id-no-schema3'},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'object schema: overridden to draft7 and other keywords are ignored',
);
};
subtest 'behaviour with a $schema keyword' => sub {
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => 'not a schema',
},
)->TO_JSON,
{ valid => true },
'object schema: no $id, has $schema, unrecognized+invalid keywords are ignored during traversal',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'object schema: no $id, has $schema, unrecognized keywords are ignored during evaluation',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'semantics can be changed to another draft version',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$schema' => 'http://json-schema.org/draft-07/schema',
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'schema is accepted with $schema without an empty fragment',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'..and is still recognized as draft7',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'https://id-and-schema1',
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => 'not a schema',
},
)->TO_JSON,
{ valid => true },
'$id and $schema, unrecognized+invalid keywords are ignored during traversal',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'https://id-and-schema2',
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'$id and $schema',
);
cmp_result(
$js->{_resource_index}{'https://id-and-schema2'},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'named resource can be changed to another draft version and other keywords are ignored',
);
my $js = JSON::Schema::Modern->new(short_circuit => 0, specification_version => 'draft2019-09');
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => 'not a schema',
},
)->TO_JSON,
{ valid => true },
'no $id, specification version overridden twice; unrecognized+invalid keywords are ignored during traversal',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'no $id, specification version overridden twice, other keywords are ignored during evaluation',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'unnamed resource can be changed to another draft version',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'https://id-and-schema3',
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => 'not a schema',
},
)->TO_JSON,
{ valid => true },
'no $id, specification version overridden twice; unrecognized+invalid keywords are ignored during traversal',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'https://id-and-schema4',
'$schema' => 'http://json-schema.org/draft-07/schema#',
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'no $id, specification version overridden twice, other keywords are ignored during evaluation',
);
cmp_result(
$js->{_resource_index}{''},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'unnamed resource can be changed to another draft version',
);
};
subtest 'setting or changing specification versions in a single document' => sub {
cmp_result(
$js->evaluate(
1,
{
'$id' => 'https://bloop2.com',
allOf => [
true,
{
'$id' => 'https://newid.com',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
},
],
},
)->TO_JSON,
{ valid => true },
'$schema can appear adjacent to any $id',
);
};
subtest 'changing specification versions across documents' => sub {
my $expected = [ re(qr!^\Qno-longer-supported "dependencies" keyword present (at location "https://iam.draft2019-09.com")!) ];
$expected = superbagof(@$expected) if not $ENV{AUTHOR_TESTING};
cmp_result(
[ warnings {
$js->add_schema({
'$id' => 'https://iam.draft2019-09.com',
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
'$ref' => 'https://iam.draft7.com',
dependencies => { foo => false },
dependentSchemas => { foo => false },
additionalProperties => { format => 'ipv6' },
})
} ],
$expected,
'no unexpected warnings',
);
$js->add_schema({
'$id' => 'https://iam.draft7.com',
'$schema' => 'http://json-schema.org/draft-07/schema#',
dependencies => { foo => false },
dependentSchemas => { foo => false },
additionalProperties => { format => 'ipv4' },
unevaluatedProperties => false, # this should be ignored
});
cmp_result(
$js->evaluate({ foo => 'hi' }, 'https://iam.draft2019-09.com')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/dependencies/foo',
absoluteKeywordLocation => 'https://iam.draft7.com#/dependencies/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/$ref/dependencies',
absoluteKeywordLocation => 'https://iam.draft7.com#/dependencies',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/$ref/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft7.com#/additionalProperties/format',
error => 'not a valid ipv4 string',
},
{
instanceLocation => '',
keywordLocation => '/$ref/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft7.com#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/dependentSchemas/foo',
absoluteKeywordLocation => 'https://iam.draft2019-09.com#/dependentSchemas/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/dependentSchemas',
absoluteKeywordLocation => 'https://iam.draft2019-09.com#/dependentSchemas',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft2019-09.com#/additionalProperties/format',
error => 'not a valid ipv6 string',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft2019-09.com#/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'switching between specification versions is acceptable when crossing document boundaries',
);
cmp_result(
$js->{_resource_index}{'https://iam.draft2019-09.com'},
superhashof({
specification_version => 'draft2019-09',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'resources for top level schema',
);
cmp_result(
$js->{_resource_index}{'https://iam.draft7.com'},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'resources for subschema',
);
$expected = [ re(qr!^\Qno-longer-supported "dependencies" keyword present (at location "https://iam.draft2020-12-2.com")!) ];
$expected = superbagof(@$expected) if not $ENV{AUTHOR_TESTING};
$js->add_schema({
'$id' => 'https://iam.draft7-2.com',
'$schema' => 'http://json-schema.org/draft-07/schema#',
allOf => [ { '$ref' => 'https://iam.draft2020-12-2.com' } ],
dependencies => { foo => false },
dependentSchemas => { foo => false },
additionalProperties => { format => 'ipv4' },
unevaluatedProperties => false, # this should be ignored
});
cmp_result(
[ warnings {
$js->add_schema({
'$id' => 'https://iam.draft2020-12-2.com',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
dependencies => { foo => false },
dependentSchemas => { foo => false },
additionalProperties => { format => 'ipv6' },
})
} ],
$expected,
'no unexpected warnings',
);
cmp_result(
$js->evaluate({ foo => 'hi' }, 'https://iam.draft7-2.com')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/dependentSchemas/foo',
absoluteKeywordLocation => 'https://iam.draft2020-12-2.com#/dependentSchemas/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/dependentSchemas',
absoluteKeywordLocation => 'https://iam.draft2020-12-2.com#/dependentSchemas',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/allOf/0/$ref/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft2020-12-2.com#/additionalProperties/format',
error => 'not a valid ipv6 string',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft2020-12-2.com#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://iam.draft7-2.com#/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/dependencies/foo',
absoluteKeywordLocation => 'https://iam.draft7-2.com#/dependencies/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/dependencies',
absoluteKeywordLocation => 'https://iam.draft7-2.com#/dependencies',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft7-2.com#/additionalProperties/format',
error => 'not a valid ipv4 string',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft7-2.com#/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'switching between specification versions is acceptable when crossing document boundaries',
);
cmp_result(
$js->{_resource_index}{'https://iam.draft7-2.com'},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'resources for top level schema',
);
cmp_result(
$js->{_resource_index}{'https://iam.draft2020-12-2.com'},
superhashof({
specification_version => 'draft2020-12',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated) ],
}),
'resources for subschema',
);
};
subtest 'changing specification versions within documents' => sub {
allow_warnings(1);
cmp_result(
$js->evaluate(
{ foo => 'hi' },
{
'$id' => 'https://iam.draft2019-09-3.com',
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
allOf => [
{
'$id' => 'https://iam.draft7-3.com',
'$schema' => 'http://json-schema.org/draft-07/schema#',
dependencies => { foo => false },
dependentSchemas => 'blurp', # this should be ignored
additionalProperties => { format => 'ipv4' },
unevaluatedProperties => 'blurp', # this should be ignored
},
],
dependencies => 'blurp', # this should be ignored
dependentSchemas => { foo => false },
additionalProperties => { format => 'ipv6' },
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/dependencies/foo',
absoluteKeywordLocation => 'https://iam.draft7-3.com#/dependencies/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/dependencies',
absoluteKeywordLocation => 'https://iam.draft7-3.com#/dependencies',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/allOf/0/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft7-3.com#/additionalProperties/format',
error => 'not a valid ipv4 string',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft7-3.com#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://iam.draft2019-09-3.com#/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/dependentSchemas/foo',
absoluteKeywordLocation => 'https://iam.draft2019-09-3.com#/dependentSchemas/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/dependentSchemas',
absoluteKeywordLocation => 'https://iam.draft2019-09-3.com#/dependentSchemas',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft2019-09-3.com#/additionalProperties/format',
error => 'not a valid ipv6 string',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft2019-09-3.com#/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'switching between specification versions is acceptable within a document, draft2019-09 -> draft7',
);
allow_warnings(0);
cmp_result(
$js->{_resource_index}{'https://iam.draft2019-09-3.com'},
superhashof({
specification_version => 'draft2019-09',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'resources for top level schema',
);
cmp_result(
$js->{_resource_index}{'https://iam.draft7-3.com'},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'resources for subschema',
);
allow_warnings(1);
cmp_result(
$js->evaluate(
{ foo => 'hi' },
{
'$id' => 'https://iam.draft7-4.com',
'$schema' => 'http://json-schema.org/draft-07/schema#',
allOf => [
{
'$id' => 'https://iam.draft2020-12-4.com',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
dependencies => { foo => false }, # this should be ignored
dependentSchemas => { foo => false },
additionalProperties => { format => 'ipv4' },
},
],
dependencies => { foo => false },
dependentSchemas => { foo => false }, # this should be ignored
additionalProperties => { format => 'ipv6' },
unevaluatedProperties => false, # this should be ignored
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/dependentSchemas/foo',
absoluteKeywordLocation => 'https://iam.draft2020-12-4.com#/dependentSchemas/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/dependentSchemas',
absoluteKeywordLocation => 'https://iam.draft2020-12-4.com#/dependentSchemas',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/allOf/0/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft2020-12-4.com#/additionalProperties/format',
error => 'not a valid ipv4 string',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft2020-12-4.com#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://iam.draft7-4.com#/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/dependencies/foo',
absoluteKeywordLocation => 'https://iam.draft7-4.com#/dependencies/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/dependencies',
absoluteKeywordLocation => 'https://iam.draft7-4.com#/dependencies',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft7-4.com#/additionalProperties/format',
error => 'not a valid ipv6 string',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft7-4.com#/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'switching between specification versions is acceptable within a document, draft7 -> draf2020-12',
);
allow_warnings(0);
cmp_result(
$js->{_resource_index}{'https://iam.draft7-4.com'},
superhashof({
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'resources for top level schema',
);
cmp_result(
$js->{_resource_index}{'https://iam.draft2020-12-4.com'},
superhashof({
specification_version => 'draft2020-12',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated) ],
}),
'resources for subschema',
);
allow_warnings(1);
cmp_result(
$js->evaluate(
{ foo => 'hi' },
{
'$id' => 'https://iam.draft2020-12-5.com',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
allOf => [
{
id => 'https://iam.draft4-5.com',
'$schema' => 'http://json-schema.org/draft-04/schema#',
definitions => { blah => { not => {} } },
dependencies => { foo => { not => {} } },
dependentSchemas => { foo => { not => {} } }, # this should be ignored
allOf => [ { '$ref' => '#/definitions/blah' } ],
additionalProperties => { format => 'ipv4' },
},
],
dependencies => { foo => false }, # this should be ignored
dependentSchemas => { foo => false },
additionalProperties => { format => 'ipv6' },
unevaluatedProperties => false,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/allOf/0/$ref/not',
absoluteKeywordLocation => 'https://iam.draft4-5.com#/definitions/blah/not',
error => 'subschema is valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/allOf',
absoluteKeywordLocation => 'https://iam.draft4-5.com#/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/dependencies/foo/not',
absoluteKeywordLocation => 'https://iam.draft4-5.com#/dependencies/foo/not',
error => 'subschema is valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/dependencies',
absoluteKeywordLocation => 'https://iam.draft4-5.com#/dependencies',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/allOf/0/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft4-5.com#/additionalProperties/format',
error => 'not a valid ipv4 string',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft4-5.com#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://iam.draft2020-12-5.com#/allOf',
error => 'subschema 0 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/dependentSchemas/foo',
absoluteKeywordLocation => 'https://iam.draft2020-12-5.com#/dependentSchemas/foo',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/dependentSchemas',
absoluteKeywordLocation => 'https://iam.draft2020-12-5.com#/dependentSchemas',
error => 'not all dependencies are satisfied',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/format',
absoluteKeywordLocation => 'https://iam.draft2020-12-5.com#/additionalProperties/format',
error => 'not a valid ipv6 string',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://iam.draft2020-12-5.com#/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'switching between specification versions is acceptable within a document, draft2020-12 -> draft4',
);
allow_warnings(0);
cmp_result(
$js->{_resource_index}{'https://iam.draft2020-12-5.com'},
superhashof({
specification_version => 'draft2020-12',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated) ],
}),
'resources for top level schema',
);
cmp_result(
$js->{_resource_index}{'https://iam.draft4-5.com'},
superhashof({
specification_version => 'draft4',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator MetaData) ],
}),
'resources for subschema',
);
};
undef $js;
subtest '$vocabulary syntax' => sub {
cmp_result(
JSON::Schema::Modern->new->evaluate(
1,
{
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'#/notauri' => false,
'https://foo' => 1,
'https://json-schema.org/draft/2019-09/vocab/validation' => true,
'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
'https://unknown' => true, # ignored.. for now
'https://unknown2' => false, # ""
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => jsonp('/$vocabulary', '#/notauri'),
error => '"#/notauri" is not a valid URI',
},
{
instanceLocation => '',
keywordLocation => jsonp('/$vocabulary', 'https://foo'),
error => '$vocabulary value at "https://foo" is not a boolean',
},
],
},
'$vocabulary syntax checks',
);
cmp_result(
JSON::Schema::Modern->new->evaluate(
1,
{
'$id' => 'http://mymetaschema',
items => { '$vocabulary' => { 'https://json-schema.org/draft/2020-12/vocab/applicator' => true } },
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/items/$vocabulary',
absoluteKeywordLocation => 'http://mymetaschema#/items/$vocabulary',
error => '$vocabulary can only appear at the schema resource root',
},
],
},
'$vocabulary location check - resource root',
);
cmp_result(
JSON::Schema::Modern->new->evaluate(
1,
{
items => {
'$id' => 'foobar',
'$vocabulary' => { 'https://json-schema.org/draft/2020-12/vocab/core' => true },
},
},
)->TO_JSON,
{ valid => true },
'$vocabulary location check - document root',
);
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
1,
{
'$id' => 'http://mymetaschema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/applicator' => false,
},
},
)->TO_JSON,
{ valid => true },
'successfully evaluated a metaschema that specifies vocabularies',
);
cmp_result(
$js->{_resource_index}{'http://mymetaschema'},
{
canonical_uri => str('http://mymetaschema'),
path => '',
specification_version => 'draft2020-12',
document => ignore,
vocabularies => [
map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated),
],
},
'metaschemas are not saved on the resource',
);
cmp_result(
$js->evaluate(1, { '$schema' => 'http://mymetaschema' })->TO_JSON,
{ valid => true },
'..but once we use the schema as a metaschema,',
);
cmp_result(
$js->{_metaschema_vocabulary_classes}{'http://mymetaschema'},
[
'draft2020-12',
[
'JSON::Schema::Modern::Vocabulary::Core',
'JSON::Schema::Modern::Vocabulary::Applicator',
],
],
'... the vocabulary information is now cached in the evaluator',
);
};
subtest 'changing dialects (same specification version)' => sub {
my $js = JSON::Schema::Modern->new(collect_annotations => 1);
$js->add_schema({
'$id' => 'https://my_metaschema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/validation' => true,
# no applicator!
},
});
$js->add_schema({
'$id' => 'https://my_other_schema',
'$schema' => 'https://my_metaschema',
type => 'object',
properties => { bar => false }, # this keyword should only annotate
zeta => 1,
});
cmp_result(
$js->evaluate(
{ foo => { bar => 1 } },
{
'$id' => 'https://example.com',
additionalProperties => {
'$ref' => 'https://my_other_schema',
},
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/properties',
absoluteKeywordLocation => 'https://my_other_schema#/properties',
annotation => { bar => false },
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/zeta',
absoluteKeywordLocation => 'https://my_other_schema#/zeta',
annotation => 1,
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://example.com#/additionalProperties',
annotation => ['foo'],
},
],
},
'evaluation of the subschema in another document correctly uses the new $id and $schema',
);
cmp_result(
$js->evaluate(
{ foo => { bar => 1 } },
{
'$id' => 'https://example2.com',
'$defs' => {
'my_def' => {
'$id' => 'https://my_other_schema2',
'$schema' => 'https://my_metaschema',
type => 'object',
properties => { bar => false }, # this keyword should only annotate
zeta => 1,
},
},
additionalProperties => {
'$ref' => 'https://my_other_schema2',
},
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/properties',
absoluteKeywordLocation => 'https://my_other_schema2#/properties',
annotation => { bar => false },
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/zeta',
absoluteKeywordLocation => 'https://my_other_schema2#/zeta',
annotation => 1,
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://example2.com#/additionalProperties',
annotation => ['foo'],
},
],
},
'evaluation of the subschema in the same document via a $ref correctly uses the new $id and $schema',
);
cmp_result(
$js->evaluate(
{ foo => { bar => 1 } },
{
'$id' => 'https://example3.com',
additionalProperties => {
'$id' => 'https://my_other_schema3',
'$schema' => 'https://my_metaschema',
type => 'object',
properties => { bar => false }, # this keyword should only annotate
zeta => 1,
},
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/properties',
absoluteKeywordLocation => 'https://my_other_schema3#/properties',
annotation => { bar => false },
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/zeta',
absoluteKeywordLocation => 'https://my_other_schema3#/zeta',
annotation => 1,
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://example3.com#/additionalProperties',
annotation => ['foo'],
},
],
},
'evaluation of the subschema in the same document with no $ref correctly uses the new $id and $schema',
);
cmp_result(
$js->traverse({
'$id' => 'https://example4.com',
additionalProperties => {
'$id' => 'https://my_other_schema4',
'$schema' => 'https://my_metaschema',
type => 'object',
properties => 1, # this is not a real keyword as the assertion vocabulary is not present
},
}),
superhashof({ errors => [] }),
'no errors found when traversing a document with a malformed keyword outside the dialect',
);
};
subtest 'standard metaschemas' => sub {
my $js = JSON::Schema::Modern->new;
my ($draft202012_metaschema) = $js->get('https://json-schema.org/draft/2020-12/schema');
cmp_result(
$js->evaluate($draft202012_metaschema, 'https://json-schema.org/draft/2020-12/schema')->TO_JSON,
{ valid => true },
'main metaschema evaluated against its own URI',
);
cmp_result(
$js->evaluate($draft202012_metaschema, $draft202012_metaschema)->TO_JSON,
{ valid => true },
'main metaschema evaluated against its own content',
);
my ($draft202012_core_metaschema) = $js->get('https://json-schema.org/draft/2020-12/meta/core');
cmp_result(
$js->evaluate($draft202012_core_metaschema, 'https://json-schema.org/draft/2020-12/schema')->TO_JSON,
{ valid => true },
'core metaschema evaluated against the main metaschema URI',
);
cmp_result(
$js->evaluate($draft202012_core_metaschema, $draft202012_core_metaschema)->TO_JSON,
{ valid => true },
'core metaschema evaluated against its own content',
);
};
subtest 'custom metaschemas, without custom vocabularies' => sub {
my $js = JSON::Schema::Modern->new;
my $metaschema_document = $js->add_schema(my $metaschema = {
'$id' => 'http://localhost:1234/my-meta-schema',
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
type => 'object',
'$recursiveAnchor' => true,
allOf => [ { '$ref' => 'https://json-schema.org/draft/2019-09/schema' } ],
});
cmp_result(
$metaschema_document,
methods(
canonical_uri => str('http://localhost:1234/my-meta-schema'),
metaschema_uri => str('https://json-schema.org/draft/2019-09/schema'),
),
'document contains correct values',
);
is($metaschema_document->_get_resource($metaschema->{'$id'})->{specification_version}, 'draft2019-09',
'specification version detected from standard metaschema URI');
cmp_result(
$js->evaluate(false, 'http://localhost:1234/my-meta-schema')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/type',
absoluteKeywordLocation => 'http://localhost:1234/my-meta-schema#/type',
error => 'got boolean, not object',
},
],
},
'custom metaschema restricts schemas to objects',
);
# the evaluation of $recursiveAnchor in the schema proves that the proper specification version
# was detected via the $schema keyword
cmp_result(
$js->evaluate(
{ allOf => [ false ] },
'http://localhost:1234/my-meta-schema',
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/allOf/0',
keywordLocation => '/allOf/0/$ref/allOf/1/$ref/properties/allOf/$ref/items/$recursiveRef/type',
absoluteKeywordLocation => 'http://localhost:1234/my-meta-schema#/type',
error => 'got boolean, not object',
},
{
instanceLocation => '/allOf',
keywordLocation => '/allOf/0/$ref/allOf/1/$ref/properties/allOf/$ref/items',
absoluteKeywordLocation => 'https://json-schema.org/draft/2019-09/meta/applicator#/$defs/schemaArray/items',
error => 'subschema is not valid against all items',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/allOf/1/$ref/properties',
absoluteKeywordLocation => 'https://json-schema.org/draft/2019-09/meta/applicator#/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/allOf',
absoluteKeywordLocation => 'https://json-schema.org/draft/2019-09/schema#/allOf',
error => 'subschema 1 is not valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'http://localhost:1234/my-meta-schema#/allOf',
error => 'subschema 0 is not valid',
},
],
},
'custom metaschema recurses to standard metaschema',
);
cmp_result(
$js->evaluate({ allOf => [ {} ] }, 'http://localhost:1234/my-meta-schema')->TO_JSON,
{ valid => true },
'objects are acceptable schemas to this metaschema',
);
cmp_result(
$js->evaluate(
1,
{
'$id' => 'https://localhost:1234/my-schema',
'$schema' => 'http://localhost:1234/my-meta-schema',
},
)->TO_JSON,
{ valid => true },
'metaschemas without $vocabulary can still be used in the $schema keyword',
);
cmp_result(
$js->{_resource_index}{'https://localhost:1234/my-schema'},
superhashof({
specification_version => 'draft2019-09',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'..and schema uses the correct spec version and vocabularies',
);
};
subtest 'custom metaschemas, with custom vocabularies' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(1, { '$schema' => 20 })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '$schema value is not a string',
},
],
},
'$schema values must be strings',
);
cmp_result(
$js->evaluate(1, { '$schema' => '#/not_a_uri' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"#/not_a_uri" is not a valid URI',
},
],
},
'$schema values must be URIs',
);
cmp_result(
$js->evaluate(1, { '$schema' => 'https://unknown/metaschema' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema',
error => 'EXCEPTION: unable to find resource "https://unknown/metaschema"',
},
],
},
'custom metaschemas are okay, but the document must be known',
);
$js->add_schema({
'$id' => 'https://metaschema/with/misplaced/vocabulary/keyword/base',
items => {
'$id' => 'subschema',
'$vocabulary' => { 'https://json-schema.org/draft/2020-12/vocab/core' => true },
},
});
cmp_result(
$js->evaluate(1, { '$schema' => 'https://metaschema/with/misplaced/vocabulary/keyword/subschema' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema/$vocabulary',
absoluteKeywordLocation => 'https://metaschema/with/misplaced/vocabulary/keyword/subschema#/$vocabulary',
error => '$vocabulary can only appear at the document root',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://metaschema/with/misplaced/vocabulary/keyword/subschema" is not a valid metaschema',
},
],
},
'$vocabulary location check - document root',
);
$js->add_schema('https://metaschema/with/no/id',
{ '$vocabulary' => { 'https://json-schema.org/draft/2020-12/vocab/core' => true } });
cmp_result(
$js->evaluate(1, { '$schema' => 'https://metaschema/with/no/id' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema/$vocabulary',
absoluteKeywordLocation => 'https://metaschema/with/no/id#/$vocabulary',
error => 'metaschemas must have an $id',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://metaschema/with/no/id" is not a valid metaschema',
},
],
},
'metaschemas must have an i$id',
);
$js->add_schema({
'$id' => 'https://metaschema/with/wrong/spec',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2019-09/vocab/validation' => true,
'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
'https://unknown' => true,
'https://unknown2' => false,
},
});
cmp_result(
$js->evaluate(1, { '$schema' => 'https://metaschema/with/wrong/spec' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => jsonp(qw(/$schema $vocabulary https://json-schema.org/draft/2019-09/vocab/validation)),
absoluteKeywordLocation => 'https://metaschema/with/wrong/spec#'.jsonp(qw(/$vocabulary https://json-schema.org/draft/2019-09/vocab/validation)),
error => '"https://json-schema.org/draft/2019-09/vocab/validation" uses draft2019-09, but the metaschema itself uses draft2020-12',
},
{
instanceLocation => '',
keywordLocation => jsonp(qw(/$schema $vocabulary https://unknown)),
absoluteKeywordLocation => 'https://metaschema/with/wrong/spec#'.jsonp(qw(/$vocabulary https://unknown)),
error => '"https://unknown" is not a known vocabulary',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://metaschema/with/wrong/spec" is not a valid metaschema',
},
],
},
'$vocabulary validation that must be deferred until used as a metaschema',
);
$js->add_schema({
'$id' => 'https://my/mismatched/metaschema',
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
'$vocabulary' => {
'https://json-schema.org/draft/2019-09/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
# note: no validation!
},
});
cmp_result(
$js->evaluate(1, { '$schema' => 'https://my/mismatched/metaschema' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => jsonp(qw(/$schema $vocabulary https://json-schema.org/draft/2020-12/vocab/applicator)),
absoluteKeywordLocation => 'https://my/mismatched/metaschema#'.jsonp(qw(/$vocabulary https://json-schema.org/draft/2020-12/vocab/applicator)),
error => '"https://json-schema.org/draft/2020-12/vocab/applicator" uses draft2020-12, but the metaschema itself uses draft2019-09',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://my/mismatched/metaschema" is not a valid metaschema',
},
],
},
'vocabularies in the metaschema must match the $schema version',
);
$js->add_schema({
'$id' => 'https://metaschema/missing/vocabs',
'$vocabulary' => {},
});
cmp_result(
$js->evaluate(1, { '$schema' => 'https://metaschema/missing/vocabs' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema/$vocabulary',
absoluteKeywordLocation => 'https://metaschema/missing/vocabs#/$vocabulary',
error => 'the first vocabulary (by evaluation_order) must be Core',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://metaschema/missing/vocabs" is not a valid metaschema',
},
],
},
'metaschemas using "$vocabulary" must contain vocabularies',
);
$js->add_schema({
'$id' => 'https://metaschema/missing/core',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
},
});
cmp_result(
$js->evaluate(1, { '$schema' => 'https://metaschema/missing/core' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema/$vocabulary',
absoluteKeywordLocation => 'https://metaschema/missing/core#/$vocabulary',
error => 'the first vocabulary (by evaluation_order) must be Core',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://metaschema/missing/core" is not a valid metaschema',
},
],
},
'metaschemas must contain the Core vocabulary',
);
$js->add_schema({
'$id' => 'https://my/first/metaschema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
'https://json-schema.org/draft/2020-12/vocab/core' => true,
# note: no validation!
},
});
cmp_result(
$js->evaluate(
1,
{
'$id' => my $id = 'https://my/first/schema/with/custom/metaschema',
'$schema' => 'https://my/first/metaschema',
minimum => 10,
},
)->TO_JSON,
{ valid => true },
'validation succeeds because "minimum" never gets run',
);
cmp_result(
$js->{_resource_index}{$id}{document},
methods(
canonical_uri => str($id),
metaschema_uri => str('https://my/first/metaschema'),
),
'document contains correct values',
);
cmp_result(
$js->{_resource_index}{$id},
{
canonical_uri => str($id),
path => '',
specification_version => 'draft2020-12',
document => ignore,
vocabularies => [
map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Applicator),
],
},
'determined vocabularies to use for this schema',
);
};
subtest 'custom vocabulary classes with add_vocabulary()' => sub {
my $js = JSON::Schema::Modern->new;
like(
dies { $js->add_vocabulary('MyVocabulary::Does::Not::Exist') },
qr!an't locate MyVocabulary/Does/Not/Exist.pm in \@INC!,
'vocabulary class must exist',
);
like(
dies { $js->add_vocabulary('MyVocabulary::MissingRole') },
qr/Value "MyVocabulary::MissingRole" did not pass type constraint/,
'vocabulary class must implement the role',
);
like(
dies { $js->add_vocabulary('MyVocabulary::MissingSub') },
qr/Can't apply JSON::Schema::Modern::Vocabulary to MyVocabulary::MissingSub - missing vocabulary, keywords/,
'vocabulary class must implement some subs',
);
cmp_result(
[ warnings {
like(
dies { $js->add_vocabulary('MyVocabulary::BadVocabularySub1') },
qr/Undef did not pass type constraint/,
'vocabulary() sub in the vocabulary class must return uri => specification_version pairs',
)
} ],
[ re(qr/Odd number of elements in pairs/) ],
'parse error from bad vocab sub',
);
like(
dies { $js->add_vocabulary('MyVocabulary::BadVocabularySub2') },
qr!Value "https://some/uri#/invalid/uri" did not pass type constraint!,
'vocabulary() sub in the vocabulary class must contain valid absolute, fragmentless URIs',
);
like(
dies { $js->add_vocabulary('MyVocabulary::BadVocabularySub3') },
qr/Value "wrongdraft" did not pass type constraint/,
'vocabulary() sub in the vocabulary class must reference a known specification version',
);
ok(
lives { $js->add_vocabulary('MyVocabulary::BadEvaluationOrder') },
'added a vocabulary sub',
);
cmp_result(
$js->{_vocabulary_classes},
superhashof({ 'https://vocabulary/with/bad/evaluation/order' => [ 'draft2020-12', 'MyVocabulary::BadEvaluationOrder' ] }),
'vocabulary was successfully added',
);
$js->add_schema({
'$id' => 'https://my/first/metaschema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/validation' => true,
'https://vocabulary/with/bad/evaluation/order' => true,
},
});
cmp_result(
$js->evaluate(1, { '$schema' => 'https://my/first/metaschema' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema/$vocabulary',
absoluteKeywordLocation => 'https://my/first/metaschema#/$vocabulary',
error => 'JSON::Schema::Modern::Vocabulary::Validation and MyVocabulary::BadEvaluationOrder have a conflicting evaluation_order',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://my/first/metaschema" is not a valid metaschema',
},
],
},
'custom vocabulary class has a conflicting evaluation_order',
);
ok(
lives { $js->add_vocabulary('MyVocabulary::StringComparison') },
'added another vocabulary sub',
);
$js->add_schema({
'$id' => 'https://my/first/working/metaschema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://vocabulary/string/comparison' => true,
},
});
cmp_result(
$js->evaluate(
'bloop',
{
'$id' => 'https://my/first/schema/with/custom/metaschema',
'$schema' => 'https://my/first/working/metaschema',
stringLessThan => 'alpha',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/stringLessThan',
absoluteKeywordLocation => 'https://my/first/schema/with/custom/metaschema#/stringLessThan',
error => 'value is not stringwise less than alpha',
},
],
},
'custom vocabulary class used by a custom metaschema used by a schema',
);
like(
dies { $js->add_vocabulary('MyVocabulary::ReservedKeyword') },
qr/^keywords starting with "\$" are reserved for core and cannot be used/,
'$ keywords are prohibited',
);
ok(
lives { $js->add_vocabulary('MyVocabulary::ConflictingKeyword') },
'added another vocabulary sub',
);
$js->add_schema({
'$id' => 'https://colliding/keyword/metaschema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/validation' => true,
'https://vocabulary/conflicting/keyword' => true,
},
});
cmp_result(
$js->evaluate(
'bloop',
{
'$id' => 'https://my/second/schema/with/custom/metaschema',
'$schema' => 'https://colliding/keyword/metaschema',
minLength => 2,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema/$vocabulary',
absoluteKeywordLocation => 'https://colliding/keyword/metaschema#/$vocabulary',
error => 'MyVocabulary::ConflictingKeyword keyword "minLength" conflicts with keyword of the same name from JSON::Schema::Modern::Vocabulary::Validation',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://colliding/keyword/metaschema" is not a valid metaschema',
},
],
},
'keywords cannot appear in more than one vocabulary in the same dialect',
);
};
subtest '$schema points to a boolean schema' => sub {
my $js = JSON::Schema::Modern->new;
$js->add_schema('https://my_boolean_schema' => true);
cmp_result(
my $result = $js->evaluate(
1,
{
'$id' => '/foo',
'$schema' => 'https://my_boolean_schema',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$schema',
# we haven't processed $id yet, so we don't know the absolute location
error => 'metaschemas must be objects',
},
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"https://my_boolean_schema" is not a valid metaschema',
},
],
},
'$schema cannot reference a boolean schema',
);
};
subtest '$ref to a different dialect' => sub {
my $js = JSON::Schema::Modern->new(collect_annotations => 1);
$js->add_schema({
'$id' => 'https://my_metaschema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/validation' => true,
},
});
cmp_result(
$js->evaluate(
{ foo => { bar => 1 } },
{
'$id' => 'https://example.com',
'$defs' => {
subschema => {
'$id' => 'https://foo.com',
'$schema' => 'https://my_metaschema',
'$defs' => {
my_def => { type => 'object', blah => 1 },
},
'$ref' => '#/$defs/my_def',
bloop => 2,
properties => { bar => false }, # this keyword should only annotate
},
},
additionalProperties => { '$ref' => '#/$defs/subschema' },
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/$ref/blah',
absoluteKeywordLocation => 'https://foo.com#/$defs/my_def/blah',
annotation => 1,
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/bloop',
absoluteKeywordLocation => 'https://foo.com#/bloop',
annotation => 2,
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/properties',
absoluteKeywordLocation => 'https://foo.com#/properties',
annotation => { bar => false },
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://example.com#/additionalProperties',
annotation => [ 'foo' ],
},
],
},
'evaluation of the subschema correctly uses the new $id and $schema',
);
};
had_no_warnings() if $ENV{AUTHOR_TESTING};
done_testing;
document.t 100640 000766 000024 114261 15114374332 16710 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use List::Util 'unpairs';
use lib 't/lib';
use Helper;
use Test::Deep::UnorderedPairs;
use Test::Memory::Cycle;
# spec version -> vocab classes
my %vocabularies = unpairs(JSON::Schema::Modern->new->__all_metaschema_vocabulary_classes);
my %dialect = (
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
);
subtest 'boolean document' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(schema => false),
listmethods(
resource_index => [
'' => {
path => '',
canonical_uri => str(''),
%dialect,
},
],
original_uri => [ str('') ],
canonical_uri => [ str('') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { '' => 0 } ],
),
'boolean schema with no canonical_uri',
);
like(
dies {
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com#/x/y/z'),
schema => false,
)
},
qr/Reference .*did not pass type constraint/,
'boolean schema with invalid canonical_uri (fragment)',
);
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com'),
schema => false,
),
listmethods(
resource_index => [
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
%dialect,
},
],
canonical_uri => [ str('https://foo.com') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { '' => 0 } ],
),
'boolean schema with valid canonical_uri',
);
};
subtest 'object document' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(
defined $_ ? (canonical_uri => $_) : (),
schema => {},
),
listmethods(
resource_index => [
str($_//'') => {
path => '',
canonical_uri => str($_//''),
%dialect,
},
],
original_uri => [ str($_//'') ],
canonical_uri => [ str($_//'') ],
_entities => [ { '' => 0 } ],
),
'object schema with originally provided uri = \''.($_//'').'\' and no root $id',
)
foreach (undef, '', '0', Mojo::URL->new, Mojo::URL->new(''), Mojo::URL->new('0'));
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com'),
schema => {},
),
listmethods(
resource_index => [
# note: no '' entry!
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
%dialect,
},
],
original_uri => [ str('https://foo.com') ],
canonical_uri => [ str('https://foo.com') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { '' => 0 } ],
),
'object schema with valid canonical_uri, no root $id',
);
cmp_result(
JSON::Schema::Modern::Document->new(
defined $_ ? (canonical_uri => $_) : (),
schema => { '$id' => 'https://foo.com' },
),
listmethods(
resource_index => [
# note: no '' entry
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
%dialect,
},
],
original_uri => [ str($_//'') ],
canonical_uri => [ str('https://foo.com') ], # note canonical_uri has been overwritten
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { '' => 0 } ],
),
'object schema with originally provided uri = \''.($_//'').'\' and absolute root $id',
)
foreach (undef, '', Mojo::URL->new);
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => $_,
schema => { '$id' => 'https://bar.com' },
),
listmethods(
resource_index => [
# note: no '' entry
'https://bar.com' => {
path => '',
canonical_uri => str('https://bar.com'),
%dialect,
},
],
original_uri => [ str($_) ],
canonical_uri => [ str('https://bar.com') ], # note canonical_uri has been overwritten
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { '' => 0 } ],
),
'originally provided uri is not indexed when overridden by an absolute root $id',
)
foreach ('0', Mojo::URL->new('0'), 'https://foo.com');
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$defs' => { foo => {} },
},
canonical_uri => 'https://example.com',
),
listmethods(
resource_index => [
'https://example.com' => {
path => '',
canonical_uri => str('https://example.com'),
%dialect,
},
],
),
'when canonical_uri provided, the empty uri is not added as a referenceable uri',
);
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com'),
schema => { '$id' => 'https://foo.com' },
),
listmethods(
resource_index => [
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
%dialect,
},
],
original_uri => [ str('https://foo.com') ],
canonical_uri => [ str('https://foo.com') ],
_entities => [ { '' => 0 } ],
),
'object schema with originally provided uri equal to root $id',
);
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com'),
schema => {
'$id' => 'https://bar.com',
allOf => [
{ '$anchor' => 'my_anchor' },
{ '$id' => 'x/y/z.json' },
],
},
),
listmethods(
resource_index => unordered_pairs(
'https://bar.com' => {
path => '',
canonical_uri => str('https://bar.com'),
%dialect,
anchors => {
my_anchor => {
path => '/allOf/0',
canonical_uri => str('https://bar.com#/allOf/0'),
},
},
},
'https://bar.com/x/y/z.json' => {
path => '/allOf/1',
canonical_uri => str('https://bar.com/x/y/z.json'),
%dialect,
},
),
original_uri => [ str('https://foo.com') ],
canonical_uri => [ str('https://bar.com') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { map +($_ => 0), '', '/allOf/0', '/allOf/1' } ],
),
'object schema with canonical_uri and root $id, and additional resource schemas as well',
);
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$id' => 'relative',
},
canonical_uri => 'https://my-base.com',
),
listmethods(
resource_index => [
'https://my-base.com/relative' => {
path => '',
canonical_uri => str('https://my-base.com/relative'),
%dialect,
},
],
original_uri => [ str('https://my-base.com') ],
canonical_uri => [ str('https://my-base.com/relative') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { '' => 0 } ],
),
'relative $id at root is resolved against provided canonical_id',
);
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$defs' => {
foo => {
'$id' => 'my_foo',
const => 'foo value',
},
},
'$ref' => 'my_foo',
},
),
listmethods(
resource_index => unordered_pairs(
'' => {
path => '', canonical_uri => str(''),
%dialect,
},
'my_foo' => {
path => '/$defs/foo',
canonical_uri => str('my_foo'),
%dialect,
},
),
original_uri => [ str('') ],
canonical_uri => [ str('') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { map +($_ => 0), '', '/$defs/foo' } ],
),
'relative uri for inner $id',
);
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$defs' => {
foo => {
'$id' => 'http://localhost:4242/my_foo',
const => 'foo value',
},
},
},
),
listmethods(
resource_index => unordered_pairs(
'' => {
path => '', canonical_uri => str(''),
%dialect,
},
'http://localhost:4242/my_foo' => {
path => '/$defs/foo',
canonical_uri => str('http://localhost:4242/my_foo'),
%dialect,
},
),
original_uri => [ str('') ],
canonical_uri => [ str('') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
_entities => [ { map +($_ => 0), '', '/$defs/foo' } ],
),
'no root $id; absolute uri with path in subschema resource',
);
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$anchor' => 'my_anchor',
},
),
listmethods(
resource_index => [
'' => {
path => '',
canonical_uri => str(''),
%dialect,
anchors => {
my_anchor => {
path => '',
canonical_uri => str(''),
},
},
},
],
original_uri => [ str('') ],
canonical_uri => [ str('') ],
),
'no root $id or canonical_uri provided; anchor is indexed at the root',
);
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$anchor' => 'my_anchor',
},
canonical_uri => 'https://example.com',
),
listmethods(
resource_index => [
'https://example.com' => {
path => '',
canonical_uri => str('https://example.com'),
%dialect,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://example.com'),
},
},
},
],
original_uri => [ str('https://example.com') ],
canonical_uri => [ str('https://example.com') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
),
'canonical_uri provided; empty uri not added as a referenceable uri when an anchor exists',
);
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$id' => 'https://my-base.com',
'$anchor' => 'my_anchor',
},
),
listmethods(
resource_index => [
'https://my-base.com' => {
path => '',
canonical_uri => str('https://my-base.com'),
%dialect,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://my-base.com'),
},
},
},
],
original_uri => [ str('') ],
canonical_uri => [ str('https://my-base.com') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
),
'absolute uri provided at root; adjacent anchor has the same canonical uri',
);
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$id' => 'https://my-base.com',
'$defs' => {
foo => {
'$anchor' => 'my_anchor',
},
},
},
),
listmethods(
resource_index => [
'https://my-base.com' => {
path => '',
canonical_uri => str('https://my-base.com'),
%dialect,
anchors => {
my_anchor => {
path => '/$defs/foo',
canonical_uri => str('https://my-base.com#/$defs/foo'),
},
},
},
],
original_uri => [ str('') ],
canonical_uri => [ str('https://my-base.com') ],
metaschema_uri => [ str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}) ],
),
'absolute uri provided at root; anchor lower down has its own canonical uri',
);
};
subtest '$id and $anchor as properties' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
type => 'object',
properties => {
'$id' => { type => 'string' },
'$anchor' => { type => 'string' },
},
},
),
listmethods(
resource_index => [
'' => {
path => '',
canonical_uri => str(''),
%dialect,
},
],
_entities => [ { map +($_ => 0), '', '/properties/$id', '/properties/$anchor' } ],
),
'did not index the $id and $anchor properties as if they were identifier keywords',
);
};
subtest '$id with an empty fragment' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(
schema => {
'$defs' => {
foo => {
'$id' => 'http://localhost:4242/my_foo#',
type => 'string',
},
},
},
),
listmethods(
resource_index => unordered_pairs(
'' => {
path => '', canonical_uri => str(''),
%dialect,
},
'http://localhost:4242/my_foo' => {
path => '/$defs/foo',
canonical_uri => str('http://localhost:4242/my_foo'),
%dialect,
},
),
_entities => [ { map +($_ => 0), '', '/$defs/foo' } ],
),
'$id is stored with the empty fragment stripped',
);
};
subtest '$id with a non-empty fragment' => sub {
my $doc = JSON::Schema::Modern::Document->new(
schema => {
'$id' => 'http://main.com',
'$defs' => {
foo => {
'$id' => 'http://secondary.com',
properties => {
bar => {
'$id' => 'http://localhost:4242/my_foo#hello',
},
},
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $doc->errors ],
[
{
instanceLocation => '',
keywordLocation => '/$defs/foo/properties/bar/$id',
absoluteKeywordLocation => 'http://secondary.com#/properties/bar/$id',
error => '$id value "http://localhost:4242/my_foo#hello" cannot have a non-empty fragment',
},
],
'did not index the $id with a non-empty fragment, nor use it as the base for other identifiers',
);
cmp_result($doc->canonical_uri, str('http://main.com'), 'canonical_uri');
cmp_result([ $doc->resource_index ], [], 'nothing was indexed');
};
subtest '$anchor not conforming to syntax' => sub {
my $doc = JSON::Schema::Modern::Document->new(
schema => {
'$defs' => {
foo => {
'$anchor' => 'my_#bad_anchor',
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $doc->errors ],
[
{
instanceLocation => '',
keywordLocation => '/$defs/foo/$anchor',
error => '$anchor value "my_#bad_anchor" does not match required syntax',
},
],
'did not index an $anchor with invalid characters',
);
cmp_result([ $doc->resource_index ], [], 'nothing was indexed');
$doc = JSON::Schema::Modern::Document->new(
specification_version => 'draft2020-12',
schema => {
'$defs' => {
foo => {
'$anchor' => 'my:bad_anchor', # legal in earlier drafts
},
qux => {
'$id' => 'https://foo.com#my_bad_id',
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $doc->errors ],
[
{
instanceLocation => '',
keywordLocation => '/$defs/foo/$anchor',
error => '$anchor value "my:bad_anchor" does not match required syntax',
},
{
instanceLocation => '',
keywordLocation => '/$defs/qux/$id',
error => '$id value "https://foo.com#my_bad_id" cannot have a non-empty fragment',
},
],
'did not index a draft2020-12 $anchor with invalid characters, or non-fragment-only $id',
);
cmp_result([ $doc->resource_index ], [], 'nothing was indexed');
$doc = JSON::Schema::Modern::Document->new(
specification_version => 'draft2019-09',
schema => {
'$defs' => {
foo => {
'$anchor' => '_my_bad_anchor', # legal in draft2020-12
},
qux => {
'$id' => 'https://foo.com#my_bad_id',
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $doc->errors ],
[
{
instanceLocation => '',
keywordLocation => '/$defs/foo/$anchor',
error => '$anchor value "_my_bad_anchor" does not match required syntax',
},
{
instanceLocation => '',
keywordLocation => '/$defs/qux/$id',
error => '$id value "https://foo.com#my_bad_id" cannot have a non-empty fragment',
},
],
'did not index a draft2019-09 $anchor with invalid characters, or non-fragment-only $id',
);
cmp_result([ $doc->resource_index ], [], 'nothing was indexed');
foreach my $version (qw(draft6 draft7)) {
$doc = JSON::Schema::Modern::Document->new(
specification_version => $version,
schema => {
definitions => {
foo => {
'$id' => '#_my_bad_anchor', # legal in draft2020-12
},
qux => {
'$id' => 'https://foo.com#my_bad_id',
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $doc->errors ],
[
{
instanceLocation => '',
keywordLocation => '/definitions/foo/$id',
error => '$id value "#_my_bad_anchor" does not match required syntax',
},
{
instanceLocation => '',
keywordLocation => '/definitions/qux/$id',
error => '$id cannot change the base uri at the same time as declaring an anchor',
},
],
'did not index a '.$version.' fragment-only $id with invalid characters, or non-fragment-only $id',
);
cmp_result([ $doc->resource_index ], [], 'nothing was indexed');
}
$doc = JSON::Schema::Modern::Document->new(
specification_version => 'draft4',
schema => {
definitions => {
foo => {
id => '#_my_bad_anchor', # legal in draft2020-12
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $doc->errors ],
[
{
instanceLocation => '',
keywordLocation => '/definitions/foo/id',
error => 'id value "#_my_bad_anchor" does not match required syntax',
},
],
'did not index a draft4 fragment-only id with invalid characters',
);
cmp_result([ $doc->resource_index ], [], 'nothing was indexed');
$doc = JSON::Schema::Modern::Document->new(
specification_version => 'draft4',
schema => {
id => 'https://foo.com',
definitions => {
qux => {
id => 'blah#weird_but_legal',
},
},
},
);
cmp_result([ map $_->TO_JSON, $doc->errors ], [], 'no errors');
cmp_result(
$doc,
listmethods(
resource_index => unordered_pairs(
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
specification_version => 'draft4',
vocabularies => $vocabularies{'draft4'},
},
'https://foo.com/blah' => {
path => '/definitions/qux',
canonical_uri => str('https://foo.com/blah'),
specification_version => 'draft4',
vocabularies => $vocabularies{'draft4'},
anchors => {
weird_but_legal => {
path => '/definitions/qux',
canonical_uri => str('https://foo.com/blah'),
},
},
},
)),
'can combine a canonical identifier with an anchor in draft4',
);
};
subtest '$schema not conforming to syntax' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(
schema => { '$schema' => 'foo' },
),
listmethods(
canonical_uri => [ str('') ],
metaschema_uri => [ str('https://json-schema.org/draft/2020-12/schema') ],
resource_index => [],
errors => [
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/$schema',
error => '"foo" is not a valid URI',
}),
],
),
'invalid $schema is detected',
);
};
subtest '$anchor and $id below an $id that is not at the document root' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com'),
schema => {
allOf => [
{
'$id' => 'https://bar.com',
'$anchor' => 'my_anchor',
not => {
'$anchor' => 'my_not',
not => { '$id' => 'inner_id' },
},
},
],
},
),
listmethods(
resource_index => unordered_pairs(
'https://foo.com' => {
path => '', canonical_uri => str('https://foo.com'),
%dialect,
},
'https://bar.com' => {
path => '/allOf/0', canonical_uri => str('https://bar.com'),
%dialect,
anchors => {
my_anchor => {
path => '/allOf/0',
canonical_uri => str('https://bar.com'),
},
my_not => {
path => '/allOf/0/not',
canonical_uri => str('https://bar.com#/not'),
},
},
},
'https://bar.com/inner_id' => {
path => '/allOf/0/not/not', canonical_uri => str('https://bar.com/inner_id'),
%dialect,
},
),
_entities => [ { map +($_ => 0), '', '/allOf/0', '/allOf/0/not', '/allOf/0/not/not' } ],
),
'canonical_uri uses the path from the innermost $id, not document root $id',
);
};
subtest 'JSON pointer and URI escaping' => sub {
cmp_result(
my $doc = JSON::Schema::Modern::Document->new(
schema => {
'$defs' => {
foo => {
patternProperties => {
'~' => {
'$id' => 'http://localhost:4242/~username',
properties => {
'~/' => {
'$anchor' => 'tilde',
},
},
},
'/' => {
'$id' => 'http://localhost:4242/my_slash',
properties => {
'~/' => {
'$anchor' => 'slash',
},
},
},
'[~/]' => {
'$id' => 'http://localhost:4242/~username/my_slash',
properties => {
'~/' => {
'$anchor' => 'tildeslash',
},
},
},
},
},
},
},
),
listmethods(
resource_index => unordered_pairs(
'' => {
path => '', canonical_uri => str(''),
%dialect,
},
'http://localhost:4242/~username' => {
path => '/$defs/foo/patternProperties/~0',
canonical_uri => str('http://localhost:4242/~username'),
%dialect,
anchors => {
tilde => {
path => '/$defs/foo/patternProperties/~0/properties/~0~1',
canonical_uri => str('http://localhost:4242/~username#/properties/~0~1'),
},
},
},
'http://localhost:4242/my_slash' => {
path => '/$defs/foo/patternProperties/~1',
canonical_uri => str('http://localhost:4242/my_slash'),
%dialect,
anchors => {
slash => {
path => '/$defs/foo/patternProperties/~1/properties/~0~1',
canonical_uri => str('http://localhost:4242/my_slash#/properties/~0~1'),
},
},
},
'http://localhost:4242/~username/my_slash' => {
path => '/$defs/foo/patternProperties/[~0~1]',
canonical_uri => str('http://localhost:4242/~username/my_slash'),
%dialect,
anchors => {
tildeslash => {
path => '/$defs/foo/patternProperties/[~0~1]/properties/~0~1',
canonical_uri => str('http://localhost:4242/~username/my_slash#/properties/~0~1'),
},
},
},
),
_entities => [ { map +($_ => 0),
my @locations = (
'',
'/$defs/foo',
'/$defs/foo/patternProperties/~0',
'/$defs/foo/patternProperties/~0/properties/~0~1',
'/$defs/foo/patternProperties/~1',
'/$defs/foo/patternProperties/~1/properties/~0~1',
'/$defs/foo/patternProperties/[~0~1]',
'/$defs/foo/patternProperties/[~0~1]/properties/~0~1',
)
}],
),
'properly escaped special characters in JSON pointers and URIs',
);
is($doc->get_entity_at_location('/$defs/foo/patternProperties/~0'), 'schema', 'schema locations are tracked');
is($doc->get_entity_at_location('/$defs/foo/patternProperties'), '', 'non-schema locations are also tracked');
cmp_result(
[ $doc->get_entity_locations('schema') ],
bag(@locations),
'schema locations can be queried',
);
};
subtest 'resource collisions' => sub {
ok(
lives {
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com/x/y/z'),
schema => { '$id' => '/x/y/z' },
);
},
'no collision when adding an identical resource (after resolving with base uri)',
);
like(
dies {
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com/x/y/z'),
schema => {
allOf => [
{ '$id' => '/x/y/z' },
{ '$id' => '/a/b/c' },
],
},
);
},
qr{^\Quri "https://foo.com/x/y/z" conflicts with an existing schema resource\E},
'detected collision between document\'s initial uri and a subschema\'s uri',
);
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com'),
schema => {
allOf => [
{ '$id' => '/x/y/z' },
{ '$id' => '/x/y/z' },
],
},
),
all(
listmethods(
resource_index => [],
errors => [
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/allOf/1/$id',
absoluteKeywordLocation => 'https://foo.com#/allOf/1/$id',
error => 'duplicate canonical uri "https://foo.com/x/y/z" found (original at path "/allOf/0")',
}),
],
),
),
'detected collision between two subschema uris in a document',
);
my $doc1 = JSON::Schema::Modern::Document->new(schema => { '$id' => 'a/b' });
my $doc2 = JSON::Schema::Modern::Document->new(schema => { '$id' => 'b' });
my $js = JSON::Schema::Modern->new;
ok(
# id resolves to https://foo.com/a/b
lives { $js->add_document('https://foo.com' => $doc1) },
'add first document, resolving resources to a base uri',
);
like(
# id resolves to https://foo.com/a/b
dies { $js->add_document('https://foo.com/a/' => $doc2) },
qr{^uri "https://foo.com/a/b" conflicts with an existing schema resource},
'the resource in the second document resolves to the same uri as from the first document',
);
ok(
lives {
JSON::Schema::Modern::Document->new(
canonical_uri => Mojo::URL->new('https://foo.com/x/y/z'),
schema => {
examples => [
{ '$id' => '/x/y/z' },
{ '$id' => 'https://foo.com/x/y/z' },
],
default => {
allOf => [
{ '$id' => '/x/y/z' },
{ '$id' => 'https://foo.com/x/y/z' },
],
},
},
);
},
'ignored "duplicate" uris embedded in non-schemas',
);
};
subtest 'create document with explicit canonical_uri set to the same as root $id' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => 'https://foo.com/x/y/z',
schema => { '$id' => 'https://foo.com/x/y/z' },
),
listmethods(
resource_index => [
'https://foo.com/x/y/z' => {
path => '',
canonical_uri => str('https://foo.com/x/y/z'),
%dialect,
},
],
canonical_uri => [ str('https://foo.com/x/y/z') ],
),
'there is one single uri indexed to the document',
);
};
subtest 'canonical_uri identification from a document with errors' => sub {
cmp_result(
JSON::Schema::Modern::Document->new(
canonical_uri => 'https://foo.com/x/y/z',
schema => {
'$id' => 'https://bar.com',
allOf => [
{
'$id' => 'https://baz.com',
oneOf => [
{ '$id' => 'https://quux.com' },
[ 'not a subschema' ],
],
},
],
},
),
listmethods(
canonical_uri => [ str('https://bar.com') ],
errors => [
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/allOf/0/oneOf/1',
absoluteKeywordLocation => 'https://baz.com#/oneOf/1',
error => 'invalid schema type: array',
}),
],
),
'error lower down in document does not result in an inner identifier being used as canonical_uri',
);
};
subtest 'custom metaschema_uri' => sub {
my $js = JSON::Schema::Modern->new;
$js->add_schema({
'$id' => 'https://my/first/metaschema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
'https://json-schema.org/draft/2020-12/vocab/core' => true,
# note: no validation!
},
});
my $doc = $js->add_document(JSON::Schema::Modern::Document->new(
schema => {
'$id' => my $id = 'https://my/first/schema/with/custom/metaschema',
# note: no $schema keyword!
allOf => [ { minimum => 'not even an integer' } ],
},
metaschema_uri => 'https://my/first/metaschema',
evaluator => $js, # needed in order to find the metaschema
));
cmp_result(
$js->{_resource_index}{$id}{document},
methods(
canonical_uri => str($id),
metaschema_uri => str('https://my/first/metaschema'),
),
'document contains correct values',
);
cmp_result(
$js->{_resource_index}{$id},
{
canonical_uri => str($id),
path => '',
specification_version => 'draft2020-12',
document => $doc,
vocabularies => [
map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Applicator),
],
},
'determined vocabularies to use for this schema',
);
cmp_result(
$js->evaluate(1, $id)->TO_JSON,
{ valid => true },
'validation succeeds because "minimum" never gets run',
);
cmp_result(
$js->evaluate(1, Mojo::URL->new($id)->fragment('/allOf/0'))->TO_JSON,
{ valid => true },
'can evaluate at a subschema as well, with the same vocabularies',
);
cmp_result(
$doc->validate->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'EXCEPTION: unable to find resource "https://my/first/metaschema"',
},
],
},
'when not providing the original evaluator, the metaschema cannot be found',
);
cmp_result(
$doc->validate(evaluator => $js)->TO_JSON,
{ valid => true },
'using the proper evaluator, schema validates against its metaschema, and "minimum" is ignored',
);
memory_cycle_ok($js, 'no leaks in the evaluator object');
};
subtest 'multiple uris used for resolution and identification, and original_uri' => sub {
my $js = JSON::Schema::Modern->new;
my $doc = $js->add_document(
'https://example.com/api/' => JSON::Schema::Modern::Document->new(
canonical_uri => 'staging/',
schema => {
'$id' => 'alpha.json', # https://example.com/staging/alpha.json
properties => {
foo => { '$id' => 'beta', not => true }, # https://example.com/staging/beta
},
not => true,
},
evaluator => $js,
)
);
cmp_result(
$doc,
listmethods(
original_uri => [ str('staging/') ],
canonical_uri => [ str('staging/alpha.json') ],
resource_index => unordered_pairs(
'staging/alpha.json' => {
path => '',
canonical_uri => str('staging/alpha.json'),
%dialect,
},
'staging/beta' => {
canonical_uri => str('staging/beta'),
path => '/properties/foo',
%dialect,
},
),
),
'document has correct resources, resolved against the provided base uri',
);
cmp_result(
$js->{_resource_index},
my $resource_index = {
'https://example.com/api/' => {
path => '',
canonical_uri => str('https://example.com/api/staging/alpha.json'),
document => $doc,
%dialect,
},
'https://example.com/api/staging/alpha.json' => {
path => '',
canonical_uri => str('https://example.com/api/staging/alpha.json'),
document => $doc,
%dialect,
},
'https://example.com/api/staging/beta' => {
path => '/properties/foo',
canonical_uri => str('https://example.com/api/staging/beta'),
document => $doc,
%dialect,
},
},
'evaluator has correct resources, resolved against the provided base uri',
);
cmp_result(
$js->evaluate({ foo => 1 }, 'https://example.com/api/staging/alpha.json')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/not',
absoluteKeywordLocation => 'https://example.com/api/staging/alpha.json#/not',
error => 'subschema is true',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/not',
absoluteKeywordLocation => 'https://example.com/api/staging/beta#/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'https://example.com/api/staging/alpha.json#/properties',
error => 'not all properties are valid',
},
],
},
'when evaluating the document using the canonical uri, error locations use the canonical uri',
);
cmp_result(
$js->evaluate({ foo => 1 }, 'https://example.com/api/')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/not',
absoluteKeywordLocation => 'https://example.com/api/staging/alpha.json#/not',
error => 'subschema is true',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/not',
absoluteKeywordLocation => 'https://example.com/api/staging/beta#/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'https://example.com/api/staging/alpha.json#/properties',
error => 'not all properties are valid',
},
],
},
'when evaluating the document using a retrieval uri, error locations still use the canonical uri',
);
my $doc2 = $js->add_document('file:///usr/local/share/api.json' => $doc);
is($doc2, $doc, 'same document is added a second time');
cmp_result(
$js->{_resource_index},
{
%$resource_index, # original entries
'file:///usr/local/share/api.json' => {
path => '',
canonical_uri => str('file:///usr/local/share/staging/alpha.json'),
document => $doc,
%dialect,
},
'file:///usr/local/share/staging/alpha.json' => {
path => '',
canonical_uri => str('file:///usr/local/share/staging/alpha.json'),
document => $doc,
%dialect,
},
'file:///usr/local/share/staging/beta' => {
path => '/properties/foo',
canonical_uri => str('file:///usr/local/share/staging/beta'),
document => $doc,
%dialect,
},
},
'document resources are added using the new base, which appears in their canonical_uri values',
);
cmp_result(
$js->evaluate({ foo => 1 }, 'https://example.com/api/')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/not',
absoluteKeywordLocation => 'https://example.com/api/staging/alpha.json#/not',
error => 'subschema is true',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/not',
absoluteKeywordLocation => 'https://example.com/api/staging/beta#/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'https://example.com/api/staging/alpha.json#/properties',
error => 'not all properties are valid',
},
],
},
'when evaluating using the first base uri, error locations are relative to the provided base uri',
);
# there are multiple resources mapped to the same document+path locations, but we want error
# locations to be using the set that we used in the evaluation call.
cmp_result(
$js->evaluate({ foo => 1 }, 'file:///usr/local/share/api.json')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/not',
absoluteKeywordLocation => 'file:///usr/local/share/staging/alpha.json#/not',
error => 'subschema is true',
},
{
instanceLocation => '/foo',
keywordLocation => '/properties/foo/not',
absoluteKeywordLocation => 'file:///usr/local/share/staging/beta#/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'file:///usr/local/share/staging/alpha.json#/properties',
error => 'not all properties are valid',
},
],
},
'when evaluating using the second base uri, error locations are relative to the original evaluation location',
);
};
done_testing;
equality.t 100640 000766 000024 20605 15114374332 16705 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use utf8;
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Scalar::Util qw(dualvar isdual);
use lib 't/lib';
use Helper;
use JSON::Schema::Modern::Utilities qw(is_type get_type is_equal);
subtest 'equality, using inflated data' => sub {
foreach my $test (
[ undef, undef, true ],
[ undef, false, false, '', 'wrong type: null vs boolean' ],
[ undef, true , false, '', 'wrong type: null vs boolean' ],
[ undef, 1, false, '', 'wrong type: null vs integer' ],
[ undef, '1', false, '', 'wrong type: null vs string' ],
[ [qw(a b c)], [qw(a b c)], true ],
[ [qw(a b c)], [qw(a b)], false, '', 'element count differs: 3 vs 2' ],
[ [qw(a b)], [qw(b a)], false, '/0', 'strings not equal' ],
[ 1, 1, true ],
[ 1, 1.0, true ],
[ 1, '1.0', false, '', 'wrong type: integer vs string' ],
[ '1.1', 1.1, false, '', 'wrong type: string vs number' ],
[ '1', 1, false, '', 'wrong type: string vs integer' ],
[ '1.1', 1.1, false, '', 'wrong type: string vs number' ],
[ [1,2], [2,1], false, '/0', 'integers not equal' ],
[ { a => 1, b => 2 }, { b => 2, a => 1 }, true ],
[ { a => 1 }, { a => 1.0 }, true ],
[ [qw(école ಠ_ಠ)], ["\x{e9}cole", "\x{0ca0}_\x{0ca0}"], true ],
[ { a => 1, b => 2 }, { a => 1, b => 3 }, false, '/b', 'integers not equal' ],
[ { a => { b => 1, c => 2 }, d => { e => 3, f => 4 } },
{ a => { b => 1, c => 2 }, d => { e => 3, f => 5 } }, false, '/d/f', 'integers not equal' ],
[ [ { a => 1 } ], [ { a => 1, b => 2 } ], false, '/0', 'property count differs: 1 vs 2' ],
[ [ { a => 1 } ], [ { b => 2 } ], false, '/0', 'property names differ starting at position 0 ("a" vs "b")' ],
[ { foo => [ [ 0 ] ] }, { foo => [ [ 0, 1 ] ] }, false, '/foo/0', 'element count differs: 1 vs 2' ],
) {
my ($x, $y, $expected, $diff_path, $error) = @$test;
my @types = map get_type($_), $x, $y;
my $result = is_equal($x, $y, my $state = {});
ok(!($result xor $expected), json_sprintf('%s == %s is %s', $x, $y, $expected));
is($state->{path}, $diff_path // '', 'two instances differ at the expected place') if not $expected;
is($state->{error}, $error // '', 'error is correct') if not $expected;
is($state->{error}, undef, 'error is undefined') if $expected;
isnt($state->{error}, 'uh oh', 'no unexpected error encountered');
ok(is_type($types[0], $x), 'type of arg 0 was not mutated while making equality check');
ok(is_type($types[1], $y), 'type of arg 1 was not mutated while making equality check');
foreach my $idx (0, 1) {
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & B::SVf_POK), "arg $idx did not gain a POK")
if $types[$idx] eq 'integer' or $types[$idx] eq 'number';
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & (B::SVf_IOK | B::SVf_NOK)), "arg $idx did not gain an NOK or IOK")
if $types[$idx] eq 'string';
}
note '';
}
};
my $decoder = JSON::Schema::Modern::_JSON_BACKEND()->new->allow_nonref(1)->utf8(0);
subtest 'equality, using JSON strings' => sub {
foreach my $test (
[ 'null', 'null', true ],
[ 'null', 1, false ],
[ '["a","b","c"]', '["a","b","c"]', true ],
[ '["a","b","c"]', '["a","b"]', false ],
[ '["a","b"]', '["b","a"]', false, '/0' ],
[ '1', '1', true ],
[ '1', '1.0', true ],
[ '10', '1e1', true ],
[ '[1,2]', '[2,1]', false, '/0' ],
[ '{"a":1,"b":2}', '{"a":1,"b":2}', true ],
[ '{"a":1}', '{"a":1.0}', true ],
[ '["école","ಠ_ಠ"]', qq{["\x{e9}cole", "\x{0ca0}_\x{0ca0}"]}, true ],
[ '{"a":1,"b":2}', '{"b":3,"a":1}', false, '/b' ],
[ '{"a":{"b":1,"c":2},"d":{"e":3,"f":4}}',
'{"a":{"b":1,"c":2},"d":{"e":3,"f":5}}', false, '/d/f' ],
) {
my ($x, $y, $expected, $diff_path) = @$test;
($x, $y) = map $decoder->decode($_), $x, $y;
my @types = map get_type($_), $x, $y;
my $result = is_equal($x, $y, my $state = {});
ok(!($result xor $expected), json_sprintf('%s == %s is %s', $x, $y, $expected));
is($state->{path}, $diff_path // '', 'two instances differ at the expected place') if not $expected;
isnt($state->{error}, 'uh oh', 'no unexpected error encountered');
ok(is_type($types[0], $x), 'type of arg 0 was not mutated while making equality check');
ok(is_type($types[1], $y), 'type of arg 1 was not mutated while making equality check');
foreach my $idx (0, 1) {
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & B::SVf_POK), "arg $idx did not gain a POK")
if $types[$idx] eq 'integer' or $types[$idx] eq 'number';
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & (B::SVf_IOK | B::SVf_NOK)), "arg $idx did not gain an NOK or IOK")
if $types[$idx] eq 'string';
}
note '';
}
};
subtest 'equality, using scalarref_booleans' => sub {
foreach my $test (
[ \0, true, false ],
[ \1, true, true ],
[ \0, false, true ],
[ \1, false, false ],
[ undef, \0, false ],
[ undef, false, false ],
) {
my ($x, $y, $expected, $diff_path) = @$test;
my @types = map get_type($_), $x, $y;
my $result = is_equal($x, $y, my $state = { scalarref_booleans => 1});
ok(!($result xor $expected), json_sprintf('%s == %s is %s', $x, $y, $expected));
is($state->{path}, $diff_path // '', 'two instances differ at the expected place') if not $expected;
isnt($state->{error}, 'uh oh', 'no unexpected error encountered');
ok(is_type($types[0], $x), 'type of arg 0 was not mutated while making equality check');
ok(is_type($types[1], $y), 'type of arg 1 was not mutated while making equality check');
foreach my $idx (0, 1) {
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & B::SVf_POK), "arg $idx did not gain a POK")
if $types[$idx] eq 'integer' or $types[$idx] eq 'number';
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & (B::SVf_IOK | B::SVf_NOK)), "arg $idx did not gain an NOK or IOK")
if $types[$idx] eq 'string';
}
note '';
}
};
subtest 'equality, using stringy_numbers' => sub {
foreach my $test (
[ 1, 1, true ],
[ 1, 1.0, true ],
[ 1, '1.0', true ],
[ '1.1', 1.1, true ],
[ '1', 1, true ],
[ '1.1', 1.1, true ],
[ '1', '1.00', true ],
[ '1.10', '1.1000', true ],
[ 'x', 'x', true ],
[ 'x', 'y', false ],
[ 'x', 0, false ],
[ 0, 'y', false ],
[ '5', dualvar(5, '5'), true ],
[ 5, dualvar(5, '5'), true ],
[ '5', dualvar(5, 'five'), false ],
[ 5, dualvar(5, 'five'), false ],
[ dualvar(5, 'five'), dualvar(5, 'five'), false ],
) {
my ($x, $y, $expected, $diff_path) = @$test;
my @types = map get_type($_), $x, $y;
my $result = is_equal($x, $y, my $state = { stringy_numbers => 1 });
ok(!($result xor $expected), json_sprintf('%s == %s is %s', $x, $y, $expected));
is($state->{path}, $diff_path // '', 'two instances differ at the expected place') if not $expected;
isnt($state->{error}, 'uh oh', 'no unexpected error encountered');
is(get_type($x), $types[0], 'type of arg 0 was not mutated while making equality check (get_type returns '.$types[0].')');
is(get_type($y), $types[1], 'type of arg 1 was not mutated while making equality check (get_type returns '.$types[1].')');
ok(
is_type($types[0], $x),
"type of arg 0 was not mutated while making equality check (is_type('$types[0]') returns true)",
) if $types[0] ne 'ambiguous type';
ok(
is_type($types[1], $y),
"type of arg 1 was not mutated while making equality check (is_type('$types[1]') returns true)",
) if $types[1] ne 'ambiguous type';
foreach my $idx (0, 1) {
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & B::SVf_POK), "arg $idx did not gain a POK")
if $types[$idx] eq 'integer' or $types[$idx] eq 'number';
ok(!(B::svref_2object(\[$x, $y]->[$idx])->FLAGS & (B::SVf_IOK | B::SVf_NOK)), "arg $idx did not gain an NOK or IOK")
if not ($idx == 1 and isdual($y) and $types[1] ne 'ambiguous type') and $types[$idx] eq 'string';
}
note '';
}
};
done_testing;
traverse.t 100640 000766 000024 61232 15114374332 16704 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
use JSON::Schema::Modern::Utilities 'canonical_uri';
subtest 'traversal with callbacks' => sub {
my $schema = {
'$id' => 'https://foo.com',
'$defs' => {
foo => {
'$id' => 'recursive_subschema',
type => [ 'integer', 'object' ],
additionalProperties => { '$ref' => 'recursive_subschema' },
},
bar => {
properties => {
description => {
'$ref' => '#/$defs/foo',
const => { '$ref' => 'this is not a real ref' },
},
},
},
},
if => 1, # bad subschema
allOf => [
{},
{ '$ref' => '#/$defs/foo' },
{ '$ref' => '#/$defs/bar' },
],
};
my %refs;
my $if_callback_called;
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse($schema, { callbacks => {
'$ref' => sub ($schema, $state) {
my $canonical_uri = canonical_uri($state);
my $ref_uri = Mojo::URL->new($schema->{'$ref'});
$ref_uri = $ref_uri->to_abs($canonical_uri) if not $ref_uri->is_abs;
$refs{$state->{traversed_keyword_path}.$state->{keyword_path}} = $ref_uri->to_string;
},
if => sub { $if_callback_called = 1; },
}});
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/if',
absoluteKeywordLocation => 'https://foo.com#/if',
error => 'invalid schema type: integer',
},
],
'errors encountered during traversal are returned',
);
ok(!$if_callback_called, 'callback for erroneous keyword was not called');
cmp_result(
\%refs,
{
'/$defs/foo/additionalProperties' => 'https://foo.com/recursive_subschema',
'/$defs/bar/properties/description' => 'https://foo.com#/$defs/foo',
# no entry for 'if' -- callbacks are not called for keywords with errors
'/allOf/1' => 'https://foo.com#/$defs/foo',
'/allOf/2' => 'https://foo.com#/$defs/bar',
},
'extracted all the real $refs out of the schema, with locations and canonical targets',
);
cmp_result(
$state->{subschemas},
bag(
'',
'/$defs/bar',
'/$defs/bar/properties/description',
'/$defs/foo',
'/$defs/foo/additionalProperties',
'/allOf/0',
'/allOf/1',
'/allOf/2',
'/if',
),
'identified all subschemas',
);
};
subtest 'errors when parsing $schema keyword' => sub {
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse({ '$schema' => true });
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '$schema value is not a string',
},
],
'$schema is not a string',
);
$state = $js->traverse({ '$schema' => 'whargarbl' });
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/$schema',
error => '"whargarbl" is not a valid URI',
},
],
'$schema is not a URI',
);
};
subtest 'default metaschema' => sub {
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse(
{
'$defs' => {
foo => {
properties => 'not an object',
},
},
},
);
cmp_result(
$state,
superhashof({
specification_version => 'draft2020-12',
metaschema_uri => str(JSON::Schema::Modern::METASCHEMA_URIS->{'draft2020-12'}),
initial_schema_uri => str(''),
vocabularies => [
map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated),
],
}),
'dialect is properly determined',
);
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/$defs/foo/properties',
error => 'properties value is not an object',
},
],
'error within $defs is found, showing both Core and Applicator vocabularies are used',
);
};
subtest 'traversing a dialect with different core keywords' => sub {
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse(
{
'$id' => 'http://localhost:1234/root',
'$schema' => 'http://json-schema.org/draft-07/schema#',
definitions => {
alpha => 1,
},
},
);
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/definitions/alpha',
absoluteKeywordLocation => 'http://localhost:1234/root#/definitions/alpha',
error => 'invalid schema type: integer',
},
],
'dialect changes at root, with $id - dialect is switched in time to get a new keyword list for the core vocabulary',
);
cmp_result(
$state,
superhashof({
metaschema_uri => 'http://json-schema.org/draft-07/schema',
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'other $state information is correct',
);
$state = $js->traverse(
{
'$id' => '#hello',
'$schema' => 'http://json-schema.org/draft-07/schema#',
definitions => {
bloop => {
'$id' => '/bloop',
type => 'object',
},
},
},
);
cmp_result($state->{errors}, [], 'no errors when parsing this schema');
cmp_result(
$state,
superhashof({
identifiers => {
'' => {
path => '',
canonical_uri => str(''),
specification_version => 'draft7',
vocabularies => [
map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData),
],
anchors => {
hello => {
path => '',
canonical_uri => str(''),
},
},
},
'/bloop' => {
path => '/definitions/bloop',
canonical_uri => str('/bloop'),
specification_version => 'draft7',
vocabularies => [
map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData),
],
},
},
metaschema_uri => 'http://json-schema.org/draft-07/schema',
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'switched dialect in time to extract all identifiers, from root and definition',
);
$state = $js->traverse(
{
'$schema' => 'http://json-schema.org/draft-07/schema#',
definitions => {
alpha => 1,
},
},
);
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/definitions/alpha',
error => 'invalid schema type: integer',
},
],
'dialect changes at root, no $id - dialect is switched in time to get a new keyword list for the core vocabulary',
);
$state = $js->traverse(
{
'$defs' => {
alpha => {
'$id' => 'http://localhost:1234/inner',
'$schema' => 'http://json-schema.org/draft-07/schema#',
definitions => {
alpha => 1,
},
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/$defs/alpha/definitions/alpha',
absoluteKeywordLocation => 'http://localhost:1234/inner#/definitions/alpha',
error => 'invalid schema type: integer',
},
],
'dialect changes below root - dialect is switched in time to get a new keyword list for the core vocabulary',
);
};
subtest '$schema without an $id, below the root' => sub {
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse(
{
'$defs' => {
alpha => {
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
minimum => 2,
},
},
},
);
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/$defs/alpha/$schema',
error => '$schema can only appear at the schema resource root',
},
],
'$schema cannot exist without an $id, or at the root',
);
};
subtest 'duplicate identifiers' => sub {
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse({
'$id' => 'https://base.com',
allOf => [
{ '$id' => 'https://foo.com' },
{ '$id' => 'https://foo.com' },
],
});
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/allOf/1/$id',
absoluteKeywordLocation => 'https://base.com#/allOf/1/$id',
error => 'duplicate canonical uri "https://foo.com" found (original at path "/allOf/0")',
},
],
'detected colliding $ids within a single schema',
);
$state = $js->traverse({
'$id' => 'https://base.com',
allOf => [
{ '$id' => 'dir1', '$anchor' => 'foo' },
{ '$id' => 'dir2', '$anchor' => 'foo' },
],
});
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[],
'two anchors with different base uris are acceptable',
);
$state = $js->traverse({
'$id' => 'https://base.com',
allOf => [
{ '$anchor' => 'foo' },
{ '$anchor' => 'foo' },
],
});
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/allOf/1/$anchor',
absoluteKeywordLocation => 'https://base.com#/allOf/1/$anchor',
error => 'duplicate anchor uri "https://base.com#foo" found (original at path "/allOf/0")',
},
],
'detected colliding $anchors within a single schema',
);
};
subtest '$anchor without $id' => sub {
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse({
'$anchor' => 'root_anchor',
});
cmp_result(
$state->{identifiers},
{
'' => {
path => '',
canonical_uri => str(''),
specification_version => 'draft2020-12',
vocabularies => ignore,
anchors => {
root_anchor => {
path => '',
canonical_uri => str(''),
},
},
},
},
'found anchor at root, without an $id to pre-populate the identifiers hash',
);
$state = $js->traverse({
properties => {
foo => {
'$anchor' => 'foo_anchor',
},
},
});
cmp_result(
$state->{identifiers},
{
'' => {
path => '',
canonical_uri => str(''),
specification_version => 'draft2020-12',
vocabularies => ignore,
anchors => {
foo_anchor => {
path => '/properties/foo',
canonical_uri => str('#/properties/foo'),
},
},
},
},
'found anchor within schema, without an $id to pre-populate the identifiers hash',
);
};
subtest 'traverse with overridden specification_version' => sub {
my $js = JSON::Schema::Modern->new(specification_version => 'draft7');
my $state = $js->traverse({});
cmp_result(
$state,
superhashof({
errors => [],
metaschema_uri => 'http://json-schema.org/draft-07/schema',
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'$state is correct with no $schema keyword, no overrides'
);
$state = $js->traverse({ '$schema' => 'https://json-schema.org/draft/2020-12/schema'});
cmp_result(
$state,
superhashof({
errors => [],
metaschema_uri => 'https://json-schema.org/draft/2020-12/schema',
specification_version => 'draft2020-12',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated) ],
}),
'$state is correct with a $schema keyword, no overrides'
);
$state = $js->traverse({}, { specification_version => 'draft2019-09' });
cmp_result(
$state,
superhashof({
errors => [],
metaschema_uri => 'https://json-schema.org/draft/2019-09/schema',
specification_version => 'draft2019-09',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'$state is correct with no $schema keyword, and an overridden specification_version'
);
$state = $js->traverse(
{ '$schema' => 'http://json-schema.org/draft-04/schema#' },
{ specification_version => 'draft2020-12' });
cmp_result(
$state,
superhashof({
errors => [],
metaschema_uri => 'http://json-schema.org/draft-04/schema',
specification_version => 'draft4',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator MetaData) ],
}),
'$state is correct with a $schema keyword, and an overridden specification_version'
);
};
subtest 'traverse with overridden metaschema_uri' => sub {
my $js = JSON::Schema::Modern->new;
my $state = $js->traverse({}, { metaschema_uri => 'https://unknown/metaschema' });
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
my $errors = [
{
instanceLocation => '',
keywordLocation => '',
error => 'EXCEPTION: unable to find resource "https://unknown/metaschema"',
},
],
'metaschema_uri is not a known uri',
);
$js->add_schema({
'$id' => 'https://metaschema/with/wrong/spec',
'$vocabulary' => {
'https://unknown' => true,
'https://unknown2' => false,
},
});
$state = $js->traverse(true, { metaschema_uri => 'https://metaschema/with/wrong/spec' });
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
$errors = [
{
instanceLocation => '',
keywordLocation => jsonp(qw(/$vocabulary https://unknown)),
absoluteKeywordLocation => 'https://metaschema/with/wrong/spec#'.jsonp(qw(/$vocabulary https://unknown)),
error => '"https://unknown" is not a known vocabulary',
},
{
instanceLocation => '',
keywordLocation => '/$vocabulary',
absoluteKeywordLocation => 'https://metaschema/with/wrong/spec#/$vocabulary',
error => 'the first vocabulary (by evaluation_order) must be Core',
},
{
instanceLocation => '',
keywordLocation => '',
error => '"https://metaschema/with/wrong/spec" is not a valid metaschema',
},
],
'boolean schema: metaschema_uri is overridden with a bad schema: same errors are returned',
);
$state = $js->traverse(
{ '$id' => 'https://my/bad/schema' },
{ metaschema_uri => 'https://metaschema/with/wrong/spec' });
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
$errors,
'object schema: metaschema_uri is overridden with a bad schema: same errors are returned',
);
# simulation of parsing a schema with a custom keyword that sets the metaschema uri
# (see OpenAPI's jsonSchemaDialect keyword)
$state = $js->traverse(
true,
{
metaschema_uri => 'https://metaschema/with/wrong/spec',
initial_schema_uri => 'https://my-poor-schema/foo.json#/$my_dialect_is',
traversed_keyword_path => '/$ref/$ref/some_keyword/$ref/$my_dialect_is',
});
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
$errors->[0]->%*,
keywordLocation => '/$ref/$ref/some_keyword/$ref/$my_dialect_is'.$errors->[0]{keywordLocation},
},
{
$errors->[1]->%*,
keywordLocation => '/$ref/$ref/some_keyword/$ref/$my_dialect_is'.$errors->[1]{keywordLocation},
},
{
$errors->[2]->%*,
keywordLocation => '/$ref/$ref/some_keyword/$ref/$my_dialect_is'.$errors->[2]{keywordLocation},
absoluteKeywordLocation => 'https://my-poor-schema/foo.json#/$my_dialect_is',
},
],
'metaschema_uri is overridden with a bad schema and there is a traversal path: errors contain the right locations',
);
$js->add_schema({
'$id' => 'https://my/first/metaschema',
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
'$vocabulary' => {
'https://json-schema.org/draft/2019-09/vocab/applicator' => true,
'https://json-schema.org/draft/2019-09/vocab/core' => true,
# note: no validation!
},
});
$state = $js->traverse(
{
'$id' => my $id = 'https://my/first/schema/with/custom/metaschema',
# note: no $schema keyword!
},
{ metaschema_uri => 'https://my/first/metaschema' },
);
cmp_result(
$state,
superhashof({
identifiers => {
$id => {
canonical_uri => str($id),
path => '',
specification_version => 'draft2019-09',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_, qw(Core Applicator) ],
},
},
metaschema_uri => 'https://my/first/metaschema',
specification_version => 'draft2019-09',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_, qw(Core Applicator) ],
}),
'determined specification version and vocabularies to use for this schema from override',
);
$state = $js->traverse(
{
'$id' => $id = 'https://my/second/schema/with/custom/metaschema',
'$schema' => 'http://json-schema.org/draft-07/schema',
},
{ metaschema_uri => 'https://my/first/metaschema' },
);
cmp_result(
my $state_copy = $state,
superhashof({
identifiers => {
$id => {
canonical_uri => str($id),
path => '',
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
},
},
metaschema_uri => 'http://json-schema.org/draft-07/schema',
specification_version => 'draft7',
vocabularies => [ map 'JSON::Schema::Modern::Vocabulary::'.$_,
qw(Core Validation FormatAnnotation Applicator Content MetaData) ],
}),
'determined specification version and vocabularies to use for this schema from $schema keyword',
);
$state = $js->traverse(
{
'$id' => my $third_id = 'https://my/third/schema/with/custom/metaschema',
'$schema' => 'http://json-schema.org/draft-07/schema',
},
{ metaschema_uri => 'https://metaschema/with/wrong/spec' },
);
cmp_result(
$state,
superhashof({
identifiers => {
$third_id => { $state_copy->{identifiers}{$id}->%*, canonical_uri => str($third_id) },
},
$state_copy->%{qw(metaschema_uri specification_version vocabularies)},
}),
'when $schema keyword is used, custom metaschema_uri is never parsed, so there are no errors',
);
};
subtest 'start traversing below the document root' => sub {
my $js = JSON::Schema::Modern->new;
# Remember: at this point the $document object may exist, but its constructor hasn't finished yet
# (until traverse() returns), and we have no idea where in the document we are, and the evaluator
# isn't provided the document object yet.
# The document data might not even be a JSON Schema.
# Let's say the document actually looks like:
# {
# $self => 'my_document.yaml',
# openapi => '3.1.1',
# components => {
# schemas => {
# alpha => {
# *** SUBSCHEMA BELOW ***
# },
# },
# },
# }
my $state = $js->traverse(
{
properties => {
myprop => {
allOf => [
{
'$id' => 'inner_document',
properties => {
foo => 'not a valid schema',
},
},
],
},
},
type => 'not a valid type',
},
{
initial_schema_uri => 'dir/my_subdocument#/subid',
traversed_keyword_path => '/components/alpha/subid',
},
);
cmp_result(
[ map $_->TO_JSON, $state->{errors}->@* ],
[
{
instanceLocation => '',
keywordLocation => '/components/alpha/subid/type',
absoluteKeywordLocation => 'dir/my_subdocument#/subid/type',
error => 'unrecognized type "not a valid type"',
},
{
instanceLocation => '',
keywordLocation => '/components/alpha/subid/properties/myprop/allOf/0/properties/foo',
absoluteKeywordLocation => 'dir/inner_document#/properties/foo',
error => 'invalid schema type: string',
},
],
'identified the overridden location of all errors during traverse',
);
$state = $js->traverse(
{
properties => {
myprop => {
allOf => [
{
'$id' => 'inner_document', # resolves to dir/inner_document
properties => { foo => true },
},
],
},
},
},
{
initial_schema_uri => 'dir/my_subdocument#/subid',
traversed_keyword_path => '/components/alpha/subid',
},
);
cmp_result(
$state->{identifiers},
{
'dir/inner_document' => {
canonical_uri => str('dir/inner_document'),
path => '/components/alpha/subid/properties/myprop/allOf/0',
specification_version => 'draft2020-12',
vocabularies => ignore,
},
},
'identifiers are correctly extracted when traversing below the document root',
);
$state = $js->traverse(
{
# the path at this position is /components/alpha/subid
properties => {
alpha => {
'$id' => 'alpha_id', # resolves to dir/alpha_id
properties => {
alpha_one => {
'$id' => 'alpha_one_id', # resolves to dir/alpha_one_id
},
alpha_two => {
'$anchor' => 'alpha_two_anchor', # resolves to dir/alpha_id#alpha_two_anchor
},
alpha_three => {
'$anchor' => 'alpha_three_anchor', # resolves to dir/alpha_id#alpha_three_anchor
},
},
},
beta => {
# produces anchor definition:
# base uri is dir/my_subdocument,
# canonical uri is dir/my_subdocument#/subid/properties/beta
# path is /components/alpha/subid/properties/beta
'$anchor' => 'beta_anchor', # resolves to dir/my_subdocument#beta_anchor
},
},
},
{
# this is used for adjusting canonical_uri in extracted 'identifiers'; and for errors.
# we can infer that there is an identifier 'dir/mysubdocument' at path '/components/alpha'
initial_schema_uri => 'dir/my_subdocument#/subid',
traversed_keyword_path => '/components/alpha/subid',
},
);
cmp_result(
$state->{identifiers},
{
'dir/alpha_id' => {
canonical_uri => str('dir/alpha_id'),
path => '/components/alpha/subid/properties/alpha',
specification_version => 'draft2020-12',
vocabularies => ignore,
anchors => {
alpha_two_anchor => {
canonical_uri => str('dir/alpha_id#/properties/alpha_two'),
path => '/components/alpha/subid/properties/alpha/properties/alpha_two',
},
alpha_three_anchor => {
canonical_uri => str('dir/alpha_id#/properties/alpha_three'),
path => '/components/alpha/subid/properties/alpha/properties/alpha_three',
},
},
},
'dir/alpha_one_id' => {
canonical_uri => str('dir/alpha_one_id'),
path => '/components/alpha/subid/properties/alpha/properties/alpha_one',
specification_version => 'draft2020-12',
vocabularies => ignore,
},
'dir/my_subdocument' => { # this is inferred when we process "$anchor": "beta_anchor"
canonical_uri => str('dir/my_subdocument'),
path => '/components/alpha',
specification_version => 'draft2020-12',
vocabularies => ignore,
anchors => {
beta_anchor => {
canonical_uri => str('dir/my_subdocument#/subid/properties/beta'),
path => '/components/alpha/subid/properties/beta',
},
},
},
},
'identifiers are correctly extracted when traversing below the document root, with anchor',
);
};
done_testing;
CONTRIBUTING 100644 000766 000024 7463 15114374332 16245 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627
CONTRIBUTING
Thank you for considering contributing to this distribution. This file
contains instructions that will help you work with the source code.
PLEASE NOTE that if you have any questions or difficulties, you can reach the
maintainer(s) through the bug queue described later in this document
(preferred), or by emailing the releaser directly. You are not required to
follow any of the steps in this document to submit a patch or bug report;
these are just recommendations, intended to help you (and help us help you
faster).
The distribution is managed with Dist::Zilla (https://metacpan.org/release/Dist-Zilla).
This means than many of the usual files you might expect are not in the
repository, but are generated at release time (e.g. Makefile.PL).
However, you can run tests directly using the 'prove' tool:
$ prove -l
$ prove -lv t/some_test_file.t
$ prove -lvr t/
In most cases, 'prove' is entirely sufficient for you to test any patches you
have.
You may need to satisfy some dependencies. The easiest way to satisfy
dependencies is to install the last release -- this is available at
https://metacpan.org/release/JSON-Schema-Modern
If you use cpanminus, you can do it without downloading the tarball first:
$ cpanm --reinstall --installdeps --with-recommends JSON::Schema::Modern
Dist::Zilla is a very powerful authoring tool, but requires a number of
author-specific plugins. If you would like to use it for contributing,
install it from CPAN, then run one of the following commands, depending on
your CPAN client:
$ cpan `dzil authordeps --missing`
or
$ dzil authordeps --missing | cpanm
You should then also install any additional requirements not needed by the
dzil build but may be needed by tests or other development:
$ cpan `dzil listdeps --author --missing`
or
$ dzil listdeps --author --missing | cpanm
Or, you can use the 'dzil stale' command to install all requirements at once:
$ cpan Dist::Zilla::App::Command::stale
$ cpan `dzil stale --all`
or
$ cpanm Dist::Zilla::App::Command::stale
$ dzil stale --all | cpanm
You can also do this via cpanm directly:
$ cpanm --reinstall --installdeps --with-develop --with-recommends JSON::Schema::Modern
Once installed, here are some dzil commands you might try:
$ dzil build
$ dzil test
$ dzil test --release
$ dzil xtest
$ dzil listdeps --json
$ dzil build --notgz
You can learn more about Dist::Zilla at http://dzil.org/.
The code for this distribution is hosted at GitHub. The repository is:
https://github.com/karenetheridge/JSON-Schema-Modern
You can submit code changes by forking the repository, pushing your code
changes to your clone, and then submitting a pull request. Please include a
suitable end-user-oriented entry in the Changes file describing your change.
Detailed instructions for doing that is available here:
https://help.github.com/articles/creating-a-pull-request
Generated files such as README, CONTRIBUTING, Makefile.PL, LICENSE etc should
*not* be included in your pull request, as they will be updated automatically
during the next release.
If you have found a bug, but do not have an accompanying patch to fix it, you
can submit an issue report here:
https://github.com/karenetheridge/JSON-Schema-Modern/issues
This is a good place to send your questions about the usage of this distribution.
If you send me a patch or pull request, your name and email address will be
included in the documentation as a contributor (using the attribution on the
commit or patch), unless you specifically request for it not to be. If you
wish to be listed under a different name or address, you should submit a pull
request to the .mailmap file to contain the correct mapping.
This file was generated via Dist::Zilla::Plugin::GenerateFile::FromShareDir 0.015
from a template file originating in Dist-Zilla-PluginBundle-Author-ETHER-0.170.
share 000755 000766 000024 0 15114374332 15343 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 LICENSE 100640 000766 000024 26733 15114374332 16537 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/share Copyright (c) 2022 JSON Schema Specification Authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
This Academic Free License (the "License") applies to any original work
of authorship (the "Original Work") whose owner (the "Licensor") has
placed the following licensing notice adjacent to the copyright notice
for the Original Work:
Licensed under the Academic Free License version 3.0
1) Grant of Copyright License. Licensor grants You a worldwide,
royalty-free, non-exclusive, sublicensable license, for the duration of
the copyright, to do the following:
a) to reproduce the Original Work in copies, either alone or as part of
a collective work;
b) to translate, adapt, alter, transform, modify, or arrange the
Original Work, thereby creating derivative works ("Derivative Works")
based upon the Original Work;
c) to distribute or communicate copies of the Original Work and
Derivative Works to the public, under any license of your choice that
does not contradict the terms and conditions, including Licensor's
reserved rights and remedies, in this Academic Free License;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor grants You a worldwide,
royalty-free, non-exclusive, sublicensable license, under patent claims
owned or controlled by the Licensor that are embodied in the Original
Work as furnished by the Licensor, for the duration of the patents, to
make, use, sell, offer for sale, have made, and import the Original Work
and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the
preferred form of the Original Work for making modifications to it
and all available documentation describing how to modify the Original
Work. Licensor agrees to provide a machine-readable copy of the Source
Code of the Original Work along with each copy of the Original Work
that Licensor distributes. Licensor reserves the right to satisfy this
obligation by placing a machine-readable copy of the Source Code in an
information repository reasonably calculated to permit inexpensive and
convenient access by You for as long as Licensor continues to distribute
the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor
the names of any contributors to the Original Work, nor any of their
trademarks or service marks, may be used to endorse or promote products
derived from this Original Work without express prior permission of the
Licensor. Except as expressly stated herein, nothing in this License
grants any license to Licensor's trademarks, copyrights, patents, trade
secrets or any other intellectual property. No patent license is granted
to make, use, sell, offer for sale, have made, or import embodiments
of any patent claims other than the licensed claims defined in Section
2. No license is granted to the trademarks of Licensor even if such
marks are included in the Original Work. Nothing in this License shall
be interpreted to prohibit Licensor from licensing under terms different
from this License any Original Work that Licensor otherwise would have a
right to license.
5) External Deployment. The term "External Deployment" means the use,
distribution, or communication of the Original Work or Derivative
Works in any way such that the Original Work or Derivative Works may
be used by anyone other than You, whether those works are distributed
or communicated to those persons or made available as an application
intended for use over a network. As an express condition for the grants
of license hereunder, You must treat any External Deployment by You of
the Original Work or a Derivative Work as a distribution under section
1(c).
6) Attribution Rights. You must retain, in the Source Code of any
Derivative Works that You create, all copyright, patent, or trademark
notices from the Source Code of the Original Work, as well as any
notices of licensing and any descriptive text identified therein as an
"Attribution Notice." You must cause the Source Code for any Derivative
Works that You create to carry a prominent Attribution Notice reasonably
calculated to inform recipients that You have modified the Original
Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants
that the copyright in and to the Original Work and the patent rights
granted herein by Licensor are owned by the Licensor or are sublicensed
to You under the terms of this License with the permission of the
contributor(s) of those copyrights and patent rights. Except as
expressly stated in the immediately preceding sentence, the Original
Work is provided under this License on an "AS IS" BASIS and WITHOUT
WARRANTY, either express or implied, including, without limitation,
the warranties of non-infringement, merchantability or fitness for a
particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL
WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential
part of this License. No license to the Original Work is granted by this
License except under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal
theory, whether in tort (including negligence), contract, or otherwise,
shall the Licensor be liable to anyone for any indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or the use of the Original Work including,
without limitation, damages for loss of goodwill, work stoppage,
computer failure or malfunction, or any and all other commercial damages
or losses. This limitation of liability shall not apply to the extent
applicable law prohibits such limitation.
9) Acceptance and Termination. If, at any time, You expressly
assented to this License, that assent indicates your clear and
irrevocable acceptance of this License and all of its terms and
conditions. If You distribute or communicate copies of the Original
Work or a Derivative Work, You must make a reasonable effort under the
circumstances to obtain the express assent of recipients to the terms
of this License. This License conditions your rights to undertake
the activities listed in Section 1, including your right to create
Derivative Works based upon the Original Work, and doing so without
honoring these terms and conditions is prohibited by copyright law and
international treaty. Nothing in this License is intended to affect
copyright exceptions and limitations (including "fair use" or "fair
dealing"). This License shall terminate immediately and You may no
longer exercise any of the rights granted to You by this License upon
your failure to honor the conditions in Section 1(c).
10) Termination for Patent Action. This License shall terminate
automatically and You may no longer exercise any of the rights granted
to You by this License as of the date You commence an action, including
a cross-claim or counterclaim, against Licensor or any licensee
alleging that the Original Work infringes a patent. This termination
provision shall not apply for an action alleging patent infringement by
combinations of the Original Work with other software or hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating
to this License may be brought only in the courts of a jurisdiction
wherein the Licensor resides or in which Licensor conducts its primary
business, and under the laws of that jurisdiction excluding its
conflict-of-law provisions. The application of the United Nations
Convention on Contracts for the International Sale of Goods is
expressly excluded. Any use of the Original Work outside the scope
of this License or after its termination shall be subject to the
requirements and penalties of copyright or patent law in the appropriate
jurisdiction. This section shall survive the termination of this
License.
12) Attorneys' Fees. In any action to enforce the terms of this License
or seeking damages relating thereto, the prevailing party shall
be entitled to recover its costs and expenses, including, without
limitation, reasonable attorneys' fees and costs incurred in connection
with such action, including any appeal of such action. This section
shall survive the termination of this License.
13) Miscellaneous. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable.
14) Definition of "You" in This License. "You" throughout this License,
whether in upper or lower case, means an individual or a legal entity
exercising rights under, and complying with all of the terms of, this
License. For legal entities, "You" includes any entity that controls,
is controlled by, or is under common control with you. For purposes of
this definition, "control" means (i) the power, direct or indirect, to
cause the direction or management of such entity, whether by contract
or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
15) Right to Use. You may use the Original Work in all ways not
otherwise restricted or conditioned by this License or by law, and
Licensor promises not to interfere with or be responsible for such uses
by You.
16) Modification of This License. This License is Copyright ©
2005 Lawrence Rosen. Permission is granted to copy, distribute, or
communicate this License without modification. Nothing in this License
permits You to modify this License as applied to the Original Work or
to Derivative Works. However, You may modify the text of this License
and copy, distribute or communicate your modified version (the "Modified
License") and apply it to other original works of authorship subject
to the following conditions: (i) You may not indicate in any way that
your Modified License is the "Academic Free License" or "AFL" and you
may not use those names in the name of your Modified License; (ii) You
must replace the notice specified in the first paragraph above with
the notice "Licensed under " or with a
notice of your own that is not confusingly similar to the notice in
this License; and (iii) You may not claim that your original works are
open source software unless your Modified License has been approved by
Open Source Initiative (OSI) and You comply with its license review and
certification process.
callbacks.t 100640 000766 000024 12565 15114374332 16775 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new;
subtest 'evaluation callbacks' => sub {
my @used_ref_at;
my $result = $js->evaluate(
[ { a => { b => { c => { d => 'e' } } } } ],
my $schema = {
'$defs' => {
object_or_string => {
anyOf => [
{
type => 'object',
additionalProperties => { '$ref' => '#/$defs/object_or_string' },
},
{
type => 'string'
},
],
},
},
contains => { '$ref' => '#/$defs/object_or_string' },
},
my $config = {
callbacks => {
'$ref' => sub ($data, $schema, $state) {
push @used_ref_at, $state->{data_path};
},
},
},
);
ok($result->valid, 'evaluation was successful');
cmp_result(
\@used_ref_at,
bag(
'/0',
'/0/a',
'/0/a/b',
'/0/a/b/c',
'/0/a/b/c/d',
),
'identified all data paths where a $ref was used',
);
undef @used_ref_at;
$result = $js->evaluate(
[ { a => { b => 2 } } ],
$schema,
$config,
);
ok(!$result->valid, 'evaluation was not successful');
cmp_result(
\@used_ref_at,
[],
'no callbacks on failure: innermost $ref failed, so all other $refs failed too',
);
undef @used_ref_at;
$result = $js->evaluate(
[
{ a => { b => 'c' } },
{ x => { y => 1 } },
],
{
'$defs' => {
object_or_string => {
anyOf => [
{
type => 'object',
additionalProperties => { '$ref' => '#/$defs/object_or_string' },
},
{
type => 'string'
},
],
},
},
contains => { '$ref' => '#/$defs/object_or_string' },
},
$config,
);
ok($result->valid, 'evaluation was successful');
cmp_result(
\@used_ref_at,
bag(
'/0',
'/0/a',
'/0/a/b',
),
'successful subschemas have callbacks called, but not failed subschemas',
);
};
subtest 'callbacks for keywords without eval subs' => sub {
my %keywords;
my $result = $js->evaluate(
'hello',
{
'$id' => 'my_weird_schema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$vocabulary' => { 'https://json-schema.org/draft/2020-12/vocab/core' => true },
'$anchor' => 'my_anchor',
'$comment' => 'my comment',
'$defs' => { foo => true },
'$dynamicAnchor' => 'dynamicanchor',
if => true, then => true, else => true,
},
{
callbacks => {
map +($_ => sub ($data, $schema, $state) {
++$keywords{$state->{keyword}}
}), qw($anchor $comment $defs $dynamicAnchor if then else $schema $vocabulary),
},
},
);
ok($result->valid, 'evaluation was successful');
cmp_result(
\%keywords,
{ map +($_ => 1), qw($anchor $comment $defs $dynamicAnchor if then else $schema $vocabulary) },
'callbacks are triggered for keywords even when they lack evaluation subs',
);
};
subtest 'callbacks that produce errors' => sub {
my $result = $js->evaluate(
my $data = {
alpha => 1,
beta => 'foo',
},
my $schema = {
properties => { alpha => { type => 'number' } },
additionalProperties => { type => 'number' },
},
my $configs = {
callbacks => {
type => sub ($data, $schema, $state) {
JSON::Schema::Modern::Utilities::E($state, 'this is a callback error');
},
},
},
);
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha/type',
error => 'this is a callback error',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '/beta',
keywordLocation => '/additionalProperties/type',
error => 'got string, not number',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'result object contains the callback error, and the other errors',
);
$result = $js->evaluate($data, $schema, { %$configs, short_circuit => 1 });
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha/type',
error => 'this is a callback error',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'result object contains the callback error, and short-circuits execution',
);
};
done_testing;
checksums.t 100640 000766 000024 4636 15114374332 17023 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Test2::V0 -no_pragmas => 1;
use if $ENV{AUTHOR_TESTING}, 'Test2::Warnings';
use Digest::MD5 'md5_hex';
use Mojo::File 'path';
foreach my $line () {
chomp $line;
my ($filename, $checksum) = split / /, $line, 2;
is(md5_hex(path($filename)->slurp), $checksum, 'checksum for '.$filename.' is correct')
or diag $filename.' is not what was shipped in the distribution!';
}
done_testing;
__DATA__
share/LICENSE 82b044426b4e3998d7eb085d98a5f916
share/draft2019-09/meta/applicator.json fd08fc5b4c3bd23ae19c919c62b86551
share/draft2019-09/meta/content.json 50fe3f49e909fb38f2bf35022139d174
share/draft2019-09/meta/core.json b4c0e7eac5bd74641d464ebb6377e8cf
share/draft2019-09/meta/format.json 067f8aa16b1e4b8f6b3c352e42ce7a04
share/draft2019-09/meta/meta-data.json f1d30b664cc43acf7d14cce61629cec8
share/draft2019-09/meta/validation.json 9d955e385dbc9cd1d2cb609725c22836
share/draft2019-09/output/schema.json bf63c8c7e3b4aa8786c6f7c27c28121f
share/draft2019-09/schema.json 235e1fd47201b751194d9e8e90969ce4
share/draft2020-12/meta/applicator.json 835180064e52815df939c2118814d80d
share/draft2020-12/meta/content.json 43fd532b134825d343a9be5aa5610234
share/draft2020-12/meta/core.json 8e6829848b79f6d6952e888b4291a639
share/draft2020-12/meta/format-annotation.json 12e1bd6b2af5bdd67240bcb5efc473ae
share/draft2020-12/meta/format-assertion.json c78476a32441e52b48f13027a37fcfa6
share/draft2020-12/meta/meta-data.json c416e310b2648f291e51992b7dc012ab
share/draft2020-12/meta/unevaluated.json 9e4dddcbb0581b939b686048713a1b8e
share/draft2020-12/meta/validation.json a5a6bc93fa352985fc455e6325237f9c
share/draft2020-12/output/schema.json 6efc8e121569c98060dc754e88c52113
share/draft2020-12/schema.json e32920982620b2b43803dc82d4640b50
share/draft4/schema.json c6be0c4792c7455a526f0e0cc9eb7d25
share/draft6/schema.json 0376a64fd48e524336d37410c50f7b74
share/draft7/schema.json d6f6ffd262250e16b20f687b94a08bdc
add-schema.t 100640 000766 000024 101227 15114374332 17056 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use List::Util 'unpairs';
use lib 't/lib';
use Helper;
use Test2::Warnings 0.038 qw(warnings :no_end_test had_no_warnings);
use constant METASCHEMA => 'https://json-schema.org/draft/2019-09/schema';
# spec version -> vocab classes
my %vocabularies = unpairs(JSON::Schema::Modern->new->__all_metaschema_vocabulary_classes);
subtest 'evaluate a document' => sub {
my $document = JSON::Schema::Modern::Document->new(
schema => {
'$id' => 'https://foo.com',
allOf => [ false, true ],
});
my $js = JSON::Schema::Modern->new;
$js->add_document($document);
cmp_result(
$js->evaluate(1, $document->canonical_uri)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '',
keywordLocation => '/allOf/0',
absoluteKeywordLocation => 'https://foo.com#/allOf/0',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://foo.com#/allOf',
error => 'subschema 0 is not valid',
},
],
},
'evaluate a Document object',
);
cmp_result(
{ $js->_resource_index },
{
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
document => shallow($document),
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
},
},
'resource index from the document is copied to the main object',
);
cmp_result(
$js->evaluate(1, $document->canonical_uri)->TO_JSON,
{
valid => false,
errors => $errors,
},
'evaluate a Document object again without error',
);
};
subtest 'evaluate a uri' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate({ '$schema' => 1 }, METASCHEMA)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '/$schema',
keywordLocation => '/allOf/0/$ref/properties/$schema/type',
absoluteKeywordLocation => 'https://json-schema.org/draft/2019-09/meta/core#/properties/$schema/type',
error => 'got integer, not string',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/properties',
absoluteKeywordLocation => 'https://json-schema.org/draft/2019-09/meta/core#/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => METASCHEMA.'#/allOf',
error => 'subschema 0 is not valid',
},
],
},
'evaluate with a uri that is not yet loaded',
);
cmp_result(
{ $js->_resource_index },
{
map +(
$_ => {
path => '',
canonical_uri => str($_),
document => isa('JSON::Schema::Modern::Document'),
specification_version => 'draft2019-09',
vocabularies => $vocabularies{'draft2019-09'},
}
),
METASCHEMA,
map 'https://json-schema.org/draft/2019-09/meta/'.$_,
qw(core applicator validation meta-data format content)
},
'the metaschema is now loaded and its resources are indexed',
);
# and again, we can use the same resource without reloading it
cmp_result(
$js->evaluate({ '$schema' => 1 }, METASCHEMA)->TO_JSON,
{
valid => false,
errors => $errors,
},
'evaluate against the metaschema again',
);
# now use a subschema at that url to evaluate with.
# multiple things are being tested here:
# - we can load a schema resource, or find an existing one, with a fragment
# - the json path we used is saved in the state, for correct errors
cmp_result(
$js->evaluate(
1,
'https://json-schema.org/draft/2019-09/meta/core#/properties/$schema',
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/type',
absoluteKeywordLocation => 'https://json-schema.org/draft/2019-09/meta/core#/properties/$schema/type',
error => 'got integer, not string',
},
],
},
'evaluate against the a subschema of the metaschema',
);
cmp_result(
$js->evaluate(
1,
METASCHEMA.'#/does/not/exist',
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'EXCEPTION: unable to find resource "'.METASCHEMA.'#/does/not/exist"',
},
],
},
'evaluate against the a fragment of the metaschema that does not exist',
);
cmp_result(
$js->evaluate(
1,
METASCHEMA.'#does_not_exist',
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'EXCEPTION: unable to find resource "'.METASCHEMA.'#does_not_exist"',
},
],
},
'evaluate against the a plain-name fragment of the metaschema that does not exist',
);
};
subtest 'add a uri resource' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
my $get_metaschema = scalar $js->get(METASCHEMA),
my $orig_metaschema = $js->_get_resource(METASCHEMA)->{document}->schema,
'->get in scalar context on a URI to the head of a document',
);
ok($get_metaschema != $orig_metaschema, 'get() did not return a reference to the original data');
cmp_result(
[ $js->get(METASCHEMA) ],
[ $js->_get_resource(METASCHEMA)->{document}->schema,
all(isa('Mojo::URL'), str(METASCHEMA)) ],
'->get in list context on a URI to the head of a document',
);
cmp_result(
scalar $js->get(METASCHEMA.'#/properties/definitions/type'),
'object', # $document->schema->{properties}{definitions}{type}
'->get in scalar context on a URI to inside of a document',
);
cmp_result(
[ $js->get(METASCHEMA.'#/properties/definitions/type') ],
[ 'object', all(isa('Mojo::URL'), str(METASCHEMA.'#/properties/definitions/type')) ],
'->get in list context on a URI to inside of a document',
);
};
subtest 'add a schema associated with a uri' => sub {
my $js = JSON::Schema::Modern->new;
like(
dies { $js->add_schema('https://foo.com#/x/y/z', {}) },
qr/^cannot add a schema with a uri with a fragment/,
'cannot use a uri with a fragment',
);
cmp_result(
my $document = $js->add_schema(
'https://foo.com',
{ '$id' => 'https://bar.com', allOf => [ false, true ] },
),
all(
isa('JSON::Schema::Modern::Document'),
listmethods(
resource_index => [
'https://bar.com' => {
path => '',
canonical_uri => str('https://bar.com'),
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
},
],
canonical_uri => [ str('https://bar.com') ],
),
),
'added the schema data with an associated uri; the document does not see the overridden uri',
);
cmp_result(
$js->evaluate(1, 'https://bar.com#/allOf/0')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
absoluteKeywordLocation => 'https://bar.com#/allOf/0',
error => 'subschema is false',
},
],
},
'can now evaluate using a uri to a subschema of a resource we loaded earlier',
);
cmp_result(
$js->evaluate(1, 'https://foo.com')->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '',
keywordLocation => '/allOf/0',
absoluteKeywordLocation => 'https://bar.com#/allOf/0',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'https://bar.com#/allOf',
error => 'subschema 0 is not valid',
},
],
},
'can also evaluate using a non-canonical uri',
);
cmp_result(
$js->add_document('https://bloop.com', $document),
shallow($document),
'can add the same document and associate it with another schema',
);
my @warnings = warnings {
cmp_result(
$js->add_schema('https://bloop.com', $document),
shallow($document),
'can add the same document twice, using deprecated interface',
);
};
cmp_result(
\@warnings,
[ re(qr/use of deprecated form of add_schema with document/) ],
'warned when using deprecated form of add_schema with URI',
);
@warnings = warnings {
cmp_result(
$js->add_schema($document),
shallow($document),
'can add the same document again, using deprecated interface',
);
};
cmp_result(
\@warnings,
[ re(qr/use of deprecated form of add_schema with document/) ],
'warned when using deprecated form of add_schema without URI',
);
# this actually does nothing, via the duplicate check in _add_resource
cmp_result(
$js->add_document($document),
shallow($document),
'can add the same document again with the proper interface',
);
cmp_result(
{ $js->_resource_index },
{
map +($_ => {
path => '',
canonical_uri => str('https://bar.com'),
document => shallow($document),
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
}), qw(https://foo.com https://bar.com https://bloop.com)
},
'now the document is available as all three uris, with the same canonical_uri',
);
};
subtest 'multiple anonymous schemas' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(1, { minimum => 1 })->TO_JSON,
{ valid => true },
'evaluate an anonymous schema',
);
cmp_result([ keys $js->{_resource_index}->%* ], [ '' ], 'one resource is indexed');
cmp_result(
$js->evaluate(2, { minimum => 2 })->TO_JSON,
{ valid => true },
'evaluate another anonymous schema',
);
cmp_result([ keys $js->{_resource_index}->%* ], [ '' ], 'still only one resource is indexed');
};
subtest 'add a document without associating it with a uri' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->add_document(
my $document = JSON::Schema::Modern::Document->new(
schema => { '$id' => 'https://bar.com', allOf => [ false, true ] },
)),
all(
isa('JSON::Schema::Modern::Document'),
listmethods(
resource_index => [
'https://bar.com' => {
path => '',
canonical_uri => str('https://bar.com'),
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
},
],
canonical_uri => [ str('https://bar.com') ],
),
),
'added the document without an associated uri',
);
cmp_result(
{ $js->_resource_index },
{
'https://bar.com' => {
path => '',
canonical_uri => str('https://bar.com'),
document => shallow($document),
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
},
},
'document only added under its canonical uri',
);
};
subtest 'add a schema without a uri' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
my $document = $js->add_schema(
{ '$id' => 'https://bar.com', allOf => [ false, true ] },
),
all(
isa('JSON::Schema::Modern::Document'),
listmethods(
resource_index => [
'https://bar.com' => {
path => '',
canonical_uri => str('https://bar.com'),
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
},
],
canonical_uri => [ str('https://bar.com') ],
),
),
'added the schema data without an associated uri',
);
cmp_result(
{ $js->_resource_index },
{
'https://bar.com' => {
path => '',
canonical_uri => str('https://bar.com'),
document => shallow($document),
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
},
},
'document only added under its canonical uri',
);
};
subtest '$ref to non-canonical uri' => sub {
my $schema = {
'$id' => 'http://localhost:4242/my_document', # the canonical_uri
properties => {
alpha => false,
beta => {
'$id' => 'beta',
properties => {
gamma => {
minimum => 2,
},
},
},
delta => {
'$ref' => 'http://otherhost:4242/another_uri#/properties/alpha',
},
},
};
my $js = JSON::Schema::Modern->new;
$js->add_schema('http://otherhost:4242/another_uri', $schema);
cmp_result(
$js->evaluate({ alpha => 1 }, 'http://otherhost:4242/another_uri')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha',
absoluteKeywordLocation => 'http://localhost:4242/my_document#/properties/alpha',
error => 'property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'http://localhost:4242/my_document#/properties',
error => 'not all properties are valid',
},
],
},
'errors use the canonical uri, not the uri used to evaluate against',
);
cmp_result(
$js->evaluate({ gamma => 1 }, 'http://otherhost:4242/beta')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'EXCEPTION: unable to find resource "http://otherhost:4242/beta"',
},
],
},
'non-canonical uri is not used to resolve inner $id keywords',
);
cmp_result(
$js->evaluate({ gamma => 1 }, 'http://localhost:4242/beta')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/gamma',
keywordLocation => '/properties/gamma/minimum',
absoluteKeywordLocation => 'http://localhost:4242/beta#/properties/gamma/minimum',
error => 'value is less than 2',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'http://localhost:4242/beta#/properties',
error => 'not all properties are valid',
},
],
},
'the canonical uri is updated when use the canonical uri, not the uri used to evaluate against',
);
cmp_result(
$js->evaluate({ delta => 1 }, 'http://otherhost:4242/another_uri')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/delta',
keywordLocation => '/properties/delta/$ref',
absoluteKeywordLocation => 'http://localhost:4242/my_document#/properties/alpha',
error => 'subschema is false',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'http://localhost:4242/my_document#/properties',
error => 'not all properties are valid',
},
],
},
'canonical_uri is not always what was in the $ref, even when no local $id is present',
);
cmp_result(
$js->evaluate(1, 'http://otherhost:4242/another_uri#/properties/alpha')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
absoluteKeywordLocation => 'http://localhost:4242/my_document#/properties/alpha',
error => 'subschema is false',
},
],
},
'canonical_uri fragment also needs to be adjusted',
);
delete $schema->{properties}{beta}{'$id'};
cmp_result(
$js->evaluate({ gamma => 1 }, 'http://otherhost:4242/another_uri#/properties/beta')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/gamma',
keywordLocation => '/properties/gamma/minimum',
absoluteKeywordLocation => 'http://localhost:4242/beta#/properties/gamma/minimum',
error => 'value is less than 2',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => 'http://localhost:4242/beta#/properties',
error => 'not all properties are valid',
},
],
},
'canonical_uri starts out containing a fragment and can be appended to during traversal',
);
};
subtest 'register a document against multiple uris, with absolute root uri' => sub {
my $js = JSON::Schema::Modern->new;
my $document = JSON::Schema::Modern::Document->new(
schema => {
'$id' => 'https://foo.com',
'$anchor' => 'my_anchor',
maximum => 1,
'$defs' => {
foo => {
'$id' => 'my_dir',
allOf => [ true ],
},
},
});
my %more_configs;
cmp_result(
{ $document->resource_index },
my $doc_resource_index = {
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
do { %more_configs = (
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
) },
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://foo.com'),
},
},
},
'https://foo.com/my_dir' => {
path => '/$defs/foo',
canonical_uri => str('https://foo.com/my_dir'),
%more_configs,
},
},
'identifiers stored for the document',
);
$js->add_document($document);
cmp_result(
{ $js->_resource_index },
my $main_resource_index = {
'https://foo.com' => {
path => '',
canonical_uri => str('https://foo.com'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://foo.com'),
},
},
},
'https://foo.com/my_dir' => {
path => '/$defs/foo',
canonical_uri => str('https://foo.com/my_dir'),
document => shallow($document),
%more_configs,
},
},
'resource index from the document is copied to the main object',
);
$js->add_document('https://uri2.com', $document);
cmp_result(
{ $js->_resource_index },
$main_resource_index = {
%$main_resource_index,
'https://uri2.com' => {
path => '',
canonical_uri => str('https://foo.com'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://foo.com'),
},
},
},
},
'add a secondary uri for the same document',
);
cmp_result(
{ $document->resource_index },
$doc_resource_index,
'secondary uri not also added to the document',
);
like(
dies { $js->add_schema('https://uri2.com', { x => 1 }) },
qr!^\Quri "https://uri2.com" conflicts with an existing schema resource\E!,
'cannot call add_schema with the same URI as for another schema',
);
like(
dies { $js->add_schema('https://uri3.com', { '$id' => 'https://foo.com', x => 1 }) },
qr!^\Quri "https://foo.com" conflicts with an existing schema resource\E!,
'cannot reuse the same $id in another document',
);
cmp_result(
{ $js->_resource_index },
$main_resource_index,
'resource index remains unchanged after erroneous add_schema calls',
);
$js->add_schema('https://uri4.com', +{ $document->schema->%* });
cmp_result(
{ $js->_resource_index },
{
%$main_resource_index,
'https://uri4.com' => {
path => '',
canonical_uri => str('https://foo.com'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://foo.com'),
},
},
},
},
'adding the same schema content again is permitted',
);
is(
scalar $js->get('https://foo.com#i_do_not_exist'),
undef,
'->get in scalar context for a nonexistent resource returns undef',
);
cmp_result(
[ $js->get('https://foo.com#i_do_not_exist') ],
[],
'->get in list context for a nonexistent resource returns empty list',
);
};
subtest 'register a document against multiple uris, with relative root uri' => sub {
my $js = JSON::Schema::Modern->new;
my $document = JSON::Schema::Modern::Document->new(
schema => {
'$id' => 'my_dir/',
'$anchor' => 'my_anchor',
maximum => 1,
'$defs' => {
foo => {
'$id' => 'my_dir2',
allOf => [ true ],
},
},
});
my %more_configs;
cmp_result(
{ $document->resource_index },
my $doc_resource_index = {
'my_dir/' => {
path => '',
canonical_uri => str('my_dir/'),
do { %more_configs = (
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
) },
anchors => {
my_anchor => {
path => '',
canonical_uri => str('my_dir/'),
},
},
},
'my_dir/my_dir2' => {
path => '/$defs/foo',
canonical_uri => str('my_dir/my_dir2'),
%more_configs,
},
},
'identifiers stored for the document',
);
$js->add_document($document);
cmp_result(
{ $js->_resource_index },
my $main_resource_index = {
'my_dir/' => {
path => '',
canonical_uri => str('my_dir/'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('my_dir/'),
},
},
},
'my_dir/my_dir2' => {
path => '/$defs/foo',
canonical_uri => str('my_dir/my_dir2'),
document => shallow($document),
%more_configs,
},
},
'resource index from the document is copied to the main object',
);
$js->add_document('https://uri2.com', $document);
cmp_result(
{ $js->_resource_index },
$main_resource_index = {
%$main_resource_index,
'https://uri2.com' => {
path => '',
canonical_uri => str('https://uri2.com/my_dir/'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://uri2.com/my_dir/'),
},
},
},
'https://uri2.com/my_dir/' => {
path => '',
canonical_uri => str('https://uri2.com/my_dir/'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://uri2.com/my_dir/'),
},
},
},
'https://uri2.com/my_dir/my_dir2' => {
path => '/$defs/foo',
canonical_uri => str('https://uri2.com/my_dir/my_dir2'),
document => shallow($document),
%more_configs,
},
},
'add a secondary (absolute) uri for the same document',
);
cmp_result(
{ $document->resource_index },
$doc_resource_index,
'secondary uri not also added to the document',
);
like(
dies { $js->add_schema('https://uri2.com', { x => 1 }) },
qr!^\Quri "https://uri2.com" conflicts with an existing schema resource\E!,
'cannot call add_schema with the same URI as for another schema',
);
like(
dies { $js->add_schema('https://uri3.com', { '$id' => 'https://uri2.com', x => 1 }) },
qr!^\Quri "https://uri2.com" conflicts with an existing schema resource\E!,
'cannot reuse the same $id in another document',
);
cmp_result(
{ $js->_resource_index },
$main_resource_index,
'resource index remains unchanged after erroneous add_schema calls',
);
$js->add_schema('https://uri4.com', +{ $document->schema->%* });
cmp_result(
{ $js->_resource_index },
{
%$main_resource_index,
'https://uri4.com' => {
path => '',
canonical_uri => str('https://uri4.com/my_dir/'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://uri4.com/my_dir/'),
},
},
},
'https://uri4.com/my_dir/' => {
path => '',
canonical_uri => str('https://uri4.com/my_dir/'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://uri4.com/my_dir/'),
},
},
},
'https://uri4.com/my_dir/my_dir2' => {
path => '/$defs/foo',
canonical_uri => str('https://uri4.com/my_dir/my_dir2'),
document => shallow($document),
%more_configs,
},
},
'adding the same schema content again is permitted',
);
};
subtest 'register a document against multiple uris, with no root uri' => sub {
my $js = JSON::Schema::Modern->new;
my $document = JSON::Schema::Modern::Document->new(
schema => {
'$anchor' => 'my_anchor',
maximum => 1,
'$defs' => {
foo => {
'$id' => 'my_dir',
allOf => [ true ],
},
},
});
my %more_configs;
cmp_result(
{ $document->resource_index },
my $doc_resource_index = {
'' => {
path => '',
canonical_uri => str(''),
do { %more_configs = (
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
) },
anchors => {
my_anchor => {
path => '',
canonical_uri => str(''),
},
},
},
'my_dir' => {
path => '/$defs/foo',
canonical_uri => str('my_dir'),
%more_configs,
},
},
'identifiers stored for the document',
);
$js->add_document($document);
cmp_result(
{ $js->_resource_index },
my $main_resource_index = {
'' => {
path => '',
canonical_uri => str(''),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str(''),
},
},
},
'my_dir' => {
path => '/$defs/foo',
canonical_uri => str('my_dir'),
document => shallow($document),
%more_configs,
},
},
'resource index from the document is copied to the main object',
);
$js->add_document('https://uri2.com', $document);
cmp_result(
{ $js->_resource_index },
$main_resource_index = {
%$main_resource_index,
'https://uri2.com' => {
path => '',
canonical_uri => str('https://uri2.com'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://uri2.com'),
},
},
},
'https://uri2.com/my_dir' => {
path => '/$defs/foo',
canonical_uri => str('https://uri2.com/my_dir'),
document => shallow($document),
%more_configs,
},
},
'add a secondary (absolute) uri for the same document',
);
cmp_result(
{ $document->resource_index },
$doc_resource_index,
'secondary uri not also added to the document',
);
like(
dies { $js->add_schema('https://uri2.com', { x => 1 }) },
qr!^\Quri "https://uri2.com" conflicts with an existing schema resource\E!,
'cannot call add_schema with the same URI as for another schema',
);
like(
dies { $js->add_schema('https://uri3.com', { '$id' => 'https://uri2.com', x => 1 }) },
qr!^\Quri "https://uri2.com" conflicts with an existing schema resource\E!,
'cannot reuse the same $id in another document',
);
cmp_result(
{ $js->_resource_index },
$main_resource_index,
'resource index remains unchanged after erroneous add_schema calls',
);
$js->add_schema('https://uri4.com', +{ $document->schema->%* });
cmp_result(
{ $js->_resource_index },
{
%$main_resource_index,
'https://uri4.com' => {
path => '',
canonical_uri => str('https://uri4.com'),
document => shallow($document),
%more_configs,
anchors => {
my_anchor => {
path => '',
canonical_uri => str('https://uri4.com'),
},
},
},
'https://uri4.com/my_dir' => {
path => '/$defs/foo',
canonical_uri => str('https://uri4.com/my_dir'),
document => shallow($document),
%more_configs,
},
},
'adding the same schema content again is permitted',
);
};
subtest 'external resource with externally-supplied uri; main resource with multiple uris' => sub {
my $js = JSON::Schema::Modern->new;
$js->add_schema('http://localhost:1234/integer.json', { type => 'integer' });
$js->add_schema(
'https://secondary.com',
my $schema = {
'$id' => 'https://main.com',
'$ref' => 'http://localhost:1234/integer.json',
type => 'object',
},
);
cmp_result(
my $result = $js->evaluate('string', 'https://secondary.com')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/type',
absoluteKeywordLocation => 'http://localhost:1234/integer.json#/type',
error => 'got string, not integer',
},
{
instanceLocation => '',
keywordLocation => '/type',
absoluteKeywordLocation => 'https://main.com#/type',
error => 'got string, not object',
},
],
},
'all uris in result are correct, using secondary uri as the target',
);
cmp_result(
$js->evaluate('string', 'https://main.com')->TO_JSON,
$result,
'all uris in result are correct, using main uri as the target',
);
};
subtest 'document with no canonical URI, but assigned a URI through add_schema' => sub {
my $js = JSON::Schema::Modern->new;
# the document itself doesn't know about this URI, but the evaluator does
$js->add_schema(
'https://localhost:1234/mydef.json',
my $def_schema = { '$defs' => { integer => { type => 'integer' } } },
);
cmp_result(
$js->evaluate(
{ foo => 'string' },
my $schema = {
# no $id here!
type => 'object',
additionalProperties => {
'$ref' => 'https://localhost:1234/mydef.json#/$defs/integer',
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/type',
# the canonical URI is what the evaluator knows it as, even if the document doesn't know
absoluteKeywordLocation => 'https://localhost:1234/mydef.json#/$defs/integer/type',
error => 'got string, not integer',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'evaluate a schema referencing a document given an ad-hoc uri',
);
# start over with a new evaluator...
$js = JSON::Schema::Modern->new;
$js->add_document(
'https://localhost:1234/mydef.json',
JSON::Schema::Modern::Document->new(schema => {
'$id' => 'https://otherhost.com/mydef.json',
%$def_schema,
}),
);
cmp_result(
$js->evaluate(
{ foo => 'string' },
$schema,
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/$ref/type',
absoluteKeywordLocation => 'https://otherhost.com/mydef.json#/$defs/integer/type',
error => 'got string, not integer',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
error => 'not all additional properties are valid',
},
],
},
'adding a uri to an existing document does not change its canonical uri',
);
};
had_no_warnings if $ENV{AUTHOR_TESTING};
done_testing;
multipleOf.t 100640 000766 000024 4546 15114374332 17156 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use utf8;
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Config;
use lib 't/lib';
use Helper;
my @tests = (
# data (dividend), schema value (divisor), expected result
[ 4, 2, true ],
[ 4, 1, true ],
[ 4, 3, false ],
[ 4.5, 1.5, true ],
[ 4.5, 1, false ],
[ 4.5, 3, false ],
[ 4, 2, true ],
[ 4, 2.5, false ],
[ 5, 2.5, true ],
[ 4.5, 2.25, true ],
[ 4.5, 2.5, false ],
[ 4.5, 2, false ],
[ -(~0 >> 1) -1, 0.5, true ], # min signed int
[ ~0 >> 1, 0.5, true ], # max signed int
[ ~0, 0.5, true ], # max unsigned int
);
my $js = JSON::Schema::Modern->new;
my $note = $ENV{AUTHOR_TESTING} || $ENV{AUTOMATED_TESTING} ? \&diag : \¬e;
sub run_test ($data, $schema_value, $expected) {
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $result = $js->evaluate($data, { multipleOf => $schema_value });
my $pass = ok(!($result->valid xor $expected), "$data is ".($expected ? '' : 'not ')."a multiple of $schema_value");
$note->('got result: '.$result->dump) if not $pass;
}
subtest 'multipleOf, native types' => sub {
foreach my $test (@tests) {
my ($data, $schema_value, $expected) = @$test;
run_test($data, $schema_value, $expected);
};
};
subtest 'multipleOf, data is a bignum' => sub {
foreach my $test (@tests) {
my ($data, $schema_value, $expected) = @$test;
run_test(Math::BigFloat->new($data), $schema_value, $expected);
};
};
subtest 'multipleOf, multipleOf is a bignum' => sub {
foreach my $test (@tests) {
my ($data, $schema_value, $expected) = @$test;
run_test($data, Math::BigFloat->new($schema_value), $expected);
};
};
subtest 'multipleOf, data and multipleOf are bignums' => sub {
foreach my $test (@tests) {
my ($data, $schema_value, $expected) = @$test;
run_test(Math::BigFloat->new($data), Math::BigFloat->new($schema_value), $expected);
};
};
done_testing;
update-schemas 100750 000766 000024 10350 15114374332 17244 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 #!/usr/bin/env perl
# vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strict;
use warnings;
use 5.020;
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
use Mojo::File 'path';
use Mojo::UserAgent;
use Digest::MD5 'md5_hex';
use Test::File::ShareDir -share => { -dist => { 'JSON-Schema-Modern' => 'share' } };
use lib 'lib';
# ATTENTION DISTRO REPACKAGERS: do NOT use fresh copies of these files
# from their source; it is important to include the original versions
# of the files as they were packaged with this cpan distribution, or
# surprising behaviour may occur.
my %files = (
'draft2020-12/meta/applicator.json' => 'https://json-schema.org/draft/2020-12/meta/applicator',
'draft2020-12/meta/content.json' => 'https://json-schema.org/draft/2020-12/meta/content',
'draft2020-12/meta/core.json' => 'https://json-schema.org/draft/2020-12/meta/core',
'draft2020-12/meta/format-annotation.json' => 'https://json-schema.org/draft/2020-12/meta/format-annotation',
'draft2020-12/meta/format-assertion.json' => 'https://json-schema.org/draft/2020-12/meta/format-assertion',
'draft2020-12/meta/meta-data.json' => 'https://json-schema.org/draft/2020-12/meta/meta-data',
'draft2020-12/meta/unevaluated.json' => 'https://json-schema.org/draft/2020-12/meta/unevaluated',
'draft2020-12/meta/validation.json' => 'https://json-schema.org/draft/2020-12/meta/validation',
'draft2020-12/output/schema.json' => 'https://json-schema.org/draft/2020-12/output/schema',
'draft2020-12/schema.json' => 'https://json-schema.org/draft/2020-12/schema',
'draft2019-09/meta/applicator.json' => 'https://json-schema.org/draft/2019-09/meta/applicator',
'draft2019-09/meta/content.json' => 'https://json-schema.org/draft/2019-09/meta/content',
'draft2019-09/meta/core.json' => 'https://json-schema.org/draft/2019-09/meta/core',
'draft2019-09/meta/format.json' => 'https://json-schema.org/draft/2019-09/meta/format',
'draft2019-09/meta/meta-data.json' => 'https://json-schema.org/draft/2019-09/meta/meta-data',
'draft2019-09/meta/validation.json' => 'https://json-schema.org/draft/2019-09/meta/validation',
'draft2019-09/output/schema.json' => 'https://json-schema.org/draft/2019-09/output/schema',
'draft2019-09/schema.json' => 'https://json-schema.org/draft/2019-09/schema',
'draft7/schema.json' => 'http://json-schema.org/draft-07/schema',
'draft6/schema.json' => 'http://json-schema.org/draft-06/schema',
'draft4/schema.json' => 'http://json-schema.org/draft-04/schema',
'LICENSE' => 'https://raw.githubusercontent.com/json-schema-org/json-schema-spec/main/LICENSE',
);
my $ua = Mojo::UserAgent->new(max_redirects => 3);
my %checksums;
foreach my $target (sort keys %files) {
my $uri = $files{$target};
say "# fetching $uri -> share/$target" if $ENV{DEBUG};
my $res = $ua->get($uri)->result;
die "Failed to fetch $uri", $res->code, " ", $res->message if $res->is_error;
$target = path('share', $target);
$target->dirname->make_path;
$target->spew(my $content = $res->body);
$checksums{$target} = md5_hex($content);
}
# lazy-load this to make sure we load the files we just downloaded
require JSON::Schema::Modern;
my $json_decoder = JSON::Schema::Modern::_JSON_BACKEND()->new->utf8(1);
# all files must be updated before any of them can be validated, since they depend on each other via
# the '$schema' and '$vocabulary' keywords
foreach my $target (sort keys %files) {
$target = path('share', $target);
next if $target->basename eq 'LICENSE';
my $schema = $json_decoder->decode($target->slurp);
say '# validating ', $schema->{'$id'}//$schema->{id}, ' -> ', $target if $ENV{DEBUG};
my $result = JSON::Schema::Modern::Document->validate(schema => $schema);
die $result->dump if not $result->valid;
}
my $checksums_file = path('t/checksums.t');
my $content = $checksums_file->slurp;
$content =~ m/^__DATA__$/mg;
$checksums_file->spew(substr($content, 0, pos($content)+1).join("\n", map $_.' '.$checksums{$_}, sort keys %checksums)."\n");
annotations.t 100640 000766 000024 111075 15114374332 17427 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use builtin::compat 'load_module';
use lib 't/lib';
use Helper;
subtest 'draft7' => sub {
like(
dies {
JSON::Schema::Modern->new(collect_annotations => 1, specification_version => 'draft7');
},
qr/collect_annotations cannot be used with specification_version draft7/,
'user cannot enable annotations for draft7',
);
cmp_result(
JSON::Schema::Modern->new(specification_version => 'draft7')
->evaluate(
1,
{
title => 'draft7 title',
allOf => [ { '$ref' => '#/definitions/draft2020-12-schema' } ],
definitions => {
'draft2020-12-schema' => {
'$id' => 'my_draft2020-12_schema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
title => 'draft2020-12 title'
},
},
},
{ collect_annotations => 1 },
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/title',
absoluteKeywordLocation => 'my_draft2020-12_schema#/title',
annotation => 'draft2020-12 title',
},
],
},
'annotation collection can be enabled even when defaulting to draft7, but they are still only collected for later drafts',
);
};
my $js = JSON::Schema::Modern->new(collect_annotations => 1, short_circuit => 0);
my $initial_state = {
depth => 0,
short_circuit => 0,
collect_annotations => 1<<8,
initial_schema_uri => Mojo::URL->new,
data_path => '',
keyword_path => '',
traversed_keyword_path => '',
specification_version => 'draft2019-09',
vocabularies => [
(map load_module($_),
map 'JSON::Schema::Modern::Vocabulary::'.$_, qw(Applicator Validation MetaData)),
],
evaluator => $js,
};
subtest 'allOf' => sub {
my $state = {
%$initial_state,
keyword => 'allOf',
annotations => [],
errors => [],
};
my $fail_schema = {
allOf => [
false, # fails; creates errors
{ title => 'allOf title' }, # passes; creates annotations
],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_allOf(1, $fail_schema, $state),
'evaluation of the allOf keyword fails',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/allOf/1/title',
annotation => 'allOf title',
}),
],
errors => [
methods(TO_JSON => { instanceLocation => '', keywordLocation => '/allOf/0', error => 'subschema is false' }),
methods(TO_JSON => { instanceLocation => '', keywordLocation => '/allOf', error => 'subschema 0 is not valid' }),
],
},
'failing allOf: state is correct after evaluating',
);
my $pass_schema = {
allOf => [
true,
{ title => 'allOf title' }, # passes; creates annotations
true,
],
};
$state->{annotations} = [];
$state->{errors} = [];
ok(
$state->{vocabularies}[0]->_eval_keyword_allOf(1, $pass_schema, $state),
'evaluation of the allOf keyword succeeds',
);
cmp_result(
$state,
{
%$new_state,
annotations => [
superhashof({
instance_location => '',
keyword_location => '/allOf/1/title',
annotation => 'allOf title',
}),
],
errors => [],
},
'passing allOf: state is correct after evaluating',
);
cmp_result(
$js->evaluate(1, $pass_schema, { collect_annotations => 0 })->TO_JSON,
{ valid => true },
'annotation collection can be turned off in evaluate()',
);
ok($js->collect_annotations, '...but the value is still true on the object');
{
my $js = JSON::Schema::Modern->new;
ok(!$js->collect_annotations, 'collect_annotations defaults to false');
cmp_result(
$js->evaluate(1, $pass_schema, { collect_annotations => 1 })->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/allOf/1/title',
annotation => 'allOf title',
},
],
},
'annotation collection can be turned on in evaluate() also',
);
}
};
subtest 'oneOf' => sub {
my $state = {
%$initial_state,
keyword => 'oneOf',
annotations => [],
errors => [],
};
my $fail_schema = {
oneOf => [
false, # fails; creates errors
{ title => 'oneOf title' }, # passes; creates annotations
{ title => 'oneOf title2' }, # passes; creates annotations
],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_oneOf(1, $fail_schema, $state),
'evaluation of the oneOf keyword fails',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/oneOf/1/title',
annotation => 'oneOf title',
}),
superhashof({
instance_location => '',
keyword_location => '/oneOf/2/title',
annotation => 'oneOf title2',
}),
],
errors => [
methods(TO_JSON => { instanceLocation => '', keywordLocation => '/oneOf', error => 'multiple subschemas are valid: 1, 2' }),
],
},
'failing oneOf: state is correct after evaluating',
);
my $pass_schema = {
oneOf => [
false,
{ title => 'oneOf title' }, # passes; creates annotations
false,
],
};
$state->{annotations} = [];
$state->{errors} = [];
ok(
$state->{vocabularies}[0]->_eval_keyword_oneOf(1, $pass_schema, $state),
'evaluation of the oneOf keyword succeeds',
);
cmp_result(
$state,
{
%$new_state,
annotations => [
superhashof({
instance_location => '',
keyword_location => '/oneOf/1/title',
annotation => 'oneOf title',
}),
],
errors => [],
},
'passing oneOf: state is correct after evaluating',
);
};
subtest 'not' => sub {
my $state = {
%$initial_state,
keyword => 'not',
annotations => [],
errors => [],
};
my $fail_schema = {
not => { title => 'not title' }, # passes; skips annotations because nothing needs them
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_not(1, $fail_schema, $state),
'evaluation of the not keyword fails',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [
methods(TO_JSON => { instanceLocation => '', keywordLocation => '/not', error => 'subschema is valid' }),
],
},
'failing not: state is correct after evaluating',
);
$state = {
%$initial_state,
keyword => 'not',
annotations => [],
errors => [],
};
$fail_schema = {
not => {
properties => { foo => true },
unevaluatedProperties => false,
}, # passes; annotations are collected because unevaluated* needs them
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_not(1, $fail_schema, $state),
'evaluation of the not keyword fails',
);
cmp_result(
$state,
$new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [
methods(TO_JSON => { instanceLocation => '', keywordLocation => '/not', error => 'subschema is valid' }),
],
},
'failing not: state is correct after evaluating (annotations will be ultimately discarded)',
);
my $pass_schema = {
not => { not => { title => 'not title' } },
};
$state->{annotations} = [];
$state->{errors} = [];
ok(
$state->{vocabularies}[0]->_eval_keyword_not(1, $pass_schema, $state),
'evaluation of the not keyword succeeds',
);
cmp_result(
$state,
{
%$new_state,
annotations => [],
errors => [],
},
'passing not: state is correct after evaluating',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
not => {
not => {
'$comment' => 'this subschema must still produce annotations internally, even though the "not" will ultimately discard them',
anyOf => [
true,
{ properties => { foo => true } },
],
unevaluatedProperties => false,
},
},
},
)->TO_JSON,
{
valid => true,
},
'annotations are still collected inside a "not", otherwise the unevaluatedProperties would have returned false',
);
};
subtest 'prefixItems' => sub {
my $state = {
%$initial_state,
keyword => 'prefixItems',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_prefixItems([], { prefixItems => [ true ] }, $state),
'no items means that "prefixItems" succeeds',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [],
},
'no items: no annotation is produced by prefixItems',
);
$state = {
%$initial_state,
keyword => 'prefixItems',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_prefixItems([ 1 ], { prefixItems => [ true ] }, $state),
'one item',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/prefixItems',
annotation => true,
}),
],
errors => [],
},
'passing prefixItems: one item is annotated',
);
$state = {
%$initial_state,
keyword => 'prefixItems',
annotations => [],
errors => [],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_prefixItems(
[ 1, 5, 9 ],
{ prefixItems => [ { title => 'hi', maximum => 3 }, { title => 'hi', maximum => 3 } ] },
$state),
'two items, one failing',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '/0',
keyword_location => '/prefixItems/0/title',
annotation => 'hi',
}),
superhashof({
instance_location => '',
keyword_location => '/prefixItems',
annotation => 1,
}),
],
errors => [
methods(TO_JSON => {
instanceLocation => '/1',
keywordLocation => '/prefixItems/1/maximum',
error => 'value is greater than 3',
}),
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/prefixItems',
error => 'not all items are valid',
}),
],
},
'failing prefixItems still collects annotations',
);
};
subtest 'schema-items' => sub {
my $state = {
%$initial_state,
keyword => 'items',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_items([], { items => true }, $state),
'no items means that "items" succeeds',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [],
},
'no items: no annotation is produced by items',
);
$state = {
%$initial_state,
keyword => 'items',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_items([ 1 ], { items => true }, $state),
'one item',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/items',
annotation => true,
}),
],
errors => [],
},
'passing items: one item is annotated',
);
$state = {
%$initial_state,
keyword => 'items',
annotations => [],
errors => [],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_items(
[ 1, 5 ],
{ items => { title => 'hi', maximum => 3 } },
$state),
'two items, one failing',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '/0',
keyword_location => '/items/title',
annotation => 'hi',
}),
superhashof({
instance_location => '',
keyword_location => '/items',
annotation => true,
}),
],
errors => [
methods(TO_JSON => {
instanceLocation => '/1',
keywordLocation => '/items/maximum',
error => 'value is greater than 3',
}),
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/items',
error => 'subschema is not valid against all items',
}),
],
},
'failing items still collects annotations',
);
};
subtest 'additionalItems' => sub {
my $state = {
%$initial_state,
keyword => 'additionalItems',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_items([], { additionalItems => true }, $state),
'no items means that "additionalItems" succeeds',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [],
},
'no items: no annotation is produced by additionaltems',
);
$state = {
%$initial_state,
keyword => 'additionalItems',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_additionalItems([ 1 ], { additionalItems => false }, $state),
'one item',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [],
},
'additionalItems does nothing without items',
);
};
subtest 'properties' => sub {
my $state = {
%$initial_state,
keyword => 'properties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_properties({}, { properties => { foo => true } }, $state),
'no items means that "properties" succeeds',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/properties',
annotation => [],
}),
],
errors => [],
},
'no properties: annotation is still produced by properties',
);
$state = {
%$initial_state,
keyword => 'properties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_properties({ foo => 1 }, { properties => { foo => true } }, $state),
'one property',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/properties',
annotation => [ 'foo' ],
}),
],
errors => [],
},
'passing properties: one property is annotated',
);
$state = {
%$initial_state,
keyword => 'properties',
annotations => [],
errors => [],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_properties(
{ foo => 1, bar => 5 },
{ properties => {
foo => { title => 'hi', maximum => 3 },
bar => { title => 'hi', maximum => 3 },
},
},
$state),
'two properties, one failing',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '/foo',
keyword_location => '/properties/foo/title',
annotation => 'hi',
}),
superhashof({
instance_location => '',
keyword_location => '/properties',
annotation => [ qw(bar foo) ],
}),
],
errors => [
methods(TO_JSON => {
instanceLocation => '/bar',
keywordLocation => '/properties/bar/maximum',
error => 'value is greater than 3',
}),
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
}),
],
},
'failing properties still collects annotations',
);
};
subtest 'patternProperties' => sub {
my $state = {
%$initial_state,
keyword => 'patternProperties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_patternProperties({}, { patternProperties => { foo => true } }, $state),
'no items means that "patternProperties" succeeds',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/patternProperties',
annotation => [],
}),
],
errors => [],
},
'no pProperties: annotation is still produced by patternProperties',
);
$state = {
%$initial_state,
keyword => 'patternProperties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_patternProperties({ foo => 1 }, { patternProperties => { foo => true } }, $state),
'one property',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/patternProperties',
annotation => [ 'foo' ],
}),
],
errors => [],
},
'passing properties: one property is annotated',
);
$state = {
%$initial_state,
keyword => 'patternProperties',
annotations => [],
errors => [],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_patternProperties(
{ foo => 1, bar => 5 },
{ patternProperties => {
foo => { title => 'hi', maximum => 3 },
bar => { title => 'hi', maximum => 3 },
},
},
$state),
'two properties, one failing',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '/foo',
keyword_location => '/patternProperties/foo/title',
annotation => 'hi',
}),
superhashof({
instance_location => '',
keyword_location => '/patternProperties',
annotation => [ qw(bar foo) ],
}),
],
errors => [
methods(TO_JSON => {
instanceLocation => '/bar',
keywordLocation => '/patternProperties/bar/maximum',
error => 'value is greater than 3',
}),
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/patternProperties',
error => 'not all properties are valid',
}),
],
},
'failing patternProperties still collects annotations',
);
};
subtest 'additionalProperties' => sub {
my $state = {
%$initial_state,
keyword => 'additionalProperties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_additionalProperties([], { additionalProperties => true }, $state),
'no items means that "additionalProperties" succeeds',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [],
},
'no properties: no annotation is produced by additionalProperties',
);
$state = {
%$initial_state,
keyword => 'additionalProperties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_additionalProperties({ foo => 1 }, { additionalProperties => true }, $state),
'one property',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/additionalProperties',
annotation => [ 'foo' ],
}),
],
errors => [],
},
'passing additionalProperties: one property is annotated',
);
$state = {
%$initial_state,
keyword => 'additionalProperties',
annotations => [],
errors => [],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_additionalProperties(
{ foo => 1, bar => 3, baz => 5 },
{
properties => { foo => true },
additionalProperties => { title => 'hi', maximum => 3 },
},
$state),
'two properties, one failing',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '/bar',
keyword_location => '/additionalProperties/title',
annotation => 'hi',
}),
superhashof({
instance_location => '',
keyword_location => '/additionalProperties',
annotation => [ qw(bar baz) ],
}),
],
errors => [
methods(TO_JSON => {
instanceLocation => '/baz',
keywordLocation => '/additionalProperties/maximum',
error => 'value is greater than 3',
}),
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/additionalProperties',
error => 'not all additional properties are valid',
}),
],
},
'failing properties still collects annotations',
);
};
subtest 'unevaluatedProperties' => sub {
my $state = {
%$initial_state,
keyword => 'unevaluatedProperties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_unevaluatedProperties([], { unevaluatedProperties => true }, $state),
'no items means that "unevaluatedProperties" succeeds',
);
cmp_result(
$state,
my $new_state = {
%$state,
initial_schema_uri => str(''),
annotations => [],
errors => [],
},
'no properties: no annotation is produced by unevaluatedProperties',
);
$state = {
%$initial_state,
keyword => 'unevaluatedProperties',
annotations => [],
errors => [],
};
ok(
$state->{vocabularies}[0]->_eval_keyword_unevaluatedProperties({ foo => 1 }, { unevaluatedProperties => true }, $state),
'one property',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
superhashof({
instance_location => '',
keyword_location => '/unevaluatedProperties',
annotation => [ 'foo' ],
}),
],
errors => [],
},
'passing unevaluatedProperties: one property is annotated',
);
$state = {
%$initial_state,
keyword => 'unevaluatedProperties',
annotations => [],
errors => [],
};
ok(
!$state->{vocabularies}[0]->_eval_keyword_unevaluatedProperties(
{ foo => 1, bar => 3, baz => 5 },
{
properties => { foo => true },
unevaluatedProperties => { title => 'hi', maximum => 3 },
},
$state),
'two properties, one failing',
);
cmp_result(
$state,
{
%$state,
initial_schema_uri => str(''),
annotations => [
(map superhashof({
instance_location => '/'.$_,
keyword_location => '/unevaluatedProperties/title',
annotation => 'hi',
}), qw(bar foo)),
superhashof({
instance_location => '',
keyword_location => '/unevaluatedProperties',
annotation => [ qw(bar baz foo) ],
}),
],
errors => [
methods(TO_JSON => {
instanceLocation => '/baz',
keywordLocation => '/unevaluatedProperties/maximum',
error => 'value is greater than 3',
}),
methods(TO_JSON => {
instanceLocation => '',
keywordLocation => '/unevaluatedProperties',
error => 'not all additional properties are valid',
}),
],
},
'failing unevaluatedProperties still collects annotations',
);
};
subtest 'collect_annotations and unevaluated keywords' => sub {
my $js = JSON::Schema::Modern->new(collect_annotations => 0);
cmp_result(
$js->evaluate(
[ 1 ],
my $schema = {
'$id' => 'unevaluatedItems.json',
prefixItems => [ true ],
unevaluatedItems => false,
},
)->TO_JSON,
{ valid => true },
'when "collect_annotations" is explicitly set to false, unevaluatedItems can still be used (valid result, no annotations in result)',
);
cmp_result(
$js->evaluate(
[ 1, 2 ],
$schema,
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/1',
keywordLocation => '/unevaluatedItems',
absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
error => 'additional item not permitted',
},
{
instanceLocation => '',
keywordLocation => '/unevaluatedItems',
absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
error => 'subschema is not valid against all additional items',
},
],
},
'when "collect_annotations" is explicitly set to false, unevaluatedItems can still be used (invalid result)',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
$schema = {
'$id' => 'unevaluatedProperties.json',
properties => { foo => true },
unevaluatedProperties => false,
},
)->TO_JSON,
{ valid => true },
'when "collect_annotations" is explicitly set to false, unevaluatedProperties can still be used (valid result, no annotations)',
);
cmp_result(
$js->evaluate(
{ foo => 1, bar => 2 },
$schema,
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/bar',
keywordLocation => '/unevaluatedProperties',
absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/unevaluatedProperties',
absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
error => 'not all additional properties are valid',
},
],
},
'when "collect_annotations" is explicitly set to false, unevaluatedProperties can still be used (invalid result)',
);
cmp_result(
$js->evaluate(
{
item => [ 1 ],
property => { foo => 1 },
},
$schema = {
properties => {
item => { '$ref' => 'unevaluatedItems.json' },
property => { '$ref' => 'unevaluatedProperties.json' },
},
},
)->TO_JSON,
{ valid => true },
'when "collect_annotations" is explicitly set to false, unevaluatedProperties still be used, even in other documents (valid result)',
);
cmp_result(
$js->evaluate(
{
item => [ 1, 2 ],
property => { foo => 1, bar => 2 },
},
$schema,
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/item/1',
keywordLocation => '/properties/item/$ref/unevaluatedItems',
absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
error => 'additional item not permitted',
},
{
instanceLocation => '/item',
keywordLocation => '/properties/item/$ref/unevaluatedItems',
absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
error => 'subschema is not valid against all additional items',
},
{
instanceLocation => '/property/bar',
keywordLocation => '/properties/property/$ref/unevaluatedProperties',
absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '/property',
keywordLocation => '/properties/property/$ref/unevaluatedProperties',
absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'when "collect_annotations" is explicitly set to false, unevaluatedProperties still be used, even in other documents (invalid result)',
);
$js = JSON::Schema::Modern->new(collect_annotations => 1);
cmp_result(
$js->evaluate(
[ 1 ],
{
prefixItems => [ true ],
unevaluatedItems => false,
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/prefixItems',
annotation => true,
},
],
},
'when "collect_annotations" is set to true, unevaluatedItems works, and annotations are returned',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
properties => { foo => true },
unevaluatedProperties => false,
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/properties',
annotation => [ 'foo' ],
},
{
instanceLocation => '',
keywordLocation => '/unevaluatedProperties',
annotation => [],
},
],
},
'when "collect_annotations" is set to true, unevaluatedProperties passes, and annotations are returned',
);
$js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
[ 1 ],
{
'$id' => 'unevaluatedItems.json',
prefixItems => [ true ],
unevaluatedItems => false,
},
)->TO_JSON,
{
valid => true,
},
'when "collect_annotations" is not set, unevaluatedItems still works, but annotations are not returned',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$id' => 'unevaluatedProperties.json',
properties => { foo => true },
unevaluatedProperties => false,
},
)->TO_JSON,
{
valid => true,
},
'when "collect_annotations" is not set, unevaluatedProperties still works, but annotations are not returned',
);
cmp_result(
$js->evaluate(
{
item => [ 1 ],
property => { foo => 1 },
},
{
properties => {
item => { '$ref' => 'unevaluatedItems.json' },
property => { '$ref' => 'unevaluatedProperties.json' },
},
},
)->TO_JSON,
{
valid => true,
},
'... still works when unevaluated keywords are in a separate document',
);
my $doc_items = $js->add_schema('prefixItems.json', { prefixItems => [ true ] });
my $doc_properties = $js->add_schema('properties.json', { properties => { foo => true } });
cmp_result(
$js->evaluate(
{
item => [ 1 ],
property => { foo => 1 },
},
{
properties => {
item => {
'$ref' => 'prefixItems.json',
unevaluatedItems => false,
},
property => {
'$ref' => 'properties.json',
unevaluatedProperties => false,
},
},
},
)->TO_JSON,
{
valid => true,
},
'referenced schemas still produce annotations internally when needed, even when not required to evaluate themselves in isolation',
);
};
subtest 'annotate unknown keywords' => sub {
my $data = {
item => [ 1 ],
property => { foo => 1 },
};
my $schema = {
properties => {
item => {
items => true,
unevaluatedItems => false,
bloop => 5,
},
property => {
properties => { foo => true },
unevaluatedProperties => false,
blap => { hi => 1 },
},
},
blip => [ 1, 2, 3 ],
};
cmp_result(
JSON::Schema::Modern->new->evaluate(
$data,
$schema,
)->TO_JSON,
{
valid => true,
},
'no annotations even when collect_annotations is false',
);
cmp_result(
(my $result = JSON::Schema::Modern->new(collect_annotations => 1)->evaluate(
$data,
$schema,
))->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/item',
keywordLocation => '/properties/item/items',
annotation => true,
},
{
instanceLocation => '/item',
keywordLocation => '/properties/item/bloop',
annotation => 5,
},
{
instanceLocation => '/property',
keywordLocation => '/properties/property/properties',
annotation => [ 'foo' ],
},
{
instanceLocation => '/property',
keywordLocation => '/properties/property/unevaluatedProperties',
annotation => [],
},
{
instanceLocation => '/property',
keywordLocation => '/properties/property/blap',
annotation => { hi => 1 },
},
{
instanceLocation => '',
keywordLocation => '/properties',
annotation => [ 'item', 'property' ],
},
{
instanceLocation => '',
keywordLocation => '/blip',
annotation => [ 1, 2, 3 ],
},
],
},
'unknown keywords are collected as annotations',
);
cmp_result(
[ $result->annotations ],
[
methods(keyword => 'items', unknown => bool(0)),
methods(keyword => 'bloop', unknown => bool(1)),
methods(keyword => 'properties', unknown => bool(0)),
methods(keyword => 'unevaluatedProperties', unknown => bool(0)),
methods(keyword => 'blap', unknown => bool(1)),
methods(keyword => 'properties', unknown => bool(0)),
methods(keyword => 'blip', unknown => bool(1)),
],
'"unknown" keyword is set on the annotation objects for unknown keywords',
);
cmp_result(
$result = JSON::Schema::Modern->new(specification_version => 'draft2019-09', collect_annotations => 1)
->evaluate(
$data,
$schema,
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/item',
keywordLocation => '/properties/item/items',
annotation => true,
},
# no bloop
{
instanceLocation => '/property',
keywordLocation => '/properties/property/properties',
annotation => [ 'foo' ],
},
{
instanceLocation => '/property',
keywordLocation => '/properties/property/unevaluatedProperties',
annotation => [],
},
# no blap
{
instanceLocation => '',
keywordLocation => '/properties',
annotation => [ 'item', 'property' ],
},
# no blip
],
},
'no annotations from unknown keywords in draft2019-09',
);
};
subtest 'items + additionalItems, prefixItems + items' => sub {
cmp_result(
JSON::Schema::Modern->new(specification_version => 'draft2019-09', collect_annotations => 1)
->evaluate(
[ 1, 2, 3 ],
{
items => { maximum => 5 },
additionalItems => { maximum => 0 },
}
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/items',
annotation => true,
},
# no error nor annotation from additionalItems
],
},
'schema-based items + additionalItems',
);
cmp_result(
my $result = JSON::Schema::Modern->new(collect_annotations => 1)->evaluate(
[ 1, 2, 3 ],
{
prefixItems => [ { maximum => 5 }, { maximum => 5 }, { maximum => 5 } ],
items => { maximum => 0 },
}
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/prefixItems',
annotation => true,
},
# no error nor annotation from items
],
},
'prefixItems + schema-based items',
);
};
done_testing;
lib 000755 000766 000024 0 15114374332 15252 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t Helper.pm 100640 000766 000024 4543 15114374332 17171 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t/lib # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
# no package, so things defined here appear in the namespace of the parent.
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Test2::V0 qw(!bag !bool !warnings), -no_pragmas => 1; # prefer Test::Deep's versions of these exports
use if $ENV{AUTHOR_TESTING}, 'Test2::Warnings';
use if $ENV{AUTHOR_TESTING} && (caller(2))[1] !~ /acceptance/, 'Test2::Plugin::BailOnFail';
use Test::Deep qw(!array !hash !blessed); # import symbols: ignore, re etc
use Test2::API 'context_do';
use Test::File::ShareDir -share => { -dist => { 'JSON-Schema-Modern' => 'share' } };
use JSON::Schema::Modern;
use JSON::Schema::Modern::Utilities qw(jsonp true false);
my $encoder = JSON::Schema::Modern::_JSON_BACKEND()->new
->allow_nonref(1)
->utf8(0)
->allow_bignum(1)
->allow_blessed(1)
->convert_blessed(1)
->canonical(1)
->pretty(1)
->indent_length(2);
# like sprintf, but all list items are JSON-encoded. assumes placeholders are %s!
sub json_sprintf {
sprintf(shift, map +(ref($_) =~ /^Math::Big(?:Int|Float)$/ ? ref($_).'->new(\''.$_.'\')' : $encoder->indent(0)->encode($_)), @_);
}
# deep comparison, with Test::Deep syntax sugar
sub cmp_result ($got, $expected, $test_name) {
context_do {
my $ctx = shift;
my ($got, $expected, $test_name) = @_;
my ($equal, $stack) = Test::Deep::cmp_details($got, $expected);
if ($equal) {
$ctx->pass($test_name);
}
else {
$ctx->fail($test_name);
my $method =
# be less noisy for expected failures
(grep $_->{todo}, Test2::API::test2_stack->top->{_pre_filters}->@*) ? 'note'
: $ENV{AUTHOR_TESTING} || $ENV{AUTOMATED_TESTING} ? 'diag' : 'note';
$ctx->$method(Test::Deep::deep_diag($stack));
$ctx->$method("got result:\n".$encoder->encode($got));
}
return $equal;
} $got, $expected, $test_name;
}
sub is_passing () {
context_do { shift->hub->is_passing };
}
1;
author 000755 000766 000024 0 15114374332 16176 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt eol.t 100644 000766 000024 17767 15114374332 17344 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author use strict;
use warnings;
# this test was generated with Dist::Zilla::Plugin::Test::EOL 0.19
use Test::More 0.88;
use Test::EOL;
my @files = (
'lib/JSON/Schema/Modern.pm',
'lib/JSON/Schema/Modern/Annotation.pm',
'lib/JSON/Schema/Modern/Document.pm',
'lib/JSON/Schema/Modern/Error.pm',
'lib/JSON/Schema/Modern/Result.pm',
'lib/JSON/Schema/Modern/ResultNode.pm',
'lib/JSON/Schema/Modern/Utilities.pm',
'lib/JSON/Schema/Modern/Vocabulary.pm',
'lib/JSON/Schema/Modern/Vocabulary/Applicator.pm',
'lib/JSON/Schema/Modern/Vocabulary/Content.pm',
'lib/JSON/Schema/Modern/Vocabulary/Core.pm',
'lib/JSON/Schema/Modern/Vocabulary/FormatAnnotation.pm',
'lib/JSON/Schema/Modern/Vocabulary/FormatAssertion.pm',
'lib/JSON/Schema/Modern/Vocabulary/MetaData.pm',
'lib/JSON/Schema/Modern/Vocabulary/Unevaluated.pm',
'lib/JSON/Schema/Modern/Vocabulary/Validation.pm',
'script/json-schema-eval',
't/00-report-prereqs.dd',
't/00-report-prereqs.t',
't/add-schema.t',
't/additional-tests-draft2019-09.t',
't/additional-tests-draft2019-09/README',
't/additional-tests-draft2019-09/anchor.json',
't/additional-tests-draft2019-09/annotation-collection.json',
't/additional-tests-draft2019-09/badRef.json',
't/additional-tests-draft2019-09/faux-buggy-schemas.json',
't/additional-tests-draft2019-09/format-date-time.json',
't/additional-tests-draft2019-09/format-date.json',
't/additional-tests-draft2019-09/format-duration.json',
't/additional-tests-draft2019-09/format-ipv4.json',
't/additional-tests-draft2019-09/format-ipv6.json',
't/additional-tests-draft2019-09/format-relative-json-pointer.json',
't/additional-tests-draft2019-09/format-time.json',
't/additional-tests-draft2019-09/formats.json',
't/additional-tests-draft2019-09/id.json',
't/additional-tests-draft2019-09/integers.json',
't/additional-tests-draft2019-09/keyword-independence.json',
't/additional-tests-draft2019-09/loose-types-const-enum.json',
't/additional-tests-draft2019-09/not.json',
't/additional-tests-draft2019-09/recursive-dynamic.json',
't/additional-tests-draft2019-09/ref-and-id.json',
't/additional-tests-draft2019-09/ref.json',
't/additional-tests-draft2019-09/short-circuit.json',
't/additional-tests-draft2019-09/unknownKeyword.json',
't/additional-tests-draft2019-09/vocabulary.json',
't/additional-tests-draft2020-12.t',
't/additional-tests-draft2020-12/README',
't/additional-tests-draft2020-12/anchor.json',
't/additional-tests-draft2020-12/annotation-collection.json',
't/additional-tests-draft2020-12/badRef.json',
't/additional-tests-draft2020-12/dynamicRef.json',
't/additional-tests-draft2020-12/faux-buggy-schemas.json',
't/additional-tests-draft2020-12/format-date-time.json',
't/additional-tests-draft2020-12/format-date.json',
't/additional-tests-draft2020-12/format-duration.json',
't/additional-tests-draft2020-12/format-ipv4.json',
't/additional-tests-draft2020-12/format-ipv6.json',
't/additional-tests-draft2020-12/format-relative-json-pointer.json',
't/additional-tests-draft2020-12/format-time.json',
't/additional-tests-draft2020-12/formats.json',
't/additional-tests-draft2020-12/id.json',
't/additional-tests-draft2020-12/integers.json',
't/additional-tests-draft2020-12/keyword-independence.json',
't/additional-tests-draft2020-12/loose-types-const-enum.json',
't/additional-tests-draft2020-12/not.json',
't/additional-tests-draft2020-12/recursive-dynamic.json',
't/additional-tests-draft2020-12/ref-and-id.json',
't/additional-tests-draft2020-12/ref.json',
't/additional-tests-draft2020-12/short-circuit.json',
't/additional-tests-draft2020-12/unknownKeyword.json',
't/additional-tests-draft2020-12/vocabulary.json',
't/additional-tests-draft4.t',
't/additional-tests-draft4/format-date-time.json',
't/additional-tests-draft4/format-ipv4.json',
't/additional-tests-draft4/format-ipv6.json',
't/additional-tests-draft4/id.json',
't/additional-tests-draft4/integers.json',
't/additional-tests-draft4/type.json',
't/additional-tests-draft7.t',
't/additional-tests-draft7/README',
't/additional-tests-draft7/badRef.json',
't/additional-tests-draft7/faux-buggy-schemas.json',
't/additional-tests-draft7/format-date-time.json',
't/additional-tests-draft7/format-date.json',
't/additional-tests-draft7/format-ipv4.json',
't/additional-tests-draft7/format-relative-json-pointer.json',
't/additional-tests-draft7/format-time.json',
't/additional-tests-draft7/id.json',
't/additional-tests-draft7/integers.json',
't/additional-tests-draft7/keyword-independence.json',
't/additional-tests-draft7/loose-types-const-enum.json',
't/additional-tests-draft7/not-an-anchor.json',
't/additional-tests-draft7/not-an-id.json',
't/additional-tests-draft7/ref-and-id.json',
't/additional-tests-draft7/ref.json',
't/additional-tests-draft7/short-circuit.json',
't/additional-tests-draft7/unknownKeyword.json',
't/additional-tests-draft7/vocabulary.json',
't/annotations.t',
't/boolean-data.t',
't/boolean-schemas.t',
't/cached-metaschemas.t',
't/callbacks.t',
't/checksums.t',
't/content-encoding.t',
't/dialects.t',
't/document.t',
't/equality.t',
't/errors.t',
't/evaluate_json_string.t',
't/find-identifiers.t',
't/formats.t',
't/invalid-schemas.t',
't/invalid-schemas/invalid-input.json',
't/invalid-schemas/ref.json',
't/invalid-schemas/vocabulary.json',
't/lib/Acceptance.pm',
't/lib/Helper.pm',
't/lib/MyVocabulary/BadEvaluationOrder.pm',
't/lib/MyVocabulary/BadVocabularySub1.pm',
't/lib/MyVocabulary/BadVocabularySub2.pm',
't/lib/MyVocabulary/BadVocabularySub3.pm',
't/lib/MyVocabulary/ConflictingKeyword.pm',
't/lib/MyVocabulary/MissingRole.pm',
't/lib/MyVocabulary/MissingSub.pm',
't/lib/MyVocabulary/ReservedKeyword.pm',
't/lib/MyVocabulary/StringComparison.pm',
't/max_traversal_depth.t',
't/multipleOf.t',
't/pattern.t',
't/read_serialized_file',
't/ref.t',
't/result-object.t',
't/results/draft2019-09-acceptance-format.txt',
't/results/draft2019-09-acceptance.txt',
't/results/draft2019-09-additional-tests.txt',
't/results/draft2019-09-invalid-schemas.txt',
't/results/draft2020-12-acceptance-format.txt',
't/results/draft2020-12-acceptance.txt',
't/results/draft2020-12-additional-tests.txt',
't/results/draft2020-12-invalid-schemas.txt',
't/results/draft4-acceptance-format.txt',
't/results/draft4-acceptance.txt',
't/results/draft4-additional-tests.txt',
't/results/draft6-acceptance-format.txt',
't/results/draft6-acceptance.txt',
't/results/draft7-acceptance-format.txt',
't/results/draft7-acceptance.txt',
't/results/draft7-additional-tests.txt',
't/serialization.t',
't/specification_version.t',
't/strict.t',
't/stringy-numbers.t',
't/traverse.t',
't/type.t',
't/unsupported-keywords.t',
't/validate-schema.t',
't/vocabularies.t',
't/zzz-acceptance-draft2019-09-format.t',
't/zzz-acceptance-draft2019-09.t',
't/zzz-acceptance-draft2020-12-format.t',
't/zzz-acceptance-draft2020-12.t',
't/zzz-acceptance-draft4-format.t',
't/zzz-acceptance-draft4.t',
't/zzz-acceptance-draft6-format.t',
't/zzz-acceptance-draft6.t',
't/zzz-acceptance-draft7-format.t',
't/zzz-acceptance-draft7.t',
't/zzz-check-breaks.t',
'xt/author/00-compile.t',
'xt/author/clean-namespaces.t',
'xt/author/distmeta.t',
'xt/author/eol.t',
'xt/author/kwalitee.t',
'xt/author/minimum-version.t',
'xt/author/mojibake.t',
'xt/author/no-tabs.t',
'xt/author/pod-coverage.t',
'xt/author/pod-spell.t',
'xt/author/pod-syntax.t',
'xt/author/portability.t',
'xt/release/changes_has_content.t',
'xt/release/cpan-changes.t'
);
eol_unix_ok($_, { trailing_whitespace => 1 }) foreach @files;
done_testing;
boolean-data.t 100640 000766 000024 11663 15114374332 17402 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Data::Dumper;
use lib 't/lib';
use Helper;
sub serialize { Data::Dumper->new([ $_[0] ])->Indent(0)->Terse(1)->Sortkeys(1)->Dump }
my ($test_schema, $failure_result);
subtest 'strict booleans (default)' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate($_, { type => 'boolean' })->TO_JSON,
{ valid => true },
'in data, '.serialize($_).' is a boolean',
)
foreach (
false,
true,
);
cmp_result(
$js->evaluate($_->[1], { type => 'boolean' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/type',
error => 'got '.$_->[0].', not boolean',
},
],
},
'correct error generated from type for '.serialize($_->[1]),
)
foreach (
[ null => undef ],
[ integer => 0 ],
[ integer => 1 ],
[ string => '0' ],
[ string => '1' ],
[ string => 'false' ],
[ string => 'true' ],
[ 'reference to SCALAR' => \0 ],
[ 'reference to SCALAR' => \1 ],
);
cmp_result(
$js->evaluate(
$_->[1],
$test_schema = {
allOf => [ { type => 'boolean' }, { type => ['boolean','object'] } ],
anyOf => [ { const => false }, { const => true } ],
enum => [ false, true ],
}
)->TO_JSON,
$failure_result = {
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/enum',
error => 'value does not match',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
error => 'got '.$_->[0].', not boolean',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/type',
error => 'got '.$_->[0].', not one of boolean, object',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschemas 0, 1 are not valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/0/const',
error => 'value does not match',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/const',
error => 'value does not match',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
error => 'no subschemas are valid',
},
],
},
'in data, '.serialize($_->[1]).' not is a boolean',
)
foreach (
[ null => undef ],
[ integer => 0 ],
[ integer => 1 ],
[ string => '0' ],
[ string => '1' ],
[ string => 'false' ],
[ string => 'true' ],
[ 'reference to SCALAR' => \0 ],
[ 'reference to SCALAR' => \1 ],
);
};
subtest 'scalarref_booleans = 1' => sub {
my $js = JSON::Schema::Modern->new(scalarref_booleans => 1);
cmp_result(
$js->evaluate($_, $test_schema)->TO_JSON,
{ valid => true },
'in data, '.serialize($_).' is a boolean',
)
foreach (
false,
true,
\0,
\1,
);
cmp_result(
$js->evaluate($_->[1], $test_schema)->TO_JSON,
{
valid => false,
errors => [
do {
my ($type, $value) = $_->@*;
map +{
$_->%*,
$_->{keywordLocation} =~ /\/type$/ ? (error => $_->{error} =~ s/^got .*, not/got $type, not/r) : (),
}, $failure_result->{errors}->@*,
},
],
},
'correct error generated from type for '.serialize($_),
)
foreach (
[ null => undef ],
[ integer => 0 ],
[ integer => 1 ],
[ string => '0' ],
[ string => '1' ],
[ string => 'false' ],
[ string => 'true' ],
);
cmp_result(
$js->evaluate(
[
undef,
0,
1,
'0',
'1',
'false',
'true',
\0,
\1,
],
{ uniqueItems => true },
)->TO_JSON,
{ valid => true },
'items are all considered unique when types differ, even when perl treats them similarly',
);
cmp_result(
$js->evaluate($_, { uniqueItems => true })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/uniqueItems',
error => 'items at indices 0 and 1 are not unique',
},
],
},
'scalarrefs compare as identical to their counterpart booleans',
)
foreach (
[ \0, false ],
[ false, \0 ],
[ \1, true ],
[ true, \1 ],
);
};
done_testing;
vocabularies.t 100640 000766 000024 17342 15114374332 17533 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use builtin::compat 'load_module';
use Mojo::File 'path';
use lib 't/lib';
use Helper;
my $DUMP = shift;
# regenerate this by running the test file with argument '1'
use constant KEYWORDS => {
# draft4 -> http://json-schema.org/draft-04/schema#
'draft4' => {
Core => [qw(
$schema
id
$ref
definitions
)],
Validation => [qw(
type
enum
multipleOf
maximum
exclusiveMaximum
minimum
exclusiveMinimum
maxLength
minLength
pattern
maxItems
minItems
uniqueItems
maxProperties
minProperties
required
)],
FormatAnnotation => [qw(
format
)],
Applicator => [qw(
allOf
anyOf
oneOf
not
dependencies
items
additionalItems
properties
patternProperties
additionalProperties
)],
MetaData => [qw(
title
description
default
)],
},
# draft6 -> http://json-schema.org/draft-06/schema#
'draft6' => {
Core => [qw(
$schema
$id
$ref
definitions
)],
Validation => [qw(
type
enum
const
multipleOf
maximum
exclusiveMaximum
minimum
exclusiveMinimum
maxLength
minLength
pattern
maxItems
minItems
uniqueItems
maxProperties
minProperties
required
)],
FormatAnnotation => [qw(
format
)],
Applicator => [qw(
allOf
anyOf
oneOf
not
dependencies
items
additionalItems
contains
properties
patternProperties
additionalProperties
propertyNames
)],
MetaData => [qw(
title
description
default
examples
)],
},
# draft7 -> http://json-schema.org/draft-07/schema#
'draft7' => {
Core => [qw(
$schema
$id
$ref
definitions
$comment
)],
Validation => [qw(
type
enum
const
multipleOf
maximum
exclusiveMaximum
minimum
exclusiveMinimum
maxLength
minLength
pattern
maxItems
minItems
uniqueItems
maxProperties
minProperties
required
)],
FormatAnnotation => [qw(
format
)],
Applicator => [qw(
allOf
anyOf
oneOf
not
if
then
else
dependencies
items
additionalItems
contains
properties
patternProperties
additionalProperties
propertyNames
)],
Content => [qw(
contentEncoding
contentMediaType
)],
MetaData => [qw(
title
description
default
readOnly
writeOnly
examples
)],
},
# draft2019-09 -> https://json-schema.org/draft/2019-09/schema
'draft2019-09' => {
Core => [qw(
$schema
$id
$anchor
$recursiveAnchor
$ref
$recursiveRef
$vocabulary
$defs
$comment
)],
Validation => [qw(
type
enum
const
multipleOf
maximum
exclusiveMaximum
minimum
exclusiveMinimum
maxLength
minLength
pattern
maxItems
minItems
uniqueItems
maxContains
minContains
maxProperties
minProperties
required
dependentRequired
)],
FormatAnnotation => [qw(
format
)],
Applicator => [qw(
allOf
anyOf
oneOf
not
if
then
else
dependentSchemas
items
additionalItems
contains
properties
patternProperties
additionalProperties
propertyNames
unevaluatedItems
unevaluatedProperties
)],
Content => [qw(
contentEncoding
contentMediaType
contentSchema
)],
MetaData => [qw(
title
description
default
deprecated
readOnly
writeOnly
examples
)],
},
# draft2020-12 -> https://json-schema.org/draft/2020-12/schema
'draft2020-12' => {
Core => [qw(
$schema
$id
$anchor
$dynamicAnchor
$ref
$dynamicRef
$vocabulary
$defs
$comment
)],
Validation => [qw(
type
enum
const
multipleOf
maximum
exclusiveMaximum
minimum
exclusiveMinimum
maxLength
minLength
pattern
maxItems
minItems
uniqueItems
maxContains
minContains
maxProperties
minProperties
required
dependentRequired
)],
FormatAnnotation => [qw(
format
)],
Applicator => [qw(
allOf
anyOf
oneOf
not
if
then
else
dependentSchemas
prefixItems
items
contains
properties
patternProperties
additionalProperties
propertyNames
)],
Content => [qw(
contentEncoding
contentMediaType
contentSchema
)],
MetaData => [qw(
title
description
default
deprecated
readOnly
writeOnly
examples
)],
Unevaluated => [qw(
unevaluatedItems
unevaluatedProperties
)],
},
};
subtest 'valid keywords' => sub {
if ($DUMP) {
my $js = JSON::Schema::Modern->new;
print STDERR "{\n";
foreach my $spec_version (sort { length($a) <=> length($b) || $a cmp $b } $js->SPECIFICATION_VERSIONS_SUPPORTED->@*) {
# specification_version -> metaschema uri
my $metaschema_uri = $js->METASCHEMA_URIS->{$spec_version};
print STDERR " # $spec_version -> $metaschema_uri\n";
# metaschema uri -> vocab list: [ specification_version, [ vocab classes ] ]
foreach my $metaschema_info ($js->_get_metaschema_vocabulary_classes($metaschema_uri)) {
print STDERR " '$spec_version' => {\n";
foreach my $class (sort $metaschema_info->[1]->@*) {
my ($short_class) = $class =~ /::([^:]+)$/;
print STDERR " $short_class => [qw(\n";
print STDERR " $_\n" foreach $class->keywords($spec_version);
print STDERR " )],\n";
}
print STDERR " },\n";
}
}
print STDERR "};\n\n";
pass('table dumped');
return;
}
my @classes =
grep load_module($_)->does('JSON::Schema::Modern::Vocabulary'),
map 'JSON::Schema::Modern::Vocabulary::'.$_,
map $_->basename =~ s/\.pm$//r,
grep /\.pm$/,
path('lib/JSON/Schema/Modern/Vocabulary/')->list->each;
my $table = {
map {
my $spec_version = $_;
$spec_version => {
map {
my $class = $_;
my @keywords = eval { $class->keywords($spec_version) };
@keywords ? (($class =~ /::([^:]+)$/) => \@keywords) : ();
} @classes,
};
}
JSON::Schema::Modern->SPECIFICATION_VERSIONS_SUPPORTED->@*
};
foreach my $spec_version (sort { length($a) <=> length($b) || $a cmp $b } keys KEYWORDS->%*) {
foreach my $short_class (sort keys KEYWORDS->{$spec_version}->%*) {
my $class = 'JSON::Schema::Modern::Vocabulary::'.$short_class;
cmp_result(
[ $class->keywords($spec_version) ],
KEYWORDS->{$spec_version}{$short_class},
"$spec_version, $short_class: calculated keyword list matches hardcoded table",
);
}
}
};
done_testing;
result-object.t 100640 000766 000024 46056 15114374332 17642 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use builtin::compat 'refaddr';
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new(short_circuit => 0, collect_annotations => 1);
is($js->output_format, 'basic', 'output_format defaults to basic');
my $result = $js->evaluate(
{ alpha => 1, beta => 1, foo => 1, gamma => [ 0, 1 ], theta => [ 1 ], zulu => 2 },
{
required => [ 'bar' ],
allOf => [
{ type => 'number' },
{ oneOf => [ { type => 'number' } ] },
{ oneOf => [ true, true ] },
],
anyOf => [ { type => 'number' }, { if => true, then => { type => 'array' }, else => false } ],
if => false, then => false, else => { type => 'number' },
not => true,
properties => {
alpha => false,
beta => { multipleOf => 2 },
gamma => {
prefixItems => [ false ],
items => false,
unevaluatedItems => false,
},
theta => { items => false },
},
patternProperties => { 'o' => false },
additionalProperties => false,
unevaluatedProperties => false,
propertyNames => { pattern => '[ao]' },
},
);
is($result->output_format, 'basic', 'Result object gets the output_format from the evaluator');
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/required',
error => 'object is missing property: bar',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
error => 'got object, not number',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/oneOf/0/type',
error => 'got object, not number',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/oneOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf/2/oneOf',
error => 'multiple subschemas are valid: 0, 1',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschemas 0, 1, 2 are not valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/0/type',
error => 'got object, not number',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/then/type',
error => 'got object, not array',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/then',
error => 'subschema is not valid',
},
{
instanceLocation => '',
keywordLocation => '/anyOf',
error => 'no subschemas are valid',
},
{
instanceLocation => '',
keywordLocation => '/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/else/type',
error => 'got object, not number',
},
{
instanceLocation => '',
keywordLocation => '/else',
error => 'subschema is not valid',
},
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha',
error => 'property not permitted',
},
{
instanceLocation => '/beta',
keywordLocation => '/properties/beta/multipleOf',
error => 'value is not a multiple of 2',
},
{
instanceLocation => '/gamma/0',
keywordLocation => '/properties/gamma/prefixItems/0',
error => 'item not permitted',
},
{
instanceLocation => '/gamma',
keywordLocation => '/properties/gamma/prefixItems',
error => 'not all items are valid',
},
{
instanceLocation => '/gamma/1',
keywordLocation => '/properties/gamma/items',
error => 'additional item not permitted',
},
{
instanceLocation => '/gamma',
keywordLocation => '/properties/gamma/items',
error => 'subschema is not valid against all items',
},
{
instanceLocation => '/theta/0',
keywordLocation => '/properties/theta/items',
error => 'item not permitted',
},
{
instanceLocation => '/theta',
keywordLocation => '/properties/theta/items',
error => 'subschema is not valid against all items',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '/foo',
keywordLocation => '/patternProperties/o',
error => 'property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/patternProperties',
error => 'not all properties are valid',
},
{
instanceLocation => '/zulu',
keywordLocation => '/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '/zulu',
keywordLocation => '/propertyNames/pattern',
error => 'pattern does not match',
},
{
instanceLocation => '',
keywordLocation => '/propertyNames',
error => 'not all property names are valid',
},
],
},
'basic format includes all errors linearly',
);
$result->output_format('flag');
cmp_result(
$result->TO_JSON,
{
valid => false,
},
'flag format only includes the valid property',
);
$result->output_format('terse');
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/required',
error => 'object is missing property: bar',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
error => 'got object, not number',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/oneOf/0/type',
error => 'got object, not number',
},
# - "summary" error from /allOf/1/oneOf is omitted
{
instanceLocation => '',
keywordLocation => '/allOf/2/oneOf',
error => 'multiple subschemas are valid: 0, 1',
},
# - "summary" error from /allOf is omitted
{
instanceLocation => '',
keywordLocation => '/anyOf/0/type',
error => 'got object, not number',
},
{
instanceLocation => '',
keywordLocation => '/anyOf/1/then/type',
error => 'got object, not array',
},
# - "summary" error from /anyOf/1/then is omitted
# - "summary" error from /anyOf is omitted
{
instanceLocation => '',
keywordLocation => '/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/else/type',
error => 'got object, not number',
},
# - "summary" error from /else is omitted
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha',
error => 'property not permitted',
},
{
instanceLocation => '/beta',
keywordLocation => '/properties/beta/multipleOf',
error => 'value is not a multiple of 2',
},
{
instanceLocation => '/gamma/0',
keywordLocation => '/properties/gamma/prefixItems/0',
error => 'item not permitted',
},
# - "summary" error from /properties/gamma/prefixItems is omitted
{
instanceLocation => '/gamma/1',
keywordLocation => '/properties/gamma/items',
error => 'additional item not permitted',
},
# - "summary" error from /properties/gamma/items is omitted
# - "summary" error from /properties/gamma/unevaluatedItems is omitted
{
instanceLocation => '/theta/0',
keywordLocation => '/properties/theta/items',
error => 'item not permitted',
},
# - "summary" error from /properties/theta/items is omitted
# - "summary" error from /properties is omitted
{
instanceLocation => '/foo',
keywordLocation => '/patternProperties/o',
error => 'property not permitted',
},
# - "summary" error from /patternProperties is omitted
{
instanceLocation => '/zulu',
keywordLocation => '/additionalProperties',
error => 'additional property not permitted',
},
# - "summary" error from /additionalProperties is omitted
# - "summary" error from /unevaluatedProperties is omitted
{
instanceLocation => '/zulu',
keywordLocation => '/propertyNames/pattern',
error => 'pattern does not match',
},
# - "summary" error from /propertyNames is omitted
],
},
'terse format omits errors from redundant applicator keywords',
);
$js = JSON::Schema::Modern->new(validate_formats => 1);
{
$result = $js->evaluate(
'foo',
{ format => 'uuid'},
);
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '',
keywordLocation => '/format',
error => 'not a valid uuid string',
},
],
},
'basic format includes all errors linearly',
);
$result->output_format('terse');
cmp_result(
$result->TO_JSON,
{
valid => false,
errors => $errors,
},
'terse format does not omit these crucial errors',
);
}
subtest 'strict_basic' => sub {
# see "JSON pointer escaping" in t/errors.t
cmp_result(
JSON::Schema::Modern->new(specification_version => 'draft2019-09', output_format => 'strict_basic')->evaluate(
{ '{}' => { 'my~tilde/slash-property' => 1 } },
{
'$id' => 'foo.json',
properties => {
'{}' => {
properties => {
'my~tilde/slash-property' => false,
},
patternProperties => {
'/' => { minimum => 6 },
'[~/]' => { minimum => 7 },
'~' => { minimum => 5 },
'~.*/' => false,
},
},
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '#/%7B%7D/my~0tilde~1slash-property',
keywordLocation => '#/properties/%7B%7D/properties/my~0tilde~1slash-property',
absoluteKeywordLocation => 'foo.json#/properties/%7B%7D/properties/my~0tilde~1slash-property',
error => 'property not permitted',
},
{
instanceLocation => '#/%7B%7D',
keywordLocation => '#/properties/%7B%7D/properties',
absoluteKeywordLocation => 'foo.json#/properties/%7B%7D/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '#/%7B%7D/my~0tilde~1slash-property',
keywordLocation => '#/properties/%7B%7D/patternProperties/~1/minimum', # /
absoluteKeywordLocation => 'foo.json#/properties/%7B%7D/patternProperties/~1/minimum', # /
error => 'value is less than 6',
},
{
instanceLocation => '#/%7B%7D/my~0tilde~1slash-property',
keywordLocation => '#/properties/%7B%7D/patternProperties/%5B~0~1%5D/minimum', # [~/]
absoluteKeywordLocation => 'foo.json#/properties/%7B%7D/patternProperties/%5B~0~1%5D/minimum', # [~/]
error => 'value is less than 7',
},
{
instanceLocation => '#/%7B%7D/my~0tilde~1slash-property',
keywordLocation => '#/properties/%7B%7D/patternProperties/~0/minimum', # ~
absoluteKeywordLocation => 'foo.json#/properties/%7B%7D/patternProperties/~0/minimum', # ~
error => 'value is less than 5',
},
{
instanceLocation => '#/%7B%7D/my~0tilde~1slash-property',
keywordLocation => '#/properties/%7B%7D/patternProperties/~0.*~1', # ~.*/
absoluteKeywordLocation => 'foo.json#/properties/%7B%7D/patternProperties/~0.*~1', # ~.*/
error => 'property not permitted',
},
{
instanceLocation => '#/%7B%7D',
keywordLocation => '#/properties/%7B%7D/patternProperties',
absoluteKeywordLocation => 'foo.json#/properties/%7B%7D/patternProperties',
error => 'not all properties are valid',
},
{
instanceLocation => '#',
keywordLocation => '#/properties',
absoluteKeywordLocation => 'foo.json#/properties',
error => 'not all properties are valid',
},
],
},
'strict_basic turns json pointers into URIs, including uri escapes',
);
};
subtest 'AND two result objects together' => sub {
my @results = map {
my $count = $_;
my $valid = $count % 2;
JSON::Schema::Modern::Result->new(
valid => $valid,
($valid ? 'annotations' : 'errors') => [
map ${\ ('JSON::Schema::Modern::'.($valid ? 'Annotation' : 'Error'))}->new(
depth => 0,
mode => 'evaluate',
keyword => 'keyword '.$count.'-'.$_,
instance_location => '/instance_location/'.$count.'-'.$_,
keyword_location => '/keyword_location/'.$count.'-'.$_,
$valid ? (annotation => 'annotation '.$count.'-'.$_) : (error => 'error '.$count.'-'.$_),
), 0..1
],
)
} 0..3;
cmp_result(
(my $one_true = $results[0] & $results[1]),
all(
methods(valid => bool(0)),
listmethods(
errors => [
map methods(TO_JSON => {
instanceLocation => '/instance_location/0-'.$_,
keywordLocation => '/keyword_location/0-'.$_,
error => 'error 0-'.$_,
}), 0..1
],
annotations => [
map methods(TO_JSON => {
instanceLocation => '/instance_location/1-'.$_,
keywordLocation => '/keyword_location/1-'.$_,
annotation => 'annotation 1-'.$_,
}), 0..1
],
),
),
'ANDing true and false results = invalid, but errors and annotations both preserved',
);
cmp_result(
(my $both_true = $results[1] & $results[3]),
all(
methods(valid => bool(1)),
listmethods(
annotations => [
map {
my $count = $_;
map methods(TO_JSON => {
instanceLocation => '/instance_location/'.$count.'-'.$_,
keywordLocation => '/keyword_location/'.$count.'-'.$_,
annotation => 'annotation '.$count.'-'.$_,
}), 0..1
} 1,3
],
),
),
'ANDing two true results = valid',
);
cmp_result(
(my $both_false = $results[0] & $results[2]),
all(
methods(valid => bool(0)),
listmethods(
errors => [
map {
my $count = $_;
map methods(TO_JSON => {
instanceLocation => '/instance_location/'.$count.'-'.$_,
keywordLocation => '/keyword_location/'.$count.'-'.$_,
error => 'error '.$count.'-'.$_,
}), 0..1
} 0,2
],
),
),
'ANDing two false results = invalid',
);
like(
dies { $results[0] & 0 },
qr/wrong type for \& operation/,
'only Result objects can be processed',
);
is(
refaddr(my $itself = $results[0] & $results[0]),
refaddr($results[0]),
'ANDing a result with itself is a no-op',
);
};
subtest annotations => sub {
my %args = (
valid => 1,
annotations => [
JSON::Schema::Modern::Annotation->new(
depth => 0,
keyword => 'foo',
instance_location => '/instance_location',
keyword_location => '/keyword_location ',
annotation => 'annotation',
)
],
);
cmp_result(
JSON::Schema::Modern::Result->new(%args)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/instance_location',
keywordLocation => '/keyword_location ',
annotation => 'annotation',
},
],
},
'by default, annotations are included in the formatted output',
);
cmp_result(
JSON::Schema::Modern::Result->new(%args, formatted_annotations => 0)->TO_JSON,
{ valid => true },
'but inclusion can be disabled',
);
};
subtest 'data_only' => sub {
my $result = JSON::Schema::Modern::Result->new(
valid => 0,
errors => [
JSON::Schema::Modern::Error->new(
depth => 1,
mode => 'evaluate',
keyword => 'hello',
instance_location => '/foo/bar',
keyword_location => '/allOf/0/hello',
error => 'schema is invalid',
),
JSON::Schema::Modern::Error->new(
depth => 1,
mode => 'evaluate',
keyword => 'goodbye',
instance_location => '/foo/bar',
keyword_location => '/allOf/1/goodbye',
error => 'schema is invalid',
),
JSON::Schema::Modern::Error->new(
depth => 0,
mode => 'evaluate',
keyword => 'allOf',
instance_location => '/foo/bar',
keyword_location => '/allOf',
error => 'subschemas 0, 1 are not valid',
),
],
);
is(
$result->format('data_only'),
"'/foo/bar': schema is invalid\n'/foo/bar': subschemas 0, 1 are not valid",
'data_only format outputs a string of data locations only, with duplicates removed',
);
is(
JSON::Schema::Modern::Result->new(
valid => 0,
errors => [
map JSON::Schema::Modern::Error->new(
do { my $e = $_; map +($_ => $e->$_), qw(depth keyword instance_location keyword_location error) },
mode => 'traverse',
), $result->errors
],
)->format('data_only'),
"'/allOf/0/hello': schema is invalid\n'/allOf/1/goodbye': schema is invalid\n'/allOf': subschemas 0, 1 are not valid",
'data_only format uses keyword locations when result came from traverse',
);
};
subtest 'construction errors' => sub {
my $error = JSON::Schema::Modern::Error->new(
error => 'oh no!',
mode => 'evaluate',
depth => 1,
keyword => 'me',
instance_location => '',
keyword_location => '',
);
like(
dies { JSON::Schema::Modern::Result->new(valid => true, errors => [$error]) },
qr/^inconsistent inputs: errors is not empty but valid is true/,
'valid results must not have errors',
);
like(
dies { JSON::Schema::Modern::Result->new(valid => false, errors => []) },
qr/^inconsistent inputs: errors is empty but valid is false/,
'invalid results must have errors',
);
ok(
lives { JSON::Schema::Modern::Result->new(valid => true, errors => []) },
'no errors when valid is true and errors is empty',
);
ok(
lives { JSON::Schema::Modern::Result->new(valid => false, errors => [$error]) },
'no errors when valid is false and errors is not empty',
);
};
done_testing;
serialization.t 100640 000766 000024 12474 15114374332 17732 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
use Test::Needs qw(Sereal::Encoder Sereal::Decoder);
use Test2::Warnings qw(:no_end_test had_no_warnings);
my $js = JSON::Schema::Modern->new(
collect_annotations => 1,
scalarref_booleans => 1,
stringy_numbers => 1,
strict => 0,
max_traversal_depth => 42,
specification_version => 'draft2019-09',
);
my $metaschema = {
'$id' => 'https://my_metaschema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true,
},
};
my $schema = {
'$id' => 'https://my_schema',
'$schema' => 'https://my_metaschema',
type => 'number',
format => 'ipv4',
unknown => 1,
properties => { hello => false },
contentMediaType => 'application/json',
contentSchema => {},
};
$js->add_schema($metaschema);
$js->add_schema($schema);
cmp_result(
$js->evaluate($schema, {})->TO_JSON,
{ valid => true },
'evaluated against an empty schema',
);
cmp_result(
$js->evaluate(1, 'https://my_schema')->TO_JSON,
my $result = {
valid => true,
annotations => [
map +{
instanceLocation => '',
keywordLocation => '/'.$_,
absoluteKeywordLocation => 'https://my_schema#/'.$_,
annotation => $schema->{$_},
}, 'format', sort qw(type unknown properties contentMediaType contentSchema),
],
},
'evaluate data against schema with custom dialect; format and unknown keywords are collected as annotations',
);
cmp_result(
$js->evaluate('foo', 'https://my_schema')->TO_JSON,
$result,
'evaluate data against schema with custom dialect; format-annotation is used',
);
my @serialized_attributes = sort qw(
specification_version
output_format
short_circuit
max_traversal_depth
validate_formats
validate_content_schemas
collect_annotations
scalarref_booleans
stringy_numbers
strict
_resource_index
_vocabulary_classes
_metaschema_vocabulary_classes
);
my $frozen = $js->FREEZE(undef);
cmp_result(
[ sort keys %$frozen ],
[ sort @serialized_attributes ],
'frozen object contains all the right keys',
);
my $thawed = JSON::Schema::Modern->THAW(undef, $frozen);
cmp_result(
[ sort keys %$thawed ],
[ sort @serialized_attributes ],
'thawed object contains all the right keys',
);
$frozen = Sereal::Encoder->new({ freeze_callbacks => 1 })->encode($js);
Sereal::Decoder->new->decode($frozen, $thawed);
cmp_result(
$thawed->evaluate($schema, {})->TO_JSON,
{ valid => true },
'evaluate again against an empty schema',
);
cmp_result(
$thawed->evaluate('hi', 'https://my_schema')->TO_JSON,
{
valid => true,
annotations => [
map +{
instanceLocation => '',
keywordLocation => '/'.$_,
absoluteKeywordLocation => 'https://my_schema#/'.$_,
annotation => $schema->{$_},
}, 'format', sort qw(type unknown properties contentMediaType contentSchema),
],
},
'in thawed object, evaluate data against schema with custom dialect; format and unknown keywords are collected as annotations',
);
ok(
lives {
my $js = JSON::Schema::Modern->new;
my $document = $js->_fetch_from_uri('https://json-schema.org/draft/2019-09/schema')->{document};
my $frozen = Sereal::Encoder->new({ freeze_callbacks => 1 })->encode($document);
my $thawed = Sereal::Decoder->new->decode($frozen);
},
'can successfully freeze and thaw a document that resides in the global cache',
);
$frozen = Sereal::Encoder->new({ freeze_callbacks => 1 })->encode($js);
$thawed = Sereal::Decoder->new->decode($frozen);
cmp_result(
$thawed->evaluate($schema, {})->TO_JSON,
{ valid => true },
'evaluate again against an empty schema',
);
ok($thawed->_get_vocabulary_class('https://json-schema.org/draft/2020-12/vocab/core'), 'core vocabulary_class for a different spec version works in a thawed object');
ok($thawed->_get_vocabulary_class('https://json-schema.org/draft/2020-12/vocab/format-assertion'), 'format-assertion vocabulary_class works in a thawed object');
ok($thawed->_get_metaschema_vocabulary_classes('https://json-schema.org/draft/2020-12/schema'), 'metaschema_vocabulary_classes works in a thawed object');
ok($thawed->get_media_type('application/json'), 'media_type works in a thawed object');
ok($thawed->get_encoding('base64'), 'encoding works in a thawed object');
# now try to thaw the file in a new process and run some more tests
if ("$]" >= '5.022' or $^O ne 'MSWin32') {
open my $child_in, '|-:raw', $^X, (-d 'blib' ? '-Mblib' : '-Ilib'), 't/read_serialized_file';
print $child_in $frozen;
close $child_in;
my $hub = Test2::API::test2_stack->top;
$hub->set_count($hub->count + ($ENV{AUTHOR_TESTING} ? 2 : 1));
is($? >> 8, 0, 'child process finished successfully');
}
had_no_warnings() if $ENV{AUTHOR_TESTING};
done_testing;
boolean-schemas.t 100640 000766 000024 4621 15114374332 20070 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new;
my @tests = (
{ schema => false, valid => false, exception => 0 },
{ schema => true, valid => true, exception => 0 },
{ schema => JSON::PP::false, valid => false, exception => 0 },
{ schema => JSON::PP::true, valid => true, exception => 0 },
{ schema => {}, valid => true, exception => 0 },
{ schema => 0, valid => false, exception => 1 },
{ schema => 1, valid => false, exception => 1 },
{ schema => \0, valid => false, exception => 1 },
{ schema => \1, valid => false, exception => 1 },
);
foreach my $test (@tests) {
my $data = 'hello';
is(
dies {
my $result = $js->evaluate($data, $test->{schema});
ok(!($result->exception xor $test->{exception}), json_sprintf('%s is not a schema', $test->{schema}));
ok(!($result->valid xor $test->{valid}), json_sprintf('schema: %s evaluates to: %s', $test->{schema}, $test->{valid}));
cmp_result(
$result->TO_JSON,
{
valid => $test->{valid},
$test->{valid} ? () : (errors => supersetof()),
},
'invalid result structure looks correct',
);
},
undef,
'no exceptions in evaluate',
);
}
cmp_result(
$js->evaluate('hello', [])->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'invalid schema type: array',
},
],
},
'invalid schema type results in error',
);
$js = JSON::Schema::Modern->new(scalarref_booleans => 1);
cmp_result(
$js->evaluate('hello', \0)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => 'invalid schema type: reference to SCALAR',
},
],
},
'scalarref for schema results in error, even when scalarref_booleans is true',
);
done_testing;
invalid-schemas.t 100640 000766 000024 2164 15114374332 20077 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
use Acceptance;
foreach my $version (qw(draft2019-09 draft2020-12)) {
acceptance_tests(
acceptance => {
specification => $version,
test_dir => 't/invalid-schemas',
include_optional => 0,
test_schemas => 0,
},
evaluator => {
specification_version => $version,
validate_formats => 1,
collect_annotations => 0,
},
output_file => $version.'-invalid-schemas.txt',
);
}
done_testing;
__END__
see results in
t/results/draft2019-09-invalid-schemas.txt
t/results/draft2020-12-invalid-schemas.txt
Acceptance.pm 100640 000766 000024 11633 15114374332 20016 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t/lib # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Safe::Isa;
use Feature::Compat::Try;
use Mojo::File 'path';
use builtin::compat 'blessed';
use if $ENV{AUTHOR_TESTING}, 'Test2::Warnings' => ':fail_on_warning'; # hooks into done_testing unless overridden
use Test::JSON::Schema::Acceptance 1.035;
use Test::Memory::Cycle;
use JSON::Schema::Modern;
# supports options:
# - acceptance: options passed to Test::JSON::Schema::Acceptance constructor
# - evaluator: options passed to JSON::Schema::Modern constructor
# - tests: options passed to Test::JSON::Schema::Acceptance::acceptance method
# - output_file: filename to print results to (default: none)
sub acceptance_tests (%options) {
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $note = $ENV{AUTHOR_TESTING} || $ENV{AUTOMATED_TESTING} ? \&diag : \¬e;
$note->('');
foreach my $env (qw(AUTHOR_TESTING AUTOMATED_TESTING EXTENDED_TESTING NO_TODO TEST_DIR NO_SHORT_CIRCUIT)) {
$note->($env.': '.($ENV{$env} // ''));
}
$note->('');
my $accepter = Test::JSON::Schema::Acceptance->new(
include_optional => 1,
verbose => $ENV{AUTOMATED_TESTING},
test_schemas => $ENV{AUTHOR_TESTING},
$options{acceptance}->%*,
$ENV{TEST_DIR} ? (test_dir => $ENV{TEST_DIR})
: $ENV{TEST_PREFIXDIR} ? (test_dir => path($ENV{TEST_PREFIXDIR}, 'tests', $options{acceptance}{specification})) : (),
supported_specifications => [ qw(draft4 draft6 draft7 draft2019-09 draft2020-12) ],
);
$accepter = $accepter->new(%$accepter,
test_dir => $accepter->test_dir->child($options{acceptance}{test_subdir}))
if not $ENV{TEST_DIR} and $options{acceptance}{test_subdir};
$note->('Using JSON decoder: ', blessed($accepter->_json_serializer), ' ', $accepter->_json_serializer->VERSION);
$note->('');
my $js = JSON::Schema::Modern->new($options{evaluator}->%*);
my $js_short_circuit = $ENV{NO_SHORT_CIRCUIT} || JSON::Schema::Modern->new($options{evaluator}->%*, short_circuit => 1);
my $add_resource = sub ($uri, $schema, %resource_options) {
return if $uri =~ m{/draft-next/};
return if $uri =~ m{/v1/};
try {
my $doc = my $document = JSON::Schema::Modern::Document->new(
schema => $schema,
evaluator => $js,
%resource_options,
);
$js->add_document($uri => $doc);
$js_short_circuit->add_document($uri => $doc) if not $ENV{NO_SHORT_CIRCUIT};
}
catch ($e) {
die $e->$_isa('JSON::Schema::Modern::Result') ? $e->dump : $e;
}
};
$accepter->acceptance(
validate_data => sub ($schema, $instance_data) {
my $result = $js->evaluate($instance_data, $schema);
my $result_short = $ENV{NO_SHORT_CIRCUIT} || $js_short_circuit->evaluate($instance_data, $schema);
die 'result is not a JSON::Schema::Modern::Result object'
if not $result->isa('JSON::Schema::Modern::Result');
note 'result: ', $result->dump;
if (not $ENV{NO_SHORT_CIRCUIT}) {
die 'short-circuited result is not a JSON::Schema::Modern::Result object'
if not $result_short->isa('JSON::Schema::Modern::Result');
note 'short-circuited result: ', $result_short->dump;
die 'results inconsistent between short_circuit = false and true'
if ($result->valid xor $result_short->valid);
}
my $in_todo;
# if any errors contain an exception, generate a warning so we can be sure
# to count that as a failure (an exception would be caught and perhaps TODO'd).
# (This might change if tests are added that are expected to produce exceptions.)
foreach my $r ($result, ($ENV{NO_SHORT_CIRCUIT} ? () : $result_short)) {
diag 'evaluation generated an exception: '.$_->dump
foreach
grep +($_->{error} =~ /^EXCEPTION/
&& $_->{error} !~ /(max|min)imum value is not a number$/) # optional/bignum.json
&& !($in_todo //= grep $_->{todo}, Test2::API::test2_stack->top->{_pre_filters}->@*),
$r->errors;
}
($result->valid, $result->TO_JSON);
},
add_resource => $add_resource,
@ARGV ? (tests => { file => \@ARGV }) : (),
($options{test} // {})->%*,
);
memory_cycle_ok($js, 'no leaks in the main evaluator object');
memory_cycle_ok($js_short_circuit, 'no leaks in the short-circuiting evaluator object')
if not $ENV{NO_SHORT_CIRCUIT};
path('t/results/'.$options{output_file})->spew($accepter->results_text, 'UTF-8')
if $ENV{AUTHOR_TESTING};
}
1;
stringy-numbers.t 100640 000766 000024 13451 15114374332 20221 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
foreach my $config (0, 1) {
note 'stringy_numbers = '.$config;
my $js = JSON::Schema::Modern->new(stringy_numbers => $config);
cmp_result(
$js->evaluate(1, { $_ => '1' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/'.$_,
error => $_.' value is not a number',
}
],
},
'strings cannot be used in place of numbers in schema for '.$_,
) foreach qw(multipleOf maximum exclusiveMaximum minimum exclusiveMinimum);
my $schema = {
allOf => [
{ type => 'string' },
{ type => 'number' },
{ type => 'integer' },
{ type => [ 'object', 'number' ] },
{ type => [ 'object', 'integer' ] },
],
};
cmp_result(
$js->evaluate('blah', $schema)->TO_JSON,
{
valid => false,
errors => my $errors = [
{
instanceLocation => '',
keywordLocation => '/allOf/1/type',
error => 'got string, not number',
},
{
instanceLocation => '',
keywordLocation => '/allOf/2/type',
error => 'got string, not integer',
},
{
instanceLocation => '',
keywordLocation => '/allOf/3/type',
error => 'got string, not one of object, number',
},
{
instanceLocation => '',
keywordLocation => '/allOf/4/type',
error => 'got string, not one of object, integer',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschemas 1, 2, 3, 4 are not valid',
},
],
},
'strings that do not look like numbers are never valid as numbers',
);
cmp_result(
$js->evaluate('1.1', $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'by default "type": "string" does not accept numbers',
) if not $config;
cmp_result(
$js->evaluate('1.1', $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/2/type',
error => 'got string, not integer',
},
{
instanceLocation => '',
keywordLocation => '/allOf/4/type',
error => 'got string, not one of object, integer',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschemas 2, 4 are not valid',
},
],
},
'using stringy numbers, numeric strings are treated as numbers but are still not always integers',
) if $config;
$schema = {
maximum => 5,
exclusiveMaximum => 5,
minimum => 15,
exclusiveMinimum => 15,
allOf => [
{ multipleOf => 2 },
{ multipleOf => 0.3 },
],
};
$errors = [
{
instanceLocation => '',
keywordLocation => '/maximum',
error => 'value is greater than 5',
},
{
instanceLocation => '',
keywordLocation => '/exclusiveMaximum',
error => 'value is greater than or equal to 5',
},
{
instanceLocation => '',
keywordLocation => '/minimum',
error => 'value is less than 15',
},
{
instanceLocation => '',
keywordLocation => '/exclusiveMinimum',
error => 'value is less than or equal to 15',
},
{
instanceLocation => '',
keywordLocation => '/allOf/0/multipleOf',
error => 'value is not a multiple of 2',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/multipleOf',
error => 'value is not a multiple of 0.3',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschemas 0, 1 are not valid',
},
];
my $data = 11e0;
cmp_result(
$js->evaluate($data, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'real numbers are always evaluated',
);
$data = '11e0';
cmp_result(
$js->evaluate($data, $schema)->TO_JSON,
{ valid => true },
'by default, stringy numbers are not evaluated by numeric keywords',
) if $config == 0;
cmp_result(
$js->evaluate($data, $schema)->TO_JSON,
{
valid => false,
errors => $errors,
},
'with the config enabled, stringy numbers are treated as numbers by numeric keywords',
) if $config == 1;
is(JSON::Schema::Modern::Utilities::get_type($data), 'string', 'data was not mutated');
$schema = {
enum => [11, 12],
const => 11,
};
cmp_result(
$js->evaluate($data, $schema)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/enum',
error => 'value does not match',
},
{
instanceLocation => '',
keywordLocation => '/const',
error => 'value does not match',
},
],
},
'by default, stringy numbers are not the same as numbers using comparison keywords',
) if $config == 0;
cmp_result(
$js->evaluate($data, $schema)->TO_JSON,
{ valid => true },
'with the config enabled, stringy numbers are the same as numbers using comparison keywords',
) if $config == 1;
is(JSON::Schema::Modern::Utilities::get_type($data), 'string', 'data was not mutated');
}
done_testing;
validate-schema.t 100640 000766 000024 5560 15114374332 20062 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->validate_schema({ type => 'bloop' })->TO_JSON,
{
valid => false,
errors => supersetof(
{
instanceLocation => '/type',
keywordLocation => re(qr{/enum$}),
absoluteKeywordLocation => 'https://json-schema.org/draft/2020-12/meta/validation#/$defs/simpleTypes/enum',
error => 'value does not match',
},
),
},
'validate_schema on simple schema with no $schema keyword',
);
cmp_result(
$js->validate_schema({
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
type => 'bloop',
})->TO_JSON,
{
valid => false,
errors => supersetof(
{
instanceLocation => '/type',
keywordLocation => re(qr{/enum$}),
absoluteKeywordLocation => 'https://json-schema.org/draft/2019-09/meta/validation#/$defs/simpleTypes/enum',
error => 'value does not match',
},
),
},
'validate_schema on schema with metaschema $schema keyword',
);
$js->add_schema('http://example.com/myschema', { '$id' => 'http://example.com/myschema', type => 'boolean' });
cmp_result(
$js->validate_schema({
'$schema' => 'http://example.com/myschema',
type => 'bloop',
})->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/type',
absoluteKeywordLocation => 'http://example.com/myschema#/type',
error => 'got object, not boolean',
},
],
},
'validate_schema with custom metaschema',
);
cmp_result(
$js->validate_schema({
'$id' => '#/$defs/foo',
'$schema' => 'http://json-schema.org/draft-07/schema#',
})->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$id',
error => '$id value "#/$defs/foo" does not match required syntax',
},
],
},
'validate_schema with schema that validates against the metaschema, but fails in extra traverse checks',
);
cmp_result(
$js->validate_schema({
'id' => 'foo',
'$schema' => 'http://json-schema.org/draft-04/schema#',
allOf => [ { '$ref' => '/some-location' } ],
}, { strict => 1 })->TO_JSON,
{ valid => true },
'draft4 schemas can be validated, even though there is no representation for $ref in the metaschema',
);
done_testing;
no-tabs.t 100644 000766 000024 17735 15114374332 20123 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author use strict;
use warnings;
# this test was generated with Dist::Zilla::Plugin::Test::NoTabs 0.15
use Test::More 0.88;
use Test::NoTabs;
my @files = (
'lib/JSON/Schema/Modern.pm',
'lib/JSON/Schema/Modern/Annotation.pm',
'lib/JSON/Schema/Modern/Document.pm',
'lib/JSON/Schema/Modern/Error.pm',
'lib/JSON/Schema/Modern/Result.pm',
'lib/JSON/Schema/Modern/ResultNode.pm',
'lib/JSON/Schema/Modern/Utilities.pm',
'lib/JSON/Schema/Modern/Vocabulary.pm',
'lib/JSON/Schema/Modern/Vocabulary/Applicator.pm',
'lib/JSON/Schema/Modern/Vocabulary/Content.pm',
'lib/JSON/Schema/Modern/Vocabulary/Core.pm',
'lib/JSON/Schema/Modern/Vocabulary/FormatAnnotation.pm',
'lib/JSON/Schema/Modern/Vocabulary/FormatAssertion.pm',
'lib/JSON/Schema/Modern/Vocabulary/MetaData.pm',
'lib/JSON/Schema/Modern/Vocabulary/Unevaluated.pm',
'lib/JSON/Schema/Modern/Vocabulary/Validation.pm',
'script/json-schema-eval',
't/00-report-prereqs.dd',
't/00-report-prereqs.t',
't/add-schema.t',
't/additional-tests-draft2019-09.t',
't/additional-tests-draft2019-09/README',
't/additional-tests-draft2019-09/anchor.json',
't/additional-tests-draft2019-09/annotation-collection.json',
't/additional-tests-draft2019-09/badRef.json',
't/additional-tests-draft2019-09/faux-buggy-schemas.json',
't/additional-tests-draft2019-09/format-date-time.json',
't/additional-tests-draft2019-09/format-date.json',
't/additional-tests-draft2019-09/format-duration.json',
't/additional-tests-draft2019-09/format-ipv4.json',
't/additional-tests-draft2019-09/format-ipv6.json',
't/additional-tests-draft2019-09/format-relative-json-pointer.json',
't/additional-tests-draft2019-09/format-time.json',
't/additional-tests-draft2019-09/formats.json',
't/additional-tests-draft2019-09/id.json',
't/additional-tests-draft2019-09/integers.json',
't/additional-tests-draft2019-09/keyword-independence.json',
't/additional-tests-draft2019-09/loose-types-const-enum.json',
't/additional-tests-draft2019-09/not.json',
't/additional-tests-draft2019-09/recursive-dynamic.json',
't/additional-tests-draft2019-09/ref-and-id.json',
't/additional-tests-draft2019-09/ref.json',
't/additional-tests-draft2019-09/short-circuit.json',
't/additional-tests-draft2019-09/unknownKeyword.json',
't/additional-tests-draft2019-09/vocabulary.json',
't/additional-tests-draft2020-12.t',
't/additional-tests-draft2020-12/README',
't/additional-tests-draft2020-12/anchor.json',
't/additional-tests-draft2020-12/annotation-collection.json',
't/additional-tests-draft2020-12/badRef.json',
't/additional-tests-draft2020-12/dynamicRef.json',
't/additional-tests-draft2020-12/faux-buggy-schemas.json',
't/additional-tests-draft2020-12/format-date-time.json',
't/additional-tests-draft2020-12/format-date.json',
't/additional-tests-draft2020-12/format-duration.json',
't/additional-tests-draft2020-12/format-ipv4.json',
't/additional-tests-draft2020-12/format-ipv6.json',
't/additional-tests-draft2020-12/format-relative-json-pointer.json',
't/additional-tests-draft2020-12/format-time.json',
't/additional-tests-draft2020-12/formats.json',
't/additional-tests-draft2020-12/id.json',
't/additional-tests-draft2020-12/integers.json',
't/additional-tests-draft2020-12/keyword-independence.json',
't/additional-tests-draft2020-12/loose-types-const-enum.json',
't/additional-tests-draft2020-12/not.json',
't/additional-tests-draft2020-12/recursive-dynamic.json',
't/additional-tests-draft2020-12/ref-and-id.json',
't/additional-tests-draft2020-12/ref.json',
't/additional-tests-draft2020-12/short-circuit.json',
't/additional-tests-draft2020-12/unknownKeyword.json',
't/additional-tests-draft2020-12/vocabulary.json',
't/additional-tests-draft4.t',
't/additional-tests-draft4/format-date-time.json',
't/additional-tests-draft4/format-ipv4.json',
't/additional-tests-draft4/format-ipv6.json',
't/additional-tests-draft4/id.json',
't/additional-tests-draft4/integers.json',
't/additional-tests-draft4/type.json',
't/additional-tests-draft7.t',
't/additional-tests-draft7/README',
't/additional-tests-draft7/badRef.json',
't/additional-tests-draft7/faux-buggy-schemas.json',
't/additional-tests-draft7/format-date-time.json',
't/additional-tests-draft7/format-date.json',
't/additional-tests-draft7/format-ipv4.json',
't/additional-tests-draft7/format-relative-json-pointer.json',
't/additional-tests-draft7/format-time.json',
't/additional-tests-draft7/id.json',
't/additional-tests-draft7/integers.json',
't/additional-tests-draft7/keyword-independence.json',
't/additional-tests-draft7/loose-types-const-enum.json',
't/additional-tests-draft7/not-an-anchor.json',
't/additional-tests-draft7/not-an-id.json',
't/additional-tests-draft7/ref-and-id.json',
't/additional-tests-draft7/ref.json',
't/additional-tests-draft7/short-circuit.json',
't/additional-tests-draft7/unknownKeyword.json',
't/additional-tests-draft7/vocabulary.json',
't/annotations.t',
't/boolean-data.t',
't/boolean-schemas.t',
't/cached-metaschemas.t',
't/callbacks.t',
't/checksums.t',
't/content-encoding.t',
't/dialects.t',
't/document.t',
't/equality.t',
't/errors.t',
't/evaluate_json_string.t',
't/find-identifiers.t',
't/formats.t',
't/invalid-schemas.t',
't/invalid-schemas/invalid-input.json',
't/invalid-schemas/ref.json',
't/invalid-schemas/vocabulary.json',
't/lib/Acceptance.pm',
't/lib/Helper.pm',
't/lib/MyVocabulary/BadEvaluationOrder.pm',
't/lib/MyVocabulary/BadVocabularySub1.pm',
't/lib/MyVocabulary/BadVocabularySub2.pm',
't/lib/MyVocabulary/BadVocabularySub3.pm',
't/lib/MyVocabulary/ConflictingKeyword.pm',
't/lib/MyVocabulary/MissingRole.pm',
't/lib/MyVocabulary/MissingSub.pm',
't/lib/MyVocabulary/ReservedKeyword.pm',
't/lib/MyVocabulary/StringComparison.pm',
't/max_traversal_depth.t',
't/multipleOf.t',
't/pattern.t',
't/read_serialized_file',
't/ref.t',
't/result-object.t',
't/results/draft2019-09-acceptance-format.txt',
't/results/draft2019-09-acceptance.txt',
't/results/draft2019-09-additional-tests.txt',
't/results/draft2019-09-invalid-schemas.txt',
't/results/draft2020-12-acceptance-format.txt',
't/results/draft2020-12-acceptance.txt',
't/results/draft2020-12-additional-tests.txt',
't/results/draft2020-12-invalid-schemas.txt',
't/results/draft4-acceptance-format.txt',
't/results/draft4-acceptance.txt',
't/results/draft4-additional-tests.txt',
't/results/draft6-acceptance-format.txt',
't/results/draft6-acceptance.txt',
't/results/draft7-acceptance-format.txt',
't/results/draft7-acceptance.txt',
't/results/draft7-additional-tests.txt',
't/serialization.t',
't/specification_version.t',
't/strict.t',
't/stringy-numbers.t',
't/traverse.t',
't/type.t',
't/unsupported-keywords.t',
't/validate-schema.t',
't/vocabularies.t',
't/zzz-acceptance-draft2019-09-format.t',
't/zzz-acceptance-draft2019-09.t',
't/zzz-acceptance-draft2020-12-format.t',
't/zzz-acceptance-draft2020-12.t',
't/zzz-acceptance-draft4-format.t',
't/zzz-acceptance-draft4.t',
't/zzz-acceptance-draft6-format.t',
't/zzz-acceptance-draft6.t',
't/zzz-acceptance-draft7-format.t',
't/zzz-acceptance-draft7.t',
't/zzz-check-breaks.t',
'xt/author/00-compile.t',
'xt/author/clean-namespaces.t',
'xt/author/distmeta.t',
'xt/author/eol.t',
'xt/author/kwalitee.t',
'xt/author/minimum-version.t',
'xt/author/mojibake.t',
'xt/author/no-tabs.t',
'xt/author/pod-coverage.t',
'xt/author/pod-spell.t',
'xt/author/pod-syntax.t',
'xt/author/portability.t',
'xt/release/changes_has_content.t',
'xt/release/cpan-changes.t'
);
notabs_ok($_) foreach @files;
done_testing;
inc 000755 000766 000024 0 15114374332 15012 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 AppendSection.pm 100640 000766 000024 2635 15114374332 20246 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/inc # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strict;
use warnings;
package inc::AppendSection;
use Moose;
extends 'Pod::Weaver::Section::AllowOverride';
# Warning: dirty hack ahead!
# This chicanery is to allow for [GenerateSection] followed by [AllowOverride],
# where the intention is the content added by that GenerateSection should be appended to the
# original section that was generated by a weaver bundle (as opposed to appearing in the literal
# .pm file).
# This plugin does its initial work (to find the node to pluck out and later append) in
# transform_document, and the Transformer phase is run before GenerateSection's weave_section
# has a chance to create the node.
# So this hack just runs the plugin's transform_document again if nothing was found the first time.
# Because it picks the first node it finds (to relocate to the location of the second match), we
# will now run GenerateSection first, before calling the weaver bundle.
# All of this really should be replaced by a new plugin called something like AppendSection,
# which subclasses GenerateSection to add the options provided by AllowOverride, letting us
# append (or prepend) to an existing section rather than generating a new one.
before weave_section => sub {
my ($self, $document, $input) = @_;
# if we haven't already found a matching section, look again now
$self->transform_document($document) if not $self->_override_with;
};
1;
content-encoding.t 100640 000766 000024 35721 15114374332 20313 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use utf8;
use lib 't/lib';
use Helper;
subtest 'unrecognized encoding formats do not result in errors, when not asserting' => sub {
my $js = JSON::Schema::Modern->new(collect_annotations => 1);
cmp_result(
my $result = $js->evaluate(
'hello',
{
contentEncoding => 'base64',
contentMediaType => 'image/png',
contentSchema => false,
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '',
keywordLocation => '/contentEncoding',
annotation => 'base64',
},
{
instanceLocation => '',
keywordLocation => '/contentMediaType',
annotation => 'image/png',
},
{
instanceLocation => '',
keywordLocation => '/contentSchema',
annotation => false,
},
],
},
'in evaluate(), annotations are collected and no validation is performed',
);
};
subtest 'media_type and encoding handlers' => sub {
my $js = JSON::Schema::Modern->new;
like(
dies { $js->add_media_type('FOO/BAR' => sub { \1 }) },
qr!Value "FOO/BAR" did not pass type constraint !,
'upper-cased names are not accepted',
);
cmp_result(
$js->get_media_type('application/json')->(\'{"alpha": "a string"}'),
\ { alpha => 'a string' },
'application/json media_type decoder',
);
cmp_result($js->get_media_type('*/*'), undef, '*/* has no default match');
cmp_result($js->get_media_type('text/plain')->(\'foo'), \'foo', 'default text/plain media_type decoder');
cmp_result($js->get_media_type('tExt/PLaIN')->(\'foo'), \'foo', 'getter uses the casefolded name');
$js->add_media_type('furble/*' => sub { \1 });
cmp_result($js->get_media_type('furble/bloop')->(\''), \'1', 'getter matches to wildcard entries');
$js->add_media_type('text/*' => sub { \'wildcard' });
cmp_result($js->get_media_type('TExT/plain')->(\'foo'), \'wildcard', 'getter uses new override entry for wildcard');
$js->add_media_type('text/plain' => sub { \'plain' });
cmp_result($js->get_media_type('TExT/plain')->(\'foo'), \'plain', 'getter prefers case-insensitive matches to wildcard entries');
cmp_result($js->get_media_type('TExT/blop')->(\'foo'), \'wildcard', 'getter matches to wildcard entries');
cmp_result($js->get_media_type('TExT/*')->(\'foo'), \'wildcard', 'text/* matches itself');
$js->add_media_type('*/*' => sub { \'wildercard' });
cmp_result($js->get_media_type('TExT/plain')->(\'foo'), \'plain', 'getter still prefers case-insensitive matches to wildcard entries');
cmp_result($js->get_media_type('TExT/blop')->(\'foo'), \'wildcard', 'text/* is preferred to */*');
cmp_result($js->get_media_type('*/*')->(\'foo'), \'wildercard', '*/* matches */*, once defined');
cmp_result($js->get_media_type('fOO/bar')->(\'foo'), \'wildercard', '*/* is returned as a last resort');
cmp_result(
$js->get_media_type('application/x-www-form-urlencoded')->(\qq!\x{c3}\x{a9}clair=\x{e0}\x{b2}\x{a0}\x{5f}\x{e0}\x{b2}\x{a0}!),
\ { 'éclair' => 'ಠ_ಠ' },
'application/x-www-form-urlencoded happy path with unicode',
);
cmp_result(
$js->get_media_type('application/x-ndjson')->(\qq!{"foo":1,"bar":2}\n["a","b",3]\r\n"\x{e0}\x{b2}\x{a0}\x{5f}\x{e0}\x{b2}\x{a0}"!),
\ [ { foo => 1, bar => 2 }, [ 'a', 'b', 3 ], 'ಠ_ಠ' ],
'application/x-ndjson happy path with unicode',
);
like(
dies { $js->get_media_type('application/x-ndjson')->(\qq!{"foo":1,"bar":2}\n["a","b",]!) },
qr/^parse error at line 2: malformed JSON string/,
'application/x-ndjson dies with line number of the bad data',
);
$js = JSON::Schema::Modern->new;
# MIME::Base64::decode("eyJmb28iOiAiYmFyIn0K") -> {"foo": "bar"}
# Cpanel::JSON::XS->new->allow_nonref(1)->utf8(0)->decode(q!{"foo": "bar"}!) -> { foo => 'bar' }
cmp_result(
$js->get_media_type('application/json')->($js->get_encoding('base64')->(\'eyJmb28iOiAiYmFyIn0K')),
\ { foo => 'bar' },
'base64 encoding decoder + application/json media_type decoder',
);
cmp_result(
$js->get_media_type('application/json')->($js->get_encoding('base64url')->(\'eyJmb28iOiJiYXIifQ')),
\ { foo => 'bar' },
'base64url encoding decoder + application/json media_type decoder',
);
};
subtest 'draft2020-12 assertions' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
my $data = { encoded_object => 'eyJmb28iOiAiYmFyIn0K' },
my $schema = {
type => 'object',
properties => {
encoded_object => {
contentEncoding => 'base64',
contentMediaType => 'application/json',
contentSchema => {
type => 'object',
additionalProperties => {
const => 'ಠ_ಠ',
},
},
},
},
},
)->TO_JSON,
{ valid => true },
'under the current spec version, content* keywords are not assertions',
);
cmp_result(
my $result = $js->evaluate(
{ encoded_object => 'blur^p=' }, # invalid base64
$schema,
{ validate_content_schemas => 1 },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentEncoding',
error => 'could not decode base64 string: invalid characters',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'contentEncoding first decodes the string, erroring if it can\'t',
);
cmp_result(
$result = $js->evaluate(
{ encoded_object => 'bm90IGpzb24=' }, # base64-encoded "not json"
$schema,
{ validate_content_schemas => 1 },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentMediaType',
error => re(qr!could not decode application/json string: \'null\' expected, at character offset 0!),
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'then contentMediaType parses the decoded string, erroring if it can\'t, and does not continue with the schema',
);
cmp_result(
$result = $js->evaluate(
{ encoded_object => 'eyJoaSI6MX0=' }, # base64-encoded, json-encoded { hi => 1 }
$schema,
{ validate_content_schemas => 1 },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/encoded_object/hi',
keywordLocation => '/properties/encoded_object/contentSchema/additionalProperties/const',
error => 'value does not match',
},
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentSchema/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentSchema',
error => 'subschema is not valid',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'contentSchema evaluates the decoded data',
);
cmp_result(
$result = $js->evaluate(
{ encoded_object => 'bnVsbA==' }, # base64-encoded, json-encoded undef
$schema,
{ validate_content_schemas => 1 },
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentSchema/type',
error => 'got null, not object',
},
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentSchema',
error => 'subschema is not valid',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'null data is handled properly',
);
cmp_result(
$result = $js->evaluate(
{ encoded_object => 'eyJoaSI6IuCyoF/gsqAifQ==' }, # base64-encoded, json-encoded { hi => "ಠ_ಠ" }
$schema,
{ validate_content_schemas => 1 },
)->TO_JSON,
{ valid => true },
'contentSchema successfully evaluates the decoded data',
);
};
subtest 'draft7 assertions' => sub {
my $js = JSON::Schema::Modern->new(specification_version => 'draft7');
cmp_result(
my $result = $js->evaluate(
{ encoded_object => 'blur^p=' }, # invalid base64
my $schema = {
type => 'object',
properties => {
encoded_object => {
contentEncoding => 'base64',
contentMediaType => 'application/json',
contentSchema => {
type => 'object',
additionalProperties => {
const => 'ಠ_ಠ',
},
},
},
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentEncoding',
error => 'could not decode base64 string: invalid characters',
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'in draft7, assertion behaviour is the default',
);
cmp_result(
$result = $js->evaluate(
{ encoded_object => 'bm90IGpzb24=' }, # base64-encoded "not json"
$schema,
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/encoded_object',
keywordLocation => '/properties/encoded_object/contentMediaType',
error => re(qr!could not decode application/json string: \'null\' expected, at character offset 0!),
},
{
instanceLocation => '',
keywordLocation => '/properties',
error => 'not all properties are valid',
},
],
},
'in draft7, then contentMediaType parses the decoded string, erroring if it can\'t, and does not continue with the schema',
);
cmp_result(
$result = $js->evaluate(
{ encoded_object => 'eyJoaSI6MX0=' }, # base64-encoded, json-encoded { hi => 1 }
$schema,
)->TO_JSON,
{ valid => true },
'under draft7, content* are assertions by default, but contentSchema does not exist',
);
};
subtest 'more assertions' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
'a string',
{
contentEncoding => 'whargarbl',
contentMediaType => 'whargarbl',
contentSchema => false,
},
{
validate_content_schemas => 1,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/contentEncoding',
error => 'cannot find decoder for contentEncoding "whargarbl"',
},
],
},
'evaluation aborts with an unrecognized contentEncoding',
);
cmp_result(
$js->evaluate(
'a string',
{
contentMediaType => 'whargarbl',
contentSchema => false,
},
{
validate_content_schemas => 1,
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/contentMediaType',
error => 'cannot find decoder for contentMediaType "whargarbl"',
},
],
},
'evaluation aborts with an unrecognized contentMediaType',
);
};
subtest 'use of an absolute URI and different dialect within contentSchema' => sub {
my $js = JSON::Schema::Modern->new(
validate_content_schemas => 1,
collect_annotations => 1,
);
$js->add_schema({
'$id' => 'https://my_metaschema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/validation' => true,
},
});
my $subschema;
cmp_result(
$js->evaluate(
{ foo => '{"bar":1}' },
{
'$id' => 'https://example.com',
additionalProperties => {
contentMediaType => 'application/json',
contentSchema => $subschema = {
'$id' => 'https://foo.com',
'$schema' => 'https://my_metaschema',
'$defs' => {
my_def => { type => 'object', blah => 1 },
},
'$ref' => '#/$defs/my_def',
bloop => 2,
properties => { bar => false }, # this keyword should only annotate
},
},
},
)->TO_JSON,
{
valid => true,
annotations => [
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/contentMediaType',
absoluteKeywordLocation => 'https://example.com#/additionalProperties/contentMediaType',
annotation => 'application/json',
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/contentSchema/$ref/blah',
absoluteKeywordLocation => 'https://foo.com#/$defs/my_def/blah',
annotation => 1,
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/contentSchema/bloop',
absoluteKeywordLocation => 'https://foo.com#/bloop',
annotation => 2,
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/contentSchema/properties',
absoluteKeywordLocation => 'https://foo.com#/properties',
annotation => { bar => false },
},
{
instanceLocation => '/foo',
keywordLocation => '/additionalProperties/contentSchema',
absoluteKeywordLocation => 'https://example.com#/additionalProperties/contentSchema',
annotation => $subschema,
},
{
instanceLocation => '',
keywordLocation => '/additionalProperties',
absoluteKeywordLocation => 'https://example.com#/additionalProperties',
annotation => [ 'foo' ],
},
],
},
'evaluation of the subschema correctly uses the new $id and $schema',
);
};
done_testing;
find-identifiers.t 100640 000766 000024 50447 15114374332 20302 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use utf8;
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use List::Util 'unpairs';
use builtin::compat 'refaddr';
use Data::Dumper ();
use lib 't/lib';
use Helper;
# spec version -> vocab classes
my %vocabularies = unpairs(JSON::Schema::Modern->new->__all_metaschema_vocabulary_classes);
my %dialect = (
specification_version => 'draft2020-12',
vocabularies => $vocabularies{'draft2020-12'},
);
subtest '$id sets canonical uri' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
1,
my $schema = {
'$defs' => {
foo => my $foo_definition = {
'$id' => 'http://localhost:4242/my_foo',
const => 'foo value',
},
},
'$ref' => 'http://localhost:4242/my_foo',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/const',
absoluteKeywordLocation => 'http://localhost:4242/my_foo#/const',
error => 'value does not match',
},
],
},
'$id was recognized - $ref was successfully traversed',
);
cmp_result(
{ $js->_resource_index },
{
'' => {
path => '', canonical_uri => str(''), document => ignore,
%dialect,
},
'http://localhost:4242/my_foo' => {
path => '/$defs/foo',
canonical_uri => str('http://localhost:4242/my_foo'),
document => methods(canonical_uri => str('')),
%dialect,
},
},
'resources indexed; document canonical_uri is still unset',
);
my $doc1 = $js->{_resource_index}{''}{document};
my $doc2 = $js->{_resource_index}{'http://localhost:4242/my_foo'}{document};
is(refaddr($doc1), refaddr($doc2), 'the same document object is indexed under both URIs');
sub _find_all_values ($data) {
if (ref $data eq 'ARRAY') {
return map __SUB__->($_), @$data;
}
elsif (ref $data eq 'HASH') {
return map __SUB__->($_), values %$data;
}
return $data;
}
my @blessed_values = grep ref($_), _find_all_values($doc1->schema);
ok(!@blessed_values, 'the schema contains no blessed leaf nodes')
or diag 'found blessed values: ',
Data::Dumper->new([ map ref, @blessed_values ])->Indent(2)->Terse(1)->Sortkeys(1)->Dump;
};
subtest 'anchors' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
1,
my $schema = {
'$defs' => {
foo => my $foo_definition = {
'$anchor' => 'my_foo',
const => 'foo value',
},
bar => my $bar_definition = {
'$anchor' => 'my_bar',
not => true,
},
},
'$id' => 'http://localhost:4242',
allOf => [
{ '$ref' => '#my_foo' },
{ '$ref' => '#my_bar' },
{ not => my $not_definition = {
'$anchor' => 'my_not',
not => false,
},
},
],
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/$ref/const',
absoluteKeywordLocation => 'http://localhost:4242#/$defs/foo/const',
error => 'value does not match',
},
{
instanceLocation => '',
keywordLocation => '/allOf/1/$ref/not',
absoluteKeywordLocation => 'http://localhost:4242#/$defs/bar/not',
error => 'subschema is true',
},
{
instanceLocation => '',
keywordLocation => '/allOf/2/not',
absoluteKeywordLocation => 'http://localhost:4242#/allOf/2/not',
error => 'subschema is valid',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
absoluteKeywordLocation => 'http://localhost:4242#/allOf',
error => 'subschemas 0, 1, 2 are not valid',
},
],
},
'$id was recognized - absolute locations use json paths, not anchors',
);
cmp_result(
{ $js->_resource_index },
{
'http://localhost:4242' => {
path => '',
canonical_uri => str('http://localhost:4242'),
document => methods(canonical_uri => str('http://localhost:4242')),
%dialect,
anchors => {
my_foo => {
path => '/$defs/foo',
canonical_uri => str('http://localhost:4242#/$defs/foo'),
},
my_bar => {
path => '/$defs/bar',
canonical_uri => str('http://localhost:4242#/$defs/bar'),
},
my_not => {
path => '/allOf/2/not',
canonical_uri => str('http://localhost:4242#/allOf/2/not'),
},
},
},
},
'internal resource index is correct',
);
};
subtest '$anchor at root without $id' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
1,
{
'$anchor' => 'root',
'$defs' => {
foo => {
'$anchor' => 'my_foo',
const => 'foo value',
},
},
'$ref' => '#my_foo',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/const',
absoluteKeywordLocation => '#/$defs/foo/const',
error => 'value does not match',
},
],
},
'$id without anchor was recognized - absolute locations use json paths, not anchors',
);
cmp_result(
{ $js->_resource_index },
{
'' => {
path => '', canonical_uri => str(''), document => ignore,
%dialect,
anchors => {
root => {
path => '',
canonical_uri => str(''),
},
my_foo => {
path => '/$defs/foo',
canonical_uri => str('#/$defs/foo'),
},
},
},
},
'internal resource index is correct',
);
};
subtest '$ids and $anchors in subschemas after $id changes' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
1,
{
'$id' => 'https://foo.com/a/alpha',
properties => {
b => {
'$id' => 'beta',
properties => {
d => {
'$anchor' => 'my_d',
},
},
},
f => {
'$id' => 'zeta',
properties => {
h => {
'$anchor' => 'my_h',
},
},
},
},
},
)->TO_JSON,
{
valid => true,
},
'$anchor is legal in a subschema',
);
cmp_result(
{ $js->_resource_index },
{
'https://foo.com/a/alpha' => {
path => '', canonical_uri => str('https://foo.com/a/alpha'), document => ignore,
%dialect,
},
'https://foo.com/a/beta' => {
path => '/properties/b', canonical_uri => str('https://foo.com/a/beta'), document => ignore,
%dialect,
anchors => {
my_d => {
path => '/properties/b/properties/d',
canonical_uri => str('https://foo.com/a/beta#/properties/d'),
},
},
},
'https://foo.com/a/zeta' => {
path => '/properties/f', canonical_uri => str('https://foo.com/a/zeta'), document => ignore,
%dialect,
anchors => {
my_h => {
path => '/properties/f/properties/h',
canonical_uri => str('https://foo.com/a/zeta#/properties/h'),
},
},
},
},
'internal resource index is correct',
);
};
subtest 'invalid $id and $anchor' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(
1,
{
'$id' => 'foo.json',
'$defs' => {
a_bad_id => {
'$id' => 'foo.json#/foo/bar',
},
anchor_a => {
'$anchor' => 'a_my$foo',
},
anchor_b => {
'$anchor' => 'b_hello123৪২',
},
anchor_c => {
'$anchor' => 'c_helloðworld',
},
},
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$defs/a_bad_id/$id',
absoluteKeywordLocation => 'foo.json#/$defs/a_bad_id/$id',
error => '$id value "foo.json#/foo/bar" cannot have a non-empty fragment',
},
map +{
instanceLocation => '',
keywordLocation => '/$defs/anchor_'.substr($_,0,1).'/$anchor',
absoluteKeywordLocation => 'foo.json#/$defs/anchor_'.substr($_,0,1).'/$anchor',
error => '$anchor value "'.$_.'" does not match required syntax',
}, qw(a_my$foo b_hello123৪২ c_helloðworld),
],
},
'bad $id and $anchor are detected, even if bad definitions are not traversed',
);
cmp_result(
$js->evaluate(
1,
{
'$id' => 'foo.json',
'$defs' => {
const_not_id => {
const => {
'$id' => 'not_a_real_id',
},
},
const_not_anchor => {
enum => [
'$anchor' => 'not_a_real_anchor',
],
},
},
anyOf => [
{ '$ref' => '#/$defs/const_not_id' },
{ '$ref' => '#/$defs/const_not_anchor' },
true,
],
},
)->TO_JSON,
{
valid => true,
},
'"bad" $ids and $anchors that are not actually keywords are not reported as errors',
);
};
subtest 'nested $ids' => sub {
my $js = JSON::Schema::Modern->new(short_circuit => 0);
my $schema = {
'$id' => '/foo/bar/baz.json',
'$ref' => '/foo/bar/baz.json#/properties/alpha', # not the canonical URI for that location
properties => {
alpha => my $alpha = {
'$id' => 'alpha.json',
additionalProperties => false,
properties => {
beta => my $beta = {
'$id' => '/beta/hello.json',
properties => {
gamma => my $gamma = {
'$id' => 'gamma.json',
const => 'hello',
},
},
},
},
},
},
};
cmp_result(
$js->evaluate(
{
alpha => {
beta => {
gamma => 'not hello',
},
},
},
$schema,
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/alpha',
keywordLocation => '/$ref/additionalProperties',
absoluteKeywordLocation => '/foo/bar/alpha.json#/additionalProperties',
error => 'additional property not permitted',
},
{
instanceLocation => '',
keywordLocation => '/$ref/additionalProperties',
absoluteKeywordLocation => '/foo/bar/alpha.json#/additionalProperties',
error => 'not all additional properties are valid',
},
{
instanceLocation => '/alpha/beta/gamma',
keywordLocation => '/properties/alpha/properties/beta/properties/gamma/const',
absoluteKeywordLocation => '/beta/gamma.json#/const',
error => 'value does not match',
},
{
instanceLocation => '/alpha/beta',
keywordLocation => '/properties/alpha/properties/beta/properties',
absoluteKeywordLocation => '/beta/hello.json#/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '/alpha',
keywordLocation => '/properties/alpha/properties',
absoluteKeywordLocation => '/foo/bar/alpha.json#/properties',
error => 'not all properties are valid',
},
{
instanceLocation => '',
keywordLocation => '/properties',
absoluteKeywordLocation => '/foo/bar/baz.json#/properties',
error => 'not all properties are valid',
},
],
},
'errors have the correct location',
);
cmp_result(
{ $js->_resource_index },
{
'/foo/bar/baz.json' => {
path => '',
canonical_uri => str('/foo/bar/baz.json'),
document => methods(canonical_uri => str('/foo/bar/baz.json')),
%dialect,
},
'/foo/bar/alpha.json' => {
path => '/properties/alpha',
canonical_uri => str('/foo/bar/alpha.json'),
document => shallow($js->_get_resource('/foo/bar/baz.json')->{document}),
%dialect,
},
'/beta/hello.json' => {
path => '/properties/alpha/properties/beta',
canonical_uri => str('/beta/hello.json'),
document => shallow($js->_get_resource('/foo/bar/baz.json')->{document}),
%dialect,
},
'/beta/gamma.json' => {
path => '/properties/alpha/properties/beta/properties/gamma',
canonical_uri => str('/beta/gamma.json'),
document => shallow($js->_get_resource('/foo/bar/baz.json')->{document}),
%dialect,
},
},
'properly resolved all the nested $ids',
);
};
subtest 'multiple documents, each using canonical_uri = ""' => sub {
my $js = JSON::Schema::Modern->new;
my $schema1 = {
allOf => [
{ '$id' => 'subschema1.json', type => 'string' },
{ '$id' => 'subschema2.json', type => 'number' },
],
};
my $schema2 = {
anyOf => [
{ '$id' => 'subschema3.json', type => 'string' },
{ '$id' => 'subschema4.json', type => 'number' },
],
};
cmp_result(
$js->evaluate(1, $schema1)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
absoluteKeywordLocation => 'subschema1.json#/type',
error => 'got integer, not string',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschema 0 is not valid',
},
],
},
'evaluation of schema1',
);
my $resource_index1 = +{ $js->_resource_index };
my $document1 = $resource_index1->{''}{document};
cmp_result(
$resource_index1,
{
'' => {
path => '',
canonical_uri => str(''),
document => shallow($document1),
%dialect,
},
'subschema1.json' => {
path => '/allOf/0',
canonical_uri => str('subschema1.json'),
document => shallow($document1),
%dialect,
},
'subschema2.json' => {
path => '/allOf/1',
canonical_uri => str('subschema2.json'),
document => shallow($document1),
%dialect,
},
},
'resources in initial schema are indexed',
);
cmp_result(
$js->evaluate(1, $schema2)->TO_JSON,
{
valid => true,
},
'successful evaluation of schema2',
);
my $resource_index2 = +{ $js->_resource_index };
my $document2 = $resource_index2->{'subschema3.json'}{document};
cmp_result(
$resource_index2,
{
'' => {
path => '',
canonical_uri => str(''),
document => shallow($document2), # same uri as earlier, but now points to document2
%dialect,
},
'subschema1.json' => {
path => '/allOf/0',
canonical_uri => str('subschema1.json'),
document => shallow($document1), # still here! there is no reason to forget about it
%dialect,
},
'subschema2.json' => {
path => '/allOf/1',
canonical_uri => str('subschema2.json'),
document => shallow($document1), # still here! there is no reason to forget about it
%dialect,
},
'subschema3.json' => {
path => '/anyOf/0',
canonical_uri => str('subschema3.json'),
document => shallow($document2),
%dialect,
},
'subschema4.json' => {
path => '/anyOf/1',
canonical_uri => str('subschema4.json'),
document => shallow($document2),
%dialect,
},
},
'resources in second schema are indexed; all resources from first schema are preserved except uri=""',
);
};
subtest 'multiple documents, each using canonical_uri = "", collisions in other resources' => sub {
my $js = JSON::Schema::Modern->new;
my $schema1 = {
allOf => [
{ '$id' => 'subschema1.json', type => 'string' },
{ '$id' => 'subschema2.json', type => 'number' },
],
};
my $schema2 = {
anyOf => [
{ '$id' => 'subschema1.json', type => 'string' },
{ '$id' => 'subschema3.json', type => 'number' },
],
};
cmp_result(
$js->evaluate(1, $schema1)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/allOf/0/type',
absoluteKeywordLocation => 'subschema1.json#/type',
error => 'got integer, not string',
},
{
instanceLocation => '',
keywordLocation => '/allOf',
error => 'subschema 0 is not valid',
},
],
},
'evaluation of schema1',
);
my $resource_index1 = +{ $js->_resource_index };
my $document1 = $resource_index1->{''}{document};
cmp_result(
$resource_index1,
{
'' => {
path => '',
canonical_uri => str(''),
document => shallow($document1),
%dialect,
},
'subschema1.json' => {
path => '/allOf/0',
canonical_uri => str('subschema1.json'),
document => shallow($document1),
%dialect,
},
'subschema2.json' => {
path => '/allOf/1',
canonical_uri => str('subschema2.json'),
document => shallow($document1),
%dialect,
},
},
'resources in initial schema are indexed',
);
cmp_result(
$js->evaluate(1, $schema2)->TO_JSON,
{
valid => false,
errors => [
{
error => re(qr/^EXCEPTION: uri "subschema1.json" conflicts with an existing schema resource/),
instanceLocation => '',
keywordLocation => '',
},
],
},
'schema2 cannot be evaluated - an internal $id collides with an existing resource',
);
};
subtest 'resource collisions in canonical uris' => sub {
my $js = JSON::Schema::Modern->new;
$js->add_schema({ '$id' => 'https://foo.com/x/y/z' });
cmp_result(
$js->evaluate(1, { '$id' => 'https://foo.com', anyOf => [ { '$id' => '/x/y/z' } ] })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => re(qr{^EXCEPTION: uri "https://foo.com/x/y/z" conflicts with an existing schema resource}),
}
],
},
'detected collision between a document\'s initial uri and a document\'s subschema\'s uri',
);
$js = JSON::Schema::Modern->new;
$js->add_schema({
'$id' => 'https://foo.com',
anyOf => [ { '$id' => '/x/y/z' } ],
});
cmp_result(
$js->evaluate(1, { allOf => [ { '$id' => 'https://foo.com/x/y/z' } ] })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => re(qr{^EXCEPTION: uri "https://foo.com/x/y/z" conflicts with an existing schema resource}),
}
],
},
'detected collision between two document subschema uris',
);
};
subtest 'relative uri in $id' => sub {
cmp_result(
JSON::Schema::Modern->new->evaluate(
1,
{
'$id' => 'foo/bar/baz.json',
type => 'object',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/type',
absoluteKeywordLocation => 'foo/bar/baz.json#/type',
error => 'got integer, not object',
},
],
},
'root schema location is correctly identified',
);
cmp_result(
JSON::Schema::Modern->new->evaluate(
[ 1, [ 2, 3 ] ],
{
'$id' => 'foo/bar/baz.json',
type => [ 'integer', 'array' ],
items => { '$ref' => '#' },
},
)->TO_JSON,
{
valid => true,
},
'properly able to traverse a recursive schema using a relative $id',
);
};
done_testing;
distmeta.t 100644 000766 000024 223 15114374332 20312 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author #!perl
# This file was automatically generated by Dist::Zilla::Plugin::MetaTests.
use strict;
use warnings;
use Test::CPAN::Meta;
meta_yaml_ok();
kwalitee.t 100644 000766 000024 275 15114374332 20314 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author # this test was generated with Dist::Zilla::Plugin::Test::Kwalitee 2.12
use strict;
use warnings;
use Test::More 0.88;
use Test::Kwalitee 1.21 'kwalitee_ok';
kwalitee_ok();
done_testing;
mojibake.t 100644 000766 000024 151 15114374332 20261 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author #!perl
use strict;
use warnings qw(all);
use Test::More;
use Test::Mojibake;
all_files_encoding_ok();
zzz-check-breaks.t 100644 000766 000024 2122 15114374332 20203 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t use strict;
use warnings;
# this test was generated with Dist::Zilla::Plugin::Test::CheckBreaks 0.020
use Test::More tests => 2;
use Term::ANSIColor 'colored';
SKIP: {
skip 'no conflicts module found to check against', 1;
}
# this data duplicates x_breaks in META.json
my $breaks = {
"JSON::Schema::Modern::Document::OpenAPI" => "< 0.097",
"JSON::Schema::Modern::Vocabulary::OpenAPI" => "< 0.080",
"Mojolicious::Plugin::OpenAPI::Modern" => "< 0.014",
"OpenAPI::Modern" => "< 0.077",
"Test::Mojo::Role::OpenAPI::Modern" => "< 0.007"
};
use CPAN::Meta::Requirements;
use CPAN::Meta::Check 0.011;
my $reqs = CPAN::Meta::Requirements->new;
$reqs->add_string_requirement($_, $breaks->{$_}) foreach keys %$breaks;
our $result = CPAN::Meta::Check::check_requirements($reqs, 'conflicts');
if (my @breaks = grep defined $result->{$_}, keys %$result) {
diag colored('Breakages found with JSON-Schema-Modern:', 'yellow');
diag colored("$result->{$_}", 'yellow') for sort @breaks;
diag "\n", colored('You should now update these modules!', 'yellow');
}
pass 'checked x_breaks data';
CheckConflicts.pm 100640 000766 000024 1752 15114374332 20373 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/inc # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strict;
use warnings;
package inc::CheckConflicts;
use Moose;
with 'Dist::Zilla::Role::InstallTool';
sub setup_installer {
my $self = shift;
my @mfpl = grep +($_->name eq 'Makefile.PL' or $_->name eq 'Build.PL'), $self->zilla->files->@*;
$self->log_fatal('No Makefile.PL or Build.PL was found.') if not @mfpl;
foreach my $file (@mfpl) {
$self->log_debug([ 'munging %s in setup_installer phase', $file->name ]);
my $orig_content = $file->content;
$self->log_fatal('could not find position in ' . $file->name . ' to modify!')
if not $orig_content =~ m/use strict;\nuse warnings;\n\n/g;
my $pos = pos($orig_content);
my $content = <<'CONTENT';
if ("$]" < 5.038) {
die "This distribution will not install where builtin::Backport exists.\n"
if eval { +require builtin::Backport; 1 };
}
CONTENT
$file->content(substr($orig_content, 0, $pos) . $content . substr($orig_content, $pos));
}
return;
}
1;
pod-spell.t 100644 000766 000024 714 15114374332 20404 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author use strict;
use warnings;
use Test::More;
# generated by Dist::Zilla::Plugin::Test::PodSpelling 2.007006
use Test::Spelling 0.17;
use Pod::Wordlist;
add_stopwords();
all_pod_files_spelling_ok( qw( examples lib script t xt ) );
__DATA__
Annotation
Applicator
Content
Core
Document
Error
Etheridge
FormatAnnotation
FormatAssertion
JSON
Karen
MetaData
Modern
Result
ResultNode
Schema
Unevaluated
Utilities
Validation
Vocabulary
ether
irc
json
lib
script
00-report-prereqs.t 100644 000766 000024 14170 15114374332 20263 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t #!perl
use strict;
use warnings;
# This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.029
use Test::More tests => 1;
use ExtUtils::MakeMaker;
use File::Spec;
# from $version::LAX
my $lax_version_re =
qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )?
|
(?:\.[0-9]+) (?:_[0-9]+)?
) | (?:
v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )?
|
(?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)?
)
)/x;
# hide optional CPAN::Meta modules from prereq scanner
# and check if they are available
my $cpan_meta = "CPAN::Meta";
my $cpan_meta_pre = "CPAN::Meta::Prereqs";
my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic
# Verify requirements?
my $DO_VERIFY_PREREQS = 1;
sub _max {
my $max = shift;
$max = ( $_ > $max ) ? $_ : $max for @_;
return $max;
}
sub _merge_prereqs {
my ($collector, $prereqs) = @_;
# CPAN::Meta::Prereqs object
if (ref $collector eq $cpan_meta_pre) {
return $collector->with_merged_prereqs(
CPAN::Meta::Prereqs->new( $prereqs )
);
}
# Raw hashrefs
for my $phase ( keys %$prereqs ) {
for my $type ( keys %{ $prereqs->{$phase} } ) {
for my $module ( keys %{ $prereqs->{$phase}{$type} } ) {
$collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module};
}
}
}
return $collector;
}
my @include = qw(
Encode
File::Temp
JSON::PP
Module::Runtime
Sub::Name
YAML::PP
YAML::XS
autodie
JSON::PP
Cpanel::JSON::XS
JSON::XS
Mojolicious
Sereal::Encoder
Sereal::Decoder
Math::BigInt
Math::BigFloat
builtin
builtin::Backport
);
my @exclude = qw(
);
# Add static prereqs to the included modules list
my $static_prereqs = do './t/00-report-prereqs.dd';
# Merge all prereqs (either with ::Prereqs or a hashref)
my $full_prereqs = _merge_prereqs(
( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ),
$static_prereqs
);
# Add dynamic prereqs to the included modules list (if we can)
my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
my $cpan_meta_error;
if ( $source && $HAS_CPAN_META
&& (my $meta = eval { CPAN::Meta->load_file($source) } )
) {
$full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs);
}
else {
$cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source)
$source = 'static metadata';
}
my @full_reports;
my @dep_errors;
my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs;
# Add static includes into a fake section
for my $mod (@include) {
$req_hash->{other}{modules}{$mod} = 0;
}
for my $phase ( qw(configure build test runtime develop other) ) {
next unless $req_hash->{$phase};
next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING});
for my $type ( qw(requires recommends suggests conflicts modules) ) {
next unless $req_hash->{$phase}{$type};
my $title = ucfirst($phase).' '.ucfirst($type);
my @reports = [qw/Module Want Have/];
for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) {
next if grep { $_ eq $mod } @exclude;
my $want = $req_hash->{$phase}{$type}{$mod};
$want = "undef" unless defined $want;
$want = "any" if !$want && $want == 0;
if ($mod eq 'perl') {
push @reports, ['perl', $want, $]];
next;
}
my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required";
my $file = $mod;
$file =~ s{::}{/}g;
$file .= ".pm";
my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC;
if ($prefix) {
my $have = MM->parse_version( File::Spec->catfile($prefix, $file) );
$have = "undef" unless defined $have;
push @reports, [$mod, $want, $have];
if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) {
if ( $have !~ /\A$lax_version_re\z/ ) {
push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)";
}
elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) {
push @dep_errors, "$mod version '$have' is not in required range '$want'";
}
}
}
else {
push @reports, [$mod, $want, "missing"];
if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) {
push @dep_errors, "$mod is not installed ($req_string)";
}
}
}
if ( @reports ) {
push @full_reports, "=== $title ===\n\n";
my $ml = _max( map { length $_->[0] } @reports );
my $wl = _max( map { length $_->[1] } @reports );
my $hl = _max( map { length $_->[2] } @reports );
if ($type eq 'modules') {
splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl];
push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports;
}
else {
splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl];
push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports;
}
push @full_reports, "\n";
}
}
}
if ( @full_reports ) {
diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports;
}
if ( $cpan_meta_error || @dep_errors ) {
diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n";
}
if ( $cpan_meta_error ) {
my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n";
}
if ( @dep_errors ) {
diag join("\n",
"\nThe following REQUIRED prerequisites were not satisfied:\n",
@dep_errors,
"\n"
);
}
pass('Reported prereqs');
# vim: ts=4 sts=4 sw=4 et:
cached-metaschemas.t 100640 000766 000024 5344 15114374332 20532 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use List::Util 'unpairs';
use constant METASCHEMA => 'https://json-schema.org/draft/2019-09/schema';
use lib 't/lib';
use Helper;
# spec version -> vocab classes
my %vocabularies = unpairs(JSON::Schema::Modern->new->__all_metaschema_vocabulary_classes);
subtest 'load cached metaschema' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->_get_resource(METASCHEMA),
undef,
'this resource is not yet known',
);
cmp_result(
$js->_get_or_load_resource(METASCHEMA),
my $resource = +{
canonical_uri => str(METASCHEMA),
path => '',
specification_version => 'draft2019-09',
vocabularies => $vocabularies{'draft2019-09'},
document => all(
isa('JSON::Schema::Modern::Document'),
methods(
schema => superhashof({
'$schema' => str(METASCHEMA),
'$id' => METASCHEMA,
}),
canonical_uri => str(METASCHEMA),
resource_index => ignore,
),
),
},
'loaded metaschema from sharedir cache',
);
cmp_result(
$js->_get_resource(METASCHEMA),
$resource,
'this resource is now in the resource index',
);
};
subtest 'resource collision with cached metaschema' => sub {
my $js = JSON::Schema::Modern->new;
cmp_result(
$js->evaluate(1, { '$id' => METASCHEMA })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => re(qr{^EXCEPTION: \Quri "${ \METASCHEMA }" conflicts with an existing cached schema resource\E}),
},
],
},
'cannot introduce another schema whose id collides with a cached schema that exists in global cache',
);
cmp_result(
$js->evaluate(1, { '$id' => 'http://json-schema.org/draft-07/schema#' })->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => re(qr{^EXCEPTION: \Quri "http://json-schema.org/draft-07/schema" conflicts with an existing cached schema resource\E}),
},
],
},
'cannot introduce another schema whose id collides with a cached schema, even if it isn\'t loaded yet',
);
};
done_testing;
read_serialized_file 100640 000766 000024 6216 15114374332 20715 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ts=8 sts=2 sw=2 tw=100 et ft=perl :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Test2::V0 ();
use Sereal::Decoder;
use lib 't/lib';
use Helper;
my $hub = Test2::API::test2_stack->top;
$hub->set_count(14);
# this is a test that is run from t/serialization.t, incorporating its results into that file.
my @serialized_attributes = sort qw(
specification_version
output_format
short_circuit
max_traversal_depth
validate_formats
validate_content_schemas
collect_annotations
scalarref_booleans
stringy_numbers
strict
_resource_index
_vocabulary_classes
_metaschema_vocabulary_classes
);
my $result = subtest 'thaw object in a separate process' => sub {
local $/;
binmode STDIN, ':raw';
my $thawed = Sereal::Decoder->new->decode(<>);
cmp_result(
[ sort keys %$thawed ],
[ sort @serialized_attributes ],
'thawed object in a new process contains all the right keys',
);
cmp_result(
$thawed->evaluate(1, 'https://my_schema')->TO_JSON,
{
valid => true,
annotations => [
map +{
instanceLocation => '',
keywordLocation => '/'.$_,
absoluteKeywordLocation => 'https://my_schema#/'.$_,
annotation => Test::Deep::ignore,
}, 'format', sort qw(type unknown properties contentMediaType contentSchema),
],
},
'in thawed object, evaluate data against schema with custom dialect; format and unknown keywords are collected as annotations',
);
my $strict_metaschema = {
'$id' => 'https://my_strict_metaschema',
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$vocabulary' => {
'https://json-schema.org/draft/2020-12/vocab/core' => true,
'https://json-schema.org/draft/2020-12/vocab/format-assertion' => true,
},
};
my $strict_schema = {
'$id' => 'https://my_strict_schema',
'$schema' => 'https://my_strict_metaschema',
type => 'number',
format => 'ipv4',
unknown => 1,
properties => { hello => false },
contentMediaType => 'application/json',
contentSchema => {},
};
$thawed->add_schema($strict_metaschema);
$thawed->add_schema($strict_schema);
cmp_result(
$thawed->evaluate('foo', 'https://my_strict_schema')->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/format',
absoluteKeywordLocation => 'https://my_strict_schema#/format',
error => 'not a valid ipv4',
},
],
},
'evaluate data against schema with custom dialect; format-assertion is used',
);
};
# skip the END block which would normally try to print a plan
Test2::API::test2_stack->top->set_no_ending(1);
exit($result ? 0 : -1);
00-compile.t 100644 000766 000024 6504 15114374332 20375 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author use 5.006;
use strict;
use warnings;
# this test was generated with Dist::Zilla::Plugin::Test::Compile 2.058
use Test::More 0.94;
plan tests => 18;
my @module_files = (
'JSON/Schema/Modern.pm',
'JSON/Schema/Modern/Annotation.pm',
'JSON/Schema/Modern/Document.pm',
'JSON/Schema/Modern/Error.pm',
'JSON/Schema/Modern/Result.pm',
'JSON/Schema/Modern/ResultNode.pm',
'JSON/Schema/Modern/Utilities.pm',
'JSON/Schema/Modern/Vocabulary.pm',
'JSON/Schema/Modern/Vocabulary/Applicator.pm',
'JSON/Schema/Modern/Vocabulary/Content.pm',
'JSON/Schema/Modern/Vocabulary/Core.pm',
'JSON/Schema/Modern/Vocabulary/FormatAnnotation.pm',
'JSON/Schema/Modern/Vocabulary/FormatAssertion.pm',
'JSON/Schema/Modern/Vocabulary/MetaData.pm',
'JSON/Schema/Modern/Vocabulary/Unevaluated.pm',
'JSON/Schema/Modern/Vocabulary/Validation.pm'
);
my @scripts = (
'script/json-schema-eval'
);
# no fake home requested
my @switches = (
-d 'blib' ? '-Mblib' : '-Ilib',
);
use File::Spec;
use IPC::Open3;
use IO::Handle;
open my $stdin, '<', File::Spec->devnull or die "can't open devnull: $!";
my @warnings;
for my $lib (@module_files)
{
# see L
my $stderr = IO::Handle->new;
diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'} . $str . q{'} }
$^X, @switches, '-e', "require q[$lib]"))
if $ENV{PERL_COMPILE_TEST_DEBUG};
my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-e', "require q[$lib]");
binmode $stderr, ':crlf' if $^O eq 'MSWin32';
my @_warnings = <$stderr>;
waitpid($pid, 0);
is($?, 0, "$lib loaded ok");
shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/
and not eval { +require blib; blib->VERSION('1.01') };
if (@_warnings)
{
warn @_warnings;
push @warnings, @_warnings;
}
}
foreach my $file (@scripts)
{ SKIP: {
open my $fh, '<', $file or warn("Unable to open $file: $!"), next;
my $line = <$fh>;
close $fh and skip("$file isn't perl", 1) unless $line =~ /^#!\s*(?:\S*perl\S*)((?:\s+-\w*)*)(?:\s*#.*)?$/;
@switches = (@switches, split(' ', $1)) if $1;
close $fh and skip("$file uses -T; not testable with PERL5LIB", 1)
if grep { $_ eq '-T' } @switches and $ENV{PERL5LIB};
my $stderr = IO::Handle->new;
diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'} . $str . q{'} }
$^X, @switches, '-c', $file))
if $ENV{PERL_COMPILE_TEST_DEBUG};
my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-c', $file);
binmode $stderr, ':crlf' if $^O eq 'MSWin32';
my @_warnings = <$stderr>;
waitpid($pid, 0);
is($?, 0, "$file compiled ok");
shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/
and not eval { +require blib; blib->VERSION('1.01') };
# in older perls, -c output is simply the file portion of the path being tested
if (@_warnings = grep { !/\bsyntax OK$/ }
grep { chomp; $_ ne (File::Spec->splitpath($file))[2] } @_warnings)
{
warn @_warnings;
push @warnings, @_warnings;
}
} }
is(scalar(@warnings), 0, 'no warnings found')
or diag 'got warnings: ', explain(\@warnings);
BAIL_OUT("Compilation problems") if !Test::More->builder->is_passing;
pod-syntax.t 100644 000766 000024 252 15114374332 20610 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author #!perl
# This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests.
use strict; use warnings;
use Test::More;
use Test::Pod 1.41;
all_pod_files_ok();
00-report-prereqs.dd 100644 000766 000024 34575 15114374332 20422 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t do { my $x = {
'configure' => {
'requires' => {
'ExtUtils::MakeMaker' => '0',
'File::ShareDir::Install' => '0.06',
'Text::ParseWords' => '0',
'perl' => '5.020'
}
},
'develop' => {
'recommends' => {
'Dist::Zilla::PluginBundle::Author::ETHER' => '0.170',
'Dist::Zilla::PluginBundle::Git::VersionManager' => '0.007'
},
'requires' => {
'Cpanel::JSON::XS' => '4.38',
'Data::Validate::Domain' => '0.13',
'DateTime::Format::RFC3339' => '0',
'Email::Address::XS' => '1.04',
'Encode' => '0',
'ExtUtils::HasCompiler' => '0.014',
'File::Spec' => '0',
'IO::Handle' => '0',
'IPC::Open3' => '0',
'JSON::PP' => '4.11',
'Net::IDN::Encode' => '0',
'Pod::Wordlist' => '0',
'Sereal' => '0',
'Test::CPAN::Changes' => '0.19',
'Test::CPAN::Meta' => '0',
'Test::CleanNamespaces' => '0.15',
'Test::EOL' => '0',
'Test::Kwalitee' => '1.21',
'Test::MinimumVersion' => '0',
'Test::Mojibake' => '0',
'Test::More' => '0.96',
'Test::NoTabs' => '0',
'Test::Pod' => '1.41',
'Test::Pod::Coverage::TrustMe' => '0.002001',
'Test::Portability::Files' => '0',
'Test::Spelling' => '0.17',
'Time::Moment' => '0'
}
},
'runtime' => {
'requires' => {
'B' => '0',
'Carp' => '0',
'Digest::MD5' => '0',
'Exporter' => '0',
'Feature::Compat::Try' => '0',
'File::ShareDir' => '0',
'Getopt::Long::Descriptive' => '0',
'List::Util' => '1.55',
'MIME::Base64' => '0',
'Math::BigFloat' => '0',
'Math::BigInt' => '1.999701',
'Mojo::File' => '0',
'Mojo::JSON' => '0',
'Mojo::JSON::Pointer' => '0',
'Mojo::Message::Response' => '0',
'Mojo::URL' => '0',
'Mojolicious' => '7.87',
'Moo' => '0',
'Moo::Role' => '0',
'MooX::TypeTiny' => '0.002002',
'Safe::Isa' => '1.000008',
'Scalar::Util' => '0',
'Storable' => '0',
'Types::Common::Numeric' => '0',
'Types::Standard' => '1.016003',
'YAML::PP' => '0',
'autovivification' => '0',
'builtin::compat' => '0.003003',
'constant' => '0',
'experimental' => '0.026',
'feature' => '0',
'if' => '0',
'namespace::clean' => '0',
'open' => '0',
'overload' => '0',
'perl' => 'v5.20.0',
'stable' => '0.031',
'strict' => '0',
'strictures' => '2',
'warnings' => '0'
},
'suggests' => {
'Class::XSAccessor' => '0',
'Cpanel::JSON::XS' => '4.38',
'Data::Validate::Domain' => '0.13',
'DateTime::Format::RFC3339' => '0',
'Email::Address::XS' => '1.04',
'JSON::PP' => '4.11',
'Net::IDN::Encode' => '0',
'Sereal' => '0',
'Time::Moment' => '0',
'Type::Tiny::XS' => '0'
}
},
'test' => {
'recommends' => {
'CPAN::Meta' => '2.120900'
},
'requires' => {
'CPAN::Meta::Check' => '0.011',
'CPAN::Meta::Requirements' => '0',
'Data::Dumper' => '0',
'ExtUtils::MakeMaker' => '0',
'File::Spec' => '0',
'Math::BigInt' => '1.999701',
'Term::ANSIColor' => '0',
'Test2::API' => '0',
'Test2::V0' => '0',
'Test2::Warnings' => '0.038',
'Test::Deep' => '0',
'Test::Deep::UnorderedPairs' => '0',
'Test::File::ShareDir' => '0',
'Test::JSON::Schema::Acceptance' => '1.035',
'Test::Memory::Cycle' => '0',
'Test::More' => '0',
'Test::Needs' => '0',
'Test::Without::Module' => '0.19',
'lib' => '0',
'perl' => 'v5.20.0',
'utf8' => '0'
}
},
'x_Dist_Zilla' => {
'requires' => {
'Dist::Zilla' => '5',
'Dist::Zilla::Plugin::Authority' => '1.009',
'Dist::Zilla::Plugin::AutoMetaResources' => '0',
'Dist::Zilla::Plugin::AutoPrereqs' => '5.038',
'Dist::Zilla::Plugin::Breaks' => '0',
'Dist::Zilla::Plugin::BumpVersionAfterRelease::Transitional' => '0.004',
'Dist::Zilla::Plugin::CheckIssues' => '0',
'Dist::Zilla::Plugin::CheckMetaResources' => '0',
'Dist::Zilla::Plugin::CheckPrereqsIndexed' => '0.019',
'Dist::Zilla::Plugin::CheckSelfDependency' => '0',
'Dist::Zilla::Plugin::CheckStrictVersion' => '0',
'Dist::Zilla::Plugin::ConfirmRelease' => '0',
'Dist::Zilla::Plugin::CopyFilesFromRelease' => '0',
'Dist::Zilla::Plugin::DynamicPrereqs' => '0',
'Dist::Zilla::Plugin::EnsureLatestPerl' => '0',
'Dist::Zilla::Plugin::ExecDir' => '0',
'Dist::Zilla::Plugin::FileFinder::ByName' => '0',
'Dist::Zilla::Plugin::GenerateFile::FromShareDir' => '0',
'Dist::Zilla::Plugin::Git::Check' => '0',
'Dist::Zilla::Plugin::Git::CheckFor::CorrectBranch' => '0.004',
'Dist::Zilla::Plugin::Git::CheckFor::MergeConflicts' => '0',
'Dist::Zilla::Plugin::Git::Commit' => '2.020',
'Dist::Zilla::Plugin::Git::Contributors' => '0.029',
'Dist::Zilla::Plugin::Git::Describe' => '0.004',
'Dist::Zilla::Plugin::Git::GatherDir' => '2.016',
'Dist::Zilla::Plugin::Git::Push' => '0',
'Dist::Zilla::Plugin::Git::Remote::Check' => '0',
'Dist::Zilla::Plugin::Git::Tag' => '0',
'Dist::Zilla::Plugin::GitHub::Update' => '0.40',
'Dist::Zilla::Plugin::GithubMeta' => '0.54',
'Dist::Zilla::Plugin::InstallGuide' => '1.200005',
'Dist::Zilla::Plugin::Keywords' => '0.004',
'Dist::Zilla::Plugin::License' => '5.038',
'Dist::Zilla::Plugin::MakeMaker' => '0',
'Dist::Zilla::Plugin::Manifest' => '0',
'Dist::Zilla::Plugin::MetaConfig' => '0',
'Dist::Zilla::Plugin::MetaJSON' => '0',
'Dist::Zilla::Plugin::MetaNoIndex' => '0',
'Dist::Zilla::Plugin::MetaProvides::Package' => '1.15000002',
'Dist::Zilla::Plugin::MetaTests' => '0',
'Dist::Zilla::Plugin::MetaYAML' => '0',
'Dist::Zilla::Plugin::MinimumPerl' => '1.006',
'Dist::Zilla::Plugin::MojibakeTests' => '0.8',
'Dist::Zilla::Plugin::NextRelease' => '5.033',
'Dist::Zilla::Plugin::PodSyntaxTests' => '5.040',
'Dist::Zilla::Plugin::PodWeaver' => '4.008',
'Dist::Zilla::Plugin::Prereqs' => '0',
'Dist::Zilla::Plugin::Prereqs::AuthorDeps' => '0.006',
'Dist::Zilla::Plugin::Prereqs::Soften' => '0',
'Dist::Zilla::Plugin::PromptIfStale' => '0',
'Dist::Zilla::Plugin::Readme' => '0',
'Dist::Zilla::Plugin::ReadmeAnyFromPod' => '0.142180',
'Dist::Zilla::Plugin::RewriteVersion::Transitional' => '0.006',
'Dist::Zilla::Plugin::Run::AfterBuild' => '0.041',
'Dist::Zilla::Plugin::Run::AfterRelease' => '0.038',
'Dist::Zilla::Plugin::Run::BeforeRelease' => '0',
'Dist::Zilla::Plugin::RunExtraTests' => '0.024',
'Dist::Zilla::Plugin::ShareDir' => '0',
'Dist::Zilla::Plugin::StaticInstall' => '0.005',
'Dist::Zilla::Plugin::Test::CPAN::Changes' => '0.012',
'Dist::Zilla::Plugin::Test::ChangesHasContent' => '0',
'Dist::Zilla::Plugin::Test::CheckBreaks' => '0',
'Dist::Zilla::Plugin::Test::CleanNamespaces' => '0.006',
'Dist::Zilla::Plugin::Test::Compile' => '2.039',
'Dist::Zilla::Plugin::Test::EOL' => '0.17',
'Dist::Zilla::Plugin::Test::Kwalitee' => '2.10',
'Dist::Zilla::Plugin::Test::MinimumVersion' => '2.000010',
'Dist::Zilla::Plugin::Test::NoTabs' => '0.08',
'Dist::Zilla::Plugin::Test::Pod::Coverage::TrustMe' => '0',
'Dist::Zilla::Plugin::Test::PodSpelling' => '2.006003',
'Dist::Zilla::Plugin::Test::Portability' => '2.000007',
'Dist::Zilla::Plugin::Test::ReportPrereqs' => '0.022',
'Dist::Zilla::Plugin::TestRelease' => '0',
'Dist::Zilla::Plugin::UploadToCPAN' => '0',
'Dist::Zilla::Plugin::UseUnsafeInc' => '0',
'Dist::Zilla::PluginBundle::Author::ETHER' => '0.154',
'Dist::Zilla::PluginBundle::Git::VersionManager' => '0.007',
'Software::License::Perl_5' => '0'
}
}
};
$x;
} script 000755 000766 000024 0 15114374332 15545 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627 json-schema-eval 100640 000766 000024 21363 15114374332 21005 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/script #!/usr/bin/perl
# vim: set ts=8 sts=2 sw=2 tw=100 et :
# PODNAME: json-schema-eval
# ABSTRACT: A command-line interface to JSON::Schema::Modern::evaluate()
use 5.020; # for fc, unicode_strings features
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Getopt::Long::Descriptive;
use Mojo::File 'path';
use Safe::Isa;
use Feature::Compat::Try;
use JSON::Schema::Modern;
my ($opt, $usage) = Getopt::Long::Descriptive::describe_options(
"$0 %o",
['help|usage|?|h', 'print usage information and exit', { shortcircuit => 1 } ],
[],
['specification_version|version=s', 'which version of the JSON Schema specification to use'],
['output_format=s', 'output format (flag, basic, terse)'],
['short_circuit', 'return early in any execution path as soon as the outcome can be determined'],
['max_traversal_depth=i', 'the maximum number of levels deep a schema traversal may go'],
['validate_formats', 'treat the "format" keyword as an assertion, not merely an annotation'],
['validate_content_schemas', 'treat the "contentMediaType" and "contentSchema" keywords as assertions'],
['collect_annotations', 'collect annotations'],
['strict', 'disallow unknown keywords'],
# scalarref_booleans, stringy_numbers make no sense in json-encoded data
[],
['validate-schema:s', 'validate the provided schema against its meta-schema and the specification. do not provide --data or --schema.' ],
['add-schema=s@', 'the filename of an extra schema to load, so it can be used by $ref' ],
['data=s', 'the filename to use for the instance data (if not provided, STDIN is used)'],
['schema=s', 'the filename to use for the schema (if not provided, STDIN is used)'],
['dump-identifiers', 'print a list of all identifiers found in the schema'],
);
print($usage->text), exit if $opt->help;
my ($data, $schema, $validate_schema) = delete $opt->@{qw(data schema validate_schema)};
die '--validate-schema and --data should not be used together' if defined $data and defined $validate_schema;
die '--validate-schema and --schema should not be used together' if defined $schema and defined $validate_schema;
my $js = JSON::Schema::Modern->new(%$opt);
foreach my $add_schema_file (@{$opt->add_schema//[]}) {
try {
$js->add_schema('file://'.$add_schema_file => parse_input(path($add_schema_file)->slurp('UTF-8')));
}
catch ($e) {
say $e->$_isa('JSON::Schema::Modern::Result') ? $e->dump: '"'.$e.'"';
exit 2;
}
}
my $result;
my $schema_filename = '';
if (defined $validate_schema) {
if (length $validate_schema) { # boolean flag is passed as ''; some other value = filename
$schema = path($schema_filename = $validate_schema)->slurp('UTF-8');
}
else {
say 'enter schema, followed by ^D:';
local $/;
$schema = ;
say '';
}
$result = $js->validate_schema(parse_input($schema));
}
else {
if (defined $data) {
$data = path($data)->slurp('UTF-8');
}
else {
say 'enter data instance, followed by ^D:';
local $/;
$data = ;
STDIN->clearerr;
}
if (defined $schema) {
$schema = path($schema_filename = $schema)->slurp('UTF-8');
}
else {
say 'enter schema, followed by ^D:';
local $/;
$schema = ;
say '';
}
$data = parse_input($data);
$schema = parse_input($schema);
# if there is no $id within the document, the filename will be used instead
$js->add_schema(my $uri = 'file://'.$schema_filename, $schema) if length $schema_filename;
$result = $js->evaluate($data, $uri // $schema);
}
say $result->output_format eq 'data_only' ? $result : $result->dump;
if ($opt->dump_identifiers) {
$js->add_schema($schema) if $validate_schema;
my %identifiers = map +(
$_->[0] => {
canonical_uri => $_->[1]{canonical_uri},
document_base => $_->[1]{document}->canonical_uri,
document_path => $_->[1]{path},
}
),
grep $_->[0] !~ m{^https://json-schema.org/},
$js->_resource_pairs;
my $encoder = JSON::Schema::Modern::_JSON_BACKEND()->new
->convert_blessed(1)
->utf8(0)
->canonical(1)
->pretty(1);
$encoder->indent_length(2) if $encoder->can('indent_length');
say $encoder->encode(\%identifiers);
}
exit($result->valid ? 0 : $result->exception ? 2 : 1);
sub parse_input ($input) {
if ($input =~ /^(\{|\[\|["0-9]|true\b|false\b|null\b)/) {
# this looks like json
state $json_decoder = JSON::Schema::Modern::_JSON_BACKEND()->new->allow_nonref(1)->utf8(0);
return $json_decoder->decode($input);
}
else {
# well I suppose it must be yaml
require YAML::PP;
state $yaml_decoder = YAML::PP->new(boolean => 'JSON::PP');
return $yaml_decoder->load_string($input);
}
}
__END__
=pod
=encoding UTF-8
=head1 NAME
json-schema-eval - A command-line interface to JSON::Schema::Modern::evaluate()
=head1 VERSION
version 0.627
=head1 SYNOPSIS
json-schema-eval \
[ --specification_version|version ] \
[ --output_format ] \
[ --short_circuit ] \
[ --max_traversal_depth ] \
[ --validate_formats ] \
[ --validate_content_schemas ] \
[ --collect_annotations ] \
[ --strict ] \
[ --data ] \
[ --schema ] \
[ --validate-schema [filename] ]
[ --add-schema ]
[ --data ]
[ --schema ]
[ --dump-identifiers ]
=head1 DESCRIPTION
A command-line interface to L.
F contains:
{"hello": 42.1}
F contains:
{"properties": {"hello": {"type": ["string", "integer"]}}}
Run:
json-schema-eval --data data.json --schema schema.json
produces output:
{
"errors" : [
{
"error" : "got number, not one of string, integer",
"instanceLocation" : "/hello",
"keywordLocation" : "/properties/hello/type"
},
{
"error" : "not all properties are valid",
"instanceLocation" : "",
"keywordLocation" : "/properties"
}
],
"valid" : false
}
Or run:
json-schema-eval --validate-schema schema.json
produces output:
{
"valid": true
}
The exit value (C<$?>) is 0 when the result is valid, 1 when it is invalid,
and some other non-zero value if an exception occurred.
=head1 OPTIONS
=for stopwords schemas
All boolean and string options used as L are available.
Additionally, C<--data> is used to provide the filename containing a JSON- or YAML-encoded data
instance, and C<--schema> provides the filename containing a JSON- or YAML-encoded schema.
If either or both of these are not provided, STDIN is used as input.
Both JSON- and YAML-encoded data and schemas are supported, using heuristics based on the
content of the first line of the data.
Alternatively, you can use C<--validate-schema> and either provide a filename containing a
JSON-encoded schema, or omit the argument to read a schema from STDIN. The schema
will be evaluated against its meta-schema for the corresponding specification version.
Additional schemas, that you wish to use via the C<$ref> keyword, can be added with
C<< --add-schema >>. The actual filename is insignificant: Make sure you use an C<$id>
keyword within that schema that matches the value you use in the C<$ref>. This option can be used
more than once.
=for stopwords OpenAPI
=head1 AVAILABILITY
This executable is available on modern Debian versions (via C) as the
C package.
=head1 GIVING THANKS
=for stopwords MetaCPAN GitHub
If you found this module to be useful, please show your appreciation by
adding a +1 in L
and a star in L.
=head1 SUPPORT
Bugs may be submitted through L.
I am also usually active on irc, as 'ether' at C and C.
=for stopwords OpenAPI
You can also find me on the L and L, which are also great resources for finding help.
=head1 AUTHOR
Karen Etheridge
=head1 COPYRIGHT AND LICENCE
This software is copyright (c) 2020 by Karen Etheridge.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
Some schema files have their own licence, in share/LICENSE.
=cut
max_traversal_depth.t 100640 000766 000024 5045 15114374332 21065 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new(max_traversal_depth => 6);
cmp_result(
$js->evaluate(
[ [ [ [ [ 1 ] ] ] ] ],
{
items => { '$ref' => '#' },
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '/0/0/0/0',
keywordLocation => '/items/$ref/items/$ref/items/$ref/items',
absoluteKeywordLocation => '#/items',
error => 'EXCEPTION: maximum evaluation depth (6) exceeded',
},
],
},
'evaluation is halted when traversal gets too deep',
);
cmp_result(
$js->evaluate(
1,
{
'$defs' => {
loop_a => {
'$ref' => '#/$defs/loop_b',
},
loop_b => {
'$ref' => '#/$defs/loop_a',
},
},
'$ref' => '#/$defs/loop_a',
},
)->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '/$ref/$ref/$ref',
absoluteKeywordLocation => '#/$defs/loop_a',
error => 'EXCEPTION: infinite loop detected (same location evaluated twice)',
},
],
},
'evaluation is halted when an instance location is evaluated against the same schema location a second time',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$defs' => { mydef => { '$id' => '/properties/foo' } },
properties => {
foo => {
'$ref' => '/properties/foo',
},
},
},
)->TO_JSON,
{ valid => true },
'the seen counter does not confuse URI paths and fragments: /properties/foo vs #/properties/foo',
);
cmp_result(
$js->evaluate(
{ foo => 1 },
{
'$defs' => {
int => { type => 'integer' },
},
anyOf => [
{ additionalProperties => { '$ref' => '#/$defs/int' } },
{ additionalProperties => { '$ref' => '#/$defs/int' } },
],
}
)->TO_JSON,
{ valid => true },
'the seen counter does not confuse two subschemas that both apply the same definition to the same instance location',
);
done_testing;
portability.t 100644 000766 000024 130 15114374332 21037 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author use strict;
use warnings;
use Test::More;
use Test::Portability::Files;
run_tests();
draft4 000755 000766 000024 0 15114374332 16527 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/share schema.json 100640 000766 000024 10405 15114374332 21036 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/share/draft4 {
"id": "http://json-schema.org/draft-04/schema#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string"
},
"$schema": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
}
draft6 000755 000766 000024 0 15114374332 16531 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/share schema.json 100640 000766 000024 10621 15114374332 21040 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/share/draft6 {
"$schema": "http://json-schema.org/draft-06/schema#",
"$id": "http://json-schema.org/draft-06/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"examples": {
"type": "array",
"items": {}
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": {},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": {}
}
draft7 000755 000766 000024 0 15114374332 16532 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/share schema.json 100640 000766 000024 11563 15114374332 21047 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/share/draft7 {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://json-schema.org/draft-07/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"$comment": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": true
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": true,
"enum": {
"type": "array",
"items": true,
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentEncoding": { "type": "string" },
"if": { "$ref": "#" },
"then": { "$ref": "#" },
"else": { "$ref": "#" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": true
}
evaluate_json_string.t 100640 000766 000024 2504 15114374332 21253 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
my $js = JSON::Schema::Modern->new;
like(ref($js->_json_decoder), qr/^(?:Cpanel::JSON::XS|JSON::PP)$/, 'we have a JSON decoder');
ok(
lives {
ok($js->evaluate_json_string('true', {})->valid, 'json data "true" is evaluated successfully');
},
'no exceptions in evaluate_json_string on good json',
);
ok(
lives {
cmp_result(
$js->evaluate_json_string('blargh', {})->TO_JSON,
{
valid => false,
errors => [
{
instanceLocation => '',
keywordLocation => '',
error => re(qr/malformed JSON string/),
},
],
},
'evaluating bad json data returns false, with error',
);
},
'no exceptions in evaluate_json_string on bad json',
);
done_testing;
unsupported-keywords.t 100640 000766 000024 5166 15114374332 21272 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/t # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use lib 't/lib';
use Helper;
use Test2::Warnings qw(warnings :no_end_test had_no_warnings);
my %strings = (
id => qr/^no-longer-supported "id" keyword present \(at location ""\): this should be rewritten as "\$id" at /,
definitions => qr/^no-longer-supported "definitions" keyword present \(at location ""\): this should be rewritten as "\$defs" at /,
dependencies => qr/^no-longer-supported "dependencies" keyword present \(at location ""\): this should be rewritten as "dependentSchemas" or "dependentRequired" at /,
);
my %schemas = (
id => 'https://localhost:1234',
definitions => {},
dependencies => {},
);
my @warnings = (
[ draft6 => [ qw(id) ] ],
[ draft7 => [ qw(id) ] ],
[ 'draft2019-09' => [ qw(id definitions dependencies) ] ],
);
foreach my $index (0 .. $#warnings) {
my ($spec_version, $removed_keywords) = $warnings[$index]->@*;
note "\n", $spec_version;
my $js = JSON::Schema::Modern->new(specification_version => $spec_version);
foreach my $keyword (@$removed_keywords) {
cmp_result(
[ warnings {
cmp_result(
$js->evaluate(true, { $keyword => $schemas{$keyword} })->TO_JSON,
{ valid => true },
'schema with "'.$keyword.'" still validates in '.$spec_version,
)
} ],
superbagof(re($strings{$keyword})),
'warned for "'.$keyword.'" in '.$spec_version,
);
}
next if $index == $#warnings;
my ($next_spec_version, $removed_next_keywords) = $warnings[$index+1]->@*;
foreach my $keyword (@$removed_next_keywords) {
next if grep $keyword eq $_, @$removed_keywords;
local $SIG{__WARN__} = sub {
warn @_ if $_[0] =~ /^no-longer-supported "$keyword" keyword present/;
};
cmp_result(
[ warnings {
cmp_result(
$js->evaluate(true, { $keyword => $schemas{$keyword} })->TO_JSON,
{ valid => true },
'schema with "'.$keyword.'" validates in '.$spec_version,
)
} ],
[],
'did not warn for "'.$keyword.'" in '.$spec_version,
);
}
}
had_no_warnings if $ENV{AUTHOR_TESTING};
done_testing;
pod-coverage.t 100644 000766 000024 1765 15114374332 21107 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/xt/author # This file was automatically generated by Dist::Zilla::Plugin::Test::Pod::Coverage::TrustMe v1.0.1
use strict;
use warnings;
use Test::More;
use Test::Pod::Coverage::TrustMe;
my $config = {};
my $modules = [
"JSON::Schema::Modern",
"JSON::Schema::Modern::Annotation",
"JSON::Schema::Modern::Document",
"JSON::Schema::Modern::Error",
"JSON::Schema::Modern::Result",
"JSON::Schema::Modern::ResultNode",
"JSON::Schema::Modern::Utilities",
"JSON::Schema::Modern::Vocabulary",
"JSON::Schema::Modern::Vocabulary::Applicator",
"JSON::Schema::Modern::Vocabulary::Content",
"JSON::Schema::Modern::Vocabulary::Core",
"JSON::Schema::Modern::Vocabulary::FormatAnnotation",
"JSON::Schema::Modern::Vocabulary::FormatAssertion",
"JSON::Schema::Modern::Vocabulary::MetaData",
"JSON::Schema::Modern::Vocabulary::Unevaluated",
"JSON::Schema::Modern::Vocabulary::Validation",
];
plan tests => scalar @$modules;
for my $module (@$modules) {
pod_coverage_ok($module, $config);
}
done_testing;
Schema 000755 000766 000024 0 15114374332 16760 5 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/lib/JSON Modern.pm 100640 000766 000024 242736 15114374332 20754 0 ustar 00ether staff 000000 000000 JSON-Schema-Modern-0.627/lib/JSON/Schema use strict;
use warnings;
package JSON::Schema::Modern; # git description: v0.626-11-ga2961894
# vim: set ts=8 sts=2 sw=2 tw=100 et :
# ABSTRACT: Validate data against a schema using a JSON Schema
# KEYWORDS: JSON Schema validator data validation structure specification
our $VERSION = '0.627';
use 5.020; # for fc, unicode_strings features
use Moo;
use strictures 2;
use stable 0.031 'postderef';
use experimental 'signatures';
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
use Carp qw(croak carp);
use List::Util 1.55 qw(pairs first uniqint pairmap uniq min);
use if "$]" < 5.041010, 'List::Util' => 'any';
use if "$]" >= 5.041010, experimental => 'keyword_any';
use builtin::compat qw(refaddr load_module);
use Mojo::URL;
use Safe::Isa;
use Mojo::File 'path';
use Storable 'dclone';
use File::ShareDir 'dist_dir';
use MooX::TypeTiny 0.002002;
use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
use Digest::MD5 'md5';
use Feature::Compat::Try;
use JSON::Schema::Modern::Error;
use JSON::Schema::Modern::Result;
use JSON::Schema::Modern::Document;
use JSON::Schema::Modern::Utilities qw(get_type canonical_uri E abort annotate_self jsonp is_type assert_uri local_annotations is_schema json_pointer_type canonical_uri_type load_cached_document);
use namespace::clean;
our @CARP_NOT = qw(
JSON::Schema::Modern::Document
JSON::Schema::Modern::Vocabulary
JSON::Schema::Modern::Vocabulary::Applicator
JSON::Schema::Modern::Document::OpenAPI
OpenAPI::Modern
);
use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft4 draft6 draft7 draft2019-09 draft2020-12)];
has specification_version => (
is => 'ro',
isa => Enum(SPECIFICATION_VERSIONS_SUPPORTED),
coerce => sub {
return $_[0] if any { $_[0] eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*;
my $real = 'draft'.($_[0]//'');
(any { $real eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*) ? $real : $_[0];
},
);
has output_format => (
is => 'ro',
isa => Enum(JSON::Schema::Modern::Result->OUTPUT_FORMATS),
default => 'basic',
);
has short_circuit => (
is => 'ro',
isa => Bool,
lazy => 1,
default => sub { $_[0]->output_format eq 'flag' && !$_[0]->collect_annotations },
);
has max_traversal_depth => (
is => 'ro',
isa => Int,
default => 50,
);
has validate_formats => (
is => 'ro',
isa => Bool,
lazy => 1,
# as specified by https://json-schema.org/draft//schema#/$vocabulary
default => sub { ($_[0]->specification_version//SPECIFICATION_VERSION_DEFAULT) =~ /^draft[467]$/ ? 1 : 0 },
);
has validate_content_schemas => (
is => 'ro',
isa => Bool,
lazy => 1,
# defaults to false in latest versions, as specified by
# https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.8.2
default => sub { ($_[0]->specification_version//'') eq 'draft7' },
);
has [qw(collect_annotations scalarref_booleans stringy_numbers strict)] => (
is => 'ro',
isa => Bool,
);
# Validation §7.1-2: "Note that the "type" keyword in this specification defines an "integer" type
# which is not part of the data model. Therefore a format attribute can be limited to numbers, but
# not specifically to integers."
my $core_types = Enum[qw(null object array boolean string number)];
my @core_formats = qw(date-time date time duration email idn-email hostname idn-hostname ipv4 ipv6 uri uri-reference iri iri-reference uuid uri-template json-pointer relative-json-pointer regex);
# { $format_name => { type => ..., sub => ... }, ... }
has _format_validations => (
is => 'bare',
isa => my $format_type = HashRef[Dict[
type => $core_types|ArrayRef[$core_types],
sub => CodeRef,
]],
init_arg => 'format_validations',
);
sub _get_format_validation ($self, $format) { ($self->{_format_validations}//{})->{$format} }
sub add_format_validation ($self, $format, $definition) {
return if exists(($self->{_format_validations}//{})->{$format});
$definition = { type => 'string', sub => $definition } if ref $definition ne 'HASH';
$format_type->({ $format => $definition });
# all core formats are of type string (so far); changing type of custom format is permitted
croak "Type for override of format $format does not match original type"
if any { $format eq $_ } @core_formats and $definition->{type} ne 'string';
use autovivification 'store';
$self->{_format_validations}{$format} = $definition;
}
around BUILDARGS => sub ($orig, $class, @args) {
my $args = $class->$orig(@args);
croak 'output_format: strict_basic can only be used with specification_version: draft2019-09'
if ($args->{output_format}//'') eq 'strict_basic'
and ($args->{specification_version}//'') ne 'draft2019-09';
croak 'collect_annotations cannot be used with specification_version '.$args->{specification_version}
if $args->{collect_annotations} and ($args->{specification_version}//'') =~ /^draft[467]$/;
$args->{format_validations} = +{
map +($_->[0] => ref $_->[1] eq 'HASH' ? $_->[1] : +{ type => 'string', sub => $_->[1] }),
pairs $args->{format_validations}->%*
} if $args->{format_validations};
return $args;
};
sub add_schema {
croak 'insufficient arguments' if @_ < 2;
my $self = shift;
if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
Carp::carp('use of deprecated form of add_schema with document');
return $self->add_document($_[0]);
}
# TODO: resolve $uri against $self->base_uri
my $uri = !is_schema($_[0]) ? Mojo::URL->new(shift)
: $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
croak 'insufficient arguments' if not @_;
if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
Carp::carp('use of deprecated form of add_schema with document');
return $self->add_document($uri, $_[0]);
}
# document BUILD will trigger $self->traverse($schema)
# Note we do not pass the uri to the document constructor, so resources in that document may still
# be relative
my $document = JSON::Schema::Modern::Document->new(
schema => $_[0],
evaluator => $self, # used mainly for traversal during document construction
);
# try to reuse the same document, if the same schema is being added twice:
# this results in _add_resource silently ignoring the duplicate add, rather than erroring.
my $schema_checksum = $document->_checksum(md5($self->_json_decoder->encode($document->schema)));
if (my $existing_doc = first {
my $existing_checksum = $_->_checksum
// $_->_checksum(md5($self->_json_decoder->encode($_->schema)));
$existing_checksum eq $schema_checksum
and $_->canonical_uri eq $document->canonical_uri
# FIXME: must also check spec version/metaschema_uri/vocabularies
} uniqint map $_->{document}, $self->_canonical_resources) {
$document = $existing_doc;
}
$self->add_document($uri, $document);
}
sub add_document {
croak 'insufficient arguments' if @_ < 2;
my $self = shift;
# TODO: resolve $uri against $self->base_uri
my $base_uri = !$_[0]->$_isa('JSON::Schema::Modern::Document') ? Mojo::URL->new(shift)
: $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
croak 'cannot add a schema with a uri with a fragment' if defined $base_uri->fragment;
croak 'insufficient arguments' if not @_;
my $document = shift;
croak 'wrong document type' if not $document->$_isa('JSON::Schema::Modern::Document');
# we will never add a document to the resource index if it has errors
die JSON::Schema::Modern::Result->new(
output_format => $self->output_format,
valid => 0,
errors => [ $document->errors ],
exception => 1,
) if $document->has_errors;
if (not length $base_uri){
foreach my $res_pair ($document->resource_pairs) {
my ($uri_string, $doc_resource) = @$res_pair;
# this might croak if there are duplicates or malformed entries.
$self->_add_resource($uri_string => +{ $doc_resource->%*, document => $document });
}
return $document;
}
my @root; # uri_string => resource hash of the resource at path ''
# document resources are added after resolving each resource against our provided base uri
foreach my $res_pair ($document->resource_pairs) {
my ($uri_string, $doc_resource) = @$res_pair;
$uri_string = Mojo::URL->new($uri_string)->to_abs($base_uri)->to_string;
my $new_resource = {
canonical_uri => Mojo::URL->new($doc_resource->{canonical_uri})->to_abs($base_uri),
$doc_resource->%{qw(path specification_version vocabularies)},
document => $document,
};
foreach my $anchor (keys (($doc_resource->{anchors}//{})->%*)) {
use autovivification 'store';
$new_resource->{anchors}{$anchor} = {
$doc_resource->{anchors}{$anchor}->%{path},
(map +($_->[1] ? @$_ : ()), [ $doc_resource->{anchors}{$anchor}->%{dynamic} ]),
canonical_uri => Mojo::URL->new($doc_resource->{anchors}{$anchor}{canonical_uri})->to_abs($base_uri),
};
}
# this might croak if there are duplicates or malformed entries.
$self->_add_resource($uri_string => $new_resource);
@root = ($uri_string => $new_resource) if $new_resource->{path} eq '' and $uri_string !~ /#./;
}
# associate the root resource with the base uri we were provided, if it does not already exist
$self->_add_resource($base_uri.'' => $root[1]) if $root[0] ne $base_uri;
return $document;
}
sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
croak 'evaluate_json_string called in void context' if not defined wantarray;
my $data;
try {
$data = $self->_json_decoder->decode($json_data)
}
catch ($e) {
return JSON::Schema::Modern::Result->new(
output_format => $self->output_format,
valid => 0,
exception => 1,
errors => [
JSON::Schema::Modern::Error->new(
depth => 0,
mode => 'evaluate',
keyword => undef,
instance_location => '',
keyword_location => '',
error => $e,
)
],
);
}
return $self->evaluate($data, $schema, $config_override);
}
# this is called whenever we need to walk a document for something.
# for now it is just called when a ::Document object is created, to verify the integrity of the
# schema structure, to identify the metaschema (via the $schema keyword), and to extract all
# embedded resources via $id and $anchor keywords within.
# Returns the internal $state object accumulated during the traversal.
sub traverse ($self, $schema_reference, $config_override = {}) {
my %overrides = %$config_override;
delete @overrides{qw(callbacks initial_schema_uri metaschema_uri traversed_keyword_path specification_version)};
croak join(', ', sort keys %overrides), ' not supported as a config override in traverse'
if keys %overrides;
# Note: the starting position is not guaranteed to be at the root of the $document,
# nor is the fragment portion of this uri necessarily empty
my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // ());
my $initial_path = $config_override->{traversed_keyword_path} // '';
my $spec_version = $config_override->{specification_version} // $self->specification_version // SPECIFICATION_VERSION_DEFAULT;
croak 'traversed_keyword_path must be a json pointer' if $initial_path !~ m{^(?:/|$)};
if (length(my $uri_path = $initial_uri->fragment)) {
croak 'initial_schema_uri fragment must be a json pointer' if $uri_path !~ m{^/};
croak 'traversed_keyword_path does not match initial_schema_uri path fragment'
if substr($initial_path, -length($uri_path)) ne $uri_path;
}
my $state = {
depth => 0,
data_path => '', # this never changes since we don't have an instance yet
initial_schema_uri => $initial_uri, # the canonical URI as of the start of this method or last $id
traversed_keyword_path => $initial_path, # the accumulated traversal path as of the start or last $id
keyword_path => '', # the rest of the path, since the start of this method or last $id
specification_version => $spec_version,
errors => [],
identifiers => {},
subschemas => [],
callbacks => $config_override->{callbacks} // {},
evaluator => $self,
traverse => 1,
};
my $valid = 1;
try {
# determine the initial value of specification_version and vocabularies, so we have something to start
# with in _traverse_subschema().
# a subsequent "$schema" keyword can still change these values, and it is always processed
# first, so the override is skipped if the keyword exists in the schema
$state->{metaschema_uri} =
(ref $schema_reference eq 'HASH' && exists $schema_reference->{'$schema'} ? undef
: $config_override->{metaschema_uri}) // $self->METASCHEMA_URIS->{$spec_version};
if (my $metaschema_info = $self->_get_metaschema_vocabulary_classes($state->{metaschema_uri})) {
$state->@{qw(specification_version vocabularies)} = @$metaschema_info;
}
else {
# metaschema has not been processed for vocabularies yet...
die 'something went wrong - cannot get metaschema data for '.$state->{metaschema_uri}
if not $config_override->{metaschema_uri};
# use the Core vocabulary to set metaschema info via the '$schema' keyword implementation
$valid = $self->_get_metaschema_vocabulary_classes($self->METASCHEMA_URIS->{$spec_version})->[1][0]
->_traverse_keyword_schema({ '$schema' => $state->{metaschema_uri}.'' }, $state);
}
$valid = $self->_traverse_subschema($schema_reference, $state) if $valid and not $state->{errors}->@*;
die 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
die 'result is true but there are errors' if $valid and $state->{errors}->@*;
}
catch ($e) {
if ($e->$_isa('JSON::Schema::Modern::Result')) {
push $state->{errors}->@*, $e->errors;
}
elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
# note: we should never be here, since traversal subs are no longer fatal
push $state->{errors}->@*, $e;
}
else {
E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
}
}
delete $state->{traverse};
return $state;
}
# the actual runtime evaluation of the schema against input data.
sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
croak 'evaluate called in void context' if not defined wantarray;
my %overrides = %$config_override;
delete @overrides{qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict callbacks data_path traversed_keyword_path _strict_schema_data)};
croak join(', ', sort keys %overrides), ' not supported as a config override in evaluate'
if keys %overrides;
my $state = {
data_path => $config_override->{data_path} // '',
traversed_keyword_path => $config_override->{traversed_keyword_path} // '', # the accumulated path as of the start of evaluation or last $id or $ref
initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start of evaluation or last $id or $ref
keyword_path => '', # the rest of the path, since the start of evaluation or last $id or $ref
errors => [],
depth => 0,
};
my $valid;
try {
if (is_schema($schema_reference)) {
# traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
$schema_reference = $self->add_schema($schema_reference)->canonical_uri;
}
elsif (ref $schema_reference and not $schema_reference->$_isa('Mojo::URL')) {
abort($state, 'invalid schema type: %s', get_type($schema_reference));
}
my $schema_info = $self->_fetch_from_uri($schema_reference);
abort($state, 'EXCEPTION: unable to find resource "%s"', $schema_reference)
if not $schema_info;
abort($state, 'EXCEPTION: "%s" is not a schema', $schema_reference)
if not $schema_info->{document}->get_entity_at_location($schema_info->{document_path});
$state = +{
%$state,
initial_schema_uri => $schema_info->{canonical_uri}, # the canonical URI as of the start of evaluation, or last $id or $ref
$schema_info->%{qw(document specification_version vocabularies)},
dynamic_scope => [ $schema_info->{canonical_uri}->clone->fragment(undef) ],
annotations => [],
seen => {},
callbacks => $config_override->{callbacks} // {},
evaluator => $self,
(map {
my $val = $config_override->{$_} // $self->$_;
defined $val ? ($_ => $val) : ()
# note: this is a subset of the allowed overrides defined above
} qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict)),
};
# this hash will be added to at each level of schema evaluation
$state->{seen_data_properties} = {} if $config_override->{_strict_schema_data};
# we're going to set collect_annotations during evaluation when we see an unevaluated* keyword
# (or for object data when the _strict_schema_data configuration is set),
# but after we pass to a new data scope we'll clear it again.. unless we've got the config set
# globally for the entire evaluation, so we store that value in a high bit.
$state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
$valid = $self->_eval_subschema($data, $schema_info->{schema}, $state);
warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
warn 'result is true but there are errors' if $valid and $state->{errors}->@*;
}
catch ($e) {
if ($e->$_isa('JSON::Schema::Modern::Result')) {
return $e;
}
elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
push $state->{errors}->@*, $e;
}
else {
$valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
}
}
if ($state->{seen_data_properties}) {
my @unevaluated_properties = grep !$state->{seen_data_properties}{$_}, keys $state->{seen_data_properties}->%*;
my %unknown_keywords;
foreach my $property (sort @unevaluated_properties) {
my ($parent, $keyword) = ($property =~ m{^(.*)/([^/]*)$});
push(($unknown_keywords{$parent}//=[])->@*, $keyword);
}
foreach my $parent (sort keys %unknown_keywords) {
$valid = E({ %$state, data_path => $parent },
'unknown keyword%s seen in schema: %s', $unknown_keywords{$parent}->@* > 1 ? 's' : '',
join(', ', sort $unknown_keywords{$parent}->@*));
}
}
die 'evaluate validity inconsistent with error count' if $valid xor !$state->{errors}->@*;
return JSON::Schema::Modern::Result->new(
output_format => $self->output_format,
valid => $valid,
$valid
# strip annotations from result if user didn't explicitly ask for them
? ($config_override->{collect_annotations} // $self->collect_annotations
? (annotations => $state->{annotations}) : ())
: (errors => $state->{errors}),
);
}
sub validate_schema ($self, $schema, $config_override = {}) {
croak 'validate_schema called in void context' if not defined wantarray;
my $metaschema_uri = ref $schema eq 'HASH' && $schema->{'$schema'} ? $schema->{'$schema'}
: $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
my $result = $self->evaluate($schema, $metaschema_uri,
{ %$config_override, $self->strict || $config_override->{strict} ? (_strict_schema_data => 1) : () });
return $result if not $result->valid;
# the traversal pass will validate all constraints that weren't handled by the metaschema
my $state = $self->traverse($schema);
return JSON::Schema::Modern::Result->new(
output_format => $self->output_format,
valid => 0,
errors => $state->{errors},
) if $state->{errors}->@*;
return $result; # valid: true
}
sub get ($self, $uri_reference) {
if (wantarray) {
my $schema_info = $self->_fetch_from_uri($uri_reference);
return if not $schema_info;
my $subschema = ref $schema_info->{schema} ? dclone($schema_info->{schema}) : $schema_info->{schema};
return ($subschema, $schema_info->{canonical_uri});
}
else { # abridged version of _fetch_from_uri
$uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
my $fragment = $uri_reference->fragment;
my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
return if not $resource;
my $schema;
if (not length($fragment) or $fragment =~ m{^/}) {
$schema = $resource->{document}->get($resource->{path}.($fragment//''));
}
else { # we are following a URI with a plain-name fragment
return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
$schema = $resource->{document}->get($subresource->{path});
}
return ref $schema ? dclone($schema) : $schema;
}
}
sub get_document ($self, $uri_reference) {
my $schema_info = $self->_fetch_from_uri($uri_reference);
return if not $schema_info;
return $schema_info->{document};
}
# defined lower down:
# sub add_media_type ($self, $media_type, $sub) { ... }
# sub get_media_type ($self, $media_type) { ... }
# sub add_encoding ($self, $encoding, $sub) { ... }
# sub get_encoding ($self, $encoding) { ... }
# sub add_vocabulary ($self, $classname) { ... }
######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
# current spec version => { keyword => undef, or arrayref of alternatives }
my %removed_keywords = (
'draft4' => {
},
'draft6' => {
id => [ '$id' ],
},
'draft7' => {
id => [ '$id' ],
},
'draft2019-09' => {
id => [ '$id' ],
definitions => [ '$defs' ],
dependencies => [ qw(dependentSchemas dependentRequired) ],
},
'draft2020-12' => {
id => [ '$id' ],
definitions => [ '$defs' ],
dependencies => [ qw(dependentSchemas dependentRequired) ],
'$recursiveAnchor' => [ '$dynamicAnchor' ],
'$recursiveRef' => [ '$dynamicRef' ],
additionalItems => [ 'items' ],
},
);
# {
# $spec_version => {
# $vocabulary_class => {
# traverse => [ [ $keyword => $subref ], [ ... ] ],
# evaluate => [ [ $keyword => $subref ], [ ... ] ],
# }
# }
# }
# If we could serialize coderefs, this could be an object attribute;
# otherwise, we might as well persist this for the lifetime of the process.
our $vocabulary_cache = {};
sub _traverse_subschema ($self, $schema, $state) {
delete $state->@{'keyword', grep /^_/, keys %$state};
return E($state, 'EXCEPTION: maximum traversal depth (%d) exceeded', $self->max_traversal_depth)
if $state->{depth}++ > $self->max_traversal_depth;
push $state->{subschemas}->@*, $state->{traversed_keyword_path}.$state->{keyword_path};
my $schema_type = get_type($schema);
return 1 if $schema_type eq 'boolean'
and ($state->{specification_version} ne 'draft4'
or $state->{keyword_path} =~ m{/(?:additional(?:Items|Properties)|uniqueItems)$});
return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
return 1 if not keys %$schema;
my $valid = 1;
my %unknown_keywords = map +($_ => undef), keys %$schema;
# we use an index rather than iterating through the lists directly because the lists of
# vocabularies and keywords can change after we have started. However, only the Core vocabulary
# and $schema keyword can make this change, and they both come first, therefore a simple index
# into the list is sufficient.
ALL_KEYWORDS:
for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
my $vocabulary = $state->{vocabularies}[$vocab_index];
my $keyword_list;
for (my $keyword_index = 0;
$keyword_index < ($keyword_list //= do {
use autovivification qw(fetch store);
$vocabulary_cache->{$state->{specification_version}}{$vocabulary}{traverse} //= [
map [ $_ => $vocabulary->can('_traverse_keyword_'.($_ =~ s/^\$//r)) ],
$vocabulary->keywords($state->{specification_version})
];
})->@*;
$keyword_index++) {
my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
next if not exists $schema->{$keyword};
# keywords adjacent to $ref are not evaluated before draft2019-09
next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]$/;
delete $unknown_keywords{$keyword};
$state->{keyword} = $keyword;
my $old_spec_version = $state->{specification_version};
my $error_count = $state->{errors}->@*;
if (not $sub->($vocabulary, $schema, $state)) {
die 'traverse result is false but there are no errors (keyword: '.$keyword.')'
if $error_count == $state->{errors}->@*;
$valid = 0;
next;
}
warn 'traverse result is true but there are errors ('.$keyword.': '.$state->{errors}[-1]->error
if $error_count != $state->{errors}->@*;
# a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
undef $keyword_list if $state->{specification_version} ne $old_spec_version;
if (my $callback = $state->{callbacks}{$keyword}) {
$error_count = $state->{errors}->@*;
if (not $callback->($schema, $state)) {
die 'callback result is false but there are no errors (keyword: '.$keyword.')'
if $error_count == $state->{errors}->@*;
$valid = 0;
next;
}
die 'callback result is true but there are errors (keyword: '.$keyword.')'
if $error_count != $state->{errors}->@*;
}
}
}
delete $state->{keyword};
if ($self->strict and keys %unknown_keywords) {
$valid = E($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
join(', ', sort keys %unknown_keywords));
}
# check for previously-supported but now removed keywords
foreach my $keyword (sort keys $removed_keywords{$state->{specification_version}}->%*) {
next if not exists $schema->{$keyword};
my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
.canonical_uri($state).'")';
if (my $alternates = $removed_keywords{$state->{specification_version}}->{$keyword}) {
my @list = map '"'.$_.'"', @$alternates;
@list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
splice(@list, -1, 0, 'or') if @list > 1;
$message .= ': this should be rewritten as '.join(' ', @list);
}
carp $message;
}
return $valid;
}
sub _eval_subschema ($self, $data, $schema, $state) {
croak '_eval_subschema called in void context' if not defined wantarray;
# callers created a new $state for us, so we do not propagate upwards changes to depth, traversed
# paths; but annotations, errors are arrayrefs so their contents will be shared
$state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
delete $state->@{'keyword', grep /^_/, keys %$state};
abort($state, 'EXCEPTION: maximum evaluation depth (%d) exceeded', $self->max_traversal_depth)
if $state->{depth}++ > $self->max_traversal_depth;
my $schema_type = get_type($schema);
return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
# this should never happen, due to checks in traverse
abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
return 1 if not keys %$schema;
# find all schema locations in effect at this data path + uri combination
# if any of them are absolute prefix of this schema location, we are in a loop.
my $canonical_uri = canonical_uri($state);
my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
{
use autovivification qw(fetch store);
abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
if grep substr($schema_location, 0, length) eq $_,
keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
$state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
}
my $valid = 1;
my %unknown_keywords = map +($_ => undef), keys %$schema;
# set aside annotations collected so far; they are not used in the current scope's evaluation
my $parent_annotations = $state->{annotations};
$state->{annotations} = [];
# in order to collect annotations from applicator keywords only when needed, we twiddle the low
# bit if we see a local unevaluated* keyword, and clear it again as we move on to a new data path.
# We also set it when _strict_schema_data is set, but only for object data instances.
$state->{collect_annotations} |=
0+((ref $data eq 'ARRAY' && exists $schema->{unevaluatedItems})
|| ((my $is_object_data = ref $data eq 'HASH')
&& (exists $schema->{unevaluatedProperties} || !!$state->{seen_data_properties})));
# we use an index rather than iterating through the lists directly because the lists of
# vocabularies and keywords can change after we have started. However, only the Core vocabulary
# and $schema keyword can make this change, and they both come first, therefore a simple index
# into the list is sufficient.
ALL_KEYWORDS:
for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
my $vocabulary = $state->{vocabularies}[$vocab_index];
my $keyword_list;
for (my $keyword_index = 0;
$keyword_index < ($keyword_list //= do {
use autovivification qw(fetch store);
$vocabulary_cache->{$state->{specification_version}}{$vocabulary}{evaluate} //= [
map [ $_ => $vocabulary->can('_eval_keyword_'.($_ =~ s/^\$//r)) ],
$vocabulary->keywords($state->{specification_version})
];
})->@*;
$keyword_index++) {
my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
next if not exists $schema->{$keyword};
# keywords adjacent to $ref are not evaluated before draft2019-09
next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]$/;
delete $unknown_keywords{$keyword};
next if not $valid and $state->{short_circuit} and $state->{strict};
$state->{keyword} = $keyword;
if ($sub) {
my $old_spec_version = $state->{specification_version};
my $error_count = $state->{errors}->@*;
try {
if (not $sub->($vocabulary, $data, $schema, $state)) {
warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
if $error_count == $state->{errors}->@*;
$valid = 0;
last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
next;
}
warn 'evaluation result is true but there are errors (keyword: '.$keyword.')'
if $error_count != $state->{errors}->@*;
}
catch ($e) {
die $e if $e->$_isa('JSON::Schema::Modern::Error');
abort($state, 'EXCEPTION: '.$e);
}
# a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
undef $keyword_list if $state->{specification_version} ne $old_spec_version;
}
if (my $callback = ($state->{callbacks}//{})->{$keyword}) {
my $error_count = $state->{errors}->@*;
if (not $callback->($data, $schema, $state)) {
warn 'callback result is false but there are no errors (keyword: '.$keyword.')'
if $error_count == $state->{errors}->@*;
$valid = 0;
last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
next;
}
warn 'callback result is true but there are errors (keyword: '.$keyword.')'
if $error_count != $state->{errors}->@*;
}
}
}
delete $state->{keyword};
if ($state->{strict} and keys %unknown_keywords) {
abort($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
join(', ', sort keys %unknown_keywords));
}
# Note: we can remove all of this entirely and just rely on strict mode when we (eventually!) remove
# the traverse phase and replace with evaluate-against-metaschema.
if ($state->{seen_data_properties} and $is_object_data) {
# record the locations of all local properties
$state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 0 foreach keys %$data;
my @evaluated_properties = map {
my $keyword = $_->{keyword};
(grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
? $_->{annotation}->@* : ();
} local_annotations($state);
# tick off properties that were recognized by this subschema
$state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 1 foreach @evaluated_properties;
# weird! the draft4 metaschema doesn't know about '$ref' at all!
$state->{seen_data_properties}{$state->{data_path}.'/$ref'} |= 1
if exists $data->{'$ref'} and $state->{specification_version} eq 'draft4';
}
if ($valid and $state->{collect_annotations} and $state->{specification_version} !~ /^draft(?:[467]|2019-09)$/) {
annotate_self(+{ %$state, keyword => $_, _unknown => 1 }, $schema)
foreach sort keys %unknown_keywords;
}
# only keep new annotations if schema is valid
push $parent_annotations->@*, $state->{annotations}->@* if $valid;
return $valid;
}
has _resource_index => (
is => 'bare',
isa => Map[my $resource_key_type = Str->where('!/#/'), my $resource_type = Dict[
canonical_uri => (InstanceOf['Mojo::URL'])->where(q{not defined $_->fragment}),
path => json_pointer_type, # JSON pointer relative to the document root
specification_version => my $spec_version_type = Enum(SPECIFICATION_VERSIONS_SUPPORTED),
document => InstanceOf['JSON::Schema::Modern::Document'],
# the vocabularies used when evaluating instance data against schema
vocabularies => ArrayRef[my $vocabulary_class_type = ClassName->where(q{$_->DOES('JSON::Schema::Modern::Vocabulary')})],
anchors => Optional[HashRef[Dict[
canonical_uri => canonical_uri_type, # equivalent uri with json pointer fragment
path => json_pointer_type, # JSON pointer relative to the document root
dynamic => Optional[Bool],
]]],
Slurpy[HashRef[Undef]], # no other fields allowed
]],
);
sub _get_resource {
die 'bad resource: ', $_[1] if $_[1] =~ /#/;
($_[0]->{_resource_index}//{})->{$_[1]}
}
# does not check for duplicate entries, or for malformed uris
sub _add_resources_unsafe {
use autovivification 'store';
$_[0]->{_resource_index}{$resource_key_type->($_->[0])} = $resource_type->($_->[1])
foreach pairs @_[1..$#_];
}
sub _resource_index { ($_[0]->{_resource_index}//{})->%* }
sub _canonical_resources { values(($_[0]->{_resource_index}//{})->%*) }
sub _resource_pairs { pairs(($_[0]->{_resource_index}//{})->%*) }
sub _add_resource ($self, @kvs) {
foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @kvs) {
my ($canonical_uri, $resource) = @$pair;
if (my $existing = $self->_get_resource($canonical_uri)) {
# we allow overwriting canonical_uri = '' to allow for ad hoc evaluation of schemas that
# lack all identifiers altogether, but preserve other resources from the original document
if ($canonical_uri ne '') {
my @diffs = (
($existing->{path} eq $resource->{path} ? () : 'path'),
($existing->{canonical_uri} eq $resource->{canonical_uri} ? () : 'canonical_uri'),
($existing->{specification_version} eq $resource->{specification_version} ? () : 'specification_version'),
(refaddr($existing->{document}) == refaddr($resource->{document}) ? () : 'refaddr'));
next if not @diffs;
croak 'uri "'.$canonical_uri.'" conflicts with an existing schema resource: documents differ by ',
join(', ', @diffs);
}
}
elsif (JSON::Schema::Modern::Utilities::get_schema_filename($canonical_uri)) {
croak 'uri "'.$canonical_uri.'" conflicts with an existing cached schema resource';
}
use autovivification 'store';
$self->{_resource_index}{$resource_key_type->($canonical_uri)} = $resource_type->($resource);
}
}
# $vocabulary uri (not its $id!) => [ specification_version, class ]
has _vocabulary_classes => (
is => 'bare',
isa => HashRef[
my $vocabulary_type = Tuple[
$spec_version_type,
$vocabulary_class_type,
]
],
reader => '__vocabulary_classes',
lazy => 1,
default => sub {
+{
map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
qw(Core Applicator Validation FormatAssertion FormatAnnotation Content MetaData Unevaluated)
}
},
);
sub _get_vocabulary_class { $_[0]->__vocabulary_classes->{$_[1]} }
sub add_vocabulary ($self, $classname) {
return if grep $_->[1] eq $classname, values $self->__vocabulary_classes->%*;
$vocabulary_class_type->(load_module($classname));
# uri => version, uri => version
foreach my $pair (pairs $classname->vocabulary) {
my ($uri_string, $spec_version) = @$pair;
Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
$spec_version_type->($spec_version);
croak 'keywords starting with "$" are reserved for core and cannot be used'
if grep /^\$/, $classname->keywords;
$self->{_vocabulary_classes}{$uri_string} = $vocabulary_type->([ $spec_version, $classname ]);
}
}
# $schema uri => [ specification_version, [ vocab classes, in evaluation order ] ].
has _metaschema_vocabulary_classes => (
is => 'bare',
isa => HashRef[
my $mvc_type = Tuple[
$spec_version_type,
ArrayRef[$vocabulary_class_type],
]
],
reader => '__metaschema_vocabulary_classes',
lazy => 1,
default => sub {
my @modules = map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated);
+{
'https://json-schema.org/draft/2020-12/schema' => [ 'draft2020-12', [ @modules ] ],
do { pop @modules; () }, # remove Unevaluated
'https://json-schema.org/draft/2019-09/schema' => [ 'draft2019-09', [ @modules ] ],
'http://json-schema.org/draft-07/schema' => [ 'draft7', [ @modules ] ],
do { splice @modules, 4, 1; () }, # remove Content
'http://json-schema.org/draft-06/schema' => [ 'draft6', \@modules ],
'http://json-schema.org/draft-04/schema' => [ 'draft4', \@modules ],
},
},
);
sub _get_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#$//r} }
sub _set_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#$//r} = $mvc_type->($_[2]) }
sub __all_metaschema_vocabulary_classes { values $_[0]->__metaschema_vocabulary_classes->%* }
# translate vocabulary URIs into classes, caching the results (if any)
sub _fetch_vocabulary_data ($self, $state, $schema_info) {
if (not exists $schema_info->{schema}{'$vocabulary'}) {
# "If "$vocabulary" is absent, an implementation MAY determine behavior based on the meta-schema
# if it is recognized from the URI value of the referring schema's "$schema" keyword."
my $metaschema_uri = $self->METASCHEMA_URIS->{$schema_info->{specification_version}};
return $self->_get_metaschema_vocabulary_classes($metaschema_uri)->@*;
}
my $valid = 1;
# Core §8.1.2-6: "The "$vocabulary" keyword SHOULD be used in the root schema of any schema
# document intended for use as a meta-schema. It MUST NOT appear in subschemas."
$valid = E($state, '$vocabulary can only appear at the document root') if length $schema_info->{document_path};
$valid = E($state, 'metaschemas must have an $id') if not exists $schema_info->{schema}{'$id'};
return (undef, []) if not $valid;
my @vocabulary_classes;
foreach my $uri (sort keys $schema_info->{schema}{'$vocabulary'}->%*) {
my $class_info = $self->_get_vocabulary_class($uri);
$valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" is not a known vocabulary', $uri), next
if $schema_info->{schema}{'$vocabulary'}{$uri} and not $class_info;
next if not $class_info; # vocabulary is not known, but marked as false in the metaschema
my ($spec_version, $class) = @$class_info;
$valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" uses %s, but the metaschema itself uses %s',
$uri, $spec_version, $schema_info->{specification_version}), next
if $spec_version ne $schema_info->{specification_version};
push @vocabulary_classes, $class;
}
@vocabulary_classes = sort {
$a->evaluation_order <=> $b->evaluation_order
|| ($a->evaluation_order == 999 ? 0
: ($valid = E($state, '%s and %s have a conflicting evaluation_order', sort $a, $b)))
} @vocabulary_classes;
$valid = E($state, 'the first vocabulary (by evaluation_order) must be Core')
if ($vocabulary_classes[0]//'') ne 'JSON::Schema::Modern::Vocabulary::Core';
my %seen_keyword;
foreach my $class (@vocabulary_classes) {
foreach my $keyword ($class->keywords($schema_info->{specification_version})) {
$valid = E($state, '%s keyword "%s" conflicts with keyword of the same name from %s',
$class, $keyword, $seen_keyword{$keyword})
if $seen_keyword{$keyword};
$seen_keyword{$keyword} = $class;
}
}
return ($schema_info->{specification_version}, $valid ? \@vocabulary_classes : []);
}
# used for determining a default '$schema' keyword where there is none
# these are also normalized as this is how we cache them
use constant METASCHEMA_URIS => {
'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
'draft7' => 'http://json-schema.org/draft-07/schema',
'draft6' => 'http://json-schema.org/draft-06/schema',
'draft4' => 'http://json-schema.org/draft-04/schema',
};
# for internal use only. files are under share/
use constant _CACHED_METASCHEMAS => {
'https://json-schema.org/draft/2020-12/meta/applicator' => 'draft2020-12/meta/applicator.json',
'https://json-schema.org/draft/2020-12/meta/content' => 'draft2020-12/meta/content.json',
'https://json-schema.org/draft/2020-12/meta/core' => 'draft2020-12/meta/core.json',
'https://json-schema.org/draft/2020-12/meta/format-annotation' => 'draft2020-12/meta/format-annotation.json',
'https://json-schema.org/draft/2020-12/meta/format-assertion' => 'draft2020-12/meta/format-assertion.json',
'https://json-schema.org/draft/2020-12/meta/meta-data' => 'draft2020-12/meta/meta-data.json',
'https://json-schema.org/draft/2020-12/meta/unevaluated' => 'draft2020-12/meta/unevaluated.json',
'https://json-schema.org/draft/2020-12/meta/validation' => 'draft2020-12/meta/validation.json',
'https://json-schema.org/draft/2020-12/output/schema' => 'draft2020-12/output/schema.json',
'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12/schema.json',
'https://json-schema.org/draft/2019-09/meta/applicator' => 'draft2019-09/meta/applicator.json',
'https://json-schema.org/draft/2019-09/meta/content' => 'draft2019-09/meta/content.json',
'https://json-schema.org/draft/2019-09/meta/core' => 'draft2019-09/meta/core.json',
'https://json-schema.org/draft/2019-09/meta/format' => 'draft2019-09/meta/format.json',
'https://json-schema.org/draft/2019-09/meta/meta-data' => 'draft2019-09/meta/meta-data.json',
'https://json-schema.org/draft/2019-09/meta/validation' => 'draft2019-09/meta/validation.json',
'https://json-schema.org/draft/2019-09/output/schema' => 'draft2019-09/output/schema.json',
'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09/schema.json',
# trailing # is omitted because we always cache documents by its canonical (fragmentless) URI
'http://json-schema.org/draft-07/schema' => 'draft7/schema.json',
'http://json-schema.org/draft-06/schema' => 'draft6/schema.json',
'http://json-schema.org/draft-04/schema' => 'draft4/schema.json',
};
# simple runtime-wide cache of metaschema document objects that are sourced from disk
my $metaschema_cache = {};
{
my $share_dir = dist_dir('JSON-Schema-Modern');
JSON::Schema::Modern::Utilities::register_schema($_, $share_dir.'/'._CACHED_METASCHEMAS->{$_})
foreach keys _CACHED_METASCHEMAS->%*;
}
# returns the same as _get_resource
sub _get_or_load_resource ($self, $uri) {
my $resource = $self->_get_resource($uri);
return $resource if $resource;
if (my $document = load_cached_document($self, $uri)) {
return $self->_get_resource($uri);
}
# TODO:
# - load from network or disk
return;
};
# returns information necessary to use a schema found at a particular URI or uri-reference:
# - schema: a schema (which may not be at a document root)
# - canonical_uri: the canonical uri for that schema,
# - document: the JSON::Schema::Modern::Document object that holds that schema
# - document_path: the path relative to the document root for this schema
# - specification_version: the specification version that applies to this schema
# - vocabularies: the vocabularies to use when considering schema keywords
# creates a Document and adds it to the resource index, if not already present.
sub _fetch_from_uri ($self, $uri_reference) {
$uri_reference = Mojo::URL->new($uri_reference) if not is_schema($uri_reference);
# this is *a* resource that would contain our desired location, but may not be the closest one
my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
return if not $resource;
my $fragment = $uri_reference->fragment;
if (not length($fragment) or $fragment =~ m{^/}) {
my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
return if not defined $subschema;
my $closest_resource;
if (not length $fragment) { # we already have the canonical resource root
$closest_resource = [ undef, $resource ];
}
else {
# determine the canonical uri by finding the closest schema resource(s)
my $doc_addr = refaddr($resource->{document});
my @closest_resources =
sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
grep { !length($_->[1]{path}) # document root
|| length($document_path)
&& $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above desired location
grep { refaddr($_->[1]{document}) == $doc_addr } # in same document
$self->_resource_pairs;
# now whittle down to all the resources with the same document path as the first candidate
if (@closest_resources > 1) {
# find the resource key that most closely matches the original query uri, by matching prefixes
my $match = $uri_reference.'';
@closest_resources =
sort { _prefix_match_length($b->[0], $match) <=> _prefix_match_length($a->[0], $match) }
grep $_->[1]{path} eq $closest_resources[0]->[1]{path},
@closest_resources;
}
$closest_resource = $closest_resources[0];
}
my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
->fragment(substr($document_path, length($closest_resource->[1]{path})));
$canonical_uri->fragment(undef) if not length($canonical_uri->fragment);
return {
schema => $subschema,
canonical_uri => $canonical_uri,
document_path => $document_path,
$closest_resource->[1]->%{qw(document specification_version vocabularies)}, # reference, not copy
};
}
else { # we are following a URI with a plain-name fragment
return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
return {
schema => $resource->{document}->get($subresource->{path}),
canonical_uri => $subresource->{canonical_uri}, # this is *not* the anchor-containing URI
document_path => $subresource->{path},
$resource->%{qw(document specification_version vocabularies)}, # reference, not copy
};
}
}
# given two strings, determines the number of characters in common, starting from the first
# character
sub _prefix_match_length ($x, $y) {
my $len = min(length($x), length($y));
foreach my $pos (0..$len) {
return $pos if substr($x, $pos, 1) ne substr($y, $pos, 1);
}
return $len;
}
# Mojo::JSON::JSON_XS is false when the environment variable $MOJO_NO_JSON_XS is set
# and also checks if Cpanel::JSON::XS is installed.
# Mojo::JSON falls back to its own pure-perl encoder/decoder but does not support all the options
# that we require here.
use constant _JSON_BACKEND =>
Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION('4.38'); 1 } ? 'Cpanel::JSON::XS'
: eval { JSON::PP->VERSION('4.11'); 1 } ? 'JSON::PP'
: die 'Cpanel::JSON::XS 4.38 or JSON::PP 4.11 is required';
# used for internal encoding as well (when caching serialized schemas)
has _json_decoder => (
is => 'ro',
isa => HasMethods[qw(encode decode)],
lazy => 1,
default => sub { _JSON_BACKEND->new->allow_nonref(1)->canonical(1)->utf8(1)->allow_bignum(1)->convert_blessed(1) },
);
# since media types are case-insensitive, all type names must be casefolded on insertion.
has _media_type => (
is => 'bare',
isa => my $media_type_type = Map[Str->where(q{$_ eq CORE::fc($_)}), CodeRef],
reader => '__media_type',
lazy => 1,
default => sub ($self) {
my $_json_media_type = sub ($content_ref) {
# utf-8 decoding is always done, as per the JSON spec.
# other charsets are not supported: see RFC8259 §11
\ _JSON_BACKEND->new->allow_nonref(1)->utf8(1)->decode($content_ref->$*);
};
+{
(map +($_ => $_json_media_type),
qw(application/json application/schema+json application/schema-instance+json)),
(map +($_ => sub ($content_ref) { $content_ref }),
qw(text/* application/octet-stream)),
'application/x-www-form-urlencoded' => sub ($content_ref) {
\ Mojo::Parameters->new->charset('UTF-8')->parse($content_ref->$*)->to_hash;
},
'application/x-ndjson' => sub ($content_ref) {
my $decoder = _JSON_BACKEND->new->allow_nonref(1)->utf8(1);
my $line = 0; # line numbers start at 1
\[ map {
do {
try { ++$line; $decoder->decode($_) }
catch ($e) { die 'parse error at line '.$line.': '.$e }
}
}
split(/\r?\n/, $content_ref->$*)
];
},
};
},
);
sub add_media_type { $media_type_type->({ @_[1..2] }); $_[0]->__media_type->{$_[1]} = $_[2]; }
# get_media_type('TExT/bloop') will fall through to matching an entry for 'text/*' or '*/*'
sub get_media_type ($self, $type) {
my $types = $self->__media_type;
my $mt = $types->{fc $type};
return $mt if $mt;
return $types->{(first { m{([^/]+)/\*$} && fc($type) =~ m{^\Q$1\E/[^/]+$} } keys %$types) // '*/*'};
};
has _encoding => (
is => 'bare',
isa => HashRef[CodeRef],
reader => '__encoding',
lazy => 1,
default => sub ($self) {
+{
identity => sub ($content_ref) { $content_ref },
base64 => sub ($content_ref) {
die "invalid characters\n"
if $content_ref->$* =~ m{[^A-Za-z0-9+/=]} or $content_ref->$* =~ m{=(?=[^=])};
require MIME::Base64; \ MIME::Base64::decode_base64($content_ref->$*);
},
base64url => sub ($content_ref) {
die "invalid characters\n"
if $content_ref->$* =~ m{[^A-Za-z0-9=_-]} or $content_ref->$* =~ m{=(?=[^=])};
require MIME::Base64; \ MIME::Base64::decode_base64url($content_ref->$*);
},
};
},
);
sub get_encoding { $_[0]->__encoding->{$_[1]} }
sub add_encoding { $_[0]->__encoding->{$_[1]} = CodeRef->($_[2]) }
# callback hook for Sereal::Encoder
sub FREEZE ($self, $serializer) {
my $data = +{ %$self };
# Cpanel::JSON::XS doesn't serialize: https://github.com/Sereal/Sereal/issues/266
# coderefs can't serialize cleanly and must be re-added by the user.
delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
return $data;
}
# callback hook for Sereal::Decoder
sub THAW ($class, $serializer, $data) {
my $self = bless($data, $class);
# load all vocabulary classes, both those used by loaded schemas, as well as all the core modules
load_module($_)
foreach uniq(
(map $_->{vocabularies}->@*, $self->_canonical_resources),
(map $_->[1], values $self->__vocabulary_classes->%*));
return $self;
}
1;
__END__
=pod
=encoding UTF-8
=for stopwords schema subschema metaschema validator evaluator
=head1 NAME
JSON::Schema::Modern - Validate data against a schema using a JSON Schema
=head1 VERSION
version 0.627
=head1 SYNOPSIS
use JSON::Schema::Modern;
$js = JSON::Schema::Modern->new(
specification_version => 'draft2020-12',
output_format => 'flag',
... # other options
);
$result = $js->evaluate($instance_data, $schema_data);
=head1 DESCRIPTION
This module aims to be a fully-compliant L evaluator and
validator, targeting the currently-latest
L
version of the specification.
=head1 CONSTRUCTOR ARGUMENTS
Unless otherwise noted, these are also available as read-only accessors.
=head2 specification_version
Indicates which version of the JSON Schema specification is used during evaluation. This value is
overridden by the value determined from the C<$schema> keyword in the schema used in evaluation
(when present), or defaults to the latest version (currently C).
The use of the C<$schema> keyword in your schema is I encouraged to ensure continued correct
operation of your schema. The current default value will not stay the same over time.
May be one of:
=over 4
=item *
L or C<2020-12>|https://json-schema.org/specification-links.html#2020-12>, corresponding to metaschema C
=item *
L or C<2019-09>|https://json-schema.org/specification-links.html#2019-09-formerly-known-as-draft-8>, corresponding to metaschema C
=item *
L or C<7>|https://json-schema.org/specification-links.html#draft-7>, corresponding to metaschema C
=item *
L or C<6>|https://json-schema.org/specification-links.html#draft-6>, corresponding to metaschema C
=item *
L or C<4>|https://json-schema.org/specification-links.html#draft-4>, corresponding to metaschema C
=back
=head2 output_format
One of: C, C, C, C. Defaults to C.
C can only be used with C.
Passed to L.
=head2 short_circuit
When true, evaluation will return early in any execution path as soon as the outcome can be
determined, rather than continuing to find all errors or annotations.
This option is safe to use in all circumstances, even in the presence of
C and C keywords: the validation result will not change;
only some errors will be omitted from the result.
Defaults to true when C is C, and false otherwise.
=head2 max_traversal_depth
The maximum number of levels deep a schema traversal may go, before evaluation is halted. This is to
protect against accidental infinite recursion, such as from two subschemas that each reference each
other, or badly-written schemas that could be optimized. Defaults to 50.
=head2 validate_formats
When true, the C keyword will be treated as an assertion, not merely an annotation. Defaults
to true when specification_version is draft4, draft6 or draft7, and false for all other versions, but this may change in the future.
Note that the use of a format that does not have a defined handler will B be interpreted as an
error in this mode; instead, the undefined format will simply be ignored. If you instead want this
to be treated as an evaluation error, you must define a custom schema dialect that uses the
format-assertion vocabulary (available in specification version C) and reference it in
your schema with the C<$schema> keyword.
=head2 format_validations
=for stopwords subref
An optional hashref that allows overriding the validation method for formats, or adding new ones.
Overrides to existing formats (see L)
must be specified in the form of C<< { $format_name => $format_sub } >>, where
the format sub is a subref that takes one argument and returns a boolean result. New formats must
be specified in the form of C<< { $format_name => { type => $type, sub => $format_sub } } >>,
where the type indicates which of the data model types (null, object, array, boolean, string,
or number) the instance value must be for the format validation to be considered.
Not available as an accessor.
=head2 validate_content_schemas
When true, the C and C keywords are not treated as pure annotations:
C (when present) is used to decode the applied data payload and then
C will be used as the media-type for decoding to produce the data payload which is
then applied to the schema in C for validation. (Note that treating these keywords as
anything beyond simple annotations is contrary to the specification, therefore this option defaults
to false.)
See L and L for adding additional type support.
=for stopwords shhh
Technically only draft4, draft6 and draft7 allow this and drafts 2019-09 and 2020-12 prohibit ever returning the
subschema evaluation results together with their parent schema's results, so shhh. I'm trying to get this
fixed for the next draft.
=head2 collect_annotations
When true, annotations are collected from keywords that produce them, when validation succeeds.
These annotations are available in the returned result (see L).
Not operational when L is C, C or C.
Defaults to false.
=head2 scalarref_booleans
When true, any value that is expected to be a boolean B may also be expressed
as the scalar references C<\0> or C<\1> (which are serialized as booleans by JSON backends).
Defaults to false.
=head2 stringy_numbers
When true, any value that is expected to be a number or integer B may also be
expressed as a string. This applies only to the following keywords:
=over 4
=item *
C (where both C and C (and possibly C) are considered valid)
=item *
C and C (where the string C<"1"> will match with C<"const": 1>)
=item *
C (where strings and numbers are compared numerically to each other, if either or both are numeric)
=item *
C
=item *
C
=item *
C
=item *
C
=item *
C
=item *
C (for formats defined to validate numbers)
=back
This allows you to write a schema like this (which validates a string representing an integer):
type: string
pattern: ^[0-9]$
multipleOf: 4
minimum: 16
maximum: 256
Such keywords are only applied if the value looks like a number, and do not generate a failure
otherwise. Values are determined to be numbers via L.
This option is only intended to be used for evaluating data from sources that can only be strings,
such as the extracted value of an HTTP header or query parameter.
Defaults to false.
=head2 strict
When true, unrecognized keywords are disallowed in schemas (they will cause an immediate abort
in L or L).
Defaults to false.
=head1 METHODS
=for Pod::Coverage BUILDARGS FREEZE THAW
METASCHEMA_URIS SPECIFICATION_VERSIONS_SUPPORTED SPECIFICATION_VERSION_DEFAULT
=head2 evaluate_json_string
$result = $js->evaluate_json_string($data_as_json_string, $schema);
$result = $js->evaluate_json_string($data_as_json_string, $schema, { collect_annotations => 1 });
Evaluates the provided instance data against the known schema document.
The data is in the form of a JSON-encoded string (in accordance with
L). B
The schema must be in one of these forms:
=over 4
=item *
a Perl data structure, such as what is returned from a JSON decode operation,
=item *
or a URI string indicating the identity of such a schema.
=back
Optionally, a hashref can be passed as a third parameter which allows changing the values of the
L, L, L,
L, L, L, and/or L
settings for just this evaluation call.
You can also pass use these keys to alter behaviour (these are generally only used by custom validation
applications that contain embedded JSON Schemas):
=over 4
=item *
C: adjusts the effective path of the data instance as of the start of evaluation
=item *
C: adjusts the accumulated path as of the start of evaluation (or last C<$id> or C<$ref>)
=item *
C: adjusts the recorded absolute keyword location of the start of evaluation
=back
The return value is a L object, which can also be used as a boolean.
=head2 evaluate
$result = $js->evaluate($instance_data, $schema);
$result = $js->evaluate($instance_data, $schema, { short_circuit => 0 });
Evaluates the provided instance data against the known schema document.
The data is in the form of an unblessed nested Perl data structure representing any type that JSON
allows: null, boolean, string, number, object, array. (See L below.)
The schema must be in one of these forms:
=over 4
=item *
a Perl data structure, such as what is returned from a JSON decode operation
=item *
or a URI string (or L) indicating the identity of such a schema.
=back
Optionally, a hashref can be passed as a third parameter which allows changing the values of the
L, L, L,
L, L, L, and/or L
settings for just this evaluation call.
You can also pass use these keys to alter behaviour (these are generally only used by custom validation
applications that contain embedded JSON Schemas):
=over 4
=item *
C: adjusts the effective path of the data instance as of the start of evaluation
=item *
C: adjusts the accumulated path as of the start of evaluation (or last C<$id> or C<$ref>)
=back
You can pass a series of callback subs to this method corresponding to keywords, which is useful for
identifying various data that are not exposed by annotations.
This feature is highly experimental and may change in the future.
For example, to find the locations where all C<$ref> keywords are applied B:
my @used_ref_at;
$js->evaluate($data, $schema_or_uri, {
callbacks => {
'$ref' => sub ($data, $schema, $state) {
push @used_ref_at, $state->{data_path};
}
},
});
The return value is a L object, which can also be used as a boolean.
Callbacks are not compatible with L mode.
=head2 validate_schema
$result = $js->validate_schema($schema);
$result = $js->validate_schema($schema, $config_override);
Evaluates the provided schema as instance data against its metaschema. Accepts C<$schema> and
C<$config_override> parameters in the same form as L.
=head2 traverse
$result = $js->traverse($schema);
$result = $js->traverse($schema, { initial_schema_uri => 'http://example.com' });
Traverses the provided schema without evaluating it against any instance data. Returns the
internal state object accumulated during the traversal, including any identifiers found therein, and
any errors found during parsing. For internal purposes only.
Optionally, a hashref can be passed as a second parameter which alters some
behaviour (these are generally only used by custom validation
applications that contain embedded JSON Schemas):
=over 4
=item *
C: adjusts the accumulated path as of the start of evaluation (or last C<$id> or C<$ref>)
=item *
C: adjusts the absolute keyword location as of the start of evaluation
=item *
C: use the indicated URI as the metaschema
=back
You can pass a series of callback subs to this method corresponding to keywords, which is useful for
extracting data from within schemas and skipping properties that may look like keywords but actually
are not (for example C<{"const": {"$ref": "this is not actually a $ref"}}>). This feature is highly
experimental and is highly likely to change in the future.
For example, to find the resolved targets of all C<$ref> keywords in a schema document:
my @refs;
JSON::Schema::Modern->new->traverse($schema, {
callbacks => {
'$ref' => sub ($schema, $state) {
push @refs, Mojo::URL->new($schema->{'$ref'})
->to_abs(JSON::Schema::Modern::Utilities::canonical_uri($state));
}
},
});
=head2 add_schema
$js->add_schema($uri => $schema);
$js->add_schema($schema);
Introduces the (unblessed, nested) Perl data structure
representing a JSON Schema to the implementation, registering it under the indicated URI if
provided, and all identifiers found within the document will be resolved against this URI (if
provided) and added as well. C<''> will be used if no other identifier can be found within.
You B call C or L (below) for any external resources that a schema may reference via C<$ref>
before calling L, other than the standard metaschemas which are loaded from a local cache
as needed.
If you add multiple schemas (either with this method, or implicitly via L) with no root
identifier (either provided explicitly in the method call, or via an C<$id> keyword at the schema
root), all such previous schemas are removed from memory and can no longer be referenced.
If there were errors in the document, will die with these errors;
otherwise returns the L that contains the added schema. URIs
identified within this document will not be resolved to the provided C<$uri> argument, so you can
re-add the document object again (with L, below) using a new base URI if you wish.
=head2 add_document
$js->add_document($uri => $document);
$js->add_document($document);
Makes the L (or subclass)
object, representing a JSON Schema, available to the evaluator. All identifiers known to the
document are added to the evaluator's resource index; if the C<$uri> argument is provided, those
identifiers are resolved against C<$uri> as they are added.
C<$uri> itself is also added to the resource index, referencing the root of the document itself.
If you add multiple documents (either with this method, or implicitly via C or L) with no root
identifier (either provided explicitly in the method call, or via an C<$id> keyword at the schema
root), all such previous schemas are removed from memory and can no longer be referenced.
If there were errors in the document, this method will die with these errors;
otherwise it returns the L object.
=head2 add_format_validation
$js->add_format_validation(all_lc => sub ($value) { lc($value) eq $value });
=for comment we are the nine Eleven Deniers
or
$js->add_format_validation(no_nines => { type => 'number', sub => sub ($value) { $value =~ m/^[0-8]+$/ });
$js->add_format_validation(8bits => { type => 'string', sub => sub ($value) { $value =~ m/^[\x00-\xFF]+$/ });
Adds support for a custom format. If not supplied, the data type(s) that this format applies to
defaults to string; all values of any other type will automatically be deemed to be valid, and will
not be passed to the subref.
Additionally, you can redefine the definition for any core format (see L), but
the data type(s) supported by that format may not be changed.
Be careful to not mutate the type of the value while checking it -- for example, if it is a string,
do not apply arithmetic operators to it -- or subsequent type checks on this value may fail.
=for stopwords OpenAPI
See the official L
for a registry of known and useful formats; for
compatibility reasons, avoid defining a format listed here with different semantics.
Format definitions cannot be overridden with a new definition.
=head2 add_vocabulary
$js->add_vocabulary('My::Custom::Vocabulary::Class');
Makes a custom vocabulary class available to metaschemas that make use of this vocabulary.
as described in the specification at
L<"Meta-Schemas and Vocabularies"|https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.8.1>.
The class must compose the L role and implement the
L and
L methods, as well as
C<< _traverse_keyword_ >> methods for each keyword. C<< _eval_keyword_ >>
methods are optional; when not provided, evaluation will always return a true result.
Vocabularies cannot be redefined; subsequent calls to add the same vocabulary will do nothing.
=head2 add_media_type
$js->add_media_type('application/furble' => sub ($content_ref) {
return ...; # data representing the deserialized text for Content-Type: application/furble
});
Takes a media-type name and a subref which takes a single scalar reference, which is expected to be
a reference to a string, which might contain wide characters (i.e. not octets), especially when used
in conjunction with L below. Must return B (which is
then dereferenced for the C keyword).
These media types are already known:
=over 4
=item *
C - see L
=item *
C - see L
=item *
C - see L
=item *
C - passes strings through unchanged
=item *
C
=item *
C - see L
=item *
C - passes strings through unchanged
=back
Media-type definitions can be overridden with a new call to C.
See the official L
for a registry of known and useful media types; for
compatibility reasons, avoid defining a media type listed here with different semantics.
=head2 get_media_type
Fetches a decoder sub for the indicated media type. Lookups are performed B.
=for stopwords thusly
You can use it thusly:
$js->add_media_type('application/furble' => sub { ... }); # as above
my $decoder = $self->get_media_type('application/furble') or die 'cannot find media type decoder';
my $content_ref = $decoder->(\$content_string);
=head2 add_encoding
$js->add_encoding('bloop' => sub ($content_ref) {
return \ ...; # data representing the deserialized content for Content-Transfer-Encoding: bloop
});
Takes an encoding name and a subref which takes a single scalar reference, which is expected to be
a reference to a string, which SHOULD be a 7-bit or 8-bit string. Result values MUST be a scalar-reference
to a string (which is then dereferenced for the C keyword).
Encoding definitions can be overridden with a new call to C.
=for stopwords natively
Encodings handled natively are:
=over 4
=item *
C - passes strings through unchanged
=item *
C - see L
=item *
C - see L
=back
See also L.
=head2 get_encoding
Fetches a decoder sub for the indicated encoding. Incoming values MUST be a reference to an octet
string. Result values will be a scalar-reference to a string, which might be passed to a media_type
decoder (see above).
You can use it thusly:
my $decoder = $self->get_encoding('base64') or die 'cannot find encoding decoder';
my $content_ref = $decoder->(\$content_string);
=head2 get
my $schema = $js->get($uri);
my ($schema, $canonical_uri) = $js->get($uri);
Fetches the Perl data structure represented by the indicated identifier (uri or
uri-reference). When called in list context, the canonical URI of that location is also returned, as
a L. Returns C if the schema with that URI has not been loaded (or cached).
Note that the data so returned may not be a JSON Schema, if the document encapsulating this location
is a subclass of L (for example
L, which contains addressable locations of various semantic
types).
=head2 get_document
my $document = $js->get_document($uri_reference);
Fetches the L object (or subclass) that contains the provided
identifier (uri or uri-reference). C if the schema with that URI has not been loaded (or
cached).
Note: this _does not download a document from the network_. It only fetches the document from the
internal cache in the C document.
=head1 CACHING
=for stopwords preforking
Very large documents, particularly those used by L, may take a noticeable time to be
loaded and parsed. You can reduce the impact to your preforking application by loading all necessary
documents at startup, and impact can be further reduced by saving objects to cache and then
reloading them (perhaps by using a timestamp or checksum to determine if a fresh reload is needed).
Custom L, L or
L are not serialized, as they are represented by subroutine references, and
will need to be manually added after thawing.
sub get_evaluator (...) {
my $serialized_file = path($filename);
my $schema_file = path($schema_filename);
my $js;
if ($serialized_file->stat->mtime < $schema_file->stat->mtime)) {
$js = JSON::Schema::Modern->new;
$js->add_schema(decode_json($schema_file->slurp_raw)); # your application schema
my $frozen = Sereal::Encoder->new({ freeze_callbacks => 1 })->encode($js);
$serialized_file->spew_raw($frozen);
}
else {
my $frozen = $serialized_file->slurp_raw;
$js = Sereal::Decoder->new->decode($frozen);
}
# add custom format validations, media types and encodings here
$js->add_media_type(...);
return $js;
}
See also L.
=head1 LIMITATIONS
=head2 Types
Perl is a more loosely-typed language than JSON. This module delves into a value's internal
representation in an attempt to derive the true "intended" type of the value.
This should not be an issue if data validation is occurring
immediately after decoding a JSON payload, or if the JSON string itself is passed to this module.
If you are having difficulties, make sure you are using Perl's fastest and most trusted and
reliable JSON decoder, L.
Other JSON decoders are known to produce data with incorrect data types,
and data from other sources may also be problematic.
For more information, see L.
=head2 Format Validation
By default (and unless you specify a custom metaschema with the C<$schema> keyword or
L),
formats are treated only as annotations, not assertions. When L is
true, strings are also checked against the format as specified in the schema. At present the
following formats are supported for the latest version of the specification
(use of any other formats than these will always evaluate as true,
but remember you can always supply custom format handlers; see L above):
=over 4
=item *
C
=item *
C
=item *
C