diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dc785b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.pl linguist-language=Perl +*.pm linguist-language=Perl +*.t linguist-language=Perl diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..b0bc75e --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1 @@ +Please read the guide for [contributing to Mojolicious](http://mojolicious.org/perldoc/Mojolicious/Guides/Contributing). diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..4db8d9b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,12 @@ +* JSON::Validator version: VERSION HERE +* Perl version: VERSION HERE +* Operating system: NAME AND VERSION HERE + +### Steps to reproduce the behavior +EXPLAIN WHAT HAPPENED HERE, PREFERABLY WITH CODE EXAMPLES + +### Expected behavior +EXPLAIN WHAT SHOULD HAPPEN HERE + +### Actual behavior +EXPLAIN WHAT HAPPENED INSTEAD HERE diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..15b8749 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +### Summary +DESCRIBE THE BIG PICTURE OF YOUR CHANGES HERE + +### Motivation +EXPLAIN WHY YOU BELIEVE THESE CHANGES ARE NECESSARY HERE + +### References +LIST RELEVANT ISSUES, PULL REQUESTS AND IRC/MAILING-LIST DISCUSSIONS HERE diff --git a/.perltidyrc b/.perltidyrc new file mode 100644 index 0000000..dc10082 --- /dev/null +++ b/.perltidyrc @@ -0,0 +1,13 @@ +-pbp # Start with Perl Best Practices +-w # Show all warnings +-iob # Ignore old breakpoints +-l=80 # 80 characters per line +-mbl=2 # No more than 2 blank lines +-i=2 # Indentation is 2 columns +-ci=2 # Continuation indentation is 2 columns +-vt=0 # Less vertical tightness +-pt=2 # High parenthesis tightness +-bt=2 # High brace tightness +-sbt=2 # High square bracket tightness +-wn # Weld nested containers +-isbc # Don't indent comments without leading space diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..449a289 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: perl +perl: + - "5.28" + - "5.26" + - "5.24" + - "5.22" + - "5.20" + - "5.18" + - "5.16" + - "5.14" + - "5.12" + - "5.10" +env: + - "HARNESS_OPTIONS=j6 TEST_RANDOM_ITERATIONS=5000" +install: + - "cpanm -n Test::Pod Test::Pod::Coverage" + - "cpanm -n Data::Validate::Domain Data::Validate::IP Cpanel::JSON::XS Net::IDN::Encode YAML::XS" + - "cpanm -n --installdeps ." +sudo: false +notifications: + email: false diff --git a/Changes b/Changes new file mode 100644 index 0000000..e8f97c8 --- /dev/null +++ b/Changes @@ -0,0 +1,378 @@ +Revision history for perl distribution JSON-Validator + +3.06 2019-02-14T18:24:29+0100 + - Fix coercing integers and numbers #147 + - Changed recursion guard to not keeping tracking of plain scalars #147 + +3.05 2019-01-31T08:45:14+0900 + - Removed testing Mojo::JSON::MaybeXS, since Mojo::JSON loads Cpanel::JSON::XS + +3.04 2019-01-21T09:39:50+0900 + - Fix "uri" check, so that it only accept ASCII characters. Note that this + fix might be an undesired change for your application. If so, then update + the "uri" format in your schema to "iri". + - Fix "hostname" format check, so it does not require a valid TLD + - Fix validating draft-07 schema against itself #144 + - Add support for more formats in JSON Schema draft-6 and 7: date, idn-email, + idn-hostname, iri, iri-reference, json-pointer, relative-json-pointer, time, + uri-reference and uri-template. + - Add support for more keywords in draft-07 + * 6.4.6. Arrays - contains + * 6.5.8. Objects - propertyNames + * 6.6.1. Objects - if + * 6.6.2. Objects - then + * 6.6.3. Objects - else + +3.03 2019-01-19T12:11:34+0900 + - Add JSON::Validator::Formats with format checks + Note that these functions work by returning a string on error, instead of + true on sucches, which was a breaking change introduced in the 3.00 release. + +3.02 2019-01-07T09:52:31+0900 + - Trying to fix more failing test reports from the smokers + +3.01 2019-01-06T08:16:33+0900 + - Fix t/jv-formats.t #140 + +3.00 2019-01-05T13:13:49+0900 + - Add enum() to Joi + - Add support for a list of types passed on to Joi #136 + - Add support for file:// scheme in $ref #138 + - Fix $ref resolving after fixes applied to Mojo::JSON::Pointer in + Mojolicious 8.11 #139 + - Fix cases where input was not coerced + - Breaking change: format callbacks need to return undef on success and a + description on error. + - Changed Joi to always coerce values + - Removed JSON::Validator::OpenAPI + +2.19 2018-12-07 + - Fix random errors when "type" is a list #126 + - Moved JSON::Validator::OpenAPI::Mojolicious to Mojolicious-Plugin-OpenAPI + - Removed JSON::Validator::OpenAPI::Dancer2 + +2.18 2018-11-15 + - Add EXPERIMETNAL support for data:// without a package + +2.17 2018-11-14 + - Add basic support for OpenAPI v3 + +2.16 2018-11-14 + - Improved openapi "date" format validation #123 + Contributor: Jason Cooper + +2.15 2018-11-07 + - Did not need to bump Mojolicious version in 2.15 #122 + Contributor: Dagfinn Ilmari Mannsåker + +2.14 2018-10-26 + - Fix guessing schema type from "required" key #118 + - Fix appending parameters for Mojolicious 8.00 #119 #120 + - Improved error return values from allOf, anyOf and oneOf validation + - Will not overwrite OpenAPI "/info/version" from "version_from_class" + +2.13 2018-10-17 + - Compatible with weak attrs in Mojolicious 8.03 + +2.12 2018-10-03 + - Improved error message when $ref is pointing to a non-existing file + +2.11 2018-09-30 + - Skipping load-from-app.t on "Gateway Timeout" as well + +2.10 2018-09-26 + - Fix handling of directory name with RFC 3986 reserved chars + Contributor: Ed J + +2.09 2018-09-26 + - Skip "remote ref" tests when running through cpantesters + +2.08 2018-06-03 + - Fix validating oneOf correctly #103 + - Fix validating "id" property #111 + - Add support for $id keyword in draft-07 #114 + - Bundle JSON Schema draft-06 and draft-07 + +2.07 2018-04-18 + - Fix joi->object->strict() + Contributor: Pierre-Aymeric Masse + +2.06 2018-04-09 + - Fix normalising file names on windows #102 + - Prevent "Use of uninitialized value $pointer in length..." for older Perls #104 + - Removed warning about coercion now, since it works well + +2.05 2018-03-11 + - Fix hash randomization problem fot t/get.t #101 + +2.04 2018-03-10 + - Add JSON::Validator::JOI and joi() #63 + - Add support for get(|"x", undef, "y"]) + - Will catch if more than one parameter has "in":"body" #97 + - Fix file-path with ".." gets false negative for same-ref check #99 + +2.03 2018-02-15 + - Will not leak file system information to bundled schema + +2.02 2018-01-30 + - Will let the user know if YAML::XS 0.67 (or later) need to be installed + +2.01 2018-01-26 + - Fix bundle method not spotting "local" fqn when schema from URL + - Forgot to remove deprecated JSON_VALIDATOR_CACHE_DIR in 2.00 + +2.00 2018-01-19 + - Fix validating against any enum value #22 + - Require YAML::XS 0.67 for proper boolean handling + - Removed support for parsing YAML with YAML::Syck + - Removed deprecated method load_and_validate_spec() + +1.08 2017-12-24 + - Fix setting default value from $ref jhthorsen/mojolicious-plugin-openapi#53 + - Skip load-from-app.t if "Service Unavailable" + +1.07 2017-11-27 + - Can load schame from internal app #85 + +1.06 2017-11-19 + - Add JSON::Validator::get() + - Add JSON::Validator::bundle() + - A $ref is tied hashes, represented by JSON::Validator::Ref + +1.05 2017-10-22 + - Fix validating headers regardless of case #77 + Contributor: Aleksandr Orlenko + - Improved boolean handling #76 + Contributor: Aleksandr Orlenko + - Improved URI validation, fixes #74 + - Resolving "$ref" on the fly #65 #75 #79 + +1.04 2017-10-05 + - Avoid autovivification of "patternProperties" in the input schema #47 + +1.03 2017-09-25 + - Fix "uri" format validation, closes #70 + +1.02 2017-09-01 + - Fix validating "type" and "enum" #69 + +1.01 2017-08-19 + - Add support for fetching specification from local application + +1.00 2017-06-20 + - Removed EXPERIMENTAL from JSON::Validator::OpenAPI (1.00) + - Coerce integer numbers into booleans #67 + Contributor: @fabzzap + +0.99 2017-06-12 + - Hopefully fixed some Windows issues #60 + +0.98 2017-05-21 + - Add support for "const" #62 + Contributor: Kevin Goess + +0.97 2017-03-21 + - Require a newer version of Test::More to build + +0.96 2017-03-06 + - Fix JSON::Validator::load_and_validate_schema() + - Add handling of header/formData/query as array #38 + - Allow alternative date-time separator #49 + - Improved recursion tracking #52 + - More tests in t/acceptance.t are ok #52 + - Avoid loading the same file multiple times #54 + - Swagger2 is deprecated + +0.95 2017-03-02 + - Add support for format "password" + - Add load_and_validate_schema() to JSON::Validator #51 + - Started deprecating load_and_validate_spec() + +0.94 2017-02-13 + - Fix t/issue-27-yaml-syck-false.t + - Removed Carp::Always #47 + +0.93 2017-02-13 + - Fix coercing YAML booleans in input specification jhthorsen/mojolicious-plugin-openapi#24 + - Replace JSON_VALIDATOR_CACHE_DIR with JSON_VALIDATOR_CACHE_PATH + - Remove deprecated cache_dir attribute + +0.92 2017-01-18 + - Fix infinite recursion when resolving self referencing data structures + +0.91 2017-01-10 + - Mojo::Util::slurp is DEPRECATED in favor of Mojo::File::slurp + +0.90 2016-12-11 + - Add support for validating Dancer2 requset/responses #34 + - Fix invalidating integer/number path part with letters #37 + +0.89 2016-11-05 + - Fix multipleOf:0.01, closes #35 + +0.88 2016-11-04 + - Fix number coercion #32 + Contributor: @melhesedek + - Add JSON::Validator::OpenAPI->load_and_validate_spec() + +0.87 2016-10-20 + - Fix validating data when boolean.pm is loaded + +0.86 2016-10-06 + - Documented bundled resources + +0.85 2016-09-26 + - Fix handling of collectionFormat where no input is defined + +0.84 2016-08-19 + - Removed support for passing $json_path to validate() + - Fix guessing type of objects that has TO_JSON() + +0.83 2016-08-16 + - Fix handling of true/false in schema, when loaded with YAML::Syck #27 + - Add EXPERIMENTAL support for passing $json_path to validate() + +0.82 2016-08-09 + - Fix finding all $ref occurances jhthorsen/swagger2#95 + +0.81 2016-08-08 + - Add support for multiple cache dir search paths + - Deperecated cache_dir() + - Fix recurring requests with same path part jhthorsen/swagger2#93 + - Fix "Use of uninitialized value $schema_type..." warnings + +0.80 2016-08-03 + - Fix parsing recursive schema + +0.79 2016-07-28 + - Reverted improved allOf, anyOf and oneOf error messages + +0.78 2016-07-28 + - Fix recursive dependencies #23 + - Add EXPERIMENTAL resolver attribute + - Improved allOf, anyOf and oneOf error messages + +0.77 2016-07-26 + - Avoid duplicate error messages with enum #22 + - Fix "false" must be false and not true in OpenAPI + +0.76 2016-07-25 + - Will write default values into Mojolicious::Controller + +0.75 2016-07-02 + - Fix uploads must not be slurped + - Fix reporting error on missing response status definition + - Add warnings on invalid (Perl) regexes + +0.74 2016-06-22 + - Fix length($data) need be defined in 5.10 + +0.73 2016-06-22 + - Add http://git.io/vcKD4 error schema to cache + - Add JSON schema for JSONPatch files + - Updated Swagger2 spec to https://github.com/OAI/OpenAPI-Specification/blob/19fed9f0f812ccebe0fc45313fea75bb6656de1c/schemas/v2.0/schema.json + +0.72 2016-06-10 + - Fix default cache_dir() path + - JSON::Validator is no longer EXPERIMENTAL + - Move Swagger2::SchemaValidator into JSON::Validator::OpenAPI + +0.71 2016-06-07 + - Fix setting schema() inside validate() + +0.70 2016-05-31 + - Fix allowing "id" as property name in objects + +0.69 2016-05-26 + - Fix failing anyOf logic in t/swagger-validate-response-object.t + +0.68 2016-05-25 + - Remove _merge_error to clarify anyOf errors #15 + +0.67 2016-04-11 + - Add JSON::Validator::Error class + +0.66 2016-02-09 + - Fix validating recursive datastructures + +0.65 2016-01-07 + - Fix t/swagger-validate-response-object.t require Swagger2 0.66 #14 + +0.64 2015-12-18 + - Fix treating JSON::PP::Boolean objects as boolean #13 + Contributor: Krasimir Berov + - Allow hash reference as arguments to coerce #13 + Contributor: Krasimir Berov + +0.63 2015-11-28 + - Fix skip test in t/booleans.t + +0.62 2015-11-27 + - Remove support for YAML.pm #jhthorsen/swagger2#50 + - Remove support for YAML::Tiny #jhthorsen/swagger2#50 + +0.61 2015-11-11 + - Fix use of TO_JSON() on objects inside arrays #12 + +0.60 2015-11-09 + - Can use TO_JSON() when validating perl objects + +0.59 2015-10-14 + - Move "collectionFormat" support to Swagger2 + +0.58 2015-10-13 + - Fix string "0" is not detected as boolean + +0.57 2015-10-11 + - Trust guesswork if input data is undefined + +0.56 2015-09-30 + - Can read YAML::XS booleans automatically #8 + - Change coerce() into a method. #8 + - Remove EXPERIMENTAL coerce attribute #8 + - Remove EXPERIMENTAL JSON_VALIDATOR_COERCE_VALUES and SWAGGER_COERCE_VALUES #8 + +0.55 2015-09-29 + - Fix "required" cannot be a boolean on properties + - Improved documentation of error object + - Change anyOf/allOf/oneOf error message + +0.54 2015-09-27 + - Add support for $ref to relative path #3 #4 #5 + - Removed Swagger specific type "file" + - Removed Swagger specific formats: "byte", "date", "double", "float", "int32" and "int64". + +0.53 2015-09-13 + - Fix properties, patternProperties, additionalProperties interaction - patternProperty invalidates property + - Fix validation for a keyword and instance SHOULD succeed when keywords does not match primitive type + - Fix allOf with base schema - mismatch base schema + - Fix checking for a boolean "required" + +0.52 2015-09-05 + - Add guessing of schema type, based on other attributes + - More strict on what is validated as "boolean" + - Fix additionalItems are allowed by default + - Fix additionalProperties allows a schema which should validate + - Fix validating "enum" + - Fix validating "array" against "additionalItems" + - Fix bugs after running + https://github.com/Relequestual/Test-JSON-Schema-Acceptance to validate + +0.51 2015-08-24 + - Fix "$ref" pointing to a file on disk #1 + +0.50 2015-08-23 + - Fix missing namespace when registering new document + - Made cache_dir() public + - Bundled spec for json-schema and swagger + +0.49 2015-08-23 + - Fix loading schema from files + +0.48 2015-08-22 + - Merged core functionality from Swagger2 and Swagger2::SchemaValidator + into this module, JSON::Validator + See https://metacpan.org/source/JHTHORSEN/Swagger2-0.47/Changes for + previous Changes (<=0.47) + - Fix coercing collectionFormat strings into integers and numbers + - Add support for reading schemas from __DATA__ section diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..3d5d4da --- /dev/null +++ b/MANIFEST @@ -0,0 +1,119 @@ +.gitattributes +.github/CONTRIBUTING.md +.github/ISSUE_TEMPLATE.md +.github/PULL_REQUEST_TEMPLATE.md +.perltidyrc +.travis.yml +Changes +lib/JSON/Validator.pm +lib/JSON/Validator/cache/10a5eeb37fcd5d829449028f7ceb0774 +lib/JSON/Validator/cache/36d1bd12eeed51e86c8695bd8876a9df +lib/JSON/Validator/cache/3d35aac549d951f4cf9182ff47bff0b4 +lib/JSON/Validator/cache/49c95b866e40f788892a7fb3c816b0e8 +lib/JSON/Validator/cache/4a31fe43be9e23ca9eb8d9e9faba8892 +lib/JSON/Validator/cache/630949337805585c8e52deea27d11419 +lib/JSON/Validator/cache/a0f5b4b4e75ea17fc09e88ec0343d148 +lib/JSON/Validator/cache/ea34d47d4e060a1c3b12d2287aff89a7 +lib/JSON/Validator/cache/eaa832720f36cff0abc20c05236a9cd9 +lib/JSON/Validator/Error.pm +lib/JSON/Validator/Formats.pm +lib/JSON/Validator/Joi.pm +lib/JSON/Validator/Ref.pm +Makefile.PL +MANIFEST This list of files +README.md +t/00-basic.t +t/acceptance.t +t/booleans.t +t/bundle.t +t/coerce.t +t/deep-mixed-ref.t +t/definitions/age.json +'t/definitions/space age.json' +t/definitions/unit.json +t/definitions/weight.json +t/draft4-tests/additionalItems.json +t/draft4-tests/additionalProperties.json +t/draft4-tests/allOf.json +t/draft4-tests/anyOf.json +t/draft4-tests/default.json +t/draft4-tests/definitions.json +t/draft4-tests/dependencies.json +t/draft4-tests/enum.json +t/draft4-tests/items.json +t/draft4-tests/maximum.json +t/draft4-tests/maxItems.json +t/draft4-tests/maxLength.json +t/draft4-tests/maxProperties.json +t/draft4-tests/minimum.json +t/draft4-tests/minItems.json +t/draft4-tests/minLength.json +t/draft4-tests/minProperties.json +t/draft4-tests/multipleOf.json +t/draft4-tests/not.json +t/draft4-tests/oneOf.json +t/draft4-tests/optional/bignum.json +t/draft4-tests/optional/format.json +t/draft4-tests/optional/zeroTerminatedFloats.json +t/draft4-tests/pattern.json +t/draft4-tests/patternProperties.json +t/draft4-tests/properties.json +t/draft4-tests/ref.json +t/draft4-tests/refRemote.json +t/draft4-tests/required.json +t/draft4-tests/type.json +t/draft4-tests/uniqueItems.json +t/get.t +t/Helper.pm +t/id-keyword-draft4.t +t/id-keyword-draft7.t +t/invalid-ref.t +t/issue-103-one-of.t +t/issue-22-duplicate-error-messages.t +t/issue-42-cache-control.t +t/issue-59-oneof-blessed-booleans.t +t/issue-71-additionalproperties.t +t/joi.t +t/jv-allof.t +t/jv-anyof.t +t/jv-array.t +t/jv-basic.t +t/jv-boolean.t +t/jv-const.t +t/jv-enum.t +t/jv-formats.t +t/jv-integer.t +t/jv-not.t +t/jv-number.t +t/jv-object.t +t/jv-oneof.t +t/jv-required.t +t/jv-string.t +t/load-data.t +t/load-file.t +t/load-http.t +t/load-json.t +t/load-yaml.t +t/random-errors.t +t/relative-ref.t +t/remotes/folder/folderInteger.json +t/remotes/integer.json +t/remotes/subSchemas.json +t/schema-as-attr.t +t/spec/bundlecheck.json +t/spec/missing-ref.json +t/spec/person.json +t/spec/petstore.json +'t/spec/space bundle.json' +t/spec/with-deep-mixed-ref.json +t/spec/with-relative-ref.json +t/stack/Some.pm +t/stack/Some/Module.pm +t/to-json.t +t/validate-draft07.t +t/validate-id.t +t/validate-json.t +t/validate-recursive.t +t/validate-schema.t +META.yml Module YAML meta-data (added by MakeMaker) +META.json Module JSON meta-data (added by MakeMaker) diff --git a/META.json b/META.json new file mode 100644 index 0000000..d55185f --- /dev/null +++ b/META.json @@ -0,0 +1,65 @@ +{ + "abstract" : "Validate data against a JSON schema", + "author" : [ + "Jan Henning Thorsen " + ], + "dynamic_config" : 0, + "generated_by" : "ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010", + "license" : [ + "artistic_2" + ], + "meta-spec" : { + "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", + "version" : 2 + }, + "name" : "JSON-Validator", + "no_index" : { + "directory" : [ + "t", + "inc", + "examples", + "t" + ] + }, + "prereqs" : { + "build" : { + "requires" : { + "ExtUtils::MakeMaker" : "0" + } + }, + "configure" : { + "requires" : { + "ExtUtils::MakeMaker" : "0" + } + }, + "runtime" : { + "requires" : { + "Mojolicious" : "7.28", + "perl" : "5.010001" + } + }, + "test" : { + "requires" : { + "Test::More" : "1.30" + } + } + }, + "release_status" : "stable", + "resources" : { + "bugtracker" : { + "web" : "https://github.com/mojolicious/json-validator/issues" + }, + "homepage" : "https://mojolicious.org", + "license" : [ + "http://www.opensource.org/licenses/artistic-license-2.0" + ], + "repository" : { + "type" : "git", + "url" : "https://github.com/mojolicious/json-validator.git", + "web" : "https://github.com/mojolicious/json-validator" + }, + "x_IRC" : "irc://irc.freenode.net/#mojo" + }, + "version" : "3.06", + "x_serialization_backend" : "JSON::PP version 2.97001" +} diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..bcfa316 --- /dev/null +++ b/META.yml @@ -0,0 +1,33 @@ +--- +abstract: 'Validate data against a JSON schema' +author: + - 'Jan Henning Thorsen ' +build_requires: + ExtUtils::MakeMaker: '0' + Test::More: '1.30' +configure_requires: + ExtUtils::MakeMaker: '0' +dynamic_config: 0 +generated_by: 'ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010' +license: artistic_2 +meta-spec: + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: '1.4' +name: JSON-Validator +no_index: + directory: + - t + - inc + - examples + - t +requires: + Mojolicious: '7.28' + perl: '5.010001' +resources: + IRC: irc://irc.freenode.net/#mojo + bugtracker: https://github.com/mojolicious/json-validator/issues + homepage: https://mojolicious.org + license: http://www.opensource.org/licenses/artistic-license-2.0 + repository: https://github.com/mojolicious/json-validator.git +version: '3.06' +x_serialization_backend: 'CPAN::Meta::YAML version 0.018' diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..b5b6874 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,44 @@ +use 5.010001; + +use strict; +use warnings; +use utf8; + +use ExtUtils::MakeMaker; + +my %WriteMakefileArgs = ( + NAME => 'JSON::Validator', + ABSTRACT_FROM => 'lib/JSON/Validator.pm', + AUTHOR => 'Jan Henning Thorsen ', + LICENSE => 'artistic_2', + VERSION_FROM => 'lib/JSON/Validator.pm', + META_MERGE => { + 'dynamic_config' => 0, + 'meta-spec' => {version => 2}, + 'no_index' => {directory => ['examples', 't']}, + 'prereqs' => {runtime => {requires => {perl => '5.010001'}}}, + 'resources' => { + bugtracker => + {web => 'https://github.com/mojolicious/json-validator/issues'}, + homepage => 'https://mojolicious.org', + license => ['http://www.opensource.org/licenses/artistic-license-2.0'], + repository => { + type => 'git', + url => 'https://github.com/mojolicious/json-validator.git', + web => 'https://github.com/mojolicious/json-validator', + }, + x_IRC => 'irc://irc.freenode.net/#mojo', + }, + }, + PREREQ_PM => {'Mojolicious' => '7.28'}, + TEST_REQUIRES => {'Test::More' => '1.30'}, + test => {TESTS => (-e 'META.yml' ? 't/*.t' : 't/*.t xt/*.t')}, +); + +unless (eval { ExtUtils::MakeMaker->VERSION('6.63_03') }) { + my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES}; + @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} + = values %$test_requires; +} + +WriteMakefile(%WriteMakefileArgs); diff --git a/README.md b/README.md index 638242e..e643032 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ -# perl-JSON-Validator -Fork of JSON::Validator Perl module + +# JSON::Validator [![Build Status](https://api.travis-ci.org/mojolicious/json-validator.svg?branch=master)](https://travis-ci.org/mojolicious/json-validator) + + A module for validating data against a [JSON Schema](https://json-schema.org/). + +```perl +use Mojolicious::Lite -signatures; +use JSON::Validator 'joi'; +use Mojo::JSON qw(false to_json true); + +post '/users' => sub ($c) { + my $user = $c->req->json; + + # Validate input JSON document + my @errors = joi( + $user, + joi->object->props( + email => joi->email->required, + username => joi->string->min(1)->required, + password => joi->string->min(12)->required, + ) + ); + + # Report back on invalid input + return $c->render(json => {errors => \@errors}, status => 400) if @errors; + + # Handle the $user in some way + $c->app->log->info(to_json $user); + + # Report back the status + return $c->render(json => {accepted => true}, status => 201); +}; + +app->start; +``` + +## Installation + + All you need is a one-liner, it takes seconds to install. + + $ curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n JSON::Validator + + We recommend the use of a [Perlbrew](http://perlbrew.pl) environment. + +## Want to know more? + + Take a look at our excellent + [documentation](https://mojolicious.org/perldoc/JSON/Validator)! diff --git a/lib/JSON/Validator.pm b/lib/JSON/Validator.pm new file mode 100644 index 0000000..9c81d3a --- /dev/null +++ b/lib/JSON/Validator.pm @@ -0,0 +1,1463 @@ +package JSON::Validator; +use Mojo::Base -base; + +use B; +use Carp 'confess'; +use Exporter 'import'; +use JSON::Validator::Error; +use JSON::Validator::Formats; +use JSON::Validator::Joi; +use JSON::Validator::Ref; +use Mojo::File 'path'; +use Mojo::JSON::Pointer; +use Mojo::JSON; +use Mojo::Loader; +use Mojo::URL; +use Mojo::Util qw(url_unescape sha1_sum); +use Scalar::Util qw(blessed refaddr looks_like_number); +use Time::Local (); + +use constant CASE_TOLERANT => File::Spec->case_tolerant; +use constant COLORS => eval { require Term::ANSIColor }; +use constant DEBUG => $ENV{JSON_VALIDATOR_DEBUG} || 0; +use constant REPORT => $ENV{JSON_VALIDATOR_REPORT} // DEBUG >= 2; +use constant RECURSION_LIMIT => $ENV{JSON_VALIDATOR_RECURSION_LIMIT} || 100; +use constant SPECIFICATION_URL => 'http://json-schema.org/draft-04/schema#'; + +our $VERSION = '3.06'; +our @EXPORT_OK = qw(joi validate_json); + +my $BUNDLED_CACHE_DIR = path(path(__FILE__)->dirname, qw(Validator cache)); +my $HTTP_SCHEME_RE = qr{^https?:}; + +sub D { + Data::Dumper->new([@_])->Sortkeys(1)->Indent(0)->Maxdepth(2)->Pair(':') + ->Useqq(1)->Terse(1)->Dump; +} +sub E { JSON::Validator::Error->new(@_) } + +sub S { + Mojo::Util::md5_sum(Data::Dumper->new([@_])->Sortkeys(1)->Useqq(1)->Dump); +} + +has cache_paths => sub { + return [split(/:/, $ENV{JSON_VALIDATOR_CACHE_PATH} || ''), + $BUNDLED_CACHE_DIR]; +}; + +has formats => sub { shift->_build_formats }; +has version => 4; + +has ua => sub { + require Mojo::UserAgent; + my $ua = Mojo::UserAgent->new; + $ua->proxy->detect; + $ua->max_redirects(3); + $ua; +}; + +sub bundle { + my ($self, $args) = @_; + my @topics = ([undef, my $bundle = {}]); + my ($cloner, $tied); + + $topics[0][0] + = $args->{schema} ? $self->_resolve($args->{schema}) : $self->schema->data; + + if ($args->{replace}) { + $cloner = sub { + my $from = shift; + my $ref = ref $from; + $from = $tied->schema if $ref eq 'HASH' and $tied = tied %$from; + my $to = $ref eq 'ARRAY' ? [] : $ref eq 'HASH' ? {} : $from; + push @topics, [$from, $to] if $ref; + return $to; + }; + } + else { + my $ref_key = $args->{ref_key} || 'x-bundled'; + $bundle->{$ref_key} = $topics[0][0]{$ref_key} || {}; + $cloner = sub { + my $from = shift; + my $ref = ref $from; + + if ($ref eq 'HASH' and my $tied = tied %$from) { + my $ref_name = $tied->fqn; + return $from if $ref_name =~ m!^\Q$self->{root_schema_url}\E\#!; + + if (-e $ref_name) { + $ref_name = sprintf '%s-%s', substr(sha1_sum($ref_name), 0, 10), + path($ref_name)->basename; + } + else { + $ref_name =~ s![^\w-]!_!g; + } + + push @topics, [$tied->schema, $bundle->{$ref_key}{$ref_name} = {}]; + tie my %ref, 'JSON::Validator::Ref', $tied->schema, + "#/$ref_key/$ref_name"; + return \%ref; + } + + my $to = $ref eq 'ARRAY' ? [] : $ref eq 'HASH' ? {} : $from; + push @topics, [$from, $to] if $ref; + return $to; + }; + } + + while (@topics) { + my ($from, $to) = @{shift @topics}; + if (ref $from eq 'ARRAY') { + for (my $i = 0; $i < @$from; $i++) { + $to->[$i] = $cloner->($from->[$i]); + } + } + elsif (ref $from eq 'HASH') { + while (my ($key, $value) = each %$from) { + $to->{$key} //= $cloner->($from->{$key}); + } + } + } + + return $bundle; +} + +sub coerce { + my $self = shift; + return $self->{coerce} ||= {} unless @_; + $self->{coerce} + = $_[0] eq '1' ? {booleans => 1, numbers => 1, strings => 1} + : ref $_[0] ? {%{$_[0]}} + : {@_}; + $self; +} + +sub get { + my ($self, $pointer) = @_; + $pointer + = [ + ref $pointer ? @$pointer + : length $pointer ? split('/', $pointer, -1) + : $pointer + ]; + shift @$pointer + if @$pointer + and defined $pointer->[0] + and !length $pointer->[0]; + $self->_get($self->schema->data, $pointer, ''); +} + +sub joi { + return JSON::Validator::Joi->new unless @_; + my ($data, $joi) = @_; + return $joi->validate($data, $joi); +} + +sub load_and_validate_schema { + my ($self, $spec, $args) = @_; + my $schema = $args->{schema} || SPECIFICATION_URL; + $self->version($1) if !$self->{version} and $schema =~ /draft-0+(\w+)/; + $spec = $self->_resolve($spec); + my @errors = $self->new(%$self)->schema($schema)->validate($spec); + confess join "\n", "Invalid JSON specification $spec:", map {"- $_"} @errors + if @errors; + $self->{schema} = Mojo::JSON::Pointer->new($spec); + $self; +} + +sub schema { + my $self = shift; + return $self->{schema} unless @_; + $self->{schema} = Mojo::JSON::Pointer->new($self->_resolve(shift)); + return $self; +} + +sub singleton { state $validator = shift->new } + +sub validate { + my ($self, $data, $schema) = @_; + $schema ||= $self->schema->data; + return E '/', 'No validation rules defined.' unless $schema and %$schema; + + local $self->{grouped} = 0; + local $self->{schema} = Mojo::JSON::Pointer->new($schema); + local $self->{seen} = {}; + local $self->{temp_schema} = []; # make sure random-errors.t does not fail + $self->{report} = []; + my @errors = $self->_validate($_[1], '', $schema); + $self->_report if DEBUG and REPORT; + return @errors; +} + +sub validate_json { + __PACKAGE__->singleton->schema($_[1])->validate($_[0]); +} + +sub _build_formats { + return { + 'date' => JSON::Validator::Formats->can('check_date'), + 'date-time' => JSON::Validator::Formats->can('check_date_time'), + 'email' => JSON::Validator::Formats->can('check_email'), + 'hostname' => JSON::Validator::Formats->can('check_hostname'), + 'idn-email' => JSON::Validator::Formats->can('check_idn_email'), + 'idn-hostname' => JSON::Validator::Formats->can('check_idn_hostname'), + 'ipv4' => JSON::Validator::Formats->can('check_ipv4'), + 'ipv6' => JSON::Validator::Formats->can('check_ipv6'), + 'iri' => JSON::Validator::Formats->can('check_iri'), + 'iri-reference' => JSON::Validator::Formats->can('check_iri_reference'), + 'json-pointer' => JSON::Validator::Formats->can('check_json_pointer'), + 'regex' => JSON::Validator::Formats->can('check_regex'), + 'relative-json-pointer' => + JSON::Validator::Formats->can('check_relative_json_pointer'), + 'time' => JSON::Validator::Formats->can('check_time'), + 'uri' => JSON::Validator::Formats->can('check_uri'), + 'uri-reference' => JSON::Validator::Formats->can('check_uri_reference'), + 'uri-reference' => JSON::Validator::Formats->can('check_uri_reference'), + 'uri-template' => JSON::Validator::Formats->can('check_uri_template'), + }; +} + +sub _get { + my ($self, $data, $path, $pos, $cb) = @_; + my $tied; + + while (@$path) { + my $p = shift @$path; + + unless (defined $p) { + my $i = 0; + return Mojo::Collection->new( + map { $self->_get($_->[0], [@$path], _path($pos, $_->[1]), $cb) } + ref $data eq 'ARRAY' ? map { [$_, $i++] } + @$data : ref $data eq 'HASH' ? map { [$data->{$_}, $_] } + sort keys %$data : [$data, '']); + } + + $p =~ s!~1!/!g; + $p =~ s/~0/~/g; + $pos = _path($pos, $p) if $cb; + + if (ref $data eq 'HASH' and exists $data->{$p}) { + $data = $data->{$p}; + } + elsif (ref $data eq 'ARRAY' and $p =~ /^\d+$/ and @$data > $p) { + $data = $data->[$p]; + } + else { + return undef; + } + + $data = $tied->schema if ref $data eq 'HASH' and $tied = tied %$data; + } + + return $cb->($data, $pos) if $cb; + return $data; +} + +sub _id_key { $_[0]->version < 7 ? 'id' : '$id' } + +sub _load_schema { + my ($self, $url) = @_; + + if ($url =~ m!^https?://!) { + warn "[JSON::Validator] Loading schema from URL $url\n" if DEBUG; + return $self->_load_schema_from_url(Mojo::URL->new($url)->fragment(undef)), + "$url"; + } + + if ($url =~ m!^data://([^/]*)/(.*)!) { + my ($file, @modules) = ($2, ($1)); + @modules = _stack() unless $modules[0]; + for my $module (@modules) { + warn "[JSON::Validator] Looking for $file in $module\n" if DEBUG; + my $text = Mojo::Loader::data_section($module, $file); + return $self->_load_schema_from_text(\$text), "$url" if $text; + } + confess "$file could not be found in __DATA__ section of @modules."; + } + + if ($url =~ m!^\s*[\[\{]!) { + warn "[JSON::Validator] Loading schema from string.\n" if DEBUG; + return $self->_load_schema_from_text(\$url), ''; + } + + my $file = $url; + $file =~ s!^file://!!; + $file =~ s!#$!!; + $file = path(split '/', $file); + if (-e $file) { + $file = $file->realpath; + warn "[JSON::Validator] Loading schema from file: $file\n" if DEBUG; + return $self->_load_schema_from_text(\$file->slurp), + CASE_TOLERANT ? path(lc $file) : $file; + } + elsif ($url =~ m!^/! and $self->ua->server->app) { + warn "[JSON::Validator] Loading schema from URL $url\n" if DEBUG; + return $self->_load_schema_from_url(Mojo::URL->new($url)->fragment(undef)), + "$url"; + } + + confess "Unable to load schema '$url' ($file)"; +} + +sub _load_schema_from_text { + my ($self, $text) = @_; + my $visit; + + # JSON + return Mojo::JSON::decode_json($$text) if $$text =~ /^\s*\{/s; + + # YAML + $visit = sub { + my $v = shift; + $visit->($_) for grep { ref $_ eq 'HASH' } values %$v; + return $v + unless $v->{type} + and $v->{type} eq 'boolean' + and exists $v->{default}; + %$v + = (%$v, default => $v->{default} ? Mojo::JSON->true : Mojo::JSON->false); + return $v; + }; + + local $YAML::XS::Boolean = 'JSON::PP'; + return $visit->($self->_yaml_module->can('Load')->($$text)); +} + +sub _load_schema_from_url { + my ($self, $url) = @_; + my $cache_path = $self->cache_paths->[0]; + my $cache_file = Mojo::Util::md5_sum("$url"); + my ($err, $tx); + + for (@{$self->cache_paths}) { + my $path = path $_, $cache_file; + warn "[JSON::Validator] Looking for cached spec $path ($url)\n" if DEBUG; + next unless -r $path; + return $self->_load_schema_from_text(\$path->slurp); + } + + $tx = $self->ua->get($url); + $err = $tx->error && $tx->error->{message}; + confess "GET $url == $err" if DEBUG and $err; + die "[JSON::Validator] GET $url == $err" if $err; + + if ($cache_path + and + ($cache_path ne $BUNDLED_CACHE_DIR or $ENV{JSON_VALIDATOR_CACHE_ANYWAYS}) + and -w $cache_path) + { + $cache_file = path $cache_path, $cache_file; + warn "[JSON::Validator] Caching $url to $cache_file\n" + unless $ENV{HARNESS_ACTIVE}; + $cache_file->spurt($tx->res->body); + } + + return $self->_load_schema_from_text(\$tx->res->body); +} + +sub _ref_to_schema { + my ($self, $schema) = @_; + + my @guard; + while (my $tied = tied %$schema) { + push @guard, $tied->ref; + confess "Seems like you have a circular reference: @guard" + if @guard > RECURSION_LIMIT; + $schema = $tied->schema; + } + + return $schema; +} + +sub _register_schema { + my ($self, $schema, $fqn) = @_; + $fqn =~ s!(.)#$!$1!; + $self->{schemas}{$fqn} = $schema; +} + +sub _report { + my $table = Mojo::Util::tablify($_[0]->{report}); + $table =~ s!^(\W*)(N?OK|<<<)(.*)!{ + my ($x, $y, $z) = ($1, $2, $3); + my $c = $y eq 'OK' ? 'green' : $y eq '<<<' ? 'blue' : 'magenta'; + $c = "$c bold" if $z =~ /\s\w+Of\s/; + Term::ANSIColor::colored([$c], "$x$y$z") + }!gme if COLORS; + warn "---\n$table"; +} + +sub _report_errors { + my ($self, $path, $type, $errors) = @_; + push @{$self->{report}}, + [ + ((' ') x $self->{grouped}) . (@$errors ? 'NOK' : 'OK'), + $path || '/', + $type, join "\n", @$errors + ]; +} + +sub _report_schema { + my ($self, $path, $type, $schema) = @_; + push @{$self->{report}}, + [((' ') x $self->{grouped}) . ('<<<'), $path || '/', $type, D $schema]; +} + +# _resolve() method is used to convert all "id" into absolute URLs and +# resolve all the $ref's that we find inside JSON Schema specification. +sub _resolve { + my ($self, $schema) = @_; + my $id_key = $self->_id_key; + my ($id, $resolved, @refs); + + local $self->{level} = $self->{level} || 0; + delete $_[0]->{schemas}{''} unless $self->{level}; + + if (ref $schema eq 'HASH') { + $id = $schema->{$id_key} // ''; + return $resolved if $resolved = $self->{schemas}{$id}; + } + elsif ($resolved = $self->{schemas}{$schema // ''}) { + return $resolved; + } + else { + ($schema, $id) = $self->_load_schema($schema); + $id = $schema->{$id_key} if $schema->{$id_key}; + } + + unless ($self->{level}) { + my $rid = $schema->{$id_key} // $id; + if ($rid) { + confess "Root schema cannot have a fragment in the 'id'. ($rid)" + if $rid =~ /\#./; + confess "Root schema cannot have a relative 'id'. ($rid)" + unless $rid =~ /^\w+:/ + or -e $rid + or $rid =~ m!^/!; + } + warn sprintf "[JSON::Validator] Using root_schema_url of '$rid'\n" if DEBUG; + $self->{root_schema_url} = $rid; + } + + $self->{level}++; + $self->_register_schema($schema, $id); + + my @topics + = ([$schema, UNIVERSAL::isa($id, 'Mojo::File') ? $id : Mojo::URL->new($id) + ]); + while (@topics) { + my ($topic, $base) = @{shift @topics}; + + if (UNIVERSAL::isa($topic, 'ARRAY')) { + push @topics, map { [$_, $base] } @$topic; + } + elsif (UNIVERSAL::isa($topic, 'HASH')) { + push @refs, [$topic, $base] and next + if $topic->{'$ref'} and !ref $topic->{'$ref'}; + + if ($topic->{$id_key} and !ref $topic->{$id_key}) { + my $fqn = Mojo::URL->new($topic->{$id_key}); + $fqn = $fqn->to_abs($base) unless $fqn->is_abs; + $self->_register_schema($topic, $fqn->to_string); + } + + push @topics, map { [$_, $base] } values %$topic; + } + } + + # Need to register "id":"..." before resolving "$ref":"..." + $self->_resolve_ref(@$_) for @refs; + + return $schema; +} + +sub _location_to_abs { + my ($location, $base) = @_; + my $location_as_url = Mojo::URL->new($location); + return $location_as_url if $location_as_url->is_abs; + + # definitely relative now + if ($base->isa('Mojo::File')) { + return $base if !length $location; + return $base->sibling(split '/', $location)->realpath; + } + return $location_as_url->to_abs($base); +} + +sub _resolve_ref { + my ($self, $topic, $url) = @_; + return if tied %$topic; + + my $other = $topic; + my ($location, $fqn, $pointer, $ref, @guard); + + while (1) { + $ref = $other->{'$ref'}; + push @guard, $other->{'$ref'}; + confess "Seems like you have a circular reference: @guard" + if @guard > RECURSION_LIMIT; + last if !$ref or ref $ref; + $fqn = $ref =~ m!^/! ? "#$ref" : $ref; + ($location, $pointer) = split /#/, $fqn, 2; + $url = $location = _location_to_abs($location, $url); + $pointer = undef if length $location and !length $pointer; + $pointer = url_unescape $pointer if defined $pointer; + $fqn = join '#', grep defined, $location, $pointer; + $other = $self->_resolve($location); + + if (defined $pointer and length $pointer and $pointer =~ m!^/!) { + $other = Mojo::JSON::Pointer->new($other)->get($pointer) + or confess + qq[Possibly a typo in schema? Could not find "$pointer" in "$location" ($ref)]; + } + } + + tie %$topic, 'JSON::Validator::Ref', $other, $topic->{'$ref'}, $fqn; +} + +sub _stack { + my @classes; + my $i = 2; + while (my $pkg = caller($i++)) { + no strict 'refs'; + push @classes, + grep { !/(^JSON::Validator$|^Mojo::Base$|^Mojolicious$|\w+::_Dynamic)/ } + $pkg, @{"$pkg\::ISA"}; + } + return @classes; +} + +sub _validate { + my ($self, $data, $path, $schema) = @_; + my ($seen_addr, $to_json, $type); + + # Do not validate against "default" in draft-07 schema + return if blessed $schema and $schema->isa('JSON::PP::Boolean'); + + $schema = $self->_ref_to_schema($schema) if $schema->{'$ref'}; + $seen_addr = join ':', refaddr($schema), + (ref $data ? refaddr $data : ++$self->{seen}{scalar}); + + # Avoid recursion + if ($self->{seen}{$seen_addr}) { + $self->_report_schema($path || '/', 'seen', $schema) if REPORT; + return @{$self->{seen}{$seen_addr}}; + } + + $self->{seen}{$seen_addr} = \my @errors; + $to_json + = (blessed $data and $data->can('TO_JSON')) ? \$data->TO_JSON : undef; + $data = $$to_json if $to_json; + $type = $schema->{type} || _guess_schema_type($schema, $data); + + # Test base schema before allOf, anyOf or oneOf + if (ref $type eq 'ARRAY') { + push @{$self->{temp_schema}}, [map { +{%$schema, type => $_} } @$type]; + push @errors, + $self->_validate_any_of($to_json ? $$to_json : $_[1], + $path, $self->{temp_schema}[-1]); + } + elsif ($type) { + my $method = sprintf '_validate_type_%s', $type; + $self->_report_schema($path || '/', $type, $schema); + @errors = $self->$method($to_json ? $$to_json : $_[1], $path, $schema); + $self->_report_errors($path, $type, \@errors) if REPORT; + return @errors if @errors; + } + + if ($schema->{enum}) { + push @errors, + $self->_validate_type_enum($to_json ? $$to_json : $_[1], $path, $schema); + $self->_report_errors($path, 'enum', \@errors) if REPORT; + return @errors if @errors; + } + + if (my $rules = $schema->{not}) { + push @errors, $self->_validate($to_json ? $$to_json : $_[1], $path, $rules); + $self->_report_errors($path, 'not', \@errors) if REPORT; + return @errors ? () : (E $path, 'Should not match.'); + } + + if (my $rules = $schema->{allOf}) { + push @errors, + $self->_validate_all_of($to_json ? $$to_json : $_[1], $path, $rules); + } + elsif ($rules = $schema->{anyOf}) { + push @errors, + $self->_validate_any_of($to_json ? $$to_json : $_[1], $path, $rules); + } + elsif ($rules = $schema->{oneOf}) { + push @errors, + $self->_validate_one_of($to_json ? $$to_json : $_[1], $path, $rules); + } + + return @errors; +} + +sub _validate_all_of { + my ($self, $data, $path, $rules) = @_; + my $type = _guess_data_type($data, $rules); + my (@errors, @expected); + + $self->_report_schema($path, 'allOf', $rules) if REPORT; + local $self->{grouped} = $self->{grouped} + 1; + + my $i = 0; + for my $rule (@$rules) { + next unless my @e = $self->_validate($_[1], $path, $rule); + my $schema_type = _guess_schema_type($rule); + push @expected, $schema_type if $schema_type; + push @errors, [$i, @e] if !$schema_type or $schema_type eq $type; + } + continue { + $i++; + } + + $self->_report_errors($path, 'allOf', \@errors) if REPORT; + return E $path, "/allOf Expected @{[join '/', _uniq(@expected)]} - got $type." + if !@errors and @expected; + return _add_path_to_error_messages(allOf => @errors) if @errors; + return; +} + +sub _validate_any_of { + my ($self, $data, $path, $rules) = @_; + my $type = _guess_data_type($data, $rules); + my (@e, @errors, @expected); + + $self->_report_schema($path, 'anyOf', $rules) if REPORT; + local $self->{grouped} = $self->{grouped} + 1; + + my $i = 0; + for my $rule (@$rules) { + @e = $self->_validate($_[1], $path, $rule); + return unless @e; + my $schema_type = _guess_schema_type($rule); + push @errors, [$i, @e] and next if !$schema_type or $schema_type eq $type; + push @expected, $schema_type; + } + continue { + $i++; + } + + $self->_report_errors($path, 'anyOf', \@errors) if REPORT; + my $expected = join '/', _uniq(@expected); + return E $path, "/anyOf Expected $expected - got $type." unless @errors; + return _add_path_to_error_messages(anyOf => @errors); +} + +sub _validate_one_of { + my ($self, $data, $path, $rules) = @_; + my $type = _guess_data_type($data, $rules); + my (@errors, @expected); + + $self->_report_schema($path, 'oneOf', $rules) if REPORT; + local $self->{grouped} = $self->{grouped} + 1; + + my $i = 0; + for my $rule (@$rules) { + my @e = $self->_validate($_[1], $path, $rule) or next; + my $schema_type = _guess_schema_type($rule); + push @errors, [$i, @e] and next if !$schema_type or $schema_type eq $type; + push @expected, $schema_type; + } + continue { + $i++; + } + + if (REPORT) { + my @e + = @errors + @expected + 1 == @$rules ? () + : @errors ? @errors + : 'All of the oneOf rules match.'; + $self->_report_errors($path, 'oneOf', \@e); + } + + return if @errors + @expected + 1 == @$rules; + my $expected = join '/', _uniq(@expected); + return E $path, "All of the oneOf rules match." unless @errors + @expected; + return E $path, "/oneOf Expected $expected - got $type." unless @errors; + return _add_path_to_error_messages(oneOf => @errors); +} + +sub _validate_type_enum { + my ($self, $data, $path, $schema) = @_; + my $enum = $schema->{enum}; + my $m = S $data; + + for my $i (@$enum) { + return if $m eq S $i; + } + + local $" = ', '; + return E $path, sprintf 'Not in enum list: %s.', join ', ', + map { (!defined or ref) ? Mojo::JSON::encode_json($_) : $_ } @$enum; +} + +sub _validate_type_const { + my ($self, $data, $path, $schema) = @_; + my $const = $schema->{const}; + my $m = S $data; + + return if $m eq S $const; + return E $path, sprintf 'Does not match const: %s.', + Mojo::JSON::encode_json($const); +} + +sub _validate_format { + my ($self, $value, $path, $schema) = @_; + my $code = $self->formats->{$schema->{format}}; + return do { warn "Format rule for '$schema->{format}' is missing"; return } + unless $code; + return unless my $err = $code->($value); + return E $path, $err; +} + +sub _validate_type_any { } + +sub _validate_type_array { + my ($self, $data, $path, $schema) = @_; + my @errors; + + if (ref $data ne 'ARRAY') { + return E $path, _expected(array => $data); + } + if (defined $schema->{minItems} and $schema->{minItems} > @$data) { + push @errors, E $path, sprintf 'Not enough items: %s/%s.', int @$data, + $schema->{minItems}; + } + if (defined $schema->{maxItems} and $schema->{maxItems} < @$data) { + push @errors, E $path, sprintf 'Too many items: %s/%s.', int @$data, + $schema->{maxItems}; + } + if ($schema->{uniqueItems}) { + my %uniq; + for (@$data) { + next if !$uniq{S($_)}++; + push @errors, E $path, 'Unique items required.'; + last; + } + } + + if ($schema->{contains}) { + my @e; + for my $i (0 .. @$data - 1) { + my @tmp = $self->_validate($data->[$i], "$path/$i", $schema->{contains}); + push @e, \@tmp if @tmp; + } + push @errors, map {@$_} @e if @e >= @$data; + } + elsif (ref $schema->{items} eq 'ARRAY') { + my $additional_items = $schema->{additionalItems} // {type => 'any'}; + my @rules = @{$schema->{items}}; + + if ($additional_items) { + push @rules, $additional_items while @rules < @$data; + } + + if (@rules == @$data) { + for my $i (0 .. @rules - 1) { + push @errors, $self->_validate($data->[$i], "$path/$i", $rules[$i]); + } + } + elsif (!$additional_items) { + push @errors, E $path, sprintf "Invalid number of items: %s/%s.", + int(@$data), int(@rules); + } + } + elsif (UNIVERSAL::isa($schema->{items}, 'HASH')) { + for my $i (0 .. @$data - 1) { + push @errors, $self->_validate($data->[$i], "$path/$i", $schema->{items}); + } + } + + return @errors; +} + +sub _validate_type_boolean { + my ($self, $value, $path, $schema) = @_; + + # Object representing a boolean + if (blessed $value + and ($value->isa('JSON::PP::Boolean') or "$value" eq "1" or !$value)) + { + return; + } + + # String that looks like a boolean + if ( + defined $value + and $self->{coerce}{booleans} + and (B::svref_2object(\$value)->FLAGS & (B::SVp_IOK | B::SVp_NOK) + or $value =~ /^(true|false)$/) + ) + { + $_[1] = $value ? Mojo::JSON->true : Mojo::JSON->false; + return; + } + + return E $path, _expected(boolean => $value); +} + +sub _validate_type_integer { + my ($self, $value, $path, $schema) = @_; + my @errors = $self->_validate_type_number($_[1], $path, $schema, 'integer'); + + return @errors if @errors; + return if $value =~ /^-?\d+$/; + return E $path, "Expected integer - got number."; +} + +sub _validate_type_null { + my ($self, $value, $path, $schema) = @_; + + return E $path, 'Not null.' if defined $value; + return; +} + +sub _validate_type_number { + my ($self, $value, $path, $schema, $expected) = @_; + my @errors; + + $expected ||= 'number'; + + if (!defined $value or ref $value) { + return E $path, _expected($expected => $value); + } + unless (_is_number($value)) { + return E $path, "Expected $expected - got string." + if !$self->{coerce}{numbers} + or !looks_like_number($value); + $_[1] = 0 + $value; # coerce input value + } + + if ($schema->{format}) { + push @errors, $self->_validate_format($value, $path, $schema); + } + if (my $e + = _cmp($schema->{minimum}, $value, $schema->{exclusiveMinimum}, '<')) + { + push @errors, E $path, "$value $e minimum($schema->{minimum})"; + } + if (my $e + = _cmp($value, $schema->{maximum}, $schema->{exclusiveMaximum}, '>')) + { + push @errors, E $path, "$value $e maximum($schema->{maximum})"; + } + if (my $d = $schema->{multipleOf}) { + if (($value / $d) =~ /\.[^0]+$/) { + push @errors, E $path, "Not multiple of $d."; + } + } + + return @errors; +} + +sub _validate_type_object { + my ($self, $data, $path, $schema) = @_; + my %required = map { ($_ => 1) } @{$schema->{required} || []}; + my ($additional, @errors, %rules); + + if (ref $data ne 'HASH') { + return E $path, _expected(object => $data); + } + + my @dkeys = sort keys %$data; + if (defined $schema->{maxProperties} and $schema->{maxProperties} < @dkeys) { + push @errors, E $path, sprintf 'Too many properties: %s/%s.', int @dkeys, + $schema->{maxProperties}; + } + if (defined $schema->{minProperties} and $schema->{minProperties} > @dkeys) { + push @errors, E $path, sprintf 'Not enough properties: %s/%s.', int @dkeys, + $schema->{minProperties}; + } + if (my $n_schema = $schema->{propertyNames}) { + for my $name (keys %$data) { + next unless my @e = $self->_validate($name, $path, $n_schema); + push @errors, + _add_path_to_error_messages(propertyName => [map { ($name, $_) } @e]); + } + } + if ($schema->{if}) { + push @errors, + $self->_validate($data, $path, $schema->{if}) + ? $self->_validate($data, $path, $schema->{else} // {}) + : $self->_validate($data, $path, $schema->{then} // {}); + } + + while (my ($k, $r) = each %{$schema->{properties}}) { + push @{$rules{$k}}, $r; + } + while (my ($p, $r) = each %{$schema->{patternProperties} || {}}) { + push @{$rules{$_}}, $r for sort grep { $_ =~ /$p/ } @dkeys; + } + + $additional + = exists $schema->{additionalProperties} + ? $schema->{additionalProperties} + : {}; + if ($additional) { + $additional = {} unless UNIVERSAL::isa($additional, 'HASH'); + $rules{$_} ||= [$additional] for @dkeys; + } + elsif (my @k = grep { !$rules{$_} } @dkeys) { + local $" = ', '; + return E $path, "Properties not allowed: @k."; + } + + for my $k (sort keys %required) { + next if exists $data->{$k}; + push @errors, E _path($path, $k), 'Missing property.'; + delete $rules{$k}; + } + + for my $k (sort keys %rules) { + for my $r (@{$rules{$k}}) { + next unless exists $data->{$k}; + my @e = $self->_validate($data->{$k}, _path($path, $k), $r); + push @errors, @e; + next if @e or !UNIVERSAL::isa($r, 'HASH'); + push @errors, + $self->_validate_type_enum($data->{$k}, _path($path, $k), $r) + if $r->{enum}; + push @errors, + $self->_validate_type_const($data->{$k}, _path($path, $k), $r) + if $r->{const}; + } + } + + return @errors; +} + +sub _validate_type_string { + my ($self, $value, $path, $schema) = @_; + my @errors; + + if (!defined $value or ref $value) { + return E $path, _expected(string => $value); + } + if ( B::svref_2object(\$value)->FLAGS & (B::SVp_IOK | B::SVp_NOK) + and 0 + $value eq $value + and $value * 0 == 0) + { + return E $path, "Expected string - got number." + unless $self->{coerce}{strings}; + $_[1] = "$value"; # coerce input value + } + if ($schema->{format}) { + push @errors, $self->_validate_format($value, $path, $schema); + } + if (defined $schema->{maxLength}) { + if (length($value) > $schema->{maxLength}) { + push @errors, E $path, sprintf "String is too long: %s/%s.", + length($value), $schema->{maxLength}; + } + } + if (defined $schema->{minLength}) { + if (length($value) < $schema->{minLength}) { + push @errors, E $path, sprintf "String is too short: %s/%s.", + length($value), $schema->{minLength}; + } + } + if (defined $schema->{pattern}) { + my $p = $schema->{pattern}; + unless ($value =~ /$p/) { + push @errors, E $path, "String does not match '$p'"; + } + } + + return @errors; +} + +# FUNCTIONS ================================================================== + +sub _add_path_to_error_messages { + my ($type, @errors_with_index) = @_; + my @errors; + + for my $e (@errors_with_index) { + my $index = shift @$e; + push @errors, map { + my $msg = sprintf '/%s/%s %s', $type, $index, $_->{message}; + $msg =~ s!(\d+)\s/!$1/!g; + E $_->path, $msg; + } @$e; + } + + return @errors; +} + +sub _cmp { + return undef if !defined $_[0] or !defined $_[1]; + return "$_[3]=" if $_[2] and $_[0] >= $_[1]; + return $_[3] if $_[0] > $_[1]; + return ""; +} + +sub _expected { + my $type = _guess_data_type($_[1], []); + return "Expected $_[0] - got different $type." if $_[0] =~ /\b$type\b/; + return "Expected $_[0] - got $type."; +} + +# _guess_data_type($data, [{type => ...}, ...]) +sub _guess_data_type { + my $ref = ref $_[0]; + my $blessed = blessed $_[0]; + return 'object' if $ref eq 'HASH'; + return lc $ref if $ref and !$blessed; + return 'null' if !defined $_[0]; + return 'boolean' if $blessed and ("$_[0]" eq "1" or !"$_[0]"); + + if (_is_number($_[0])) { + return 'integer' if grep { ($_->{type} // '') eq 'integer' } @{$_[1] || []}; + return 'number'; + } + + return $blessed || 'string'; +} + +# _guess_schema_type($schema, $data) +sub _guess_schema_type { + return $_[0]->{type} if $_[0]->{type}; + return _guessed_right(object => $_[1]) if $_[0]->{additionalProperties}; + return _guessed_right(object => $_[1]) if $_[0]->{patternProperties}; + return _guessed_right(object => $_[1]) if $_[0]->{properties}; + return _guessed_right(object => $_[1]) if $_[0]->{propertyNames}; + return _guessed_right(object => $_[1]) if $_[0]->{required}; + return _guessed_right(object => $_[1]) if $_[0]->{if}; + return _guessed_right(object => $_[1]) + if defined $_[0]->{maxProperties} + or defined $_[0]->{minProperties}; + return _guessed_right(array => $_[1]) if $_[0]->{additionalItems}; + return _guessed_right(array => $_[1]) if $_[0]->{items}; + return _guessed_right(array => $_[1]) if $_[0]->{uniqueItems}; + return _guessed_right(array => $_[1]) + if defined $_[0]->{maxItems} + or defined $_[0]->{minItems}; + return _guessed_right(string => $_[1]) if $_[0]->{pattern}; + return _guessed_right(string => $_[1]) + if defined $_[0]->{maxLength} + or defined $_[0]->{minLength}; + return _guessed_right(number => $_[1]) if $_[0]->{multipleOf}; + return _guessed_right(number => $_[1]) + if defined $_[0]->{maximum} + or defined $_[0]->{minimum}; + return 'const' if $_[0]->{const}; + return undef; +} + +# _guessed_right($type, $data); +sub _guessed_right { + return $_[0] if !defined $_[1]; + return $_[0] if $_[0] eq _guess_data_type($_[1], [{type => $_[0]}]); + return undef; +} + +sub _is_number { + B::svref_2object(\$_[0])->FLAGS & (B::SVp_IOK | B::SVp_NOK) + && 0 + $_[0] eq $_[0] + && $_[0] * 0 == 0; +} + +sub _path { + local $_ = $_[1]; + s!~!~0!g; + s!/!~1!g; + "$_[0]/$_"; +} + +sub _uniq { + my %uniq; + grep { !$uniq{$_}++ } @_; +} + +# Please report if you need to manually monkey patch this function +# https://github.com/jhthorsen/json-validator/issues +sub _yaml_module { + state $yaml_module = eval qq[use YAML::XS 0.67; "YAML::XS"] + || die + "[JSON::Validator] The optional YAML::XS module is missing or could not be loaded: $@"; +} + +1; + +=encoding utf8 + +=head1 NAME + +JSON::Validator - Validate data against a JSON schema + +=head1 SYNOPSIS + + use JSON::Validator; + my $validator = JSON::Validator->new; + + # Define a schema - http://json-schema.org/learn/miscellaneous-examples.html + # You can also load schema from disk or web + $validator->schema({ + type => "object", + required => ["firstName", "lastName"], + properties => { + firstName => {type => "string"}, + lastName => {type => "string"}, + age => {type => "integer", minimum => 0, description => "Age in years"} + } + }); + + # Validate your data + my @errors = $validator->validate({firstName => "Jan Henning", lastName => "Thorsen", age => -42}); + + # Do something if any errors was found + die "@errors" if @errors; + + # Use joi() to build the schema + use JSON::Validator 'joi'; + + $validator->schema(joi->object->props({ + firstName => joi->string->required, + lastName => joi->string->required, + age => joi->integer->min(0), + })); + + # joi() can also validate directly + my @errors = joi( + {firstName => "Jan Henning", lastName => "Thorsen", age => -42}, + joi->object->props({ + firstName => joi->string->required, + lastName => joi->string->required, + age => joi->integer->min(0), + }); + ); + +=head1 DESCRIPTION + +L is a data structure validation library based around +L. This module can be used directly with +a JSON schema or you can use the elegant DSL schema-builder +L to define the schema programmatically. + +=head2 Supported schema formats + +L can load JSON schemas in multiple formats: Plain perl data +structured (as shown in L), JSON or YAML. The JSON parsing is done +with L, while YAML files require the optional module L to +be installed. + +=head2 Resources + +Here are some resources that are related to JSON schemas and validation: + +=over 4 + +=item * L + +=item * L + +=item * L + +=back + +=head2 Bundled specifications + +This module comes with some JSON specifications bundled, so your application +don't have to fetch those from the web. These specifications should be up to +date, but please submit an issue if they are not. + +Files referenced to an URL will automatically be cached if the first element in +L is a writable directory. Note that the cache headers for the +remote assets are B honored, so you will manually need to remove any +cached file, should you need to refresh them. + +To download and cache an online asset, do this: + + JSON_VALIDATOR_CACHE_PATH=/some/writable/directory perl myapp.pl + +Here is the list of the bundled specifications: + +=over 2 + +=item * JSON schema, draft 4, 6, 7 + +Web page: L + +C<$ref>: L, +L, +L. + +=item * JSON schema for JSONPatch files + +Web page: L + +C<$ref>: L + +=item * Swagger / OpenAPI specification, version 2 + +Web page: L + +C<$ref>: L + +=item * OpenAPI specification, version 3 + +Web page: L + +C<$ref>: L + +This specification is still EXPERIMENTAL. + +=item * Swagger Petstore + +This is used for unit tests, and should not be relied on by external users. + +=back + +=head1 ERROR OBJECT + +The methods L and the function L returns a list of +L objects when the input data violates the L. + +=head1 FUNCTIONS + +=head2 joi + + use JSON::Validator "joi"; + my $joi = joi; + my @errors = joi($data, $joi); # same as $joi->validate($data); + +Used to construct a new L object or perform validation. + +=head2 validate_json + + use JSON::Validator "validate_json"; + my @errors = validate_json $data, $schema; + +This can be useful in web applications: + + my @errors = validate_json $c->req->json, "data://main/spec.json"; + +See also L and L for more details. + +=head1 ATTRIBUTES + +=head2 cache_paths + + my $validator = $validator->cache_paths(\@paths); + my $array_ref = $validator->cache_paths; + +A list of directories to where cached specifications are stored. Defaults to +C environment variable and the specs that is bundled +with this distribution. + +C can be a list of directories, each separated by ":". + +See L for more details. + +=head2 formats + + my $hash_ref = $validator->formats; + my $validator = $validator->formats(\%hash); + +Holds a hash-ref, where the keys are supported JSON type "formats", and +the values holds a code block which can validate a given format. A code +block should return C on success and an error string on error: + + sub { return defined $_[0] && $_[0] eq "42" ? undef : "Not the answer." }; + +See L for a list of supported formats. + +=head2 ua + + my $ua = $validator->ua; + my $validator = $validator->ua(Mojo::UserAgent->new); + +Holds a L object, used by L to load a JSON schema +from remote location. + +The default L will detect proxy settings and have +L set to 3. + +=head2 version + + my $int = $validator->version; + my $validator = $validator->version(7); + +Used to set the JSON Schema version to use. Will be set automatically when +using L, unless already set. + +=head1 METHODS + +=head2 bundle + + my $schema = $validator->bundle(\%args); + +Used to create a new schema, where the C<$ref> are resolved. C<%args> can have: + +=over 2 + +=item * C<{replace => 1}> + +Used if you want to replace the C<$ref> inline in the schema. This currently +does not work if you have circular references. The default is to move all the +C<$ref> definitions into the main schema with custom names. Here is an example +on how a C<$ref> looks before and after: + + {"$ref":"../some/place.json#/foo/bar"} + => {"$ref":"#/definitions/____some_place_json-_foo_bar"} + + {"$ref":"http://example.com#/foo/bar"} + => {"$ref":"#/definitions/_http___example_com-_foo_bar"} + +=item * C<{schema => {...}}> + +Default is to use the value from the L attribute. + +=back + +=head2 coerce + + my $validator = $validator->coerce(booleans => 1, numbers => 1, strings => 1); + my $validator = $validator->coerce({booleans => 1, numbers => 1, strings => 1}); + my $hash_ref = $validator->coerce; + +Set the given type to coerce. Before enabling coercion this module is very +strict when it comes to validating types. Example: The string C<"1"> is not +the same as the number C<1>, unless you have coercion enabled. + +Loading a YAML document will enable "booleans" automatically. This feature is +experimental, but was added since YAML has no real concept of booleans, such +as L or other JSON parsers. + +=head2 get + + my $sub_schema = $validator->get("/x/y"); + my $sub_schema = $validator->get(["x", "y"]); + +Extract value from L identified by the given JSON Pointer. Will at the +same time resolve C<$ref> if found. Example: + + $validator->schema({x => {'$ref' => '#/y'}, y => {'type' => 'string'}}); + $validator->schema->get('/x') == undef + $validator->schema->get('/x')->{'$ref'} == '#/y' + $validator->get('/x') == {type => 'string'} + +The argument can also be an array-ref with the different parts of the pointer +as each elements. + +=head2 load_and_validate_schema + + my $validator = $validator->load_and_validate_schema($schema, \%args); + +Will load and validate C<$schema> against the OpenAPI specification. C<$schema> +can be anything L accepts. The expanded specification +will be stored in L on success. See +L for the different version of C<$url> that can be +accepted. + +C<%args> can be used to further instruct the validation process: + +=over 2 + +=item * schema + +Defaults to "http://json-schema.org/draft-04/schema#", but can be any +structured that can be used to validate C<$schema>. + +=back + +=head2 schema + + my $validator = $validator->schema($json_or_yaml_string); + my $validator = $validator->schema($url); + my $validator = $validator->schema(\%schema); + my $validator = $validator->schema(JSON::Validator::Joi->new); + my $schema = $validator->schema; + +Used to set a schema from either a data structure or a URL. + +C<$schema> will be a L object when loaded, +and C by default. + +The C<$url> can take many forms, but needs to point to a text file in the +JSON or YAML format. + +=over 4 + +=item * file://... + +A file on disk. Note that it is required to use the "file" scheme if you want +to reference absolute paths on your file system. + +=item * http://... or https://... + +A web resource will be fetched using the L, stored in L. + +=item * data://Some::Module/spec.json + +Will load a given "spec.json" file from C using +L. + +=item * data:///spec.json + +A "data" URL without a module name will use the current package and search up +the call/inheritance tree. + +=item * Any other URL + +An URL (without a recognized scheme) will be treated as a path to a file on +disk. + +=back + +=head2 singleton + + my $validator = JSON::Validator->singleton; + +Returns the L object used by L. + +=head2 validate + + my @errors = $validator->validate($data); + my @errors = $validator->validate($data, $schema); + +Validates C<$data> against a given JSON L. C<@errors> will +contain validation error objects or be an empty list on success. + +See L for details. + +C<$schema> is optional, but when specified, it will override schema stored in +L. Example: + + $validator->validate({hero => "superwoman"}, {type => "object"}); + +=head2 SEE ALSO + +=over 2 + +=item * L + +L is a plugin for L that utilize +L and the L +to build routes with input and output validation. + +=back + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2014-2018, Jan Henning Thorsen + +This program is free software, you can redistribute it and/or modify it under +the terms of the Artistic License version 2.0. + +=head1 AUTHOR + +Jan Henning Thorsen - C + +Daniel Böhmer - C + +Ed J - C + +Kevin Goess - C + +Martin Renvoize - C + +=cut diff --git a/lib/JSON/Validator/Error.pm b/lib/JSON/Validator/Error.pm new file mode 100644 index 0000000..5453259 --- /dev/null +++ b/lib/JSON/Validator/Error.pm @@ -0,0 +1,83 @@ +package JSON::Validator::Error; +use Mojo::Base -base; + +use overload q("") => \&to_string, bool => sub {1}, fallback => 1; + +sub new { + my $self = bless {}, shift; + @$self{qw(path message)} = ($_[0] || '/', $_[1] || ''); + $self; +} + +sub message { shift->{message} } +sub path { shift->{path} } +sub to_string { sprintf '%s: %s', @{$_[0]}{qw(path message)} } +sub TO_JSON { {message => $_[0]->{message}, path => $_[0]->{path}} } + +1; + +=encoding utf8 + +=head1 NAME + +JSON::Validator::Error - JSON::Validator error object + +=head1 SYNOPSIS + + use JSON::Validator::Error; + my $err = JSON::Validator::Error->new($path, $message); + +=head1 DESCRIPTION + +L is a class representing validation errors from +L. + +=head1 ATTRIBUTES + +=head2 message + + my $str = $error->message; + +A human readable description of the error. Defaults to empty string. + +=head2 path + + my $str = $error->path; + +A JSON pointer to where the error occurred. Defaults to "/". + +=head1 METHODS + +=head2 new + + my $error = JSON::Validator::Error->new($path, $message); + +Object constructor. + +=head2 to_string + + my $str = $error->to_string; + +Returns the "path" and "message" part as a string: "$path: $message". + +=head1 OPERATORS + +L overloads the following operators: + +=head2 bool + + my $bool = !!$error; + +Always true. + +=head2 stringify + + my $str = "$error"; + +Alias for L. + +=head1 SEE ALSO + +L. + +=cut diff --git a/lib/JSON/Validator/Formats.pm b/lib/JSON/Validator/Formats.pm new file mode 100644 index 0000000..557b5f6 --- /dev/null +++ b/lib/JSON/Validator/Formats.pm @@ -0,0 +1,339 @@ +package JSON::Validator::Formats; +use Mojo::Base -strict; + +use constant DATA_VALIDATE_DOMAIN => eval 'require Data::Validate::Domain;1'; +use constant DATA_VALIDATE_IP => eval 'require Data::Validate::IP;1'; +use constant NET_IDN_ENCODE => eval 'require Net::IDN::Encode;1'; +use constant WARN_MISSING_MODULE => $ENV{JSON_VALIDATOR_WARN} // 1; + +our $IRI_TEST_NAME = 'iri-reference'; + +sub check_date { + my @date = $_[0] =~ m!^(\d{4})-(\d\d)-(\d\d)$!io; + return 'Does not match date format.' unless @date; + @date = map { s/^0+//; $_ || 0 } reverse @date; + $date[1] -= 1; # month are zero based + local $@; + return undef if eval { Time::Local::timegm(0, 0, 0, @date); 1 }; + my $err = (split / at /, $@)[0]; + $err =~ s!('-?\d+'\s|\s[\d\.]+)!!g; + $err .= '.'; + return $err; +} + +sub check_date_time { + my @dt = $_[0] + =~ m!^(\d{4})-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d(?:\.\d+)?)(?:Z|([+-])(\d+):(\d+))?$!io; + return 'Does not match date-time format.' unless @dt; + @dt = map { s/^0//; $_ } reverse @dt[0 .. 5]; + $dt[4] -= 1; # month are zero based + local $@; + return undef if eval { Time::Local::timegm(@dt); 1 }; + my $err = (split / at /, $@)[0]; + $err =~ s!('-?\d+'\s|\s[\d\.]+)!!g; + $err .= '.'; + return $err; +} + +sub check_email { + state $email_rfc5322_re = do { + my $atom = qr;[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+;o; + my $quoted_string = qr/"(?:\\[^\r\n]|[^\\"])*"/o; + my $domain_literal + = qr/\[(?:\\[\x01-\x09\x0B-\x0c\x0e-\x7f]|[\x21-\x5a\x5e-\x7e])*\]/o; + my $dot_atom = qr/$atom(?:[.]$atom)*/o; + my $local_part = qr/(?:$dot_atom|$quoted_string)/o; + my $domain = qr/(?:$dot_atom|$domain_literal)/o; + + qr/$local_part\@$domain/o; + }; + + return $_[0] =~ $email_rfc5322_re ? undef : 'Does not match email format.'; +} + +sub check_hostname { + return _module_missing(hostname => 'Data::Validate::Domain') + unless DATA_VALIDATE_DOMAIN; + return undef if Data::Validate::Domain::is_hostname($_[0]); + return 'Does not match hostname format.'; +} + +sub check_idn_email { + return _module_missing('idn-email' => 'Net::IDN::Encode') + unless NET_IDN_ENCODE; + + local $@; + my $err = eval { + my @email = split /@/, $_[0], 2; + check_email( + join '@', + Net::IDN::Encode::to_ascii($email[0] // ''), + Net::IDN::Encode::domain_to_ascii($email[1] // ''), + ); + }; + + return $err ? 'Does not match idn-email format.' : $@ || undef; +} + +sub check_idn_hostname { + return _module_missing('idn-hostname' => 'Net::IDN::Encode') + unless NET_IDN_ENCODE; + + local $@; + my $err = eval { check_hostname(Net::IDN::Encode::domain_to_ascii($_[0])) }; + return $err ? 'Does not match idn-hostname format.' : $@ || undef; +} + +sub check_iri { + local $IRI_TEST_NAME = 'iri'; + return 'Scheme missing.' unless $_[0] =~ m!^\w+:!; + return check_iri_reference($_[0]); +} + +sub check_iri_reference { + return "Does not match $IRI_TEST_NAME format." + unless $_[0] + =~ m!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!; + + my ($scheme, $auth_host, $path, $query, $has_fragment, $fragment) + = map { $_ // '' } ($2, $4, $5, $7, $8, $9); + + return 'Scheme missing.' if length $auth_host and !length $scheme; + return 'Scheme, path or fragment are required.' + unless length($scheme) + length($path) + length($has_fragment); + return 'Scheme must begin with a letter.' + if length $scheme and lc($scheme) !~ m!^[a-z][a-z0-9\+\-\.]*$!; + return 'Invalid hex escape.' if $_[0] =~ /%[^0-9a-f]/i; + return 'Hex escapes are not complete.' + if $_[0] =~ /%[0-9a-f](:?[^0-9a-f]|$)/i; + + if (defined $auth_host and length $auth_host) { + return 'Path cannot be empty and must begin with a /' + unless !length $path or $path =~ m!^/!; + } + elsif ($path =~ m!^//!) { + return 'Path cannot not start with //.'; + } + + return undef; +} + +sub check_json_pointer { + return !length $_[0] + || $_[0] =~ m!^/! ? undef : 'Does not match json-pointer format.'; +} + +sub check_ipv4 { + return undef if DATA_VALIDATE_IP and Data::Validate::IP::is_ipv4($_[0]); + my (@octets) = $_[0] =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + return undef + if 4 == grep { $_ >= 0 && $_ <= 255 && $_ !~ /^0\d{1,2}$/ } @octets; + return 'Does not match ipv4 format.'; +} + +sub check_ipv6 { + return _module_missing(ipv6 => 'Data::Validate::IP') unless DATA_VALIDATE_IP; + return undef if Data::Validate::IP::is_ipv6($_[0]); + return 'Does not match ipv6 format.'; +} + +sub check_relative_json_pointer { + return 'Relative JSON Pointer must start with a non-negative-integer.' + unless $_[0] =~ m!^\d+!; + return undef if $_[0] =~ m!^(\d+)#?$!; + return 'Relative JSON Pointer must have "#" or a JSON Pointer.' + unless $_[0] =~ m!^\d+(.+)!; + return 'Does not match relative-json-pointer format.' + if check_json_pointer($1); + return undef; +} + +sub check_regex { + eval {qr{$_[0]}} ? undef : 'Does not match regex format.'; +} + +sub check_time { + my @time + = $_[0] =~ m!^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(?:Z|([+-])(\d+):(\d+))?$!io; + return 'Does not match time format.' unless @time; + @time = map { s/^0//; $_ } reverse @time[0 .. 2]; + local $@; + return undef if eval { Time::Local::timegm(@time, 31, 11, 1947); 1 }; + my $err = (split / at /, $@)[0]; + $err =~ s!('-?\d+'\s|\s[\d\.]+)!!g; + $err .= '.'; + return $err; +} + +sub check_uri { + return 'An URI can only only contain ASCII characters.' + if $_[0] =~ m!\P{ASCII}!; + local $IRI_TEST_NAME = 'uri'; + return check_iri_reference($_[0]); +} + +sub check_uri_reference { + local $IRI_TEST_NAME = 'uri-reference'; + return check_iri_reference($_[0]); +} + +sub check_uri_template { + return check_iri($_[0]); +} + +sub _module_missing { + warn "[JSON::Validator] Cannot validate $_[0] format: $_[1] is missing" + if WARN_MISSING_MODULE; + return undef; +} + +1; + +=encoding utf8 + +=head1 NAME + +JSON::Validator::Formats - Functions for valiating JSON schema formats + +=head1 SYNOPSIS + + use JSON::Validator::Formats; + my $error = JSON::Validator::Formats::check_uri($str); + die $error if $error; + + my $jv = JSON::Validator->new; + $jv->formats({ + "date-time" => JSON::Validator::Formats->can("check_date_time"), + "email" => JSON::Validator::Formats->can("check_email"), + "hostname" => JSON::Validator::Formats->can("check_hostname"), + "ipv4" => JSON::Validator::Formats->can("check_ipv4"), + "ipv6" => JSON::Validator::Formats->can("check_ipv6"), + "regex" => JSON::Validator::Formats->can("check_regex"), + "uri" => JSON::Validator::Formats->can("check_uri"), + "uri-reference" => JSON::Validator::Formats->can("check_uri_reference"), + }); + +=head1 DESCRIPTION + +L is a module with utility functions used by +L to match JSON Schema formats. + +=head1 FUNCTIONS + +=head2 check_date + + my $str_or_undef = check_date $str; + +Validates the date part of a RFC3339 string. + +=head2 check_date_time + + my $str_or_undef = check_date_time $str; + +Validated against RFC3339 timestamp in UTC time. This is formatted as +"YYYY-MM-DDThh:mm:ss.fffZ". The milliseconds portion (".fff") is optional + +=head2 check_email + + my $str_or_undef = check_email $str; + +Validated against the RFC5322 spec. + +=head2 check_hostname + + my $str_or_undef = check_hostname $str; + +Will be validated using L, if installed. + +=head2 check_idn_email + + my $str_or_undef = check_idn_email $str; + +Will validate an email with non-ASCII characters using L if +installed. + +=head2 check_idn_hostname + + my $str_or_undef = check_idn_hostname $str; + +Will validate a hostname with non-ASCII characters using L if +installed. + +=head2 check_ipv4 + + my $str_or_undef = check_ipv4 $str; + +Will be validated using L, if installed or fall +back to a plain IPv4 IP regex. + +=head2 check_ipv6 + + my $str_or_undef = check_ipv6 $str; + +Will be validated using L, if installed. + +=head2 check_iri + + my $str_or_undef = check_iri $str; + +Validate either an absolute IRI containing ASCII or non-ASCII characters, +against the RFC3986 spec. + +=head2 check_iri_reference + + my $str_or_undef = check_iri_reference $str; + +Validate either a relative or absolute IRI containing ASCII or non-ASCII +characters, against the RFC3986 spec. + +=head2 check_json_pointer + + my $str_or_undef = check_json_pointer $str; + +Validates a JSON pointer, such as "/foo/bar/42". + +=head2 check_regex + + my $str_or_undef = check_regex $str; + +Will check if the string is a regex, using C. + +=head2 check_relative_json_pointer + + my $str_or_undef = check_relative_json_pointer $str; + +Validates a relative JSON pointer, such as "0/foo" or "3#". + +=head2 check_time + + my $str_or_undef = check_time $str; + +Validates the time and optionally the offset part of a RFC3339 string. + +=head2 check_uri + + my $str_or_undef = check_uri $str; + +Validate either a relative or absolute URI containing just ASCII characters, +against the RFC3986 spec. + +Note that this might change in the future to only check absolute URI. + +=head2 check_uri_reference + + my $str_or_undef = check_uri_reference $str; + +Validate either a relative or absolute URI containing just ASCII characters, +against the RFC3986 spec. + +=head2 check_uri_template + + my $str_or_undef = check_uri_reference $str; + +Validate an absolute URI with template characters. + +=head1 SEE ALSO + +L. + +=cut diff --git a/lib/JSON/Validator/Joi.pm b/lib/JSON/Validator/Joi.pm new file mode 100644 index 0000000..f3657ed --- /dev/null +++ b/lib/JSON/Validator/Joi.pm @@ -0,0 +1,447 @@ +package JSON::Validator::Joi; +use Mojo::Base -base; + +use Exporter 'import'; +use JSON::Validator; +use Mojo::JSON qw(false true); +use Mojo::Util; + +has enum => sub { +[] }; +has [qw(format max min multiple_of regex)] => undef; +has type => 'object'; + +for my $attr (qw(required strict unique)) { + Mojo::Util::monkey_patch(__PACKAGE__, + $attr => sub { $_[0]->{$attr} = $_[1] // 1; $_[0]; }); +} + +sub alphanum { shift->_type('string')->regex('^\w*$') } +sub boolean { shift->type('boolean') } + +sub compile { + my $self = shift; + my $merged = {}; + + for (ref $self->type eq 'ARRAY' ? @{$self->type} : $self->type) { + my $method = "_compile_$_"; + my $compiled = $self->$method; + @$merged{keys %$compiled} = values %$compiled; + } + + return $merged; +} + +sub date_time { shift->_type('string')->format('date-time') } +sub email { shift->_type('string')->format('email') } + +sub extend { + my ($self, $by) = @_; + die "Cannot extend joi '@{[$self->type]}' by '@{[$by->type]}'" + unless $self->type eq $by->type; + + my $clone = shift->new(%$self, %$by); + + if ($self->type eq 'object') { + $clone->{properties}{$_} ||= $self->{properties}{$_} + for keys %{$self->{properties} || {}}; + } + + return $clone; +} + +sub array { shift->type('array') } +sub integer { shift->type('integer') } +sub iso_date { shift->date_time } +sub items { $_[0]->{items} = $_[1]; $_[0] } +sub length { shift->min($_[0])->max($_[0]) } +sub lowercase { shift->_type('string')->regex('^\p{Lowercase}*$') } +sub negative { shift->_type('number')->max(0) } +sub number { shift->type('number') } +sub object { shift->type('object') } +sub pattern { shift->regex(@_) } +sub positive { shift->number->min(0) } + +sub props { + my $self = shift->type('object'); + my %properties = ref $_[0] ? %{$_[0]} : @_; + + while (my ($name, $property) = each %properties) { + push @{$self->{required}}, $name if $property->{required}; + $self->{properties}{$name} = $property->compile; + } + + return $self; +} + +sub string { shift->type('string') } +sub token { shift->_type('string')->regex('^[a-zA-Z0-9_]+$') } +sub uppercase { shift->_type('string')->regex('^\p{Uppercase}*$') } +sub uri { shift->_type('string')->format('uri') } + +sub validate { + my ($self, $data) = @_; + state $validator = JSON::Validator->new->coerce(1); + return $validator->validate($data, $self->compile); +} + +sub _compile_array { + my $self = shift; + my $json = {type => $self->type}; + + $json->{additionalItems} = false if $self->{strict}; + $json->{items} = $self->{items} if $self->{items}; + $json->{maxItems} = $self->{max} if defined $self->{max}; + $json->{minItems} = $self->{min} if defined $self->{min}; + $json->{uniqueItems} = true if $self->{unique}; + + return $json; +} + +sub _compile_boolean { +{type => 'boolean'} } + +sub _compile_integer { shift->_compile_number } + +sub _compile_null { {type => shift->type} } + +sub _compile_number { + my $self = shift; + my $json = {type => $self->type}; + + $json->{enum} = $self->{enum} if defined $self->{enum} and @{$self->{enum}}; + $json->{maximum} = $self->{max} if defined $self->{max}; + $json->{minimum} = $self->{min} if defined $self->{min}; + $json->{multipleOf} = $self->{multiple_of} if defined $self->{multiple_of}; + + return $json; +} + +sub _compile_object { + my $self = shift; + my $json = {type => $self->type}; + + $json->{additionalProperties} = false if $self->{strict}; + $json->{maxProperties} = $self->{max} if defined $self->{max}; + $json->{minProperties} = $self->{min} if defined $self->{min}; + $json->{patternProperties} = $self->{regex} if $self->{regex}; + $json->{properties} = $self->{properties} + if ref $self->{properties} eq 'HASH'; + $json->{required} = $self->{required} if ref $self->{required} eq 'ARRAY'; + + return $json; +} + +sub _compile_string { + my $self = shift; + my $json = {type => $self->type}; + + $json->{enum} = $self->{enum} if defined $self->{enum} and @{$self->{enum}}; + $json->{format} = $self->{format} if defined $self->{format}; + $json->{maxLength} = $self->{max} if defined $self->{max}; + $json->{minLength} = $self->{min} if defined $self->{min}; + $json->{pattern} = $self->{regex} if defined $self->{regex}; + + return $json; +} + +sub _type { + $_[0]->{type} = $_[1] unless $_[0]->{type}; + return $_[0]; +} + +sub TO_JSON { shift->compile } + +1; + +=encoding utf8 + +=head1 NAME + +JSON::Validator::Joi - Joi validation sugar for JSON::Validator + +=head1 SYNOPSIS + + use JSON::Validator "joi"; + + my @errors = joi( + { + name => "Jan Henning", + age => 34, + email => "jhthorsen@cpan.org", + }, + joi->object->props( + age => joi->integer->min(0)->max(200), + email => joi->regex(".@.")->required, + name => joi->string->min(1), + ) + ); + + die "@errors" if @errors; + +=head1 DESCRIPTION + +L is an elegant DSL schema-builder. The main purpose is +to build a L for L, but +it can also validate data directly with sane defaults. + +=head1 ATTRIBUTES + +=head2 enum + + my $joi = $joi->enum(["foo", "bar"]); + my $array_ref = $joi->enum; + +Defines a list of enum values for L, L and L. + +=head2 format + + my $joi = $joi->format("email"); + my $str = $joi->format; + +Used to set the format of the L. +See also L, L and L. + +=head2 max + + my $joi = $joi->max(10); + my $int = $joi->max; + +=over 2 + +=item * array + +Defines the max number of items in the array. + +=item * integer, number + +Defined the max value. + +=item * object + +Defines the max number of items in the object. + +=item * string + +Defines how long the string can be. + +=back + +=head2 min + + my $joi = $joi->min(10); + my $int = $joi->min; + +=over 2 + +=item * array + +Defines the minimum number of items in the array. + +=item * integer, number + +Defined the minimum value. + +=item * object + +Defines the minimum number of items in the object. + +=item * string + +Defines how short the string can be. + +=back + +=head2 multiple_of + + my $joi = $joi->multiple_of(3); + my $int = $joi->multiple_of; + +Used by L and L to define what the number must be a multiple +of. + +=head2 regex + + my $joi = $joi->regex("^\w+$"); + my $str = $joi->regex; + +Defines a pattern that L will be validated against. + +=head2 type + + my $joi = $joi->type("string"); + my $joi = $joi->type([qw(null integer)]); + my $any = $joi->type; + +Sets the required type. This attribute is set by the convenience methods +L, L, L and L, but can be set manually if +you need to check against a list of type. + +=head1 METHODS + +=head2 TO_JSON + +Alias for L. + +=head2 alphanum + + my $joi = $joi->alphanum; + +Sets L to "^\w*$". + +=head2 array + + my $joi = $joi->array; + +Sets L to "array". + +=head2 boolean + + my $joi = $joi->boolean; + +Sets L to "boolean". + +=head2 compile + + my $hash_ref = $joi->compile; + +Will convert this object into a JSON-Schema data structure that +L understands. + +=head2 date_time + + my $joi = $joi->date_time; + +Sets L to L. + +=head2 email + + my $joi = $joi->email; + +Sets L to L. + +=head2 extend + + my $new_joi = $joi->extend($joi); + +Will extend C<$joi> with the definitions in C<$joi> and return a new object. + +=head2 iso_date + +Alias for L. + +=head2 integer + + my $joi = $joi->integer; + +Sets L to "integer". + +=head2 items + + my $joi = $joi->items($joi); + my $joi = $joi->items([$joi, ...]); + +Defines a list of items for the L type. + +=head2 length + + my $joi = $joi->length(10); + +Sets both L and L to the number provided. + +=head2 lowercase + + my $joi = $joi->lowercase; + +Will set L to only match lower case strings. + +=head2 negative + + my $joi = $joi->negative; + +Sets L to C<0>. + +=head2 number + + my $joi = $joi->number; + +Sets L to "number". + +=head2 object + + my $joi = $joi->object; + +Sets L to "object". + +=head2 pattern + +Alias for L. + +=head2 positive + + my $joi = $joi->positive; + +Sets L to C<0>. + +=head2 props + + my $joi = $joi->props(name => JSON::Validator::Joi->new->string, ...); + +Used to define properties for an L type. Each key is the name of the +parameter and the values must be a L object. + +=head2 required + + my $joi = $joi->required; + +Marks the current property as required. + +=head2 strict + + my $joi = $joi->strict; + +Sets L and L to not allow any more items/keys than what is defined. + +=head2 string + + my $joi = $joi->string; + +Sets L to "string". + +=head2 token + + my $joi = $joi->token; + +Sets L to C<^[a-zA-Z0-9_]+$>. + +=head2 validate + + my @errors = $joi->validate($data); + +Used to validate C<$data> using L. Returns a list of +L objects on invalid +input. + +=head2 unique + + my $joi = $joi->unique; + +Used to force the L to only contain unique items. + +=head2 uppercase + + my $joi = $joi->uppercase; + +Will set L to only match upper case strings. + +=head2 uri + + my $joi = $joi->uri; + +Sets L to L. + +=head1 SEE ALSO + +L + +L. + +=cut diff --git a/lib/JSON/Validator/Ref.pm b/lib/JSON/Validator/Ref.pm new file mode 100644 index 0000000..1c6d07f --- /dev/null +++ b/lib/JSON/Validator/Ref.pm @@ -0,0 +1,82 @@ +package JSON::Validator::Ref; +use Mojo::Base -strict; + +use Tie::Hash (); +use base 'Tie::StdHash'; + +my $private = '%%'; + +sub fqn { $_[0]->{"${private}fqn"} } +sub ref { $_[0]->{'$ref'} } +sub schema { $_[0]->{"${private}schema"} } + +# Make it look like there is only one key in the hash +sub EXISTS { + exists $_[0]->{$_[1]} || exists $_[0]->{"${private}schema"}{$_[1]}; +} + +sub FETCH { + exists $_[0]->{$_[1]} ? $_[0]->{$_[1]} : $_[0]->{"${private}schema"}{$_[1]}; +} +sub FIRSTKEY {'$ref'} +sub KEYS {'$ref'} +sub NEXTKEY {undef} +sub SCALAR {1} + +sub TIEHASH { + my ($class, $schema, $ref, $fqn) = @_; + bless { + '$ref' => $ref, + "${private}fqn" => $fqn // $ref, + "${private}schema" => $schema + }, $class; +} + +# jhthorsen: This cannot return schema() since it might cause circular references +sub TO_JSON { {'$ref' => $_[0]->ref} } + +1; + +=encoding utf8 + +=head1 NAME + +JSON::Validator::Ref - JSON::Validator $ref representation + +=head1 SYNOPSIS + + use JSON::Validator::Ref; + my $ref = JSON::Validator::Ref->new({ref => "...", schema => {...}); + +=head1 DESCRIPTION + +L is a class representing a C<$ref> inside a JSON Schema. + +This module SHOULD be considered internal to the L project and +the API is subject to change. + +=head1 ATTRIBUTES + +=head2 fqn + + $str = $ref->fqn; + +The fully qualified version of L. + +=head2 ref + + $str = $ref->ref; + +The original C<$ref> from the document. + +=head2 schema + + $hash_ref = $ref->schema; + +A reference to the schema that the C points to. + +=head1 SEE ALSO + +L. + +=cut diff --git a/lib/JSON/Validator/cache/10a5eeb37fcd5d829449028f7ceb0774 b/lib/JSON/Validator/cache/10a5eeb37fcd5d829449028f7ceb0774 new file mode 100644 index 0000000..5963c2c --- /dev/null +++ b/lib/JSON/Validator/cache/10a5eeb37fcd5d829449028f7ceb0774 @@ -0,0 +1,1490 @@ +type: object +required: + - openapi + - info + - paths +properties: + openapi: + type: string + pattern: ^3\.0\.\d(-.+)?$ + info: + $ref: '#/definitions/Info' + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + servers: + type: array + items: + $ref: '#/definitions/Server' + security: + type: array + items: + $ref: '#/definitions/SecurityRequirement' + tags: + type: array + items: + $ref: '#/definitions/Tag' + paths: + $ref: '#/definitions/Paths' + components: + $ref: '#/definitions/Components' +patternProperties: + '^x-': {} +additionalProperties: false +definitions: + Reference: + type: object + required: + - $ref + patternProperties: + '^\$ref$': + type: string + format: uri-reference + Info: + type: object + required: + - title + - version + properties: + title: + type: string + description: + type: string + termsOfService: + type: string + format: uri-reference + contact: + $ref: '#/definitions/Contact' + license: + $ref: '#/definitions/License' + version: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + + Contact: + type: object + properties: + name: + type: string + url: + type: string + format: uri-reference + email: + type: string + format: email + patternProperties: + '^x-': {} + additionalProperties: false + + License: + type: object + required: + - name + properties: + name: + type: string + url: + type: string + format: uri-reference + patternProperties: + '^x-': {} + additionalProperties: false + + Server: + type: object + required: + - url + properties: + url: + type: string + description: + type: string + variables: + type: object + additionalProperties: + $ref: '#/definitions/ServerVariable' + patternProperties: + '^x-': {} + additionalProperties: false + + ServerVariable: + type: object + required: + - default + properties: + enum: + type: array + items: + type: string + default: + type: string + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + Components: + type: object + properties: + schemas: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Schema' + responses: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Response' + parameters: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Parameter' + examples: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Example' + requestBodies: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/RequestBody' + headers: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Header' + securitySchemes: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/SecurityScheme' + links: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Link' + callbacks: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Callback' + patternProperties: + '^x-': {} + additionalProperties: false + + Schema: + type: object + properties: + title: + type: string + multipleOf: + type: number + minimum: 0 + exclusiveMinimum: true + maximum: + type: number + exclusiveMaximum: + type: boolean + default: false + minimum: + type: number + exclusiveMinimum: + type: boolean + default: false + maxLength: + type: integer + minimum: 0 + minLength: + type: integer + minimum: 0 + default: 0 + pattern: + type: string + format: regex + maxItems: + type: integer + minimum: 0 + minItems: + type: integer + minimum: 0 + default: 0 + uniqueItems: + type: boolean + default: false + maxProperties: + type: integer + minimum: 0 + minProperties: + type: integer + minimum: 0 + default: 0 + required: + type: array + items: + type: string + minItems: 1 + uniqueItems: true + enum: + type: array + items: {} + minItems: 1 + uniqueItems: true + type: + type: string + enum: + - array + - boolean + - integer + - number + - object + - string + not: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + allOf: + type: array + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + oneOf: + type: array + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + anyOf: + type: array + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + properties: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + additionalProperties: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + - type: boolean + default: true + description: + type: string + format: + type: string + default: {} + nullable: + type: boolean + default: false + discriminator: + $ref: '#/definitions/Discriminator' + readOnly: + type: boolean + default: false + writeOnly: + type: boolean + default: false + example: {} + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + deprecated: + type: boolean + default: false + xml: + $ref: '#/definitions/XML' + patternProperties: + '^x-': {} + additionalProperties: false + + Discriminator: + type: object + required: + - propertyName + properties: + propertyName: + type: string + mapping: + type: object + additionalProperties: + type: string + + XML: + type: object + properties: + name: + type: string + namespace: + type: string + format: uri-reference + prefix: + type: string + attribute: + type: boolean + default: false + wrapped: + type: boolean + default: false + patternProperties: + '^x-': {} + additionalProperties: false + + Response: + type: object + required: + - description + properties: + description: + type: string + headers: + additionalProperties: + oneOf: + - $ref: '#/definitions/Header' + - $ref: '#/definitions/Reference' + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + links: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Link' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + MediaType: + oneOf: + - $ref: '#/definitions/MediaTypeWithExample' + - $ref: '#/definitions/MediaTypeWithExamples' + + MediaTypeWithExample: + type: object + properties: + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + example: {} + encoding: + type: object + additionalProperties: + $ref: '#/definitions/Encoding' + patternProperties: + '^x-': {} + additionalProperties: false + + MediaTypeWithExamples: + type: object + required: + - examples + properties: + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + encoding: + type: object + additionalProperties: + $ref: '#/definitions/Encoding' + patternProperties: + '^x-': {} + additionalProperties: false + + Example: + type: object + properties: + summary: + type: string + description: + type: string + value: {} + externalValue: + type: string + format: uri-reference + patternProperties: + '^x-': {} + additionalProperties: false + + Header: + oneOf: + - $ref: '#/definitions/HeaderWithSchema' + - $ref: '#/definitions/HeaderWithContent' + + HeaderWithSchema: + oneOf: + - $ref: '#/definitions/HeaderWithSchemaWithExample' + - $ref: '#/definitions/HeaderWithSchemaWithExamples' + + HeaderWithSchemaWithExample: + type: object + required: + - schema + properties: + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - simple + default: simple + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + example: {} + patternProperties: + '^x-': {} + additionalProperties: false + + HeaderWithSchemaWithExamples: + type: object + required: + - schema + - examples + properties: + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - simple + default: simple + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + HeaderWithContent: + type: object + required: + - content + properties: + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + minProperties: 1 + maxProperties: 1 + patternProperties: + '^x-': {} + additionalProperties: false + + Paths: + type: object + patternProperties: + '^\/': + $ref: '#/definitions/PathItem' + '^x-': {} + additionalProperties: false + + PathItem: + type: object + properties: + $ref: + type: string + summary: + type: string + description: + type: string + get: + $ref: '#/definitions/Operation' + put: + $ref: '#/definitions/Operation' + post: + $ref: '#/definitions/Operation' + delete: + $ref: '#/definitions/Operation' + options: + $ref: '#/definitions/Operation' + head: + $ref: '#/definitions/Operation' + patch: + $ref: '#/definitions/Operation' + trace: + $ref: '#/definitions/Operation' + servers: + type: array + items: + $ref: '#/definitions/Server' + parameters: + type: array + items: + oneOf: + - $ref: '#/definitions/Parameter' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + Operation: + type: object + required: + - responses + properties: + tags: + type: array + items: + type: string + summary: + type: string + description: + type: string + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + operationId: + type: string + parameters: + type: array + items: + oneOf: + - $ref: '#/definitions/Parameter' + - $ref: '#/definitions/Reference' + requestBody: + oneOf: + - $ref: '#/definitions/RequestBody' + - $ref: '#/definitions/Reference' + responses: + $ref: '#/definitions/Responses' + callbacks: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Callback' + - $ref: '#/definitions/Reference' + deprecated: + type: boolean + default: false + security: + type: array + items: + $ref: '#/definitions/SecurityRequirement' + servers: + type: array + items: + $ref: '#/definitions/Server' + patternProperties: + '^x-': {} + additionalProperties: false + + Responses: + type: object + properties: + default: + oneOf: + - $ref: '#/definitions/Response' + - $ref: '#/definitions/Reference' + patternProperties: + '[1-5](?:\d{2}|XX)': + oneOf: + - $ref: '#/definitions/Response' + - $ref: '#/definitions/Reference' + minProperties: 1 + additionalProperties: false + + + SecurityRequirement: + type: object + additionalProperties: + type: array + items: + type: string + + Tag: + type: object + required: + - name + properties: + name: + type: string + description: + type: string + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + patternProperties: + '^x-': {} + additionalProperties: false + + ExternalDocumentation: + type: object + required: + - url + properties: + description: + type: string + url: + type: string + format: uri-reference + patternProperties: + '^x-': {} + additionalProperties: false + + Parameter: + oneOf: + - $ref: '#/definitions/ParameterWithSchema' + - $ref: '#/definitions/ParameterWithContent' + + ParameterWithSchema: + oneOf: + - $ref: '#/definitions/ParameterWithSchemaWithExample' + - $ref: '#/definitions/ParameterWithSchemaWithExamples' + + ParameterWithSchemaWithExample: + oneOf: + - $ref: '#/definitions/ParameterWithSchemaWithExampleInPath' + - $ref: '#/definitions/ParameterWithSchemaWithExampleInQuery' + - $ref: '#/definitions/ParameterWithSchemaWithExampleInHeader' + - $ref: '#/definitions/ParameterWithSchemaWithExampleInCookie' + + ParameterWithSchemaWithExampleInPath: + type: object + required: + - name + - in + - schema + - required + properties: + name: + type: string + in: + type: string + enum: + - path + description: + type: string + required: + type: boolean + enum: + - true + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - matrix + - label + - simple + default: simple + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + example: {} + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithSchemaWithExampleInQuery: + type: object + required: + - name + - in + - schema + properties: + name: + type: string + in: + type: string + enum: + - query + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - form + - spaceDelimited + - pipeDelimited + - deepObject + default: form + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + example: {} + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithSchemaWithExampleInHeader: + type: object + required: + - name + - in + - schema + properties: + name: + type: string + in: + type: string + enum: + - header + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - simple + default: simple + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + example: {} + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithSchemaWithExampleInCookie: + type: object + required: + - name + - in + - schema + properties: + name: + type: string + in: + type: string + enum: + - cookie + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - form + default: form + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + example: {} + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithSchemaWithExamples: + oneOf: + - $ref: '#/definitions/ParameterWithSchemaWithExamplesInPath' + - $ref: '#/definitions/ParameterWithSchemaWithExamplesInQuery' + - $ref: '#/definitions/ParameterWithSchemaWithExamplesInHeader' + - $ref: '#/definitions/ParameterWithSchemaWithExamplesInCookie' + + ParameterWithSchemaWithExamplesInPath: + type: object + required: + - name + - in + - schema + - required + - examples + properties: + name: + type: string + in: + type: string + enum: + - path + description: + type: string + required: + type: boolean + enum: + - true + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - matrix + - label + - simple + default: simple + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithSchemaWithExamplesInQuery: + type: object + required: + - name + - in + - schema + - examples + properties: + name: + type: string + in: + type: string + enum: + - query + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - form + - spaceDelimited + - pipeDelimited + - deepObject + default: form + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithSchemaWithExamplesInHeader: + type: object + required: + - name + - in + - schema + - examples + properties: + name: + type: string + in: + type: string + enum: + - header + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - simple + default: simple + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithSchemaWithExamplesInCookie: + type: object + required: + - name + - in + - schema + - examples + properties: + name: + type: string + in: + type: string + enum: + - cookie + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - form + default: form + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithContent: + oneOf: + - $ref: '#/definitions/ParameterWithContentInPath' + - $ref: '#/definitions/ParameterWithContentNotInPath' + + ParameterWithContentInPath: + type: object + required: + - name + - in + - content + properties: + name: + type: string + in: + type: string + enum: + - path + description: + type: string + required: + type: boolean + enum: + - true + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + minProperties: 1 + maxProperties: 1 + patternProperties: + '^x-': {} + additionalProperties: false + + ParameterWithContentNotInPath: + type: object + required: + - name + - in + - content + properties: + name: + type: string + in: + type: string + enum: + - query + - header + - cookie + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + minProperties: 1 + maxProperties: 1 + patternProperties: + '^x-': {} + additionalProperties: false + + RequestBody: + type: object + required: + - content + properties: + description: + type: string + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + required: + type: boolean + default: false + patternProperties: + '^x-': {} + additionalProperties: false + + SecurityScheme: + oneOf: + - $ref: '#/definitions/APIKeySecurityScheme' + - $ref: '#/definitions/HTTPSecurityScheme' + - $ref: '#/definitions/OAuth2SecurityScheme' + - $ref: '#/definitions/OpenIdConnectSecurityScheme' + + APIKeySecurityScheme: + type: object + required: + - type + - name + - in + properties: + type: + type: string + enum: + - apiKey + name: + type: string + in: + type: string + enum: + - header + - query + - cookie + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + HTTPSecurityScheme: + oneOf: + - $ref: '#/definitions/NonBearerHTTPSecurityScheme' + - $ref: '#/definitions/BearerHTTPSecurityScheme' + + NonBearerHTTPSecurityScheme: + type: object + required: + - scheme + - type + properties: + scheme: + type: string + not: + enum: + - bearer + description: + type: string + type: + type: string + enum: + - http + patternProperties: + '^x-': {} + additionalProperties: false + + BearerHTTPSecurityScheme: + type: object + required: + - type + - scheme + properties: + scheme: + type: string + enum: + - bearer + bearerFormat: + type: string + type: + type: string + enum: + - http + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + OAuth2SecurityScheme: + type: object + required: + - type + - flows + properties: + type: + type: string + enum: + - oauth2 + flows: + $ref: '#/definitions/OAuthFlows' + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + OpenIdConnectSecurityScheme: + type: object + required: + - type + - openIdConnectUrl + properties: + type: + type: string + enum: + - openIdConnect + openIdConnectUrl: + type: string + format: uri-reference + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + OAuthFlows: + type: object + properties: + implicit: + $ref: '#/definitions/ImplicitOAuthFlow' + password: + $ref: '#/definitions/PasswordOAuthFlow' + clientCredentials: + $ref: '#/definitions/ClientCredentialsFlow' + authorizationCode: + $ref: '#/definitions/AuthorizationCodeOAuthFlow' + patternProperties: + '^x-': {} + additionalProperties: false + + ImplicitOAuthFlow: + type: object + required: + - authorizationUrl + - scopes + properties: + authorizationUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + PasswordOAuthFlow: + type: object + required: + - tokenUrl + properties: + tokenUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + ClientCredentialsFlow: + type: object + required: + - tokenUrl + properties: + tokenUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + AuthorizationCodeOAuthFlow: + type: object + required: + - authorizationUrl + - tokenUrl + properties: + authorizationUrl: + type: string + format: uri-reference + tokenUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + Link: + oneOf: + - $ref: '#/definitions/LinkWithOperationRef' + - $ref: '#/definitions/LinkWithOperationId' + + LinkWithOperationRef: + type: object + properties: + operationRef: + type: string + format: uri-reference + parameters: + type: object + additionalProperties: {} + requestBody: {} + description: + type: string + server: + $ref: '#/definitions/Server' + patternProperties: + '^x-': {} + additionalProperties: false + + LinkWithOperationId: + type: object + properties: + operationId: + type: string + parameters: + type: object + additionalProperties: {} + requestBody: {} + description: + type: string + server: + $ref: '#/definitions/Server' + patternProperties: + '^x-': {} + additionalProperties: false + + Callback: + type: object + additionalProperties: + $ref: '#/definitions/PathItem' + patternProperties: + '^x-': {} + + Encoding: + type: object + properties: + contentType: + type: string + headers: + type: object + additionalProperties: + $ref: '#/definitions/Header' + style: + type: string + enum: + - form + - spaceDelimited + - pipeDelimited + - deepObject + explode: + type: boolean + allowReserved: + type: boolean + default: false + additionalProperties: false diff --git a/lib/JSON/Validator/cache/36d1bd12eeed51e86c8695bd8876a9df b/lib/JSON/Validator/cache/36d1bd12eeed51e86c8695bd8876a9df new file mode 100644 index 0000000..f12a8c0 --- /dev/null +++ b/lib/JSON/Validator/cache/36d1bd12eeed51e86c8695bd8876a9df @@ -0,0 +1,1607 @@ +{ + "title": "A JSON Schema for Swagger 2.0 API.", + "id": "http://swagger.io/v2/schema.json#", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ + "swagger", + "info", + "paths" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "swagger": { + "type": "string", + "enum": [ + "2.0" + ], + "description": "The Swagger version of this document." + }, + "info": { + "$ref": "#/definitions/info" + }, + "host": { + "type": "string", + "pattern": "^[^{}/ :\\\\]+(?::\\d+)?$", + "description": "The host (name or ip) of the API. Example: 'swagger.io'" + }, + "basePath": { + "type": "string", + "pattern": "^/", + "description": "The base path to the API. Example: '/api'." + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "consumes": { + "description": "A list of MIME types accepted by the API.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "paths": { + "$ref": "#/definitions/paths" + }, + "definitions": { + "$ref": "#/definitions/definitions" + }, + "parameters": { + "$ref": "#/definitions/parameterDefinitions" + }, + "responses": { + "$ref": "#/definitions/responseDefinitions" + }, + "security": { + "$ref": "#/definitions/security" + }, + "securityDefinitions": { + "$ref": "#/definitions/securityDefinitions" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "definitions": { + "info": { + "type": "object", + "description": "General information about the API.", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." + }, + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed." + }, + "termsOfService": { + "type": "string", + "description": "The terms of service for the API." + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "license": { + "$ref": "#/definitions/license" + } + } + }, + "contact": { + "type": "object", + "description": "Contact information for the owners of the API.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The identifying name of the contact person/organization." + }, + "url": { + "type": "string", + "description": "The URL pointing to the contact information.", + "format": "uri" + }, + "email": { + "type": "string", + "description": "The email address of the contact person/organization.", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "license": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the license type. It's encouraged to use an OSI compatible license." + }, + "url": { + "type": "string", + "description": "The URL pointing to the license.", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "paths": { + "type": "object", + "description": "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + }, + "^/": { + "$ref": "#/definitions/pathItem" + } + }, + "additionalProperties": false + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "description": "One or more JSON objects describing the schemas being consumed and produced by the API." + }, + "parameterDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + }, + "description": "One or more JSON representations for parameters" + }, + "responseDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/response" + }, + "description": "One or more JSON representations for parameters" + }, + "externalDocs": { + "type": "object", + "additionalProperties": false, + "description": "information about external documentation", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "examples": { + "type": "object", + "additionalProperties": true + }, + "mimeType": { + "type": "string", + "description": "The MIME type of the HTTP message." + }, + "operation": { + "type": "object", + "required": [ + "responses" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the operation." + }, + "description": { + "type": "string", + "description": "A longer description of the operation, GitHub Flavored Markdown is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string", + "description": "A unique identifier of the operation." + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "consumes": { + "description": "A list of MIME types the API can consume.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "parameters": { + "$ref": "#/definitions/parametersList" + }, + "responses": { + "$ref": "#/definitions/responses" + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "$ref": "#/definitions/security" + } + } + }, + "pathItem": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "get": { + "$ref": "#/definitions/operation" + }, + "put": { + "$ref": "#/definitions/operation" + }, + "post": { + "$ref": "#/definitions/operation" + }, + "delete": { + "$ref": "#/definitions/operation" + }, + "options": { + "$ref": "#/definitions/operation" + }, + "head": { + "$ref": "#/definitions/operation" + }, + "patch": { + "$ref": "#/definitions/operation" + }, + "parameters": { + "$ref": "#/definitions/parametersList" + } + } + }, + "responses": { + "type": "object", + "description": "Response objects names can either be any valid HTTP status code or 'default'.", + "minProperties": 1, + "additionalProperties": false, + "patternProperties": { + "^([0-9]{3})$|^(default)$": { + "$ref": "#/definitions/responseValue" + }, + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "not": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + } + }, + "responseValue": { + "oneOf": [ + { + "$ref": "#/definitions/response" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "$ref": "#/definitions/fileSchema" + } + ] + }, + "headers": { + "$ref": "#/definitions/headers" + }, + "examples": { + "$ref": "#/definitions/examples" + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/header" + } + }, + "header": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "vendorExtension": { + "description": "Any property starting with x- is valid.", + "additionalProperties": true, + "additionalItems": true + }, + "bodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "schema" + ], + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "body" + ] + }, + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "schema": { + "$ref": "#/definitions/schema" + } + }, + "additionalProperties": false + }, + "headerParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "header" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "queryParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "query" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "formDataParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "formData" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array", + "file" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "pathParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "required" + ], + "properties": { + "required": { + "type": "boolean", + "enum": [ + true + ], + "description": "Determines whether or not this parameter is required or optional." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "path" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "nonBodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "type" + ], + "oneOf": [ + { + "$ref": "#/definitions/headerParameterSubSchema" + }, + { + "$ref": "#/definitions/formDataParameterSubSchema" + }, + { + "$ref": "#/definitions/queryParameterSubSchema" + }, + { + "$ref": "#/definitions/pathParameterSubSchema" + } + ] + }, + "parameter": { + "oneOf": [ + { + "$ref": "#/definitions/bodyParameter" + }, + { + "$ref": "#/definitions/nonBodyParameter" + } + ] + }, + "schema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "maxProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "boolean" + } + ], + "default": {} + }, + "type": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/type" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + } + ], + "default": {} + }, + "allOf": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "discriminator": { + "type": "string" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/xml" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "fileSchema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "type" + ], + "properties": { + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "primitivesItems": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/securityRequirement" + }, + "uniqueItems": true + }, + "securityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "xml": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "securityDefinitions": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/basicAuthenticationSecurity" + }, + { + "$ref": "#/definitions/apiKeySecurity" + }, + { + "$ref": "#/definitions/oauth2ImplicitSecurity" + }, + { + "$ref": "#/definitions/oauth2PasswordSecurity" + }, + { + "$ref": "#/definitions/oauth2ApplicationSecurity" + }, + { + "$ref": "#/definitions/oauth2AccessCodeSecurity" + } + ] + } + }, + "basicAuthenticationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "basic" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "apiKeySecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ImplicitSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "implicit" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2PasswordSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "password" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ApplicationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "application" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2AccessCodeSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "accessCode" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2Scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mediaTypeList": { + "type": "array", + "items": { + "$ref": "#/definitions/mimeType" + }, + "uniqueItems": true + }, + "parametersList": { + "type": "array", + "description": "The parameters needed to send a valid API call.", + "additionalItems": false, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/parameter" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "uniqueItems": true + }, + "schemesList": { + "type": "array", + "description": "The transfer protocol of the API.", + "items": { + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss" + ] + }, + "uniqueItems": true + }, + "collectionFormat": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes" + ], + "default": "csv" + }, + "collectionFormatWithMulti": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes", + "multi" + ], + "default": "csv" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "jsonReference": { + "type": "object", + "required": [ + "$ref" + ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/lib/JSON/Validator/cache/3d35aac549d951f4cf9182ff47bff0b4 b/lib/JSON/Validator/cache/3d35aac549d951f4cf9182ff47bff0b4 new file mode 100644 index 0000000..5656240 --- /dev/null +++ b/lib/JSON/Validator/cache/3d35aac549d951f4cf9182ff47bff0b4 @@ -0,0 +1,154 @@ +{ + "$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": "#" }, + "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": {} +} diff --git a/lib/JSON/Validator/cache/49c95b866e40f788892a7fb3c816b0e8 b/lib/JSON/Validator/cache/49c95b866e40f788892a7fb3c816b0e8 new file mode 100644 index 0000000..85eb502 --- /dev/null +++ b/lib/JSON/Validator/cache/49c95b866e40f788892a7fb3c816b0e8 @@ -0,0 +1,150 @@ +{ + "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", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "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 + } + ] + }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/lib/JSON/Validator/cache/4a31fe43be9e23ca9eb8d9e9faba8892 b/lib/JSON/Validator/cache/4a31fe43be9e23ca9eb8d9e9faba8892 new file mode 100644 index 0000000..5bee90e --- /dev/null +++ b/lib/JSON/Validator/cache/4a31fe43be9e23ca9eb8d9e9faba8892 @@ -0,0 +1,168 @@ +{ + "$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 + }, + "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 +} diff --git a/lib/JSON/Validator/cache/630949337805585c8e52deea27d11419 b/lib/JSON/Validator/cache/630949337805585c8e52deea27d11419 new file mode 100644 index 0000000..3168cc2 --- /dev/null +++ b/lib/JSON/Validator/cache/630949337805585c8e52deea27d11419 @@ -0,0 +1,17 @@ +{ + "type" : "object", + "required": [ "errors" ], + "properties": { + "errors": { + "type": "array", + "items": { + "type" : "object", + "required": [ "message", "path" ], + "properties": { + "message": { "type": "string" }, + "path": { "type": "string" } + } + } + } + } +} diff --git a/lib/JSON/Validator/cache/a0f5b4b4e75ea17fc09e88ec0343d148 b/lib/JSON/Validator/cache/a0f5b4b4e75ea17fc09e88ec0343d148 new file mode 100644 index 0000000..65f2dfc --- /dev/null +++ b/lib/JSON/Validator/cache/a0f5b4b4e75ea17fc09e88ec0343d148 @@ -0,0 +1,127 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "contact": { "name": "wordnik api team", "url": "http://developer.wordnik.com" }, + "license": { "name": "Creative Commons 4.0 International", "url": "http://creativecommons.org/licenses/by/4.0/" } + }, + "host": "petstore.swagger.wordnik.com", + "basePath": "/api", + "schemes": [ "http" ], + "parameters": { + "limit": { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "type": "integer", + "format": "int32" + } + }, + "paths": { + "/pets": { + "get": { + "x-mojo-controller": "t::Api", + "tags": [ "pets" ], + "summary": "finds pets in the system", + "operationId": "listPets", + "parameters": [ + { "$ref": "#/parameters/limit" } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "type": "array", + "items": { "$ref": "#/definitions/Pet" } + }, + "headers": { + "x-expires": { + "type": "string" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { "$ref": "#/definitions/Error" } + } + } + }, + "post": { + "x-mojo-controller": "t::Api", + "tags": [ "pets" ], + "summary": "add pets to the system", + "operationId": "addPet", + "parameters": [ + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "parameters": { + "name": { "type": "string" }, + "tag": { "type": "string" } + } + } + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { "$ref": "#/definitions/Pet" } + }, + "default": { + "description": "unexpected error", + "schema": { "$ref": "#/definitions/Error" } + } + } + } + }, + "/pets/{petId}": { + "post": { + "x-mojo-controller": "t::Api", + "tags": [ "pets" ], + "summary": "Info for a specific pet", + "operationId": "showPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to receive", + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "schema": { "$ref": "#/definitions/Pet" } + }, + "default": { + "description": "unexpected error", + "schema": { "$ref": "#/definitions/Error" } + } + } + } + } + }, + "definitions": { + "Pet": { + "required": [ "id", "name" ], + "properties": { + "id": { "type": "integer", "format": "int64" }, + "name": { "type": "string" }, + "tag": { "type": "string" } + } + }, + "Error": { + "required": [ "code", "message" ], + "properties": { + "code": { "type": "integer", "format": "int32" }, + "message": { "type": "string" } + } + } + } +} diff --git a/lib/JSON/Validator/cache/ea34d47d4e060a1c3b12d2287aff89a7 b/lib/JSON/Validator/cache/ea34d47d4e060a1c3b12d2287aff89a7 new file mode 100644 index 0000000..589edc2 --- /dev/null +++ b/lib/JSON/Validator/cache/ea34d47d4e060a1c3b12d2287aff89a7 @@ -0,0 +1,64 @@ +{ + "title": "JSON schema for JSONPatch files", + "$schema": "http://json-schema.org/draft-04/schema#", + + "type": "array", + + "items": { + "$ref": "#/definitions/operation" + }, + + "definitions": { + "operation": { + "type": "object", + "required": [ "op", "path" ], + "allOf": [ { "$ref": "#/definitions/path" } ], + "oneOf": [ + { + "required": [ "value" ], + "properties": { + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "add", "replace", "test" ] + }, + "value": { + "description": "The value to add, replace or test." + } + } + }, + { + "properties": { + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "remove" ] + } + } + }, + { + "required": [ "from" ], + "properties": { + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "move", "copy" ] + }, + "from": { + "description": "A JSON Pointer path pointing to the location to move/copy from.", + "type": "string" + } + } + } + ] + }, + "path": { + "properties": { + "path": { + "description": "A JSON Pointer path.", + "type": "string" + } + } + } + } +} diff --git a/lib/JSON/Validator/cache/eaa832720f36cff0abc20c05236a9cd9 b/lib/JSON/Validator/cache/eaa832720f36cff0abc20c05236a9cd9 new file mode 100644 index 0000000..3168cc2 --- /dev/null +++ b/lib/JSON/Validator/cache/eaa832720f36cff0abc20c05236a9cd9 @@ -0,0 +1,17 @@ +{ + "type" : "object", + "required": [ "errors" ], + "properties": { + "errors": { + "type": "array", + "items": { + "type" : "object", + "required": [ "message", "path" ], + "properties": { + "message": { "type": "string" }, + "path": { "type": "string" } + } + } + } + } +} diff --git a/t/00-basic.t b/t/00-basic.t new file mode 100644 index 0000000..9eb10df --- /dev/null +++ b/t/00-basic.t @@ -0,0 +1,43 @@ +use strict; +use Test::More; +use File::Find; + +if (($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/) { + plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/'; +} +if (!eval 'use Test::Pod; 1') { + *Test::Pod::pod_file_ok = sub { + SKIP: { skip "pod_file_ok(@_) (Test::Pod is required)", 1 } + }; +} +if (!eval 'use Test::Pod::Coverage; 1') { + *Test::Pod::Coverage::pod_coverage_ok = sub { + SKIP: { skip "pod_coverage_ok(@_) (Test::Pod::Coverage is required)", 1 } + }; +} +if (!eval 'use Test::CPAN::Changes; 1') { + *Test::CPAN::Changes::changes_file_ok = sub { + SKIP: { skip "changes_ok(@_) (Test::CPAN::Changes is required)", 4 } + }; +} + +my @files; +find( + {wanted => sub { /\.pm$/ and push @files, $File::Find::name }, no_chdir => 1}, + -e 'blib' ? 'blib' : 'lib', +); + +plan tests => @files * 3 + 4; + +for my $file (@files) { + my $module = $file; + $module =~ s,\.pm$,,; + $module =~ s,.*/?lib/,,; + $module =~ s,/,::,g; + ok eval "use $module; 1", "use $module" or diag $@; + Test::Pod::pod_file_ok($file); + Test::Pod::Coverage::pod_coverage_ok($module, + {also_private => [qr/^[A-Z_]+$/]}); +} + +Test::CPAN::Changes::changes_file_ok(); diff --git a/t/Helper.pm b/t/Helper.pm new file mode 100644 index 0000000..d52750b --- /dev/null +++ b/t/Helper.pm @@ -0,0 +1,58 @@ +package t::Helper; +use Mojo::Base -base; + +use Mojo::JSON 'encode_json'; +use Mojo::Util 'monkey_patch'; +use JSON::Validator; +use Test::More; + +$ENV{TEST_VALIDATOR_CLASS} = 'JSON::Validator'; + +sub dump_validator { + my $class = shift; + my $jv = shift || $class->validator; + local $Data::Dumper::Indent = 1; + local $jv->{ua} = 'Mojo::UserAgent'; + Test::More::note(Data::Dumper::Dumper($jv)); +} + +sub edj { + return Mojo::JSON::decode_json(Mojo::JSON::encode_json(@_)); +} + +sub joi_ok { + my ($data, $joi, @expected) = @_; + my $description ||= @expected ? "errors: @expected" : "valid: " . encode_json($data); + my @errors = JSON::Validator::joi($data, $joi); + Test::More::is_deeply([map { $_->TO_JSON } sort { $a->path cmp $b->path } @errors], + [map { $_->TO_JSON } sort { $a->path cmp $b->path } @expected], $description) + or Test::More::diag(encode_json(\@errors)); +} + +sub validate_ok { + my ($data, $schema, @expected) = @_; + my $description ||= @expected ? "errors: @expected" : "valid: " . encode_json($data); + my @errors = validator()->schema($schema)->validate($data); + Test::More::is_deeply([map { $_->TO_JSON } sort { $a->path cmp $b->path } @errors], + [map { $_->TO_JSON } sort { $a->path cmp $b->path } @expected], $description) + or Test::More::diag(encode_json(\@errors)); +} + +sub validator { state $obj = $ENV{TEST_VALIDATOR_CLASS}->new } + +sub import { + my $class = shift; + my $caller = caller; + + strict->import; + warnings->import; + monkey_patch $caller => E => \&JSON::Validator::E; + monkey_patch $caller => done_testing => \&Test::More::done_testing; + monkey_patch $caller => false => \&Mojo::JSON::false; + monkey_patch $caller => edj => \&edj; + monkey_patch $caller => true => \&Mojo::JSON::true; + monkey_patch $caller => validate_ok => \&validate_ok; + monkey_patch $caller => joi_ok => \&joi_ok; +} + +1; diff --git a/t/acceptance.t b/t/acceptance.t new file mode 100644 index 0000000..cd5a3dd --- /dev/null +++ b/t/acceptance.t @@ -0,0 +1,58 @@ +use Mojo::Base -strict; +use JSON::Validator; +use Mojo::File 'path'; +use Mojo::JSON qw(encode_json false decode_json true); +use Test::Mojo; +use Test::More; +use JSON::Validator 'validate_json'; + +my $test_suite = path(qw(t draft4-tests)); +my $remotes = path(qw(t remotes)); +plan skip_all => 'Cannot find test files in t/draft4-tests' + unless -d $test_suite; + +use Mojolicious::Lite; +app->static->paths(["$remotes"]); +my $t = Test::Mojo->new; +$t->get_ok('/integer.json')->status_is(200); +my $host_port = $t->ua->server->url->host_port; + +my $test_only_re = $ENV{TEST_ONLY} || ''; +my $todo_re = join('|', + 'dependencies', + 'change resolution scope - changed scope ref valid', + $ENV{TEST_ONLINE} ? () : ('remote ref'), +); + +for my $file (sort $test_suite->list->each) { + for my $group (@{decode_json($file->slurp)}) { + for my $test (@{$group->{tests}}) { + my $schema = encode_json $group->{schema}; + my $descr = "$group->{description} - $test->{description}"; + + next if $test_only_re and $descr !~ /$test_only_re/; + diag <<"HERE" if $test_only_re; +--- +description: $descr +schema: $schema +data: @{[encode_json $test->{data}]} +expect_valid: @{[$test->{valid} ? 'Yes' : 'No']} +HERE + + $schema =~ s!http\W+localhost:1234\b!http://$host_port!g; + $schema = decode_json $schema; + + my @errors = eval { + JSON::Validator->new->ua($t->ua)->load_and_validate_schema($schema) + ->validate($test->{data}); + }; + + my $e = $@ || join ', ', @errors; + local $TODO = $descr =~ /$todo_re/ ? 'TODO' : undef; + note "ERROR: $e" if $e; + is $e ? 'invalid' : 'valid', $test->{valid} ? 'valid' : 'invalid', $descr; + } + } +} + +done_testing(); diff --git a/t/booleans.t b/t/booleans.t new file mode 100644 index 0000000..4a8400e --- /dev/null +++ b/t/booleans.t @@ -0,0 +1,48 @@ +use lib '.'; +use t::Helper; +use Test::More; + +my $schema = {properties => {v => {type => 'boolean'}}}; + +validate_ok {v => '0'}, $schema, E('/v', 'Expected boolean - got string.'); +validate_ok {v => 'false'}, $schema, E('/v', 'Expected boolean - got string.'); +validate_ok {v => 1}, $schema, E('/v', 'Expected boolean - got number.'); +validate_ok {v => 0.5}, $schema, E('/v', 'Expected boolean - got number.'); +validate_ok {v => Mojo::JSON->true}, $schema; +validate_ok {v => Mojo::JSON->false}, $schema; + +t::Helper->validator->coerce(booleans => 1); +validate_ok {v => !!1}, $schema; +validate_ok {v => !!0}, $schema; +validate_ok {v => 'false'}, $schema; +validate_ok {v => 'true'}, $schema; +validate_ok {v => 1}, $schema; +validate_ok {v => 0.5}, $schema; +validate_ok {v => '1'}, $schema, E('/v', 'Expected boolean - got string.'); +validate_ok {v => '0'}, $schema, E('/v', 'Expected boolean - got string.'); +validate_ok {v => ''}, $schema, E('/v', 'Expected boolean - got string.'); + +SKIP: { + skip 'YAML::XS is not installed', 1 + unless eval q[require YAML::XS;YAML::XS->VERSION('0.67');1]; + my $data = t::Helper->validator->_load_schema_from_text(\"---\nv: true\n"); + isa_ok($data->{v}, 'JSON::PP::Boolean'); + validate_ok $data, $schema; +} + +SKIP: { + skip 'boolean not installed', 1 unless eval 'require boolean;1'; + validate_ok {type => 'boolean'}, + {type => 'object', properties => {type => {type => 'string'}}}; +} + +SKIP: { + skip 'Cpanel::JSON::XS not installed', 2 + unless eval 'require Cpanel::JSON::XS;1'; + validate_ok {disabled => Mojo::JSON->true}, + {properties => {disabled => {type => 'boolean'}}}; + validate_ok {disabled => Mojo::JSON->false}, + {properties => {disabled => {type => 'boolean'}}}; +} + +done_testing; diff --git a/t/bundle.t b/t/bundle.t new file mode 100644 index 0000000..45b7ea0 --- /dev/null +++ b/t/bundle.t @@ -0,0 +1,111 @@ +use Mojo::Base -strict; +use Mojo::File 'path'; +use Test::More; +use JSON::Validator; + +my $validator = JSON::Validator->new; +my $bundled; + +# Bundle files +{ + local $ENV{JSON_VALIDATOR_CACHE_ANYWAYS} = 1; + $validator->_load_schema_from_url("http://json-schema.org/draft-04/schema"); + $validator->_load_schema_from_url("http://json-schema.org/draft-06/schema"); + $validator->_load_schema_from_url("http://json-schema.org/draft-07/schema"); +} + +# Run multiple times to make sure _reset() works +for my $n (1 .. 3) { + note "[$n] replace=1"; + $bundled = $validator->bundle({ + ref_key => 'definitions', + replace => 1, + schema => { + name => {'$ref' => '#/definitions/name'}, + definitions => {name => {type => 'string'}} + }, + }); + + is $bundled->{name}{type}, 'string', "[$n] replace=1"; + + note "[$n] replace=0"; + $bundled = $validator->schema({ + name => {'$ref' => '#/definitions/name'}, + age => {'$ref' => 'b.json#/definitions/age'}, + definitions => {name => {type => 'string'}}, + B => {id => 'b.json', definitions => {age => {type => 'integer'}}}, + })->bundle({ref_key => 'definitions'}); + is $bundled->{definitions}{name}{type}, 'string', + "[$n] name still in definitions"; + is $bundled->{definitions}{b_json__definitions_age}{type}, 'integer', + "[$n] added to definitions"; + isnt $bundled->{age}, $validator->schema->get('/age'), "[$n] new age ref"; + is $bundled->{name}, $validator->schema->get('/name'), "[$n] same name ref"; + is $bundled->{age}{'$ref'}, '#/definitions/b_json__definitions_age', + "[$n] age \$ref point to /definitions/b_json__definitions_age"; + is $bundled->{name}{'$ref'}, '#/definitions/name', + "[$n] name \$ref point to /definitions/name"; +} + +is $validator->get([qw(name type)]), 'string', 'get /name/$ref'; +is $validator->get('/name/type'), 'string', 'get /name/type'; +is $validator->get('/name/$ref'), undef, 'get /name/$ref'; +is $validator->schema->get('/name/type'), 'string', 'schema get /name/type'; +is $validator->schema->get('/name/$ref'), '#/definitions/name', + 'schema get /name/$ref'; + +$bundled = $validator->schema('data://main/api.json') + ->bundle({ref_key => 'definitions'}); +is_deeply [sort keys %{$bundled->{definitions}}], ['objtype'], + 'no dup definitions'; + +my @pathlists = ( + ['spec', 'with-deep-mixed-ref.json'], + ['spec', File::Spec->updir, 'spec', 'with-deep-mixed-ref.json'], +); +for my $pathlist (@pathlists) { + my $file = path(path(__FILE__)->dirname, @$pathlist); + $bundled = $validator->schema($file)->bundle({ref_key => 'definitions'}); + is_deeply [sort map { s!^[a-z0-9]{10}!SHA!; $_ } + keys %{$bundled->{definitions}}], [ + qw( + SHA-age.json + SHA-unit.json + SHA-weight.json + height + ) + ], + 'right definitions in disk spec' + or diag explain $bundled->{definitions}; +} + +# ensure filenames with funny characters not mangled by Mojo::URL +my $file3 = path(__FILE__)->sibling('spec', 'space bundle.json'); +eval { $bundled = $validator->schema($file3)->bundle }; +is $@, '', 'loaded absolute filename with space'; +is $bundled->{properties}{age}{description}, 'Age in years', + 'right definitions in disk spec' + or diag explain $bundled; + +done_testing; + +__DATA__ + +@@ api.json +{ + "definitions" : { + "objtype" : { + "type" : "object", + "properties" : { "propname" : { "type" : "string" } } + } + }, + "paths" : { + "/withdots" : { + "get" : { + "responses" : { + "200" : { "schema" : { "$ref" : "#/definitions/objtype" } } + } + } + } + } +} diff --git a/t/coerce.t b/t/coerce.t new file mode 100644 index 0000000..647467c --- /dev/null +++ b/t/coerce.t @@ -0,0 +1,59 @@ +use Mojo::Base -strict; +use JSON::Validator; +use Mojo::JSON 'to_json'; +use Test::More; + +my $validator = JSON::Validator->new; +my %coerce = (booleans => 1); +is_deeply( + $validator->coerce(%coerce)->coerce, + {booleans => 1}, + 'hash is accepted' +); +is_deeply( + $validator->coerce(\%coerce)->coerce, + {booleans => 1}, + 'hash reference is accepted' +); + +note + 'coerce(1) is here for back compat reasons, even though not documented any more'; +is_deeply( + $validator->coerce(1)->coerce, + {%coerce, numbers => 1, strings => 1}, + '1 is accepted' +); + +note 'make sure input is coerced'; +my @items = ([boolean => 'true'], [integer => '42'], [number => '4.2']); +for my $i (@items) { + for my $schema (schemas($i->[0])) { + my $x = $i->[1]; + $validator->validate($x, $schema); + is to_json($x), $i->[1], sprintf 'no quotes around %s %s', $i->[0], + to_json($schema); + + $x = {v => $i->[1]}; + $validator->validate($x, {type => 'object', properties => {v => $schema}}); + is to_json($x->{v}), $i->[1], sprintf 'no quotes around %s %s', $i->[0], + to_json($schema); + + $x = [$i->[1]]; + $validator->validate($x, {type => 'array', items => $schema}); + is to_json($x->[0]), $i->[1], sprintf 'no quotes around %s %s', $i->[0], + to_json($schema); + } +} + +done_testing; + +sub schemas { + my $base = {type => shift}; + return ( + $base, + {type => ['array', $base->{type}]}, + {allOf => [$base]}, + {anyOf => [{type => 'array'}, $base]}, + {oneOf => [$base, {type => 'array'}]}, + ); +} diff --git a/t/deep-mixed-ref.t b/t/deep-mixed-ref.t new file mode 100644 index 0000000..e89a46c --- /dev/null +++ b/t/deep-mixed-ref.t @@ -0,0 +1,21 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; +use Mojo::File 'path'; + +my $workdir = path(__FILE__)->dirname; +my $file = path($workdir, 'spec', 'with-deep-mixed-ref.json'); +my $validator = JSON::Validator->new(cache_paths => [])->schema($file); +my @errors = $validator->validate( + {age => 1, weight => {mass => 72, unit => 'kg'}, height => 100}); +is int(@errors), 0, 'valid input'; + +use Mojolicious::Lite; +push @{app->static->paths}, $workdir; +$validator->ua(app->ua); +$validator->schema( + app->ua->server->url->clone->path('/spec/with-relative-ref.json')); +@errors = $validator->validate({age => 'not a number'}); +is int(@errors), 1, 'invalid age'; + +done_testing; diff --git a/t/definitions/age.json b/t/definitions/age.json new file mode 100644 index 0000000..0011815 --- /dev/null +++ b/t/definitions/age.json @@ -0,0 +1,5 @@ +{ + "type": "integer", + "minimum": 0, + "description": "Age in years" +} diff --git a/t/definitions/space age.json b/t/definitions/space age.json new file mode 100644 index 0000000..0011815 --- /dev/null +++ b/t/definitions/space age.json @@ -0,0 +1,5 @@ +{ + "type": "integer", + "minimum": 0, + "description": "Age in years" +} diff --git a/t/definitions/unit.json b/t/definitions/unit.json new file mode 100644 index 0000000..dd8092b --- /dev/null +++ b/t/definitions/unit.json @@ -0,0 +1,5 @@ +{ + "type": "string", + "description": "Unit of Mass", + "pattern": "^kg|st|lb$" +} diff --git a/t/definitions/weight.json b/t/definitions/weight.json new file mode 100644 index 0000000..15d39ae --- /dev/null +++ b/t/definitions/weight.json @@ -0,0 +1,8 @@ +{ + "type": "object", + "description": "Weight with Units", + "properties": { + "mass": { "type": "integer" }, + "unit": { "$ref": "./unit.json" } + } +} diff --git a/t/draft4-tests/additionalItems.json b/t/draft4-tests/additionalItems.json new file mode 100644 index 0000000..521745c --- /dev/null +++ b/t/draft4-tests/additionalItems.json @@ -0,0 +1,82 @@ +[ + { + "description": "additionalItems as schema", + "schema": { + "items": [{}], + "additionalItems": {"type": "integer"} + }, + "tests": [ + { + "description": "additional items match schema", + "data": [ null, 2, 3, 4 ], + "valid": true + }, + { + "description": "additional items do not match schema", + "data": [ null, 2, 3, "foo" ], + "valid": false + } + ] + }, + { + "description": "items is schema, no additionalItems", + "schema": { + "items": {}, + "additionalItems": false + }, + "tests": [ + { + "description": "all items match schema", + "data": [ 1, 2, 3, 4, 5 ], + "valid": true + } + ] + }, + { + "description": "array of items with no additionalItems", + "schema": { + "items": [{}, {}, {}], + "additionalItems": false + }, + "tests": [ + { + "description": "no additional items present", + "data": [ 1, 2, 3 ], + "valid": true + }, + { + "description": "additional items are not permitted", + "data": [ 1, 2, 3, 4 ], + "valid": false + } + ] + }, + { + "description": "additionalItems as false without items", + "schema": {"additionalItems": false}, + "tests": [ + { + "description": + "items defaults to empty schema so everything is valid", + "data": [ 1, 2, 3, 4, 5 ], + "valid": true + }, + { + "description": "ignores non-arrays", + "data": {"foo" : "bar"}, + "valid": true + } + ] + }, + { + "description": "additionalItems are allowed by default", + "schema": {"items": [{"type": "integer"}]}, + "tests": [ + { + "description": "only the first item is validated", + "data": [1, "foo", false], + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/additionalProperties.json b/t/draft4-tests/additionalProperties.json new file mode 100644 index 0000000..40831f9 --- /dev/null +++ b/t/draft4-tests/additionalProperties.json @@ -0,0 +1,88 @@ +[ + { + "description": + "additionalProperties being false does not allow other properties", + "schema": { + "properties": {"foo": {}, "bar": {}}, + "patternProperties": { "^v": {} }, + "additionalProperties": false + }, + "tests": [ + { + "description": "no additional properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "an additional property is invalid", + "data": {"foo" : 1, "bar" : 2, "quux" : "boom"}, + "valid": false + }, + { + "description": "ignores non-objects", + "data": [1, 2, 3], + "valid": true + }, + { + "description": "patternProperties are not additional properties", + "data": {"foo":1, "vroom": 2}, + "valid": true + } + ] + }, + { + "description": + "additionalProperties allows a schema which should validate", + "schema": { + "properties": {"foo": {}, "bar": {}}, + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "no additional properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "an additional valid property is valid", + "data": {"foo" : 1, "bar" : 2, "quux" : true}, + "valid": true + }, + { + "description": "an additional invalid property is invalid", + "data": {"foo" : 1, "bar" : 2, "quux" : 12}, + "valid": false + } + ] + }, + { + "description": + "additionalProperties can exist by itself", + "schema": { + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "an additional valid property is valid", + "data": {"foo" : true}, + "valid": true + }, + { + "description": "an additional invalid property is invalid", + "data": {"foo" : 1}, + "valid": false + } + ] + }, + { + "description": "additionalProperties are allowed by default", + "schema": {"properties": {"foo": {}, "bar": {}}}, + "tests": [ + { + "description": "additional properties are allowed", + "data": {"foo": 1, "bar": 2, "quux": true}, + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/allOf.json b/t/draft4-tests/allOf.json new file mode 100644 index 0000000..bbb5f89 --- /dev/null +++ b/t/draft4-tests/allOf.json @@ -0,0 +1,112 @@ +[ + { + "description": "allOf", + "schema": { + "allOf": [ + { + "properties": { + "bar": {"type": "integer"} + }, + "required": ["bar"] + }, + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "allOf", + "data": {"foo": "baz", "bar": 2}, + "valid": true + }, + { + "description": "mismatch second", + "data": {"foo": "baz"}, + "valid": false + }, + { + "description": "mismatch first", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "wrong type", + "data": {"foo": "baz", "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "allOf with base schema", + "schema": { + "properties": {"bar": {"type": "integer"}}, + "required": ["bar"], + "allOf" : [ + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + }, + { + "properties": { + "baz": {"type": "null"} + }, + "required": ["baz"] + } + ] + }, + "tests": [ + { + "description": "valid", + "data": {"foo": "quux", "bar": 2, "baz": null}, + "valid": true + }, + { + "description": "mismatch base schema", + "data": {"foo": "quux", "baz": null}, + "valid": false + }, + { + "description": "mismatch first allOf", + "data": {"bar": 2, "baz": null}, + "valid": false + }, + { + "description": "mismatch second allOf", + "data": {"foo": "quux", "bar": 2}, + "valid": false + }, + { + "description": "mismatch both", + "data": {"bar": 2}, + "valid": false + } + ] + }, + { + "description": "allOf simple types", + "schema": { + "allOf": [ + {"maximum": 30}, + {"minimum": 20} + ] + }, + "tests": [ + { + "description": "valid", + "data": 25, + "valid": true + }, + { + "description": "mismatch one", + "data": 35, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/anyOf.json b/t/draft4-tests/anyOf.json new file mode 100644 index 0000000..a58714a --- /dev/null +++ b/t/draft4-tests/anyOf.json @@ -0,0 +1,68 @@ +[ + { + "description": "anyOf", + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "tests": [ + { + "description": "first anyOf valid", + "data": 1, + "valid": true + }, + { + "description": "second anyOf valid", + "data": 2.5, + "valid": true + }, + { + "description": "both anyOf valid", + "data": 3, + "valid": true + }, + { + "description": "neither anyOf valid", + "data": 1.5, + "valid": false + } + ] + }, + { + "description": "anyOf with base schema", + "schema": { + "type": "string", + "anyOf" : [ + { + "maxLength": 2 + }, + { + "minLength": 4 + } + ] + }, + "tests": [ + { + "description": "mismatch base schema", + "data": 3, + "valid": false + }, + { + "description": "one anyOf valid", + "data": "foobar", + "valid": true + }, + { + "description": "both anyOf invalid", + "data": "foo", + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/default.json b/t/draft4-tests/default.json new file mode 100644 index 0000000..1762977 --- /dev/null +++ b/t/draft4-tests/default.json @@ -0,0 +1,49 @@ +[ + { + "description": "invalid type for default", + "schema": { + "properties": { + "foo": { + "type": "integer", + "default": [] + } + } + }, + "tests": [ + { + "description": "valid when property is specified", + "data": {"foo": 13}, + "valid": true + }, + { + "description": "still valid when the invalid default is used", + "data": {}, + "valid": true + } + ] + }, + { + "description": "invalid string value for default", + "schema": { + "properties": { + "bar": { + "type": "string", + "minLength": 4, + "default": "bad" + } + } + }, + "tests": [ + { + "description": "valid when property is specified", + "data": {"bar": "good"}, + "valid": true + }, + { + "description": "still valid when the invalid default is used", + "data": {}, + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/definitions.json b/t/draft4-tests/definitions.json new file mode 100644 index 0000000..cf935a3 --- /dev/null +++ b/t/draft4-tests/definitions.json @@ -0,0 +1,32 @@ +[ + { + "description": "valid definition", + "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, + "tests": [ + { + "description": "valid definition schema", + "data": { + "definitions": { + "foo": {"type": "integer"} + } + }, + "valid": true + } + ] + }, + { + "description": "invalid definition", + "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, + "tests": [ + { + "description": "invalid definition schema", + "data": { + "definitions": { + "foo": {"type": 1} + } + }, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/dependencies.json b/t/draft4-tests/dependencies.json new file mode 100644 index 0000000..7b9b16a --- /dev/null +++ b/t/draft4-tests/dependencies.json @@ -0,0 +1,113 @@ +[ + { + "description": "dependencies", + "schema": { + "dependencies": {"bar": ["foo"]} + }, + "tests": [ + { + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependant", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "with dependency", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "missing dependency", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "ignores non-objects", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "multiple dependencies", + "schema": { + "dependencies": {"quux": ["foo", "bar"]} + }, + "tests": [ + { + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependants", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "with dependencies", + "data": {"foo": 1, "bar": 2, "quux": 3}, + "valid": true + }, + { + "description": "missing dependency", + "data": {"foo": 1, "quux": 2}, + "valid": false + }, + { + "description": "missing other dependency", + "data": {"bar": 1, "quux": 2}, + "valid": false + }, + { + "description": "missing both dependencies", + "data": {"quux": 1}, + "valid": false + } + ] + }, + { + "description": "multiple dependencies subschema", + "schema": { + "dependencies": { + "bar": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "integer"} + } + } + } + }, + "tests": [ + { + "description": "valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "no dependency", + "data": {"foo": "quux"}, + "valid": true + }, + { + "description": "wrong type", + "data": {"foo": "quux", "bar": 2}, + "valid": false + }, + { + "description": "wrong type other", + "data": {"foo": 2, "bar": "quux"}, + "valid": false + }, + { + "description": "wrong type both", + "data": {"foo": "quux", "bar": "quux"}, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/enum.json b/t/draft4-tests/enum.json new file mode 100644 index 0000000..f124436 --- /dev/null +++ b/t/draft4-tests/enum.json @@ -0,0 +1,72 @@ +[ + { + "description": "simple enum validation", + "schema": {"enum": [1, 2, 3]}, + "tests": [ + { + "description": "one of the enum is valid", + "data": 1, + "valid": true + }, + { + "description": "something else is invalid", + "data": 4, + "valid": false + } + ] + }, + { + "description": "heterogeneous enum validation", + "schema": {"enum": [6, "foo", [], true, {"foo": 12}]}, + "tests": [ + { + "description": "one of the enum is valid", + "data": [], + "valid": true + }, + { + "description": "something else is invalid", + "data": null, + "valid": false + }, + { + "description": "objects are deep compared", + "data": {"foo": false}, + "valid": false + } + ] + }, + { + "description": "enums in properties", + "schema": { + "type":"object", + "properties": { + "foo": {"enum":["foo"]}, + "bar": {"enum":["bar"]} + }, + "required": ["bar"] + }, + "tests": [ + { + "description": "both properties are valid", + "data": {"foo":"foo", "bar":"bar"}, + "valid": true + }, + { + "description": "missing optional property is valid", + "data": {"bar":"bar"}, + "valid": true + }, + { + "description": "missing required property is invalid", + "data": {"foo":"foo"}, + "valid": false + }, + { + "description": "missing all properties is invalid", + "data": {}, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/items.json b/t/draft4-tests/items.json new file mode 100644 index 0000000..f5e18a1 --- /dev/null +++ b/t/draft4-tests/items.json @@ -0,0 +1,46 @@ +[ + { + "description": "a schema given for items", + "schema": { + "items": {"type": "integer"} + }, + "tests": [ + { + "description": "valid items", + "data": [ 1, 2, 3 ], + "valid": true + }, + { + "description": "wrong type of items", + "data": [1, "x"], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": {"foo" : "bar"}, + "valid": true + } + ] + }, + { + "description": "an array of schemas for items", + "schema": { + "items": [ + {"type": "integer"}, + {"type": "string"} + ] + }, + "tests": [ + { + "description": "correct types", + "data": [ 1, "foo" ], + "valid": true + }, + { + "description": "wrong types", + "data": [ "foo", 1 ], + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/maxItems.json b/t/draft4-tests/maxItems.json new file mode 100644 index 0000000..3b53a6b --- /dev/null +++ b/t/draft4-tests/maxItems.json @@ -0,0 +1,28 @@ +[ + { + "description": "maxItems validation", + "schema": {"maxItems": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": [1], + "valid": true + }, + { + "description": "exact length is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "too long is invalid", + "data": [1, 2, 3], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "foobar", + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/maxLength.json b/t/draft4-tests/maxLength.json new file mode 100644 index 0000000..811d35b --- /dev/null +++ b/t/draft4-tests/maxLength.json @@ -0,0 +1,33 @@ +[ + { + "description": "maxLength validation", + "schema": {"maxLength": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": "f", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too long is invalid", + "data": "foo", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + }, + { + "description": "two supplementary Unicode code points is long enough", + "data": "\uD83D\uDCA9\uD83D\uDCA9", + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/maxProperties.json b/t/draft4-tests/maxProperties.json new file mode 100644 index 0000000..d282446 --- /dev/null +++ b/t/draft4-tests/maxProperties.json @@ -0,0 +1,28 @@ +[ + { + "description": "maxProperties validation", + "schema": {"maxProperties": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "exact length is valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "too long is invalid", + "data": {"foo": 1, "bar": 2, "baz": 3}, + "valid": false + }, + { + "description": "ignores non-objects", + "data": "foobar", + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/maximum.json b/t/draft4-tests/maximum.json new file mode 100644 index 0000000..86c7b89 --- /dev/null +++ b/t/draft4-tests/maximum.json @@ -0,0 +1,42 @@ +[ + { + "description": "maximum validation", + "schema": {"maximum": 3.0}, + "tests": [ + { + "description": "below the maximum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "exclusiveMaximum validation", + "schema": { + "maximum": 3.0, + "exclusiveMaximum": true + }, + "tests": [ + { + "description": "below the maximum is still valid", + "data": 2.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 3.0, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/minItems.json b/t/draft4-tests/minItems.json new file mode 100644 index 0000000..ed51188 --- /dev/null +++ b/t/draft4-tests/minItems.json @@ -0,0 +1,28 @@ +[ + { + "description": "minItems validation", + "schema": {"minItems": 1}, + "tests": [ + { + "description": "longer is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "exact length is valid", + "data": [1], + "valid": true + }, + { + "description": "too short is invalid", + "data": [], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "", + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/minLength.json b/t/draft4-tests/minLength.json new file mode 100644 index 0000000..3f09158 --- /dev/null +++ b/t/draft4-tests/minLength.json @@ -0,0 +1,33 @@ +[ + { + "description": "minLength validation", + "schema": {"minLength": 2}, + "tests": [ + { + "description": "longer is valid", + "data": "foo", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too short is invalid", + "data": "f", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 1, + "valid": true + }, + { + "description": "one supplementary Unicode code point is not long enough", + "data": "\uD83D\uDCA9", + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/minProperties.json b/t/draft4-tests/minProperties.json new file mode 100644 index 0000000..a72c7d2 --- /dev/null +++ b/t/draft4-tests/minProperties.json @@ -0,0 +1,28 @@ +[ + { + "description": "minProperties validation", + "schema": {"minProperties": 1}, + "tests": [ + { + "description": "longer is valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "exact length is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "too short is invalid", + "data": {}, + "valid": false + }, + { + "description": "ignores non-objects", + "data": "", + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/minimum.json b/t/draft4-tests/minimum.json new file mode 100644 index 0000000..d5bf000 --- /dev/null +++ b/t/draft4-tests/minimum.json @@ -0,0 +1,42 @@ +[ + { + "description": "minimum validation", + "schema": {"minimum": 1.1}, + "tests": [ + { + "description": "above the minimum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "below the minimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "exclusiveMinimum validation", + "schema": { + "minimum": 1.1, + "exclusiveMinimum": true + }, + "tests": [ + { + "description": "above the minimum is still valid", + "data": 1.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 1.1, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/multipleOf.json b/t/draft4-tests/multipleOf.json new file mode 100644 index 0000000..ca3b761 --- /dev/null +++ b/t/draft4-tests/multipleOf.json @@ -0,0 +1,60 @@ +[ + { + "description": "by int", + "schema": {"multipleOf": 2}, + "tests": [ + { + "description": "int by int", + "data": 10, + "valid": true + }, + { + "description": "int by int fail", + "data": 7, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "by number", + "schema": {"multipleOf": 1.5}, + "tests": [ + { + "description": "zero is multiple of anything", + "data": 0, + "valid": true + }, + { + "description": "4.5 is multiple of 1.5", + "data": 4.5, + "valid": true + }, + { + "description": "35 is not multiple of 1.5", + "data": 35, + "valid": false + } + ] + }, + { + "description": "by small number", + "schema": {"multipleOf": 0.0001}, + "tests": [ + { + "description": "0.0075 is multiple of 0.0001", + "data": 0.0075, + "valid": true + }, + { + "description": "0.00751 is not multiple of 0.0001", + "data": 0.00751, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/not.json b/t/draft4-tests/not.json new file mode 100644 index 0000000..cbb7f46 --- /dev/null +++ b/t/draft4-tests/not.json @@ -0,0 +1,96 @@ +[ + { + "description": "not", + "schema": { + "not": {"type": "integer"} + }, + "tests": [ + { + "description": "allowed", + "data": "foo", + "valid": true + }, + { + "description": "disallowed", + "data": 1, + "valid": false + } + ] + }, + { + "description": "not multiple types", + "schema": { + "not": {"type": ["integer", "boolean"]} + }, + "tests": [ + { + "description": "valid", + "data": "foo", + "valid": true + }, + { + "description": "mismatch", + "data": 1, + "valid": false + }, + { + "description": "other mismatch", + "data": true, + "valid": false + } + ] + }, + { + "description": "not more complex schema", + "schema": { + "not": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + }, + "tests": [ + { + "description": "match", + "data": 1, + "valid": true + }, + { + "description": "other match", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "mismatch", + "data": {"foo": "bar"}, + "valid": false + } + ] + }, + { + "description": "forbidden property", + "schema": { + "properties": { + "foo": { + "not": {} + } + } + }, + "tests": [ + { + "description": "property present", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "property absent", + "data": {"bar": 1, "baz": 2}, + "valid": true + } + ] + } + +] diff --git a/t/draft4-tests/oneOf.json b/t/draft4-tests/oneOf.json new file mode 100644 index 0000000..1eaa4e4 --- /dev/null +++ b/t/draft4-tests/oneOf.json @@ -0,0 +1,68 @@ +[ + { + "description": "oneOf", + "schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "tests": [ + { + "description": "first oneOf valid", + "data": 1, + "valid": true + }, + { + "description": "second oneOf valid", + "data": 2.5, + "valid": true + }, + { + "description": "both oneOf valid", + "data": 3, + "valid": false + }, + { + "description": "neither oneOf valid", + "data": 1.5, + "valid": false + } + ] + }, + { + "description": "oneOf with base schema", + "schema": { + "type": "string", + "oneOf" : [ + { + "minLength": 2 + }, + { + "maxLength": 4 + } + ] + }, + "tests": [ + { + "description": "mismatch base schema", + "data": 3, + "valid": false + }, + { + "description": "one oneOf valid", + "data": "foobar", + "valid": true + }, + { + "description": "both oneOf valid", + "data": "foo", + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/optional/bignum.json b/t/draft4-tests/optional/bignum.json new file mode 100644 index 0000000..ccc7c17 --- /dev/null +++ b/t/draft4-tests/optional/bignum.json @@ -0,0 +1,107 @@ +[ + { + "description": "integer", + "schema": {"type": "integer"}, + "tests": [ + { + "description": "a bignum is an integer", + "data": 12345678910111213141516171819202122232425262728293031, + "valid": true + } + ] + }, + { + "description": "number", + "schema": {"type": "number"}, + "tests": [ + { + "description": "a bignum is a number", + "data": 98249283749234923498293171823948729348710298301928331, + "valid": true + } + ] + }, + { + "description": "integer", + "schema": {"type": "integer"}, + "tests": [ + { + "description": "a negative bignum is an integer", + "data": -12345678910111213141516171819202122232425262728293031, + "valid": true + } + ] + }, + { + "description": "number", + "schema": {"type": "number"}, + "tests": [ + { + "description": "a negative bignum is a number", + "data": -98249283749234923498293171823948729348710298301928331, + "valid": true + } + ] + }, + { + "description": "string", + "schema": {"type": "string"}, + "tests": [ + { + "description": "a bignum is not a string", + "data": 98249283749234923498293171823948729348710298301928331, + "valid": false + } + ] + }, + { + "description": "integer comparison", + "schema": {"maximum": 18446744073709551615}, + "tests": [ + { + "description": "comparison works for high numbers", + "data": 18446744073709551600, + "valid": true + } + ] + }, + { + "description": "float comparison with high precision", + "schema": { + "maximum": 972783798187987123879878123.18878137, + "exclusiveMaximum": true + }, + "tests": [ + { + "description": "comparison works for high numbers", + "data": 972783798187987123879878123.188781371, + "valid": false + } + ] + }, + { + "description": "integer comparison", + "schema": {"minimum": -18446744073709551615}, + "tests": [ + { + "description": "comparison works for very negative numbers", + "data": -18446744073709551600, + "valid": true + } + ] + }, + { + "description": "float comparison with high precision on negative numbers", + "schema": { + "minimum": -972783798187987123879878123.18878137, + "exclusiveMinimum": true + }, + "tests": [ + { + "description": "comparison works for very negative numbers", + "data": -972783798187987123879878123.188781371, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/optional/format.json b/t/draft4-tests/optional/format.json new file mode 100644 index 0000000..aacfd11 --- /dev/null +++ b/t/draft4-tests/optional/format.json @@ -0,0 +1,148 @@ +[ + { + "description": "validation of date-time strings", + "schema": {"format": "date-time"}, + "tests": [ + { + "description": "a valid date-time string", + "data": "1963-06-19T08:30:06.283185Z", + "valid": true + }, + { + "description": "an invalid date-time string", + "data": "06/19/1963 08:30:06 PST", + "valid": false + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "2013-350T01:01:01", + "valid": false + } + ] + }, + { + "description": "validation of URIs", + "schema": {"format": "uri"}, + "tests": [ + { + "description": "a valid URI", + "data": "http://foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid protocol-relative URI", + "data": "//foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "an invalid URI", + "data": "\\\\WINDOWS\\fileshare", + "valid": false + }, + { + "description": "an invalid URI though valid URI reference", + "data": "abc", + "valid": false + } + ] + }, + { + "description": "validation of e-mail addresses", + "schema": {"format": "email"}, + "tests": [ + { + "description": "a valid e-mail address", + "data": "joe.bloggs@example.com", + "valid": true + }, + { + "description": "an invalid e-mail address", + "data": "2962", + "valid": false + } + ] + }, + { + "description": "validation of IP addresses", + "schema": {"format": "ipv4"}, + "tests": [ + { + "description": "a valid IP address", + "data": "192.168.0.1", + "valid": true + }, + { + "description": "an IP address with too many components", + "data": "127.0.0.0.1", + "valid": false + }, + { + "description": "an IP address with out-of-range values", + "data": "256.256.256.256", + "valid": false + }, + { + "description": "an IP address without 4 components", + "data": "127.0", + "valid": false + }, + { + "description": "an IP address as an integer", + "data": "0x7f000001", + "valid": false + } + ] + }, + { + "description": "validation of IPv6 addresses", + "schema": {"format": "ipv6"}, + "tests": [ + { + "description": "a valid IPv6 address", + "data": "::1", + "valid": true + }, + { + "description": "an IPv6 address with out-of-range values", + "data": "12345::", + "valid": false + }, + { + "description": "an IPv6 address with too many components", + "data": "1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", + "valid": false + }, + { + "description": "an IPv6 address containing illegal characters", + "data": "::laptop", + "valid": false + } + ] + }, + { + "description": "validation of host names", + "schema": {"format": "hostname"}, + "tests": [ + { + "description": "a valid host name", + "data": "www.example.com", + "valid": true + }, + { + "description": "a host name starting with an illegal character", + "data": "-a-host-name-that-starts-with--", + "valid": false + }, + { + "description": "a host name containing illegal characters", + "data": "not_a_valid_host_name", + "valid": false + }, + { + "description": "a host name with a component too long", + "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/optional/zeroTerminatedFloats.json b/t/draft4-tests/optional/zeroTerminatedFloats.json new file mode 100644 index 0000000..9b50ea2 --- /dev/null +++ b/t/draft4-tests/optional/zeroTerminatedFloats.json @@ -0,0 +1,15 @@ +[ + { + "description": "some languages do not distinguish between different types of numeric value", + "schema": { + "type": "integer" + }, + "tests": [ + { + "description": "a float is not an integer even without fractional part", + "data": 1.0, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/pattern.json b/t/draft4-tests/pattern.json new file mode 100644 index 0000000..25e7299 --- /dev/null +++ b/t/draft4-tests/pattern.json @@ -0,0 +1,34 @@ +[ + { + "description": "pattern validation", + "schema": {"pattern": "^a*$"}, + "tests": [ + { + "description": "a matching pattern is valid", + "data": "aaa", + "valid": true + }, + { + "description": "a non-matching pattern is invalid", + "data": "abc", + "valid": false + }, + { + "description": "ignores non-strings", + "data": true, + "valid": true + } + ] + }, + { + "description": "pattern is not anchored", + "schema": {"pattern": "a+"}, + "tests": [ + { + "description": "matches a substring", + "data": "xxaayy", + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/patternProperties.json b/t/draft4-tests/patternProperties.json new file mode 100644 index 0000000..18586e5 --- /dev/null +++ b/t/draft4-tests/patternProperties.json @@ -0,0 +1,110 @@ +[ + { + "description": + "patternProperties validates properties matching a regex", + "schema": { + "patternProperties": { + "f.*o": {"type": "integer"} + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "multiple valid matches is valid", + "data": {"foo": 1, "foooooo" : 2}, + "valid": true + }, + { + "description": "a single invalid match is invalid", + "data": {"foo": "bar", "fooooo": 2}, + "valid": false + }, + { + "description": "multiple invalid matches is invalid", + "data": {"foo": "bar", "foooooo" : "baz"}, + "valid": false + }, + { + "description": "ignores non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "multiple simultaneous patternProperties are validated", + "schema": { + "patternProperties": { + "a*": {"type": "integer"}, + "aaa*": {"maximum": 20} + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": {"a": 21}, + "valid": true + }, + { + "description": "a simultaneous match is valid", + "data": {"aaaa": 18}, + "valid": true + }, + { + "description": "multiple matches is valid", + "data": {"a": 21, "aaaa": 18}, + "valid": true + }, + { + "description": "an invalid due to one is invalid", + "data": {"a": "bar"}, + "valid": false + }, + { + "description": "an invalid due to the other is invalid", + "data": {"aaaa": 31}, + "valid": false + }, + { + "description": "an invalid due to both is invalid", + "data": {"aaa": "foo", "aaaa": 31}, + "valid": false + } + ] + }, + { + "description": "regexes are not anchored by default and are case sensitive", + "schema": { + "patternProperties": { + "[0-9]{2,}": { "type": "boolean" }, + "X_": { "type": "string" } + } + }, + "tests": [ + { + "description": "non recognized members are ignored", + "data": { "answer 1": "42" }, + "valid": true + }, + { + "description": "recognized members are accounted for", + "data": { "a31b": null }, + "valid": false + }, + { + "description": "regexes are case sensitive", + "data": { "a_x_3": 3 }, + "valid": true + }, + { + "description": "regexes are case sensitive, 2", + "data": { "a_X_3": 3 }, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/properties.json b/t/draft4-tests/properties.json new file mode 100644 index 0000000..cd1644d --- /dev/null +++ b/t/draft4-tests/properties.json @@ -0,0 +1,92 @@ +[ + { + "description": "object properties validation", + "schema": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "string"} + } + }, + "tests": [ + { + "description": "both properties present and valid is valid", + "data": {"foo": 1, "bar": "baz"}, + "valid": true + }, + { + "description": "one property invalid is invalid", + "data": {"foo": 1, "bar": {}}, + "valid": false + }, + { + "description": "both properties invalid is invalid", + "data": {"foo": [], "bar": {}}, + "valid": false + }, + { + "description": "doesn't invalidate other properties", + "data": {"quux": []}, + "valid": true + }, + { + "description": "ignores non-objects", + "data": [], + "valid": true + } + ] + }, + { + "description": + "properties, patternProperties, additionalProperties interaction", + "schema": { + "properties": { + "foo": {"type": "array", "maxItems": 3}, + "bar": {"type": "array"} + }, + "patternProperties": {"f.o": {"minItems": 2}}, + "additionalProperties": {"type": "integer"} + }, + "tests": [ + { + "description": "property validates property", + "data": {"foo": [1, 2]}, + "valid": true + }, + { + "description": "property invalidates property", + "data": {"foo": [1, 2, 3, 4]}, + "valid": false + }, + { + "description": "patternProperty invalidates property", + "data": {"foo": []}, + "valid": false + }, + { + "description": "patternProperty validates nonproperty", + "data": {"fxo": [1, 2]}, + "valid": true + }, + { + "description": "patternProperty invalidates nonproperty", + "data": {"fxo": []}, + "valid": false + }, + { + "description": "additionalProperty ignores property", + "data": {"bar": []}, + "valid": true + }, + { + "description": "additionalProperty validates others", + "data": {"quux": 3}, + "valid": true + }, + { + "description": "additionalProperty invalidates others", + "data": {"quux": "foo"}, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/ref.json b/t/draft4-tests/ref.json new file mode 100644 index 0000000..7e80552 --- /dev/null +++ b/t/draft4-tests/ref.json @@ -0,0 +1,159 @@ +[ + { + "description": "root pointer ref", + "schema": { + "properties": { + "foo": {"$ref": "#"} + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "match", + "data": {"foo": false}, + "valid": true + }, + { + "description": "recursive match", + "data": {"foo": {"foo": false}}, + "valid": true + }, + { + "description": "mismatch", + "data": {"bar": false}, + "valid": false + }, + { + "description": "recursive mismatch", + "data": {"foo": {"bar": false}}, + "valid": false + } + ] + }, + { + "description": "relative pointer ref to object", + "schema": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"$ref": "#/properties/foo"} + } + }, + "tests": [ + { + "description": "match", + "data": {"bar": 3}, + "valid": true + }, + { + "description": "mismatch", + "data": {"bar": true}, + "valid": false + } + ] + }, + { + "description": "relative pointer ref to array", + "schema": { + "items": [ + {"type": "integer"}, + {"$ref": "#/items/0"} + ] + }, + "tests": [ + { + "description": "match array", + "data": [1, 2], + "valid": true + }, + { + "description": "mismatch array", + "data": [1, "foo"], + "valid": false + } + ] + }, + { + "description": "escaped pointer ref", + "schema": { + "tilda~field": {"type": "integer"}, + "slash/field": {"type": "integer"}, + "percent%field": {"type": "integer"}, + "properties": { + "tilda": {"$ref": "#/tilda~0field"}, + "slash": {"$ref": "#/slash~1field"}, + "percent": {"$ref": "#/percent%25field"} + } + }, + "tests": [ + { + "description": "slash invalid", + "data": {"slash": "aoeu"}, + "valid": false + }, + { + "description": "tilda invalid", + "data": {"tilda": "aoeu"}, + "valid": false + }, + { + "description": "percent invalid", + "data": {"percent": "aoeu"}, + "valid": false + }, + { + "description": "slash valid", + "data": {"slash": 123}, + "valid": true + }, + { + "description": "tilda valid", + "data": {"tilda": 123}, + "valid": true + }, + { + "description": "percent valid", + "data": {"percent": 123}, + "valid": true + } + ] + }, + { + "description": "nested refs", + "schema": { + "definitions": { + "a": {"type": "integer"}, + "b": {"$ref": "#/definitions/a"}, + "c": {"$ref": "#/definitions/b"} + }, + "$ref": "#/definitions/c" + }, + "tests": [ + { + "description": "nested ref valid", + "data": 5, + "valid": true + }, + { + "description": "nested ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "remote ref, containing refs itself", + "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, + "tests": [ + { + "description": "remote ref valid", + "data": {"minLength": 1}, + "valid": true + }, + { + "description": "remote ref invalid", + "data": {"minLength": -1}, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/refRemote.json b/t/draft4-tests/refRemote.json new file mode 100644 index 0000000..4ca8047 --- /dev/null +++ b/t/draft4-tests/refRemote.json @@ -0,0 +1,74 @@ +[ + { + "description": "remote ref", + "schema": {"$ref": "http://localhost:1234/integer.json"}, + "tests": [ + { + "description": "remote ref valid", + "data": 1, + "valid": true + }, + { + "description": "remote ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "fragment within remote ref", + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "tests": [ + { + "description": "remote fragment valid", + "data": 1, + "valid": true + }, + { + "description": "remote fragment invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref within remote ref", + "schema": { + "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + }, + "tests": [ + { + "description": "ref within ref valid", + "data": 1, + "valid": true + }, + { + "description": "ref within ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "change resolution scope", + "schema": { + "id": "http://localhost:1234/", + "items": { + "id": "folder/", + "items": {"$ref": "folderInteger.json"} + } + }, + "tests": [ + { + "description": "changed scope ref valid", + "data": [[1]], + "valid": true + }, + { + "description": "changed scope ref invalid", + "data": [["a"]], + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/required.json b/t/draft4-tests/required.json new file mode 100644 index 0000000..612f73f --- /dev/null +++ b/t/draft4-tests/required.json @@ -0,0 +1,39 @@ +[ + { + "description": "required validation", + "schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "required": ["foo"] + }, + "tests": [ + { + "description": "present required property is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "non-present required property is invalid", + "data": {"bar": 1}, + "valid": false + } + ] + }, + { + "description": "required default validation", + "schema": { + "properties": { + "foo": {} + } + }, + "tests": [ + { + "description": "not required by default", + "data": {}, + "valid": true + } + ] + } +] diff --git a/t/draft4-tests/type.json b/t/draft4-tests/type.json new file mode 100644 index 0000000..db42a44 --- /dev/null +++ b/t/draft4-tests/type.json @@ -0,0 +1,330 @@ +[ + { + "description": "integer type matches integers", + "schema": {"type": "integer"}, + "tests": [ + { + "description": "an integer is an integer", + "data": 1, + "valid": true + }, + { + "description": "a float is not an integer", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an integer", + "data": "foo", + "valid": false + }, + { + "description": "an object is not an integer", + "data": {}, + "valid": false + }, + { + "description": "an array is not an integer", + "data": [], + "valid": false + }, + { + "description": "a boolean is not an integer", + "data": true, + "valid": false + }, + { + "description": "null is not an integer", + "data": null, + "valid": false + } + ] + }, + { + "description": "number type matches numbers", + "schema": {"type": "number"}, + "tests": [ + { + "description": "an integer is a number", + "data": 1, + "valid": true + }, + { + "description": "a float is a number", + "data": 1.1, + "valid": true + }, + { + "description": "a string is not a number", + "data": "foo", + "valid": false + }, + { + "description": "an object is not a number", + "data": {}, + "valid": false + }, + { + "description": "an array is not a number", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a number", + "data": true, + "valid": false + }, + { + "description": "null is not a number", + "data": null, + "valid": false + } + ] + }, + { + "description": "string type matches strings", + "schema": {"type": "string"}, + "tests": [ + { + "description": "1 is not a string", + "data": 1, + "valid": false + }, + { + "description": "a float is not a string", + "data": 1.1, + "valid": false + }, + { + "description": "a string is a string", + "data": "foo", + "valid": true + }, + { + "description": "an object is not a string", + "data": {}, + "valid": false + }, + { + "description": "an array is not a string", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a string", + "data": true, + "valid": false + }, + { + "description": "null is not a string", + "data": null, + "valid": false + } + ] + }, + { + "description": "object type matches objects", + "schema": {"type": "object"}, + "tests": [ + { + "description": "an integer is not an object", + "data": 1, + "valid": false + }, + { + "description": "a float is not an object", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an object", + "data": "foo", + "valid": false + }, + { + "description": "an object is an object", + "data": {}, + "valid": true + }, + { + "description": "an array is not an object", + "data": [], + "valid": false + }, + { + "description": "a boolean is not an object", + "data": true, + "valid": false + }, + { + "description": "null is not an object", + "data": null, + "valid": false + } + ] + }, + { + "description": "array type matches arrays", + "schema": {"type": "array"}, + "tests": [ + { + "description": "an integer is not an array", + "data": 1, + "valid": false + }, + { + "description": "a float is not an array", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an array", + "data": "foo", + "valid": false + }, + { + "description": "an object is not an array", + "data": {}, + "valid": false + }, + { + "description": "an array is an array", + "data": [], + "valid": true + }, + { + "description": "a boolean is not an array", + "data": true, + "valid": false + }, + { + "description": "null is not an array", + "data": null, + "valid": false + } + ] + }, + { + "description": "boolean type matches booleans", + "schema": {"type": "boolean"}, + "tests": [ + { + "description": "an integer is not a boolean", + "data": 1, + "valid": false + }, + { + "description": "a float is not a boolean", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not a boolean", + "data": "foo", + "valid": false + }, + { + "description": "an object is not a boolean", + "data": {}, + "valid": false + }, + { + "description": "an array is not a boolean", + "data": [], + "valid": false + }, + { + "description": "a boolean is a boolean", + "data": true, + "valid": true + }, + { + "description": "null is not a boolean", + "data": null, + "valid": false + } + ] + }, + { + "description": "null type matches only the null object", + "schema": {"type": "null"}, + "tests": [ + { + "description": "an integer is not null", + "data": 1, + "valid": false + }, + { + "description": "a float is not null", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not null", + "data": "foo", + "valid": false + }, + { + "description": "an object is not null", + "data": {}, + "valid": false + }, + { + "description": "an array is not null", + "data": [], + "valid": false + }, + { + "description": "a boolean is not null", + "data": true, + "valid": false + }, + { + "description": "null is null", + "data": null, + "valid": true + } + ] + }, + { + "description": "multiple types can be specified in an array", + "schema": {"type": ["integer", "string"]}, + "tests": [ + { + "description": "an integer is valid", + "data": 1, + "valid": true + }, + { + "description": "a string is valid", + "data": "foo", + "valid": true + }, + { + "description": "a float is invalid", + "data": 1.1, + "valid": false + }, + { + "description": "an object is invalid", + "data": {}, + "valid": false + }, + { + "description": "an array is invalid", + "data": [], + "valid": false + }, + { + "description": "a boolean is invalid", + "data": true, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + } + ] + } +] diff --git a/t/draft4-tests/uniqueItems.json b/t/draft4-tests/uniqueItems.json new file mode 100644 index 0000000..c1f4ab9 --- /dev/null +++ b/t/draft4-tests/uniqueItems.json @@ -0,0 +1,79 @@ +[ + { + "description": "uniqueItems validation", + "schema": {"uniqueItems": true}, + "tests": [ + { + "description": "unique array of integers is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "non-unique array of integers is invalid", + "data": [1, 1], + "valid": false + }, + { + "description": "numbers are unique if mathematically unequal", + "data": [1.0, 1.00, 1], + "valid": false + }, + { + "description": "unique array of objects is valid", + "data": [{"foo": "bar"}, {"foo": "baz"}], + "valid": true + }, + { + "description": "non-unique array of objects is invalid", + "data": [{"foo": "bar"}, {"foo": "bar"}], + "valid": false + }, + { + "description": "unique array of nested objects is valid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : false}}} + ], + "valid": true + }, + { + "description": "non-unique array of nested objects is invalid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : true}}} + ], + "valid": false + }, + { + "description": "unique array of arrays is valid", + "data": [["foo"], ["bar"]], + "valid": true + }, + { + "description": "non-unique array of arrays is invalid", + "data": [["foo"], ["foo"]], + "valid": false + }, + { + "description": "1 and true are unique", + "data": [1, true], + "valid": true + }, + { + "description": "0 and false are unique", + "data": [0, false], + "valid": true + }, + { + "description": "unique heterogeneous types are valid", + "data": [{}, [1], true, null, 1], + "valid": true + }, + { + "description": "non-unique heterogeneous types are invalid", + "data": [{}, [1], true, null, {}, 1], + "valid": false + } + ] + } +] diff --git a/t/get.t b/t/get.t new file mode 100644 index 0000000..2ec4257 --- /dev/null +++ b/t/get.t @@ -0,0 +1,25 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; + +my $jv = JSON::Validator->new->schema( + { + foo => [{y => 'foo'}], + bar => [{y => 'first'}, {y => 'second'}, {z => 'zzz'}], + } +); + +is $jv->get('/bar/2/z'), 'zzz', 'get /bar/2/z'; +is $jv->get([qw(nope 404)]), undef, 'get /nope/404'; +is_deeply $jv->get([qw(bar 0)]), {y => 'first'}, 'get /bar/0'; + +# This is not officially supported. I think maybe the callback version is the way to go, +# since it allows the JSON pointer to be passed on as well. +is_deeply $jv->get(['bar', undef, 'y']), ['first', 'second', undef], + 'get /bar/undef/y'; +is_deeply $jv->get([undef, undef, 'y']), [['first', 'second', undef], ['foo']], + 'get /undef/undef/y'; +is_deeply $jv->get([undef, undef, 'y'])->flatten, + ['first', 'second', undef, 'foo'], 'get /undef/undef/y flatten'; + +done_testing; diff --git a/t/id-keyword-draft4.t b/t/id-keyword-draft4.t new file mode 100644 index 0000000..4dae689 --- /dev/null +++ b/t/id-keyword-draft4.t @@ -0,0 +1,87 @@ +use Mojo::Base -strict; +use Mojo::JSON 'encode_json'; +use Test::Mojo; +use Test::More; +use JSON::Validator; + +my ($base_url, $jv, $t, @e); + +use Mojolicious::Lite; +get '/invalid-fragment' => 'invalid-fragment'; +get '/invalid-relative' => 'invalid-relative'; +get '/relative-to-the-root' => 'relative-to-the-root'; + +$t = Test::Mojo->new; +$jv = JSON::Validator->new(ua => $t->ua); +$t->get_ok('/relative-to-the-root.json')->status_is(200); + +$base_url = $t->tx->req->url->to_abs->path('/'); +like $base_url, qr{^http}, 'got base_url to web server'; +is $jv->version, 4, 'default version'; +is $jv->_id_key, 'id', 'default id_key'; + +delete $jv->{version}; +eval { $jv->load_and_validate_schema("${base_url}relative-to-the-root.json") }; +ok !$@, "${base_url}relative-to-the-root.json" or diag $@; +is $jv->{version}, 4, 'detected version from draft-04'; + +my $schema = $jv->schema; +is $schema->get('/id'), 'http://example.com/relative-to-the-root.json', + 'get /id'; +is $schema->get('/definitions/B/id'), 'b.json', 'id /definitions/B/id'; +is $schema->get('/definitions/B/definitions/X/id'), '#bx', + 'id /definitions/B/definitions/X/id'; +is $schema->get('/definitions/B/definitions/Y/id'), 't/inner.json', + 'id /definitions/B/definitions/Y/id'; +is $schema->get('/definitions/C/definitions/X/id'), + 'urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f', + 'id /definitions/C/definitions/X/id'; +is $schema->get('/definitions/C/definitions/Y/id'), '#cy', + 'id /definitions/C/definitions/Y/id'; + +my $ref = $schema->get('/definitions/R1'); +ok $ref->{$_}, "got $_" for qw($ref %%fqn %%schema); +is encode_json($ref), '{"$ref":"b.json#bx"}', 'ref encode_json'; +$ref = tied %$ref; +is $ref->ref, 'b.json#bx', 'ref ref'; +is $ref->fqn, 'http://example.com/b.json#bx', 'ref fqn'; +ok $ref->schema->{definitions}{Y}, 'ref schema'; + +eval { $jv->load_and_validate_schema("${base_url}invalid-fragment.json") }; +like $@, qr{cannot have a fragment}, 'Root id cannot have a fragment' + or diag $@; + +eval { $jv->load_and_validate_schema("${base_url}invalid-relative.json") }; +like $@, qr{cannot have a relative}, 'Root id cannot be relative' or diag $@; + +done_testing; + +__DATA__ +@@ invalid-fragment.json.ep +{"id": "http://example.com/invalid-fragment.json#cannot_be_here"} +@@ invalid-relative.json.ep +{"id": "whatever"} +@@ relative-to-the-root.json.ep +{ + "id": "http://example.com/relative-to-the-root.json", + "definitions": { + "A": { "id": "#a" }, + "B": { + "id": "b.json", + "definitions": { + "X": { "id": "#bx" }, + "Y": { "id": "t/inner.json" } + } + }, + "C": { + "id": "c.json", + "definitions": { + "X": { "id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" }, + "Y": { "id": "#cy" } + } + }, + "R1": { "$ref": "b.json#bx" }, + "R2": { "$ref": "#a" }, + "R3": { "$ref": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" } + } +} diff --git a/t/id-keyword-draft7.t b/t/id-keyword-draft7.t new file mode 100644 index 0000000..5f03ea8 --- /dev/null +++ b/t/id-keyword-draft7.t @@ -0,0 +1,46 @@ +use Mojo::Base -strict; +use Mojo::JSON 'encode_json'; +use Test::Mojo; +use Test::More; +use JSON::Validator; + +my ($base_url, $jv, $t, @e); + +use Mojolicious::Lite; +get '/person' => 'person'; +get '/invalid-relative' => 'invalid-relative'; + +$t = Test::Mojo->new; +$jv = JSON::Validator->new(ua => $t->ua); + +eval { + $t->get_ok('/person.json')->status_is(200); + $base_url = $t->tx->req->url->to_abs->path('/'); + $jv->load_and_validate_schema("${base_url}person.json", + {schema => 'http://json-schema.org/draft-07/schema'}); +}; +ok !$@, "${base_url}schema.json" or diag $@; + +is $jv->version, 7, 'detected version from draft-07'; +is $jv->_id_key, '$id', 'detected id_key from draft-07'; + +eval { $jv->load_and_validate_schema("${base_url}invalid-relative.json") }; +like $@, qr{cannot have a relative}, 'Root id cannot be relative' or diag $@; + +done_testing; + +__DATA__ +@@ invalid-relative.json.ep +{"$id": "whatever"} +@@ person.json.ep +{ + "$id": "http://example.com/person.json", + "definitions": { + "Person": { + "type": "object", + "properties": { + "firstName": { "type": "string" } + } + } + } +} diff --git a/t/invalid-ref.t b/t/invalid-ref.t new file mode 100644 index 0000000..dbe3434 --- /dev/null +++ b/t/invalid-ref.t @@ -0,0 +1,27 @@ +use Mojo::Base -strict; +use Mojo::File 'path'; +use Test::More; +use JSON::Validator; + +eval { JSON::Validator->new->schema('data://main/spec.json') }; +like $@, qr{Could not find.*/definitions/Pet"}, 'missing definition'; + +my $workdir = path(__FILE__)->dirname; +eval { + JSON::Validator->new->schema(path($workdir, 'spec', 'missing-ref.json')); +}; + +ok $@, 'loading missing ref failed'; +like $@, qr{Unable to load schema.*missing\.json}, 'error message' + unless $^O eq 'MSWin32'; + +done_testing; + +__DATA__ +@@ spec.json +{ + "schema": { + "type": "array", + "items": { "$ref": "#/definitions/Pet" } + } +} diff --git a/t/issue-103-one-of.t b/t/issue-103-one-of.t new file mode 100644 index 0000000..58f60db --- /dev/null +++ b/t/issue-103-one-of.t @@ -0,0 +1,66 @@ +use lib '.'; +use t::Helper; + +validate_ok {who_id => 'WHO', expire => '2018-01-01', amount => 1000, + desc => 'foo'}, 'data://main/example.json', + E('/sym', '/oneOf/0/allOf/0/allOf/0 Missing property.'), + E('/template', '/oneOf/0/allOf/2 Missing property.'), + E('/sym', '/oneOf/1/allOf/0 Missing property.'); + +done_testing; + +__DATA__ +@@ example.json +{ + "oneOf": [ + {"$ref": "#/definitions/template_1"}, + {"$ref": "#/definitions/bar_header"} + ], + "definitions": { + "hwho":{ + "required": [ "who_id" ], + "properties": { + "who_id": { "type": "string" }, + "sub_who_id": { "type": "string" } + } + }, + "header": { + "required": [ "sym", "expire" ], + "properties": { + "sym": { "type": "string" }, + "expire": { "type": "string" } + } + }, + "foo_header": { + "allOf": [ + { "$ref": "#/definitions/header" }, + { + "required": [ "amount", "desc" ], + "properties": { + "amount": { "type": "integer" }, + "desc": { "enum": [ "foo" ] } + } + } + ] + }, + "template_1": { + "allOf": [ + { "$ref": "#/definitions/foo_header" }, + { "$ref": "#/definitions/hwho" }, + { "required": [ "template" ], "properties": { "template": { "type": "string" } } } + ] + }, + "bar_header" : { + "allOf": [ + { "$ref": "#/definitions/header" }, + { + "required": [ "amount", "desc" ], + "properties": { + "amount": { "type": "integer" }, + "desc": { "enum": [ "foo" ] } + } + } + ] + } + } +} diff --git a/t/issue-22-duplicate-error-messages.t b/t/issue-22-duplicate-error-messages.t new file mode 100644 index 0000000..2e18641 --- /dev/null +++ b/t/issue-22-duplicate-error-messages.t @@ -0,0 +1,21 @@ +use lib '.'; +use t::Helper; + +# https://github.com/jhthorsen/json-validator/issues/22 +validate_ok {foo => 'x'}, 'data://main/test.schema', + E('/foo', 'Not in enum list: bar, baz.'); +validate_ok {foo => 123}, 'data://main/test.schema', + E('/foo', 'Expected string - got number.'); + +done_testing; + +__DATA__ +@@ test.schema +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "test", + "type": "object", + "properties": { + "foo": {"type": "string", "enum": ["bar", "baz"]} + } +} diff --git a/t/issue-42-cache-control.t b/t/issue-42-cache-control.t new file mode 100644 index 0000000..70ba8f5 --- /dev/null +++ b/t/issue-42-cache-control.t @@ -0,0 +1,34 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; +use Mojo::File 'tempdir'; + +plan skip_all => 'TEST_ONLINE=1' unless $ENV{TEST_ONLINE}; + +$ENV{JSON_VALIDATOR_CACHE_DIR} = '/tmp/whatever'; +my $validator = JSON::Validator->new; +my @old_files = get_cached_files($validator); + +is $validator->cache_paths->[0], '/tmp/whatever', 'back compat env'; +shift @{$validator->cache_paths}; + +$validator->schema('https://za.payprop.com/api/docs/api_spec.yaml'); +my @new_files = get_cached_files($validator); +ok @old_files == @new_files, 'remote file not cached in default cache dir'; + +my $tempdir = tempdir; +$ENV{JSON_VALIDATOR_CACHE_PATH} = join ':', $tempdir->dirname, '/tmp/whatever'; +$validator = JSON::Validator->new; +is $validator->cache_paths->[0], $tempdir->dirname, 'env'; +$validator->schema('https://za.payprop.com/api/docs/api_spec.yaml'); +@new_files = get_cached_files($validator); +ok @new_files > @old_files, + 'remote file cached when cache_paths not the default' + or diag join "\n", @new_files; + +done_testing; + +sub get_cached_files { + my ($validator) = @_; + return sort map { glob "$_/*" } @{$validator->cache_paths}; +} diff --git a/t/issue-59-oneof-blessed-booleans.t b/t/issue-59-oneof-blessed-booleans.t new file mode 100644 index 0000000..037dd85 --- /dev/null +++ b/t/issue-59-oneof-blessed-booleans.t @@ -0,0 +1,39 @@ +use Mojo::Base -strict; +use Test::More; +use Mojo::JSON; +use JSON::Validator; + +my $validator = JSON::Validator->new->schema('data://main/spec.json'); +my @errors = $validator->validate( + {prop1 => Mojo::JSON->false, prop2 => Mojo::JSON->false}); + +is "@errors", ""; + +done_testing; + +__DATA__ + +@@ spec.json +{ + "type": "object", + "properties": { + "prop1": { + "$ref": "data://main/defs.json#/definitions/item" + }, + "prop2": { + "$ref": "data://main/defs.json#/definitions/item" + } + } +} + +@@ defs.json +{ + "definitions": { + "item": { + "oneOf": [ + {"type": "object"}, + {"type": "boolean"} + ] + } + } +} diff --git a/t/issue-71-additionalproperties.t b/t/issue-71-additionalproperties.t new file mode 100644 index 0000000..a474755 --- /dev/null +++ b/t/issue-71-additionalproperties.t @@ -0,0 +1,15 @@ +use lib '.'; +use t::Helper; +use Test::More; + +my $schema = { + required => ['link'], + type => 'object', + additionalProperties => false, + properties => {link => {format => 'uri'}}, +}; + +validate_ok {haha => 'hehe', link => 'http://a'}, $schema, + E('/', 'Properties not allowed: haha.'); + +done_testing; diff --git a/t/joi.t b/t/joi.t new file mode 100644 index 0000000..0390751 --- /dev/null +++ b/t/joi.t @@ -0,0 +1,136 @@ +use lib '.'; +use t::Helper; +use JSON::Validator 'joi'; +use Test::More; + +is_deeply( + edj(joi->object->strict->props( + age => joi->integer->min(0)->max(200), + alphanum => joi->alphanum->length(12), + color => joi->string->min(2)->max(12)->pattern('^\w+$'), + date_time => joi->iso_date, + email => joi->string->email->required, + exists => joi->boolean, + lc => joi->lowercase, + name => joi->string->min(1), + pos => joi->positive, + token => joi->token, + uc => joi->uppercase, + uri => joi->uri, + )), + { + type => 'object', + required => ['email'], + properties => { + age => {type => 'integer', minimum => 0, maximum => 200}, + alphanum => { + type => 'string', + minLength => 12, + maxLength => 12, + pattern => '^\w*$' + }, + color => + {type => 'string', minLength => 2, maxLength => 12, pattern => '^\w+$'}, + date_time => {type => 'string', format => 'date-time'}, + email => {type => 'string', format => 'email'}, + exists => {type => 'boolean'}, + lc => {type => 'string', pattern => '^\p{Lowercase}*$'}, + name => {type => 'string', minLength => 1}, + pos => {type => 'number', minimum => 0}, + token => {type => 'string', pattern => '^[a-zA-Z0-9_]+$'}, + uc => {type => 'string', pattern => '^\p{Uppercase}*$'}, + uri => {type => 'string', format => 'uri'}, + }, + additionalProperties => false + }, + 'generated correct object schema' +); + +is_deeply( + edj(joi->array->min(0)->max(10)->strict->items(joi->integer->negative)), + { + additionalItems => false, + type => 'array', + minItems => 0, + maxItems => 10, + items => {type => 'integer', maximum => 0} + }, + 'generated correct array schema' +); + +is_deeply( + edj(joi->string->enum([qw(1.0 2.0)])), + {type => 'string', enum => [qw(1.0 2.0)]}, + 'enum for string' +); + +is_deeply( + edj(joi->integer->enum([qw(1 2 4 8 16)])), + {type => 'integer', enum => [qw(1 2 4 8 16)]}, + 'enum for integer' +); + +joi_ok( + {age => 34, email => 'jhthorsen@cpan.org', name => 'Jan Henning Thorsen'}, + joi->props( + age => joi->integer->min(0)->max(200), + email => joi->string->email->required, + name => joi->string->min(1), + ), +); + +joi_ok( + {age => -1, name => 'Jan Henning Thorsen'}, + joi->props( + age => joi->integer->min(0)->max(200), + email => joi->string->email->required, + name => joi->string->min(1), + ), + E('/age', '-1 < minimum(0)'), + E('/email', 'Missing property.'), +); + +eval { joi->number->extend(joi->integer) }; +like $@, qr{Cannot extend joi 'number' by 'integer'}, + 'need to extend same type'; + +is_deeply( + edj(joi->array->min(0)->max(10)->extend(joi->array->min(5))), + {type => 'array', minItems => 5, maxItems => 10}, + 'extended array', +); + +is_deeply( + edj(joi->integer->min(0)->max(10)->extend(joi->integer->min(5))), + {type => 'integer', minimum => 5, maximum => 10}, + 'extended integer', +); + +is_deeply( + edj( + joi->object->props(x => joi->integer, y => joi->integer) + ->extend(joi->object->props(x => joi->number)) + ), + { + type => 'object', + properties => {x => {type => 'number'}, y => {type => 'integer'}} + }, + 'extended object', +); + +is_deeply( + edj(joi->object->props( + ip => joi->type([qw(string null)])->format('ip'), + ns => joi->string + )), + { + type => 'object', + properties => { + ip => {format => 'ip', type => [qw(string null)]}, + ns => {type => 'string'}, + } + }, + 'null or string', +); + +done_testing; diff --git a/t/jv-allof.t b/t/jv-allof.t new file mode 100644 index 0000000..f67d26c --- /dev/null +++ b/t/jv-allof.t @@ -0,0 +1,20 @@ +use lib '.'; +use t::Helper; + +my $schema + = {allOf => + [{type => 'string', maxLength => 5}, {type => 'string', minLength => 3}] + }; + +validate_ok 'short', $schema; +validate_ok 12, $schema, E('/', '/allOf Expected string - got number.'); + +$schema + = {allOf => + [{type => 'string', maxLength => 7}, {type => 'string', maxLength => 5}] + }; +validate_ok 'superlong', $schema, E('/', '/allOf/0 String is too long: 9/7.'), + E('/', '/allOf/1 String is too long: 9/5.'); +validate_ok 'toolong', $schema, E('/', '/allOf/1 String is too long: 7/5.'); + +done_testing; diff --git a/t/jv-anyof.t b/t/jv-anyof.t new file mode 100644 index 0000000..31fd35f --- /dev/null +++ b/t/jv-anyof.t @@ -0,0 +1,66 @@ +use lib '.'; +use t::Helper; + +my $schema + = {anyOf => + [{type => "string", maxLength => 5}, {type => "number", minimum => 0}] + }; + +validate_ok 'short', $schema; +validate_ok 'too long', $schema, E('/', '/anyOf/0 String is too long: 8/5.'); +validate_ok 12, $schema; +validate_ok int(-1), $schema, E('/', '/anyOf/1 -1 < minimum(0)'); +validate_ok {}, $schema, E('/', '/anyOf Expected string/number - got object.'); + +# anyOf with explicit integer (where _guess_data_type returns 'number') +my $schemaB = {anyOf => [{type => 'integer'}, {minimum => 2}]}; +validate_ok 1, $schemaB; + +validate_ok( + {type => 'string'}, + { + properties => { + type => { + anyOf => [ + {'$ref' => '#/definitions/simpleTypes'}, + { + type => 'array', + items => {'$ref' => '#/definitions/simpleTypes'}, + minItems => 1, + uniqueItems => Mojo::JSON::true, + } + ] + }, + }, + definitions => { + simpleTypes => + {enum => [qw(array boolean integer null number object string)]} + } + } +); + +validate_ok( + {age => 6}, + { + '$schema' => 'http://json-schema.org/draft-04/schema#', + type => 'object', + title => 'test', + description => 'test', + properties => { + age => {type => 'number', anyOf => [{multipleOf => 5}, {multipleOf => 3}]} + } + } +); + +validate_ok( + {c => 'c present, a/b is missing'}, + { + type => 'object', + properties => {a => {type => 'number'}, b => {type => 'string'}}, + anyOf => [{required => ['a']}, {required => ['b']}], + }, + E('/a', '/anyOf/0 Missing property.'), + E('/b', '/anyOf/1 Missing property.'), +); + +done_testing; diff --git a/t/jv-array.t b/t/jv-array.t new file mode 100644 index 0000000..00cb93a --- /dev/null +++ b/t/jv-array.t @@ -0,0 +1,58 @@ +use lib '.'; +use Mojo::Base -strict; +use Mojo::JSON 'encode_json'; +use Test::More; +use t::Helper; + +my $simple = {type => 'array', items => {type => 'number'}}; +my $length = {type => 'array', minItems => 2, maxItems => 2}; +my $unique = {type => 'array', uniqueItems => 1, items => {type => 'integer'}}; +my $tuple = { + type => 'array', + items => [ + {type => 'number'}, + {type => 'string'}, + {type => 'string', enum => ['Street', 'Avenue', 'Boulevard']}, + {type => 'string', enum => ['NW', 'NE', 'SW', 'SE']} + ] +}; + +validate_ok [1], $simple; +validate_ok [1, 'foo'], $simple, E('/1', 'Expected number - got string.'); +validate_ok [1], $length, E('/', 'Not enough items: 1/2.'); +validate_ok [1, 2], $length; +validate_ok [1, 2, 3], $length, E('/', 'Too many items: 3/2.'); +validate_ok [123, 124], $unique; +validate_ok [1, 2, 1], $unique, E('/', 'Unique items required.'); +validate_ok [1600, 'Pennsylvania', 'Avenue', 'NW'], $tuple; +validate_ok [24, 'Sussex', 'Drive'], $tuple; +validate_ok [10, 'Downing', 'Street'], $tuple; +validate_ok [1600, 'Pennsylvania', 'Avenue', 'NW', 'Washington'], $tuple; + +$tuple->{additionalItems} = Mojo::JSON->false; +validate_ok [1600, 'Pennsylvania', 'Avenue', 'NW', 'Washington'], $tuple, + E('/', 'Invalid number of items: 5/4.'); + +validate_ok [1600, 'NW'], + {type => 'array', contains => {type => 'string', enum => ['NW']}}; +validate_ok [1600, 'NW'], + {type => 'array', contains => {type => 'string', enum => ['Nope']}}, + E('/0', 'Expected string - got number.'), E('/1', 'Not in enum list: Nope.'); + +# Make sure all similar numbers gets converted from strings +my $jv = JSON::Validator->new->coerce(1); +my @numbers; + +$jv->schema({type => 'array', items => {type => 'number'}}); +@numbers = qw(1.42 2.3 1.42 1.42); +ok !$jv->validate(\@numbers), 'numbers are valid'; +is encode_json(\@numbers), encode_json([1.42, 2.3, 1.42, 1.42]), + 'coerced into integers'; + +$jv->schema({type => 'array', items => {type => 'integer'}}); +@numbers = qw(1 2 1 1 3 1); +ok !$jv->validate(\@numbers), 'integers are valid'; +is encode_json(\@numbers), encode_json([1, 2, 1, 1, 3, 1]), + 'coerced into numbers'; + +done_testing; diff --git a/t/jv-basic.t b/t/jv-basic.t new file mode 100644 index 0000000..a093a75 --- /dev/null +++ b/t/jv-basic.t @@ -0,0 +1,10 @@ +use lib '.'; +use t::Helper; + +sub j { Mojo::JSON::decode_json(Mojo::JSON::encode_json($_[0])); } + +validate_ok j($_), {type => 'any'} for undef, [], {}, 123, 'foo'; +validate_ok j(undef), {type => 'null'}; +validate_ok j(1), {type => 'null'}, E('/', 'Not null.'); + +done_testing; diff --git a/t/jv-boolean.t b/t/jv-boolean.t new file mode 100644 index 0000000..7128c04 --- /dev/null +++ b/t/jv-boolean.t @@ -0,0 +1,35 @@ +use lib '.'; +use t::Helper; + +sub j { Mojo::JSON::decode_json(Mojo::JSON::encode_json($_[0])); } + +my $schema = {type => 'object', properties => {nick => {type => 'boolean'}}}; + +validate_ok {nick => true}, $schema; +validate_ok {nick => 1000}, $schema, + E('/nick', 'Expected boolean - got number.'); +validate_ok {nick => 0.5}, $schema, + E('/nick', 'Expected boolean - got number.'); +validate_ok {nick => 'nick'}, $schema, + E('/nick', 'Expected boolean - got string.'); +validate_ok {nick => bless({}, 'BoolTestOk')}, $schema; +validate_ok {nick => bless({}, 'BoolTestFail')}, $schema, + E('/nick', 'Expected boolean - got BoolTestFail.'); + +validate_ok j(Mojo::JSON->false), {type => 'boolean'}; +validate_ok j(Mojo::JSON->true), {type => 'boolean'}; +validate_ok j('foo'), {type => 'boolean'}, + E('/', 'Expected boolean - got string.'); +validate_ok undef, {properties => {}}, E('/', 'Expected object - got null.'); + +t::Helper->validator->coerce(1); +validate_ok {nick => 1000}, $schema; +validate_ok {nick => 0.5}, $schema; + +done_testing; + +package BoolTestOk; +use overload '""' => sub {1}; + +package BoolTestFail; +use overload '""' => sub {2}; diff --git a/t/jv-const.t b/t/jv-const.t new file mode 100644 index 0000000..20f21f3 --- /dev/null +++ b/t/jv-const.t @@ -0,0 +1,74 @@ +use lib '.'; +use t::Helper; + +my $faithful = { + type => 'object', + properties => {constancy => {const => "as the northern star"}} +}; +my $ambitious = { + type => 'object', + properties => + {constancy => {const => "there is a tide in the affairs of men"}} +}; + +validate_ok {name => "Caesar", constancy => "as the northern star"}, $faithful; +validate_ok {name => "Brutus", + constancy => "there is a tide in the affairs of men"}, $ambitious; + +validate_ok {name => "Cassius", + constancy => "Cassius from bondage will deliver Cassius"}, $faithful, + E('/constancy', q{Does not match const: "as the northern star".}); + +validate_ok( + { + name => "Calpurnia", + constancy => + "Do not go forth today. Call it my fear That keeps you in the house" + }, + $ambitious, + E( + '/constancy', + q{Does not match const: "there is a tide in the affairs of men".} + ) +); + +# Now oneOf should work right +# before the fix, this failed with: "All of the oneOf rules match." +# because "likes: chocolate" vs. "peanutbutter" wasn't being considered +my $schema = { + type => 'object', + properties => { + people => { + type => 'array', + items => { + oneOf => [ + {'$ref' => '#/definitions/chocolate'}, + {'$ref' => '#/definitions/peanutbutter'} + ], + }, + }, + }, + definitions => { + chocolate => { + type => 'object', + properties => { + name => {type => 'string'}, + age => {type => 'number'}, + likes => {const => 'chocolate'} + }, + }, + peanutbutter => { + type => 'object', + properties => { + name => {type => 'string'}, + age => {type => 'number'}, + likes => {const => 'peanutbutter'}, + }, + }, + }, +}; +validate_ok { + people => [{name => 'mr. chocolate fan', age => 42, likes => 'peanutbutter'}] +}, $schema; + +done_testing; diff --git a/t/jv-enum.t b/t/jv-enum.t new file mode 100644 index 0000000..6ed1246 --- /dev/null +++ b/t/jv-enum.t @@ -0,0 +1,64 @@ +use lib '.'; +use t::Helper; + +my $male = { + type => 'object', + properties => {chromosomes => {enum => [[qw(X Y)], [qw(Y X)]]}} +}; +my $female + = {type => 'object', properties => {chromosomes => {enum => [[qw(X X)]]}}}; + +validate_ok {name => "Kate", chromosomes => [qw(X X)]}, $female; +validate_ok {name => "Dave", chromosomes => [qw(X Y)]}, $male; +validate_ok {name => "Arnie", chromosomes => [qw(Y X)]}, $male; + +validate_ok {name => "Kate", chromosomes => [qw(X X)]}, $male, + E('/chromosomes', 'Not in enum list: ["X","Y"], ["Y","X"].'); +validate_ok {name => "Eddie", chromosomes => [qw(X YY )]}, $male, + E('/chromosomes', 'Not in enum list: ["X","Y"], ["Y","X"].'); +validate_ok {name => "Steve", chromosomes => 'XY'}, $male, + E('/chromosomes', 'Not in enum list: ["X","Y"], ["Y","X"].'); + +# https://github.com/jhthorsen/json-validator/issues/69 +validate_ok( + {some_prop => ['foo']}, + { + type => 'object', + required => ['some_prop'], + properties => { + some_prop => { + type => 'array', + minItems => 1, + maxItems => 1, + items => [{type => 'string', enum => [qw(x y)]}], + }, + }, + }, + E('/some_prop/0', 'Not in enum list: x, y.') +); + +for my $v (undef, false, true) { + validate_ok( + {name => $v}, + { + type => 'object', + required => ['name'], + properties => + {name => {type => [qw(boolean null)], enum => [undef, false, true]}}, + }, + ); +} + +validate_ok( + {name => undef}, + { + type => 'object', + required => ['name'], + properties => + {name => {type => ['string'], enum => [qw(n yes true false)]}}, + }, + E('/name', '/anyOf Expected string - got null.'), + E('/name', 'Not in enum list: n, yes, true, false.'), +); + +done_testing; diff --git a/t/jv-formats.t b/t/jv-formats.t new file mode 100644 index 0000000..c0ab028 --- /dev/null +++ b/t/jv-formats.t @@ -0,0 +1,192 @@ +use lib '.'; +use t::Helper; +use Mojo::Util 'decode'; +use Test::More; + +my $schema = {type => 'object', properties => {v => {type => 'string'}}}; + +{ + local $schema->{properties}{v}{format} = 'date'; + validate_ok {v => '2014-12-09'}, $schema; + validate_ok {v => '0000-00-00'}, $schema, E('/v', 'Month out of range.'); + validate_ok {v => '0000-01-00'}, $schema, E('/v', 'Day out of range.'); + validate_ok {v => '2014-12-09T20:49:37Z'}, $schema, + E('/v', 'Does not match date format.'); + validate_ok {v => '0-0-0'}, $schema, E('/v', 'Does not match date format.'); + validate_ok {v => '09-12-2014'}, $schema, + E('/v', 'Does not match date format.'); + validate_ok {v => '09-DEC-2014'}, $schema, + E('/v', 'Does not match date format.'); + validate_ok {v => '09/12/2014'}, $schema, + E('/v', 'Does not match date format.'); +} + +{ + local $schema->{properties}{v}{format} = 'date-time'; + + validate_ok {v => $_}, + $schema + for ( + '2017-03-29T23:02:55.831Z', '2017-03-29t23:02:55.01z', + '2017-03-29 23:02:55-12:00', '2016-02-29T23:02:55+05:00' + ); + + validate_ok {v => 'xxxx-xx-xxtxx:xx:xxz'}, $schema, + E('/v', 'Does not match date-time format.'); + validate_ok {v => '2017-03-29\t23:02:55-12:00'}, $schema, + E('/v', 'Does not match date-time format.'); + validate_ok {v => '2017-03-29T23:02:60Z'}, $schema, + E('/v', 'Second out of range.'); + validate_ok {v => '2017-03-29T23:61:55Z'}, $schema, + E('/v', 'Minute out of range.'); + validate_ok {v => '2017-03-29T24:02:55Z'}, $schema, + E('/v', 'Hour out of range.'); + validate_ok {v => '2017-03-32T23:02:55Z'}, $schema, + E('/v', 'Day out of range.'); + validate_ok {v => '2017-02-30T23:02:55Z'}, $schema, + E('/v', 'Day out of range.'); + validate_ok {v => '2017-02-29T23:02:55Z'}, $schema, + E('/v', 'Day out of range.'); + validate_ok {v => '2017-03-00T23:02:55Z'}, $schema, + E('/v', 'Day out of range.'); + validate_ok {v => '2017-13-29T23:02:55Z'}, $schema, + E('/v', 'Month out of range.'); + validate_ok {v => '2017-00-29T23:02:55Z'}, $schema, + E('/v', 'Month out of range.'); +} + +{ + local $schema->{properties}{v}{format} = 'email'; + validate_ok {v => 'jhthorsen@cpan.org'}, $schema; + validate_ok {v => 'foo'}, $schema, E('/v', 'Does not match email format.'); + validate_ok {v => '用户@例子.广告'}, $schema, + E('/v', 'Does not match email format.'); +} + +{ + local $TODO + = eval 'require Data::Validate::Domain;1' ? undef : 'Missing module'; + local $schema->{properties}{v}{format} = 'hostname'; + validate_ok {v => 'mojolicio.us'}, $schema; + validate_ok {v => '[]'}, $schema, E('/v', 'Does not match hostname format.'); +} + +{ + validate_ok {v => decode('UTF-8', '用户@例子.广告')}, $schema; + local $TODO = eval 'require Net::IDN::Encode;1' ? undef : 'Missing module'; + local $schema->{properties}{v}{format} = 'idn-email'; + validate_ok {v => decode('UTF-8', '用户@')}, $schema, + E('/v', 'Does not match idn-email format.'); +} + +{ + local $schema->{properties}{v}{format} = 'idn-hostname'; + validate_ok {v => decode('UTF-8', '例子.广告')}, $schema; +} + +{ + local $schema->{properties}{v}{format} = 'iri'; + validate_ok {v => 'http://mojolicio.us/?ø=123'}, $schema; + validate_ok {v => decode('UTF-8', 'https://例子.广告/Ῥόδος')}, + $schema; + validate_ok {v => '/Ῥόδος'}, $schema, E('/v', 'Scheme missing.'); +} + +{ + local $schema->{properties}{v}{format} = 'iri-reference'; + validate_ok {v => '/Ῥόδος'}, $schema; + validate_ok {v => 'Ῥόδος'}, $schema; + validate_ok {v => 'http:///Ῥόδος'}, $schema, +} + +{ + local $TODO = eval 'require Data::Validate::IP;1' ? undef : 'Missing module'; + local $schema->{properties}{v}{format} = 'ipv4'; + validate_ok {v => '255.100.30.1'}, $schema; + validate_ok {v => '300.0.0.0'}, $schema, + E('/v', 'Does not match ipv4 format.'); +} + +{ + local $TODO = eval 'require Data::Validate::IP;1' ? undef : 'Missing module'; + local $schema->{properties}{v}{format} = 'ipv6'; + validate_ok {v => '::1'}, $schema; + validate_ok {v => '300.0.0.0'}, $schema, + E('/v', 'Does not match ipv6 format.'); +} + +{ + local $schema->{properties}{v}{format} = 'json-pointer'; + validate_ok {v => ''}, $schema; + validate_ok {v => '/foo/bar'}, $schema; + validate_ok {v => 'foo/bar'}, $schema, + E('/v', 'Does not match json-pointer format.'); +} + +{ + local $schema->{properties}{v}{format} = 'regex'; + validate_ok {v => '(\w+)'}, $schema; + validate_ok {v => '(\w'}, $schema, E('/v', 'Does not match regex format.'); +} + +{ + local $schema->{properties}{v}{format} = 'relative-json-pointer'; + validate_ok {v => '0'}, $schema; + validate_ok {v => '42#'}, $schema; + validate_ok {v => '100/foo/bar'}, $schema; + validate_ok {v => '#'}, $schema, + E('/v', 'Relative JSON Pointer must start with a non-negative-integer.'); + validate_ok {v => '42foo/bar'}, $schema, + E('/v', 'Does not match relative-json-pointer format.'); +} + +{ + local $schema->{properties}{v}{format} = 'time'; + validate_ok {v => $_}, $schema + for qw(23:02:55.831Z 23:02:55.01z 23:02:55-12:00 23:02:55+05:00); + validate_ok {v => 'xx:xx:xxz'}, $schema, + E('/v', 'Does not match time format.'); + validate_ok {v => '23:02:60Z'}, $schema, E('/v', 'Second out of range.'); + validate_ok {v => '23:61:55Z'}, $schema, E('/v', 'Minute out of range.'); + validate_ok {v => '24:02:55Z'}, $schema, E('/v', 'Hour out of range.'); +} + +{ + local $schema->{properties}{v}{format} = 'uri'; + validate_ok {v => '//example.com/no-scheme'}, $schema, + E('/v', 'Scheme missing.'); + validate_ok {v => ''}, $schema, + E('/v', 'Scheme, path or fragment are required.'); + validate_ok {v => '0://mojolicio.us/?x=123'}, $schema, + E('/v', 'Scheme must begin with a letter.'); + validate_ok {v => 'http://example.com/%z'}, $schema, + E('/v', 'Invalid hex escape.'); + validate_ok {v => 'http://example.com/%a'}, $schema, + E('/v', 'Hex escapes are not complete.'); + validate_ok {v => 'http:////'}, $schema, + E('/v', 'Path cannot not start with //.'); + validate_ok {v => 'http://mojolicio.us/?x=123'}, $schema; + + note 'TODO: relative paths should only be valid in draft4'; + validate_ok {v => '/relative-path'}, $schema; + validate_ok {v => 'relative-path'}, $schema; +} + +{ + local $schema->{properties}{v}{format} = 'uri-reference'; + validate_ok {v => 'http:///whatever'}, $schema; + validate_ok {v => '/relative-path'}, $schema; + validate_ok {v => 'relative-path'}, $schema; +} + +{ + local $schema->{properties}{v}{format} = 'uri-template'; + validate_ok {v => 'http://mojolicio.us/?x={x}'}, $schema; +} + +{ + local $schema->{properties}{v}{format} = 'unknown'; + validate_ok {v => 'whatever'}, $schema; +} + +done_testing; diff --git a/t/jv-integer.t b/t/jv-integer.t new file mode 100644 index 0000000..cb1b9be --- /dev/null +++ b/t/jv-integer.t @@ -0,0 +1,29 @@ +use lib '.'; +use t::Helper; + +my $schema = { + type => 'object', + properties => {mynumber => {type => 'integer', minimum => 1, maximum => 4}} +}; + +validate_ok {mynumber => 1}, $schema; +validate_ok {mynumber => 4}, $schema; +validate_ok {mynumber => 2}, $schema; +validate_ok {mynumber => 0}, $schema, E('/mynumber', '0 < minimum(1)'); +validate_ok {mynumber => -1}, $schema, E('/mynumber', '-1 < minimum(1)'); +validate_ok {mynumber => 5}, $schema, E('/mynumber', '5 > maximum(4)'); +validate_ok {mynumber => '2'}, $schema, + E('/mynumber', 'Expected integer - got string.'); + +$schema->{properties}{mynumber}{multipleOf} = 2; +validate_ok {mynumber => 3}, $schema, E('/mynumber', 'Not multiple of 2.'); + +t::Helper->validator->coerce(numbers => 1); +validate_ok {mynumber => '2'}, $schema; +validate_ok {mynumber => '2xyz'}, $schema, + E('/mynumber', 'Expected integer - got string.'); + +$schema->{properties}{mynumber}{minimum} = -3; +validate_ok {mynumber => '-2'}, $schema; + +done_testing; diff --git a/t/jv-not.t b/t/jv-not.t new file mode 100644 index 0000000..033b0a5 --- /dev/null +++ b/t/jv-not.t @@ -0,0 +1,9 @@ +use lib '.'; +use t::Helper; + +my $schema = {not => {type => 'string'}}; + +validate_ok 12, $schema; +validate_ok 'str', $schema, E('/', 'Should not match.'); + +done_testing; diff --git a/t/jv-number.t b/t/jv-number.t new file mode 100644 index 0000000..8bdb782 --- /dev/null +++ b/t/jv-number.t @@ -0,0 +1,26 @@ +use lib '.'; +use t::Helper; + +my $schema = { + type => 'object', + properties => + {mynumber => {type => 'number', minimum => -0.5, maximum => 2.7}} +}; + +validate_ok {mynumber => 1}, $schema; +validate_ok {mynumber => '2'}, $schema, + E('/mynumber', 'Expected number - got string.'); + +t::Helper->validator->coerce(numbers => 1); +validate_ok {mynumber => '-0.3'}, $schema; +validate_ok {mynumber => '0.1e+1'}, $schema; +validate_ok {mynumber => '2xyz'}, $schema, + E('/mynumber', 'Expected number - got string.'); +validate_ok {mynumber => '.1'}, $schema; +validate_ok {validNumber => 2.01}, + { + type => 'object', + properties => {validNumber => {type => 'number', multipleOf => 0.01}} + }; + +done_testing; diff --git a/t/jv-object.t b/t/jv-object.t new file mode 100644 index 0000000..d16fdcc --- /dev/null +++ b/t/jv-object.t @@ -0,0 +1,135 @@ +use lib '.'; +use t::Helper; +use Test::More; + +my $schema; + +{ + $schema = {type => 'object'}; + validate_ok {mynumber => 1}, $schema; + validate_ok [1], $schema, E('/', 'Expected object - got array.'); +} + +{ + $schema->{properties} = { + number => {type => 'number'}, + street_name => {type => 'string'}, + street_type => {type => 'string', enum => ['Street', 'Avenue', 'Boulevard']} + }; + local $schema->{patternProperties} + = {'^S_' => {type => 'string'}, '^I_' => {type => 'integer'}}; + + validate_ok {number => 1600, street_name => 'Pennsylvania', + street_type => 'Avenue'}, $schema; + validate_ok {number => '1600', street_name => 'Pennsylvania', + street_type => 'Avenue'}, $schema, + E('/number', 'Expected number - got string.'); + validate_ok {number => 1600, street_name => 'Pennsylvania'}, $schema; + validate_ok { + number => 1600, + street_name => 'Pennsylvania', + street_type => 'Avenue', + direction => 'NW' + }, $schema; + validate_ok {'S_25' => 'This is a string', 'I_0' => 42}, $schema; + validate_ok {'S_0' => 42}, $schema, + E('/S_0', 'Expected string - got number.'); +} + +{ + local $TODO = 't/openapi-set-request.t fails because of some oneOf logic'; + my $data = {}; + validate_ok $data, + { + type => 'object', + properties => {number => {type => 'number', default => 42}} + }; + is $data->{number}, 42, 'default value was set'; +} + +{ + local $schema->{additionalProperties} = 0; + validate_ok { + number => 1600, + street_name => 'Pennsylvania', + street_type => 'Avenue', + direction => 'NW' + }, + $schema, E('/', 'Properties not allowed: direction.'); + + $schema->{additionalProperties} = {type => 'string'}; + validate_ok { + number => 1600, + street_name => 'Pennsylvania', + street_type => 'Avenue', + direction => 'NW' + }, $schema; +} + +{ + local $schema->{required} = ['number', 'street_name']; + validate_ok {number => 1600, street_type => 'Avenue'}, $schema, + E('/street_name', 'Missing property.'); +} + +{ + $schema = {type => 'object', minProperties => 1}; + validate_ok {}, $schema, E('/', 'Not enough properties: 0/1.'); + $schema = {type => 'object', minProperties => 2, maxProperties => 3}; + validate_ok {a => 1}, $schema, E('/', 'Not enough properties: 1/2.'); + validate_ok {a => 1, b => 2}, $schema; + validate_ok {a => 1, b => 2, c => 3, d => 4}, $schema, + E('/', 'Too many properties: 4/3.'); +} + +{ + local $TODO = 'Add support for dependencies'; + $schema = { + type => 'object', + properties => { + name => {type => 'string'}, + credit_card => {type => 'number'}, + billing_address => {type => 'string'}, + }, + required => ['name'], + dependencies => {credit_card => ['billing_address']} + }; + + validate_ok {name => 'John Doe', credit_card => 5555555555555555}, $schema, + E('/credit_card', 'Missing billing_address.', 'credit_card'); +} + +{ + my $schema = {type => 'object', properties => {name => {type => 'string'}}}; + validate_ok {}, $schema; # does not matter + ok !$schema->{patternProperties}, 'patternProperties was not added issue#47'; +} + +{ + my $schema = {propertyNames => {minLength => 3, maxLength => 5}}; + validate_ok {name => 'John', surname => 'Doe'}, $schema, + E('/', '/propertyName/surname String is too long: 7/5.'); + + $schema->{propertyNames}{maxLength} = 7; + validate_ok {name => 'John', surname => 'Doe'}, $schema; +} + +{ + my $schema = { + if => {properties => {ifx => {type => 'string'}}}, + then => {properties => {ifx => {maxLength => 3}}}, + else => {properties => {ifx => {type => 'number'}}}, + }; + + validate_ok {ifx => 'foo'}, $schema; + validate_ok {ifx => 'foobar'}, $schema, E('/ifx', 'String is too long: 6/3.'); + validate_ok {ifx => 42}, $schema; + validate_ok {ifx => []}, $schema, E('/ifx', 'Expected number - got array.'); +} + +sub TO_JSON { return {age => shift->{age}} } +my $obj = bless {age => 'not_a_string'}, 'main'; +validate_ok $obj, {properties => {age => {type => 'integer'}}}, + E('/age', 'Expected integer - got string.', 'age is not a string'); + +done_testing; diff --git a/t/jv-oneof.t b/t/jv-oneof.t new file mode 100644 index 0000000..3e8a535 --- /dev/null +++ b/t/jv-oneof.t @@ -0,0 +1,39 @@ +use lib '.'; +use t::Helper; + +my $schema + = {oneOf => + [{type => 'string', maxLength => 5}, {type => 'number', minimum => 0}] + }; + +validate_ok 'short', $schema; +validate_ok 12, $schema; + +$schema + = {oneOf => + [{type => 'number', multipleOf => 5}, {type => 'number', multipleOf => 3}] + }; +validate_ok 10, $schema; +validate_ok 9, $schema; +validate_ok 15, $schema, E('/', 'All of the oneOf rules match.'); +validate_ok 13, $schema, E('/', '/oneOf/0 Not multiple of 5.'), + E('/', '/oneOf/1 Not multiple of 3.'); + +$schema = {oneOf => [{type => 'object'}, {type => 'string', multipleOf => 3}]}; +validate_ok 13, $schema, E('/', '/oneOf Expected object/string - got number.'); + +$schema = {oneOf => [{type => 'object'}, {type => 'number', multipleOf => 3}]}; +validate_ok 13, $schema, E('/', '/oneOf/1 Not multiple of 3.'); + +# Alternative oneOf +# http://json-schema.org/latest/json-schema-validation.html#anchor79 +$schema = { + type => 'object', + properties => {x => {type => ['string', 'null'], format => 'date-time'}} +}; +validate_ok {x => 'foo'}, $schema, + E('/x', '/anyOf/0 Does not match date-time format.'); +validate_ok {x => '2015-04-21T20:30:43.000Z'}, $schema; +validate_ok {x => undef}, $schema; + +done_testing; diff --git a/t/jv-required.t b/t/jv-required.t new file mode 100644 index 0000000..3131eb6 --- /dev/null +++ b/t/jv-required.t @@ -0,0 +1,25 @@ +use lib '.'; +use t::Helper; + +my $schema0 = { + type => 'object', + properties => {mynumber => {type => 'string', required => 1}} +}; +my $schema1 = { + type => 'object', + properties => {mynumber => {type => 'string'}}, + required => ['mynumber'] +}; +my $schema2 + = {type => 'object', properties => {mynumber => {type => 'string'}}}; + +my $data1 = {mynumber => 'yay'}; +my $data2 = {mynumbre => 'err'}; + +validate_ok $data1, $schema1; +validate_ok $data2, $schema0; # Cannot have required on properties +validate_ok $data2, $schema1, E('/mynumber', 'Missing property.'); +validate_ok $data1, $schema2; +validate_ok $data2, $schema2; + +done_testing; diff --git a/t/jv-string.t b/t/jv-string.t new file mode 100644 index 0000000..ae4c0e4 --- /dev/null +++ b/t/jv-string.t @@ -0,0 +1,43 @@ +use lib '.'; +use t::Helper; +use Test::More; +use utf8; + +my $schema = { + type => 'object', + properties => { + nick => + {type => 'string', minLength => 3, maxLength => 10, pattern => qr{^\w+$}} + } +}; + +validate_ok {nick => 'batman'}, $schema; +validate_ok {nick => 1000}, $schema, + E('/nick', 'Expected string - got number.'); +validate_ok {nick => '1000'}, $schema; +validate_ok {nick => 'aa'}, $schema, E('/nick', 'String is too short: 2/3.'); +validate_ok {nick => 'a' x 11}, $schema, + E('/nick', 'String is too long: 11/10.'); +like +join('', t::Helper->validator->validate({nick => '[nick]'})), + qr{/nick: String does not match}, 'String does not match'; + +delete $schema->{properties}{nick}{pattern}; +validate_ok {nick => 'Déjà vu'}, $schema; + +t::Helper->validator->coerce(1); +validate_ok {nick => 1000}, $schema; + +# https://github.com/mojolicious/json-validator/issues/134 +validate_ok( + {credit_card_number => '5252525252525252'}, + { + type => "object", + required => ["credit_card_number"], + properties => { + credit_card_number => + {type => "string", minLength => 15, maxLength => 16}, + } + } +); + +done_testing; diff --git a/t/load-data.t b/t/load-data.t new file mode 100644 index 0000000..ea07e66 --- /dev/null +++ b/t/load-data.t @@ -0,0 +1,41 @@ + use Mojo::Base -strict; + use Test::More; + use JSON::Validator; + + my $validator = JSON::Validator->new; + my @errors = $validator->schema('data://main/spec.json') + ->validate({firstName => 'yikes!'}); + + is int(@errors), 1, 'one error'; + is $errors[0]->path, '/lastName', 'lastName'; + is $errors[0]->message, 'Missing property.', 'required'; + is_deeply $errors[0]->TO_JSON, + {path => '/lastName', message => 'Missing property.'}, 'TO_JSON'; + + use Mojo::File 'path'; + push @INC, path(path(__FILE__)->dirname, 'stack')->to_string; + require Some::Module; + + eval { Some->validate_age1({age => 1}) }; + like $@, qr{age1\.json}, 'could not find age1.json'; + + ok !Some->validate_age0({age => 1}), 'validate_age0'; + ok !Some::Module->validate_age0({age => 1}), 'validate_age0'; + ok !Some::Module->validate_age1({age => 1}), 'validate_age1'; + + done_testing; + +__DATA__ +@@ spec.json + +{ + "title": "Example Schema", + "type": "object", + "required": ["firstName", "lastName"], + "properties": { + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "age": { "type": "integer", "minimum": 0, "description": "Age in years" } + } +} + diff --git a/t/load-file.t b/t/load-file.t new file mode 100644 index 0000000..c6eea15 --- /dev/null +++ b/t/load-file.t @@ -0,0 +1,13 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; + +my $spec = Mojo::File::path(qw(t spec person.json))->to_abs; +my $validator = JSON::Validator->new; + +note "file://$spec"; +ok eval { $validator->schema("file://$spec") }, 'loaded from file://'; +isa_ok($validator->schema, 'Mojo::JSON::Pointer'); +is $validator->schema->get('/title'), 'Example Schema', 'got example schema'; + +done_testing; diff --git a/t/load-http.t b/t/load-http.t new file mode 100644 index 0000000..e38f1a9 --- /dev/null +++ b/t/load-http.t @@ -0,0 +1,16 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; + +plan skip_all => 'TEST_ONLINE=1' unless $ENV{TEST_ONLINE}; + +my $validator = JSON::Validator->new; + +$validator->schema('http://swagger.io/v2/schema.json'); + +isa_ok($validator->schema, 'Mojo::JSON::Pointer'); +like $validator->schema->get('/title'), qr{swagger}i, 'got swagger spec'; +ok $validator->schema->get('/patternProperties/^x-/description'), + 'resolved vendorExtension $ref'; + +done_testing; diff --git a/t/load-json.t b/t/load-json.t new file mode 100644 index 0000000..307b0b3 --- /dev/null +++ b/t/load-json.t @@ -0,0 +1,31 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; +use Mojo::File 'path'; + +my $file = path(path(__FILE__)->dirname, 'spec', 'person.json'); +my $validator = JSON::Validator->new->schema($file); +my @errors = $validator->validate({firstName => 'yikes!'}); + +is int(@errors), 1, 'one error'; +is $errors[0]->path, '/lastName', 'lastName'; +is $errors[0]->message, 'Missing property.', 'required'; +is_deeply $errors[0]->TO_JSON, + {path => '/lastName', message => 'Missing property.'}, 'TO_JSON'; + +my $spec = path($file)->slurp; +$spec =~ s!"#!"person.json#! or die "Invalid spec: $spec"; +path("$file.2")->spurt($spec); +ok eval { JSON::Validator->new->schema("$file.2") }, + 'test issue #1 where $ref could not point to a file' + or diag $@; +unlink "$file.2"; + +# load from cache +is( + eval { JSON::Validator->new->schema('http://swagger.io/v2/schema.json'); 42 }, + 42, + 'loaded from cache' +) or diag $@; + +done_testing; diff --git a/t/load-yaml.t b/t/load-yaml.t new file mode 100644 index 0000000..aa7a32d --- /dev/null +++ b/t/load-yaml.t @@ -0,0 +1,37 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; + +plan skip_all => $@ unless eval { JSON::Validator::_yaml_module() }; + +my $validator = JSON::Validator->new; +my @errors = $validator->schema('data://Some::Module/s_pec-/-ficaTion') + ->validate({firstName => 'yikes!'}); + +is int(@errors), 1, 'one error'; +is $errors[0]->path, '/lastName', 'lastName'; +is $errors[0]->message, 'Missing property.', 'required'; +is_deeply $errors[0]->TO_JSON, + {path => '/lastName', message => 'Missing property.'}, 'TO_JSON'; + +done_testing; + +package Some::Module; +__DATA__ +@@ s_pec-/-ficaTion + +--- +title: Example Schema +type: object +required: + - firstName + - lastName +properties: + firstName: + type: string + lastName: + type: string + age: + type: integer + minimum: 0 + description: Age in years diff --git a/t/random-errors.t b/t/random-errors.t new file mode 100644 index 0000000..3671337 --- /dev/null +++ b/t/random-errors.t @@ -0,0 +1,47 @@ +use Mojo::Base -strict; +use JSON::Validator; +use Test::More; + +# Note that you might have to run this test many times before it fails: +# while TEST_RANDOM_ITERATIONS=10000 prove -l t/random-errors.t; do echo "---"; done +plan skip_all => 'TEST_RANDOM_ITERATIONS=10000' + unless my $iterations = $ENV{TEST_RANDOM_ITERATIONS}; + +my $validator = JSON::Validator->new->schema({ + items => { + properties => { + prop1 => {type => [qw(string null)]}, + prop2 => {type => [qw(string null)], format => 'ipv4'}, + prop3 => {type => [qw(string null)], format => 'ipv4'}, + prop4 => {type => 'string', enum => [qw(foo bar)]}, + prop5 => {type => [qw(string null)]}, + prop6 => {type => 'string'}, + prop7 => {type => 'string', enum => [qw(foo bar)]}, + prop8 => {type => [qw(string null)], format => 'ipv4'}, + prop9 => {type => [qw(string null)]}, + }, + type => 'object', + }, + type => 'array', +}); + +my @errors; +for (1 .. $iterations) { + push @errors, + $validator->validate([{ + prop1 => undef, + prop2 => undef, + prop3 => undef, + prop4 => 'foo', + prop5 => undef, + prop6 => 'foo', + prop7 => 'bar', + prop8 => undef, + prop9 => undef, + }]); + last if @errors; +} + +ok !@errors, 'no random error' or diag @errors; + +done_testing; diff --git a/t/relative-ref.t b/t/relative-ref.t new file mode 100644 index 0000000..26db732 --- /dev/null +++ b/t/relative-ref.t @@ -0,0 +1,16 @@ +use lib '.'; +use t::Helper; +use Mojo::File 'path'; + +my $file = path(path(__FILE__)->dirname, 'spec', 'with-relative-ref.json'); +my $validator = t::Helper->validator->cache_paths([]); +validate_ok {age => -1}, $file, E('/age', '-1 < minimum(0)'); + +use Mojolicious::Lite; +push @{app->static->paths}, path(__FILE__)->dirname; +$validator->ua(app->ua); +validate_ok {age => -2}, + app->ua->server->url->clone->path('/spec/with-relative-ref.json'), + E('/age', '-2 < minimum(0)'); + +done_testing; diff --git a/t/remotes/folder/folderInteger.json b/t/remotes/folder/folderInteger.json new file mode 100644 index 0000000..dbe5c75 --- /dev/null +++ b/t/remotes/folder/folderInteger.json @@ -0,0 +1,3 @@ +{ + "type": "integer" +} \ No newline at end of file diff --git a/t/remotes/integer.json b/t/remotes/integer.json new file mode 100644 index 0000000..dbe5c75 --- /dev/null +++ b/t/remotes/integer.json @@ -0,0 +1,3 @@ +{ + "type": "integer" +} \ No newline at end of file diff --git a/t/remotes/subSchemas.json b/t/remotes/subSchemas.json new file mode 100644 index 0000000..acafd08 --- /dev/null +++ b/t/remotes/subSchemas.json @@ -0,0 +1,8 @@ +{ + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/integer" + } +} diff --git a/t/schema-as-attr.t b/t/schema-as-attr.t new file mode 100644 index 0000000..9b5a544 --- /dev/null +++ b/t/schema-as-attr.t @@ -0,0 +1,14 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; + +my $json = JSON::Validator->new; +my $schema; + +no warnings 'redefine'; +*JSON::Validator::_validate = sub { $schema = shift->schema }; +$json->validate({data => 1}, {type => 'object'}); +is_deeply $schema->data, {type => 'object'}, 'schema() localized'; +is $json->schema, undef, 'schema() is not set'; + +done_testing; diff --git a/t/spec/bundlecheck.json b/t/spec/bundlecheck.json new file mode 100644 index 0000000..c677b87 --- /dev/null +++ b/t/spec/bundlecheck.json @@ -0,0 +1,61 @@ +{ + "basePath": "/api", + "consumes": [ + "application/json" + ], + "host": "localhost:3000", + "info": { + "license": { + "name": "Apache License, Version 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "title": "t-app", + "version": "0.1.0" + }, + "paths": { + "/t": { + "get": { + "operationId": "listT", + "responses": { + "200": { + "description": "Self sufficient", + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "default": { + "$ref": "#/responses/error" + } + }, + "tags": [ + "t" + ], + "x-mojo-to": "Controller::OpenAPI::T#list" + } + } + }, + "produces": [ + "application/json" + ], + "responses": { + "error": { + "description": "Self sufficient", + "schema": { + "additionalProperties": false, + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ], + "type": "object" + } + } + }, + "swagger": "2.0" +} diff --git a/t/spec/missing-ref.json b/t/spec/missing-ref.json new file mode 100644 index 0000000..3817256 --- /dev/null +++ b/t/spec/missing-ref.json @@ -0,0 +1,6 @@ +{ + "type": "object", + "properties": { + "missing": { "$ref": "../definitions/missing.json#" } + } +} diff --git a/t/spec/person.json b/t/spec/person.json new file mode 100644 index 0000000..db757ae --- /dev/null +++ b/t/spec/person.json @@ -0,0 +1,13 @@ +{ + "title": "Example Schema", + "type": "object", + "required": ["firstName", "lastName"], + "properties": { + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "age": { "$ref": "#/definitions/age" } + }, + "definitions": { + "age": { "type": "integer", "minimum": 0, "description": "Age in years" } + } +} diff --git a/t/spec/petstore.json b/t/spec/petstore.json new file mode 100644 index 0000000..fd81803 --- /dev/null +++ b/t/spec/petstore.json @@ -0,0 +1,52 @@ +{ + "parameters": { + "limit": { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "type": "integer", + "format": "int32" + } + }, + "paths": { + "/pets": { + "get": { + "tags": [ "pets" ], + "parameters": [ + { "$ref": "#/parameters/limit" } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "type": "array", + "items": { "$ref": "#/definitions/Pet" } + } + }, + "default": { + "description": "unexpected error", + "schema": { "$ref": "#/definitions/Error" } + } + } + } + } + }, + "definitions": { + "Pet": { + "required": [ "id", "name" ], + "properties": { + "id": { "type": "integer", "format": "int64" }, + "name": { "type": "string" }, + "tag": { "type": "string" } + } + }, + "Error": { + "required": [ "code", "message" ], + "properties": { + "code": { "type": "integer", "format": "int32" }, + "message": { "type": "string" } + } + } + } +} diff --git a/t/spec/space bundle.json b/t/spec/space bundle.json new file mode 100644 index 0000000..f6c1fd4 --- /dev/null +++ b/t/spec/space bundle.json @@ -0,0 +1,6 @@ +{ + "type": "object", + "properties": { + "age": { "$ref": "../definitions/space age.json#" } + } +} diff --git a/t/spec/with-deep-mixed-ref.json b/t/spec/with-deep-mixed-ref.json new file mode 100644 index 0000000..dcc3f23 --- /dev/null +++ b/t/spec/with-deep-mixed-ref.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "age": { "$ref": "../definitions/age.json#" }, + "weight": { "$ref": "../definitions/weight.json" }, + "height": { "$ref": "#/definitions/height" } + }, + "definitions": { + "height": { "type": "integer", "minimum": 5 } + } +} diff --git a/t/spec/with-relative-ref.json b/t/spec/with-relative-ref.json new file mode 100644 index 0000000..dcc6b40 --- /dev/null +++ b/t/spec/with-relative-ref.json @@ -0,0 +1,6 @@ +{ + "type": "object", + "properties": { + "age": { "$ref": "../definitions/age.json#" } + } +} diff --git a/t/stack/Some.pm b/t/stack/Some.pm new file mode 100644 index 0000000..e29e4e0 --- /dev/null +++ b/t/stack/Some.pm @@ -0,0 +1,17 @@ +package Some; +use Mojo::Base -base; + +sub j { JSON::Validator->new } +sub validate_age0 { shift->j->schema('data:///age0.json')->validate(shift) } +sub validate_age1 { shift->j->schema('data:///age1.json')->validate(shift) } + +1; +__DATA__ +@@ age0.json +{ + "title": "Some module", + "type": "object", + "properties": { + "age": { "type": "integer", "minimum": 0, "description": "Age in years" } + } +} diff --git a/t/stack/Some/Module.pm b/t/stack/Some/Module.pm new file mode 100644 index 0000000..ac31921 --- /dev/null +++ b/t/stack/Some/Module.pm @@ -0,0 +1,16 @@ +package Some::Module; +use Mojo::Base 'Some'; + +sub validate_age0 { shift->j->schema('data:///age0.json')->validate(shift) } +sub validate_age1 { shift->j->schema('data://Some::Module/age1.json')->validate(shift) } + +1; +__DATA__ +@@ age1.json +{ + "title": "Some module", + "type": "object", + "properties": { + "age": { "type": "integer", "minimum": 1, "description": "Age in years" } + } +} diff --git a/t/to-json.t b/t/to-json.t new file mode 100644 index 0000000..515d6d2 --- /dev/null +++ b/t/to-json.t @@ -0,0 +1,57 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator 'validate_json'; + +my @errors + = validate_json( + bless({path => '', message => 'yikes'}, 'JSON::Validator::Error'), + 'data://main/error_object.json'); +ok !@errors, 'TO_JSON on objects' or diag join ', ', @errors; + +my $input = { + errors => [ + JSON::Validator::Error->new('/', 'foo'), + JSON::Validator::Error->new('/', 'bar') + ], + valid => Mojo::JSON->false, +}; +@errors = validate_json $input, 'data://main/error_array.json'; +ok !@errors, 'TO_JSON on objects inside arrays' or diag join ', ', @errors; +is_deeply $input, + { + errors => [ + JSON::Validator::Error->new('/', 'foo'), + JSON::Validator::Error->new('/', 'bar') + ], + valid => Mojo::JSON->false, + }, + 'input objects are not changed'; + +done_testing; +__DATA__ +@@ error_object.json +{ + "type": "object", + "properties": { "message": { "type": "string" } }, + "required": ["message"] +} + +@@ error_array.json +{ + "type": "object", + "required": [ "errors" ], + "properties": { + "valid": { "type": "boolean" }, + "errors": { + "type": "array", + "items": { + "type": "object", + "required": [ "message" ], + "properaties": { + "message": { "type": "string" }, + "path": { "type": "string" } + } + } + } + } +} diff --git a/t/validate-draft07.t b/t/validate-draft07.t new file mode 100644 index 0000000..20a4466 --- /dev/null +++ b/t/validate-draft07.t @@ -0,0 +1,16 @@ +use Mojo::Base -strict; +use Mojo::File 'path'; +use Mojo::JSON 'decode_json'; +use Test::More; + +use JSON::Validator 'validate_json'; + +my $draft07 + = path(qw(lib JSON Validator cache 4a31fe43be9e23ca9eb8d9e9faba8892)); +plan skip_all => "Cannot open $draft07" unless -r $draft07; + +my $schema = decode_json($draft07->slurp); +my @errors = validate_json $schema, $schema; +ok !@errors, "validated draft07" or map { diag $_ } @errors; + +done_testing; diff --git a/t/validate-id.t b/t/validate-id.t new file mode 100644 index 0000000..2b50d9f --- /dev/null +++ b/t/validate-id.t @@ -0,0 +1,14 @@ + use lib '.'; + use t::Helper; + + validate_ok {id => 1}, {type => 'object'}; + + validate_ok {id => 1, message => 'cannot exclude "id" #111'}, + { + type => 'object', + additionalProperties => 0, + properties => {message => {type => "string"}} + }, + E('/', 'Properties not allowed: id.'); + + done_testing; diff --git a/t/validate-json.t b/t/validate-json.t new file mode 100644 index 0000000..8085355 --- /dev/null +++ b/t/validate-json.t @@ -0,0 +1,27 @@ +use Mojo::Base -strict; +use Test::Mojo; +use Test::More; +use JSON::Validator 'validate_json'; + +{ + use Mojolicious::Lite; + post '/' => sub { + my $c = shift; + my @errors = validate_json $c->req->json, 'data://main/spec.json'; + $c->render(status => @errors ? 400 : 200, text => "@errors"); + }; +} + +my $t = Test::Mojo->new; + +$t->post_ok('/', json => {})->status_is(400)->content_like(qr{/name}); +$t->post_ok('/', json => {name => "foo"})->status_is(200); + +done_testing; +__DATA__ +@@ spec.json +{ + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] +} diff --git a/t/validate-recursive.t b/t/validate-recursive.t new file mode 100644 index 0000000..0a5335d --- /dev/null +++ b/t/validate-recursive.t @@ -0,0 +1,56 @@ +use Mojo::Base -strict; +use Test::Mojo; +use Test::More; +use JSON::Validator 'validate_json'; + +{ + use Mojolicious::Lite; + post '/' => sub { + my $c = shift; + my @errors = validate_json $c->req->json, 'data://main/spec.json'; + $c->render(status => @errors ? 400 : 200, json => \@errors); + }; +} + +my $t = Test::Mojo->new; + +$t->post_ok('/', json => {})->status_is(400)->content_like(qr{/person}); +$t->post_ok('/', json => {person => {name => 'superwoman'}})->status_is(200); +$t->post_ok('/', + json => {person => {name => 'superwoman', children => [{name => 'batboy'}]}}) + ->status_is(200); +$t->post_ok('/', json => {person => {name => 'superwoman', children => [{}]}}) + ->status_is(400)->json_is('/0/path' => '/person/children/0/name'); + +done_testing; + +__DATA__ +@@ spec.json +{ + "type": "object", + "properties": { + "person": { + "$ref": "#/definitions/person" + } + }, + "required": [ + "person" + ], + "definitions": { + "person": { + "type": "object", + "required": [ "name" ], + "properties": { + "name": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/person" + } + } + } + } + } +} diff --git a/t/validate-schema.t b/t/validate-schema.t new file mode 100644 index 0000000..aad21e1 --- /dev/null +++ b/t/validate-schema.t @@ -0,0 +1,40 @@ +use Mojo::Base -strict; +use Test::More; +use JSON::Validator; +use lib '.'; +use t::Helper (); + +my $should_fail = JSON::Validator->new->schema('data://main/invalid.json'); +my $json_schema + = JSON::Validator->new->schema('http://json-schema.org/draft-04/schema#'); +my @errors; + +# The schema is invalid... +@errors = $json_schema->validate($should_fail->schema->data); +is $errors[0], '/properties/should_fail: Expected object - got array.', + 'invalid property element'; + +# ...but can still be used to validate data. +@errors = $should_fail->validate({foo => 123}); +is int(@errors), 0, 'data is valid'; + +# Can also use load_and_validate_schema() to do the same as above +eval { + JSON::Validator->new->load_and_validate_schema('data://main/invalid.json'); +}; +like $@, qr{Expected object - got array}, 'invalid schema'; + +done_testing; + +__DATA__ +@@ invalid.json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Example Schema That Should Fail To Load", + "description": "There is an array as the value of an object property, which should not be allowed.", + "type": "object", + "properties": { + "foo": { "type": "integer" }, + "should_fail": [] + } +}