From 75038f879bbacd6c9fc059cf919c8c982f85a065 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 21 Jul 2020 18:23:08 +0100 Subject: [PATCH 001/322] format with prettier - no other changes --- .github/ISSUE_TEMPLATE.md | 11 - .github/ISSUE_TEMPLATE/bug-or-error-report.md | 18 +- .github/ISSUE_TEMPLATE/change.md | 7 +- .github/ISSUE_TEMPLATE/compatibility.md | 7 +- .github/ISSUE_TEMPLATE/installation.md | 7 +- .github/ISSUE_TEMPLATE/typescript.md | 12 +- .github/PULL_REQUEST_TEMPLATE.md | 2 - .prettierignore | 7 + .tonic_example.js | 28 +- CODE_OF_CONDUCT.md | 26 +- COERCION.md | 33 +- CONTRIBUTING.md | 14 +- CUSTOM.md | 269 ++- FAQ.md | 28 +- KEYWORDS.md | 435 ++-- README.md | 563 +++--- bower.json | 22 +- karma.conf.js | 42 +- karma.sauce.js | 137 +- lib/ajv.d.ts | 550 ++--- lib/ajv.js | 544 +++-- lib/cache.js | 29 +- lib/compile/async.js | 95 +- lib/compile/equal.js | 4 +- lib/compile/error_classes.js | 34 +- lib/compile/formats.js | 139 +- lib/compile/index.js | 366 ++-- lib/compile/resolve.js | 304 +-- lib/compile/rules.js | 131 +- lib/compile/schema_obj.js | 8 +- lib/compile/ucs2length.js | 24 +- lib/compile/util.js | 301 +-- lib/data.js | 74 +- lib/definition_schema.js | 48 +- lib/dotjs/index.js | 60 +- lib/keyword.js | 113 +- lib/refs/data.json | 27 +- lib/refs/json-schema-draft-04.json | 278 ++- lib/refs/json-schema-draft-06.json | 289 ++- lib/refs/json-schema-draft-07.json | 317 ++- lib/refs/json-schema-secure.json | 10 +- package.json | 15 + scripts/bundle.js | 109 +- scripts/compile-dots.js | 112 +- spec/after_test.js | 22 +- spec/ajv-async.js | 5 +- spec/ajv.js | 5 +- spec/ajv.spec.js | 1062 +++++----- spec/ajv_async_instances.js | 46 +- spec/ajv_instances.js | 36 +- spec/ajv_options.js | 28 +- spec/async.spec.js | 674 +++---- spec/async/boolean.json | 16 +- spec/async/compound.json | 8 +- spec/async/format.json | 16 +- spec/async/items.json | 14 +- spec/async/keyword.json | 44 +- spec/async/properties.json | 10 +- spec/async_schemas.spec.js | 135 +- spec/async_validate.spec.js | 600 +++--- spec/boolean.spec.js | 666 +++--- spec/browser_test_suite.js | 14 +- spec/chai.js | 4 +- spec/coercion.spec.js | 718 ++++--- spec/custom.spec.js | 1796 +++++++++-------- spec/custom_rules/index.js | 14 +- spec/errors.spec.js | 1652 +++++++++------ spec/extras.spec.js | 37 +- spec/extras/$data/absolute_ref.json | 61 +- spec/extras/$data/const.json | 54 +- spec/extras/$data/enum.json | 4 +- spec/extras/$data/exclusiveMaximum.json | 24 +- spec/extras/$data/exclusiveMinimum.json | 22 +- spec/extras/$data/format.json | 2 +- spec/extras/$data/maxItems.json | 2 +- spec/extras/$data/maxLength.json | 2 +- spec/extras/$data/maxProperties.json | 2 +- spec/extras/$data/maximum.json | 10 +- spec/extras/$data/minItems.json | 2 +- spec/extras/$data/minLength.json | 2 +- spec/extras/$data/minProperties.json | 2 +- spec/extras/$data/minimum.json | 10 +- spec/extras/$data/multipleOf.json | 26 +- spec/extras/$data/pattern.json | 8 +- spec/extras/$data/required.json | 2 +- spec/extras/$data/uniqueItems.json | 2 +- spec/extras/const.json | 14 +- spec/extras/contains.json | 4 +- spec/extras/propertyNames.json | 2 +- ...1_addKeyword_and_schema_without_id.spec.js | 29 +- ...1_allErrors_custom_keyword_skipped.spec.js | 73 +- spec/issues/182_nan_validation.spec.js | 37 +- .../204_options_schemas_data_together.spec.js | 33 +- spec/issues/210_mutual_recur_frags.spec.js | 121 +- .../240_mutual_recur_frags_common_ref.spec.js | 252 +-- .../259_validate_meta_against_itself.spec.js | 21 +- .../273_error_schemaPath_refd_schema.spec.js | 41 +- .../342_uniqueItems_non-json_objects.spec.js | 57 +- .../485_type_validation_priority.spec.js | 43 +- spec/issues/50_refs_with_definitions.spec.js | 57 +- .../521_wrong_warning_id_property.spec.js | 39 +- .../533_missing_ref_error_when_ignore.spec.js | 47 +- spec/issues/617_full_format_leap_year.spec.js | 67 +- ...3_removeAdditional_to_remove_proto.spec.js | 59 +- .../768_passContext_recursive_ref.spec.js | 196 +- spec/issues/8_shared_refs.spec.js | 49 +- ...5_removeAdditional_custom_keywords.spec.js | 67 +- spec/json-schema.spec.js | 113 +- spec/options/comment.spec.js | 129 +- spec/options/meta_validateSchema.spec.js | 146 +- spec/options/nullable.spec.js | 121 +- spec/options/options_add_schemas.spec.js | 254 +-- spec/options/options_code.spec.js | 168 +- spec/options/options_refs.spec.js | 247 ++- spec/options/options_reporting.spec.js | 211 +- spec/options/options_validation.spec.js | 197 +- spec/options/ownProperties.spec.js | 251 ++- spec/options/removeAdditional.spec.js | 183 +- spec/options/schemaId.spec.js | 109 +- spec/options/strictDefaults.spec.js | 225 ++- spec/options/strictKeywords.spec.js | 113 +- spec/options/strictNumbers.spec.js | 92 +- spec/options/unknownFormats.spec.js | 147 +- spec/options/useDefaults.spec.js | 316 +-- spec/promise.js | 12 +- spec/remotes/buu.json | 2 +- spec/remotes/first.json | 4 +- spec/remotes/foo.json | 2 +- spec/remotes/hyper-schema.json | 120 +- spec/remotes/name.json | 5 +- spec/remotes/node.json | 4 +- spec/remotes/scope_change.json | 4 +- spec/remotes/second.json | 10 +- spec/remotes/tree.json | 4 +- spec/resolve.spec.js | 489 +++-- spec/schema-tests.spec.js | 69 +- spec/security.spec.js | 37 +- spec/security/array.json | 16 +- spec/security/object.json | 4 +- spec/security/string.json | 8 +- .../12_restoring_root_after_resolve.json | 18 +- .../13_root_ref_in_ref_in_remote_ref.json | 2 +- .../issues/14_ref_in_remote_ref_with_id.json | 4 +- .../issues/170_ref_and_id_in_sibling.json | 62 +- .../issues/17_escaping_pattern_property.json | 10 +- spec/tests/issues/1_ids_in_refs.json | 12 +- .../issues/20_failing_to_parse_schema.json | 58 +- .../issues/226_json_with_control_chars.json | 16 +- .../issues/27_1_recursive_raml_schema.json | 21 +- spec/tests/issues/27_recursive_reference.json | 32 +- .../issues/28_escaping_pattern_error.json | 2 +- spec/tests/issues/2_root_ref_in_ref.json | 32 +- spec/tests/issues/311_quotes_in_refs.json | 4 +- .../issues/413_dependencies_with_quote.json | 2 +- ...ty_array_with_ref_in_another_property.json | 6 +- .../issues/5_adding_dependency_after.json | 4 +- spec/tests/issues/5_recursive_references.json | 24 +- .../issues/62_resolution_scope_change.json | 30 +- .../issues/63_id_property_not_in_schema.json | 10 +- ...70_1_recursive_hash_ref_in_remote_ref.json | 52 +- spec/tests/issues/70_swagger_schema.json | 233 +-- spec/tests/issues/87_$_property.json | 4 +- spec/tests/issues/94_dependencies_fail.json | 20 +- spec/tests/rules/allOf.json | 19 +- spec/tests/rules/anyOf.json | 5 +- spec/tests/rules/dependencies.json | 2 +- spec/tests/rules/if.json | 20 +- spec/tests/rules/items.json | 58 +- spec/tests/rules/oneOf.json | 104 +- spec/tests/rules/required.json | 2 +- spec/tests/rules/type.json | 8 +- spec/tests/rules/uniqueItems.json | 8 +- spec/tests/schemas/advanced.json | 460 ++--- spec/tests/schemas/basic.json | 270 +-- spec/tests/schemas/complex.json | 72 +- spec/tests/schemas/complex2.json | 71 +- spec/tests/schemas/complex3.json | 72 +- spec/tests/schemas/cosmicrealms.json | 83 +- spec/tests/schemas/medium.json | 28 +- spec/typescript/index.ts | 66 +- 180 files changed, 10346 insertions(+), 10288 deletions(-) create mode 100644 .prettierignore diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 5585010365..46d6f970fe 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -12,18 +12,13 @@ This template is for bug or error reports. For other issues please use: **What version of Ajv are you using? Does the issue happen if you use the latest version?** - - **Ajv options object** ```javascript - - ``` - **JSON Schema** @@ -33,7 +28,6 @@ This template is for bug or error reports. For other issues please use: ``` - **Sample data** @@ -43,7 +37,6 @@ This template is for bug or error reports. For other issues please use: ``` - **Your code** ```javascript - - ``` - **Validation result, data AFTER validation, error messages** ``` @@ -72,5 +62,4 @@ Thank you! **What results did you expect?** - **Are you going to resolve the issue?** diff --git a/.github/ISSUE_TEMPLATE/bug-or-error-report.md b/.github/ISSUE_TEMPLATE/bug-or-error-report.md index c590321ca9..9f4aeed90d 100644 --- a/.github/ISSUE_TEMPLATE/bug-or-error-report.md +++ b/.github/ISSUE_TEMPLATE/bug-or-error-report.md @@ -1,10 +1,9 @@ --- name: Bug or error report about: Please use for issues related to incorrect validation behaviour -title: '' -labels: 'bug report' -assignees: '' - +title: "" +labels: "bug report" +assignees: "" --- ```javascript - - ``` - **JSON Schema** @@ -38,7 +32,6 @@ For other issues please see https://github.com/ajv-validator/ajv/blob/master/CON ``` - **Sample data** @@ -48,7 +41,6 @@ For other issues please see https://github.com/ajv-validator/ajv/blob/master/CON ``` - **Your code** ```javascript - - ``` - **Validation result, data AFTER validation, error messages** ``` @@ -77,5 +66,4 @@ Thank you! **What results did you expect?** - **Are you going to resolve the issue?** diff --git a/.github/ISSUE_TEMPLATE/change.md b/.github/ISSUE_TEMPLATE/change.md index 0c8035d1a6..13eb1ed0fd 100644 --- a/.github/ISSUE_TEMPLATE/change.md +++ b/.github/ISSUE_TEMPLATE/change.md @@ -1,10 +1,9 @@ --- name: Feature or change proposal about: For proposals of new features, options or some other improvements -title: '' -labels: 'enhancement' -assignees: '' - +title: "" +labels: "enhancement" +assignees: "" --- ```typescript - - ``` - **Typescript compiler error messages** ``` @@ -38,5 +33,4 @@ Please make it as small as posssible to reproduce the issue **Describe the change that should be made to address the issue?** - **Are you going to resolve the issue?** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7abf655f1f..3595869ae2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,8 +11,6 @@ Please answer the questions below. **What issue does this pull request resolve?** - **What changes did you make?** - **Is there anything that requires more attention while reviewing?** diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..ca92f675b1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +spec/JSON-Schema-Test-Suite +.browser +coverage +dist +node +.nyc_output +lib/dotjs diff --git a/.tonic_example.js b/.tonic_example.js index aa11812d87..2b0d6683ee 100644 --- a/.tonic_example.js +++ b/.tonic_example.js @@ -1,20 +1,20 @@ -var Ajv = require('ajv'); -var ajv = new Ajv({allErrors: true}); +var Ajv = require("ajv") +var ajv = new Ajv({allErrors: true}) var schema = { - "properties": { - "foo": { "type": "string" }, - "bar": { "type": "number", "maximum": 3 } - } -}; + properties: { + foo: {type: "string"}, + bar: {type: "number", maximum: 3}, + }, +} -var validate = ajv.compile(schema); +var validate = ajv.compile(schema) -test({"foo": "abc", "bar": 2}); -test({"foo": 2, "bar": 4}); +test({foo: "abc", bar: 2}) +test({foo: 2, bar: 4}) function test(data) { - var valid = validate(data); - if (valid) console.log('Valid!'); - else console.log('Invalid: ' + ajv.errorsText(validate.errors)); -} \ No newline at end of file + var valid = validate(data) + if (valid) console.log("Valid!") + else console.log("Invalid: " + ajv.errorsText(validate.errors)) +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 410cda6416..b8026a2cf8 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/COERCION.md b/COERCION.md index 6a0a41a68c..c2b159f391 100644 --- a/COERCION.md +++ b/COERCION.md @@ -3,6 +3,7 @@ To enable type coercion pass option `coerceTypes` to Ajv with `true` or `array` (it is `false` by default). See [example](https://github.com/ajv-validator/ajv#coercing-data-types). The coercion rules are different from JavaScript: + - to validate user input as expected - to have the coercion reversible - to correctly validate cases where different types are required in subschemas (e.g., in `anyOf`). @@ -11,64 +12,58 @@ Type coercion only happens if there is `type` keyword and if without coercion th If there are multiple types allowed in `type` keyword the coercion will only happen if none of the types match the data and some of the scalar types are present (coercion to/from `object`/`array` is not possible). In this case the validating function will try coercing the data to each type in order until some of them succeeds. -Application of these rules can have some unexpected consequences. Ajv may coerce the same value multiple times (this is why coercion reversibility is required) as needed at different points in the schema. This is particularly evident when using `oneOf`, which must test all of the subschemas. Ajv will coerce the type for each subschema, possibly resulting in unexpected failure if it can coerce to match more than one of the subschemas. Even if it succeeds, Ajv will not backtrack, so you'll get the type of the final coercion even if that's not the one that allowed the data to pass validation. If possible, structure your schema with `anyOf`, which won't validate subsequent subschemas as soon as it encounters one subschema that matches. +Application of these rules can have some unexpected consequences. Ajv may coerce the same value multiple times (this is why coercion reversibility is required) as needed at different points in the schema. This is particularly evident when using `oneOf`, which must test all of the subschemas. Ajv will coerce the type for each subschema, possibly resulting in unexpected failure if it can coerce to match more than one of the subschemas. Even if it succeeds, Ajv will not backtrack, so you'll get the type of the final coercion even if that's not the one that allowed the data to pass validation. If possible, structure your schema with `anyOf`, which won't validate subsequent subschemas as soon as it encounters one subschema that matches. Possible type coercions: -|from type →
to type ↓|string|number|boolean|null|array*| -|---|:-:|:-:|:-:|:-:|:-:| -|string |-|`x`→`""+x`|`false`→`"false"`
`true`→`"true"`|`null`→`""`|`[x]`→`x`| -|number /
integer|Valid number /
integer: `x`→`+x`
|-|`false`→`0`
`true`→`1`|`null`→`0`|`[x]`→`x`| -|boolean |`"false"`→`false`
`"true"`→`true`
`"abc"`⇸
`""`⇸|`0`→`false`
`1`→`true`
`x`⇸|-|`null`→`false`|`[false]`→`false`
`[true]`→`true`| -|null |`""`→`null`
`"null"`⇸
`"abc"`⇸|`0`→`null`
`x`⇸|`false`→`null`
`true`⇸|-|`[null]`→`null`| -|array* |`x`→`[x]`|`x`→`[x]`|`false`→`[false]`
`true`→`[true]`|`null`→`[null]`|-| +| from type →
to type ↓ | string | number | boolean | null | array\* | +| ------------------------------------------------------ | :-----------------------------------------------------------------------------: | :-----------------------------------------------: | :--------------------------------------------: | :------------------: | :--------------------------------------------: | +| string | - | `x`→`""+x` | `false`→`"false"`
`true`→`"true"` | `null`→`""` | `[x]`→`x` | +| number /
integer | Valid number /
integer: `x`→`+x`
| - | `false`→`0`
`true`→`1` | `null`→`0` | `[x]`→`x` | +| boolean | `"false"`→`false`
`"true"`→`true`
`"abc"`⇸
`""`⇸ | `0`→`false`
`1`→`true`
`x`⇸ | - | `null`→`false` | `[false]`→`false`
`[true]`→`true` | +| null | `""`→`null`
`"null"`⇸
`"abc"`⇸ | `0`→`null`
`x`⇸ | `false`→`null`
`true`⇸ | - | `[null]`→`null` | +| array\* | `x`→`[x]` | `x`→`[x]` | `false`→`[false]`
`true`→`[true]` | `null`→`[null]` | - | \* Requires option `{coerceTypes: 'array'}` - ## Coercion from string values #### To number type Coercion to `number` is possible if the string is a valid number, `+data` is used. - #### To integer type Coercion to `integer` is possible if the string is a valid number without fractional part (`data % 1 === 0`). - #### To boolean type Unlike JavaScript, only these strings can be coerced to `boolean`: + - `"true"` -> `true` - `"false"` -> `false` - #### To null type Empty string is coerced to `null`, other strings can't be coerced. - ## Coercion from number values #### To string type Always possible, `'' + data` is used - #### To boolean type Unlike JavaScript, only these numbers can be coerced to `boolean`: + - `1` -> `true` - `0` -> `false` - #### To null type `0` coerces to `null`, other numbers can't be coerced. - ## Coercion from boolean values #### To string type @@ -76,35 +71,29 @@ Unlike JavaScript, only these numbers can be coerced to `boolean`: - `true` -> `"true"` - `false` -> `"false"` - #### To number/integer types - `true` -> `1` - `false` -> `0` - #### To null type `false` coerces to `null`, `true` can't be coerced. - ## Coercion from null #### To string type `null` coerses to the empty string. - #### To number/integer types `null` coerces to `0` - #### To boolean type `null` coerces to `false` - ## Coercion to and from array These coercions require that the option `coerceTypes` is `"array"`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f2f8aaede..00a6afcd1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,19 +16,16 @@ Thank you for your help making Ajv better! Every contribution is appreciated. If - [Pull requests](#pull-requests) - [Contributions license](#contributions-license) - ## Documentation Ajv has a lot of features and maintaining documentation takes time. I appreciate the time you spend correcting or clarifying the documentation. - ## Issues Before submitting the issue please search the existing issues and also review [Frequently Asked Questions](https://github.com/ajv-validator/ajv/blob/master/FAQ.md). I would really appreciate the time you spend providing all the information and reducing both your schema and data to the smallest possible size when they still have the issue. Simplifying the issue also makes it more valuable for other users (in cases it turns out to be an incorrect usage rather than a bug). - #### Bug reports Please make sure to include the following information in the issue: @@ -44,7 +41,6 @@ Please include the link to the working code sample at Runkit.com (please clone h [Create bug report](https://github.com/ajv-validator/ajv/issues/new?template=bug-or-error-report.md). - #### Security vulnerabilities To report a security vulnerability, please use the @@ -53,7 +49,6 @@ Tidelift will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues. - #### Change proposals [Create a proposal](https://github.com/ajv-validator/ajv/issues/new?template=change.md) for a new feature, option or some other improvement. @@ -73,7 +68,6 @@ If you’re requesting a change, it would be helpful to include this as well: Please include as much details as possible. - #### Browser and compatibility issues [Create an issue](https://github.com/ajv-validator/ajv/issues/new?template=compatibility.md) to report a compatibility problem that only happens in a particular environment (when your code works correctly in node.js v8+ in linux systems but fails in some other environment). @@ -87,12 +81,12 @@ Please include this information: 5. Results in node.js v8+. 6. Results and error messages in your platform. - #### Installation and dependency issues [Create an issue](https://github.com/ajv-validator/ajv/issues/new?template=installation.md) to report problems that happen during Ajv installation or when Ajv is missing some dependency. Before submitting the issue, please try the following: + - use the latest stable Node.js and `npm` - use `yarn` instead of `npm` - the issue can be related to https://github.com/npm/npm/issues/19877 - remove `node_modules` and `package-lock.json` and run install again @@ -106,26 +100,22 @@ If nothing helps, please submit: 5. Error messages 6. The output of `npm ls` - #### Using JSON Schema standard Ajv implements JSON Schema standard draft-04 and draft-06/07. If it is a general issue related to using the standard keywords included in JSON Schema or implementing some advanced validation logic please ask the question on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=jsonschema,ajv) (my account is [esp](https://stackoverflow.com/users/1816503/esp)) or submitting the question to [JSON-Schema.org](https://github.com/json-schema-org/json-schema-spec/issues/new). Please mention @epoberezkin. - #### Ajv usage questions The best place to ask a question about using Ajv is [Gitter chat](https://gitter.im/ajv-validator/ajv). If the question is advanced, it can be submitted to [Stack Overflow](http://stackoverflow.com/questions/ask?tags=jsonschema,ajv). - ## Code Thanks a lot for considering contributing to Ajv. Many very useful features were created by its users. - #### Development Running tests: @@ -149,7 +139,6 @@ All validation functions are generated using doT templates in [dot](https://gith `npm run watch` - automatically compiles templates when files in dot folder change - #### Pull requests To make accepting your changes faster please follow these steps: @@ -162,7 +151,6 @@ To make accepting your changes faster please follow these steps: 6. Please run the tests before committing your code. 7. If tests fail in Travis after you make a PR please investigate and fix the issue. - #### Contributions license When contributing the code you confirm that: diff --git a/CUSTOM.md b/CUSTOM.md index 68cac5ab34..49d5b7a907 100644 --- a/CUSTOM.md +++ b/CUSTOM.md @@ -13,10 +13,10 @@ - [Reporting errors in custom keywords](#reporting-errors-in-custom-keywords) - [Short-circuit validation](#short-circuit-validation) - ### Define keyword with validation function Validation function will be called during data validation and it will be passed: + - schema - data - parent schema @@ -34,50 +34,49 @@ This way to define keywords is useful for: - testing your keywords before converting them to compiled/inlined keywords - defining keywords that do not depend on the schema value (e.g., when the value is always `true`). In this case you can add option `schema: false` to the keyword definition and the schemas won't be passed to the validation function, it will only receive the same 4 parameters as compiled validation function (see the next section). - defining keywords where the schema is a value used in some expression. -- defining keywords that support [$data reference](https://github.com/ajv-validator/ajv#data-reference) - in this case validation function is required, either as the only option or in addition to compile, macro or inline function (see below). - -__Please note__: In cases when validation flow is different depending on the schema and you have to use `if`s, this way to define keywords will have worse performance than compiled keyword returning different validation functions depending on the schema. +- defining keywords that support [\$data reference](https://github.com/ajv-validator/ajv#data-reference) - in this case validation function is required, either as the only option or in addition to compile, macro or inline function (see below). +**Please note**: In cases when validation flow is different depending on the schema and you have to use `if`s, this way to define keywords will have worse performance than compiled keyword returning different validation functions depending on the schema. Example. `constant` keyword (a synonym for draft-06 keyword `const`, it is equivalent to `enum` keyword with one item): ```javascript -ajv.addKeyword('constant', { +ajv.addKeyword("constant", { validate: function (schema, data) { - return typeof schema == 'object' && schema !== null - ? deepEqual(schema, data) - : schema === data; + return typeof schema == "object" && schema !== null + ? deepEqual(schema, data) + : schema === data }, - errors: false -}); + errors: false, +}) var schema = { - "constant": 2 -}; -var validate = ajv.compile(schema); -console.log(validate(2)); // true -console.log(validate(3)); // false + constant: 2, +} +var validate = ajv.compile(schema) +console.log(validate(2)) // true +console.log(validate(3)) // false var schema = { - "constant": { - "foo": "bar" - } -}; -var validate = ajv.compile(schema); -console.log(validate({foo: 'bar'})); // true -console.log(validate({foo: 'baz'})); // false + constant: { + foo: "bar", + }, +} +var validate = ajv.compile(schema) +console.log(validate({foo: "bar"})) // true +console.log(validate({foo: "baz"})) // false ``` `const` keyword is already available in Ajv. -__Please note:__ If the keyword does not define custom errors (see [Reporting errors in custom keywords](#reporting-errors-in-custom-keywords)) pass `errors: false` in its definition; it will make generated code more efficient. +**Please note:** If the keyword does not define custom errors (see [Reporting errors in custom keywords](#reporting-errors-in-custom-keywords)) pass `errors: false` in its definition; it will make generated code more efficient. To add asynchronous keyword pass `async: true` in its definition. - ### Define keyword with "compilation" function Compilation function will be called during schema compilation. It will be passed schema, parent schema and [schema compilation context](#schema-compilation-context) and it should return a validation function. This validation function will be passed during validation: + - data - current data path - parent data object @@ -97,41 +96,41 @@ Custom keyword can also have an optional `dependencies` property in their defini Example. `range` and `exclusiveRange` keywords using compiled schema: ```javascript -ajv.addKeyword('range', { - type: 'number', +ajv.addKeyword("range", { + type: "number", compile: function (sch, parentSchema) { - var min = sch[0]; - var max = sch[1]; + var min = sch[0] + var max = sch[1] return parentSchema.exclusiveRange === true - ? function (data) { return data > min && data < max; } - : function (data) { return data >= min && data <= max; } + ? function (data) { + return data > min && data < max + } + : function (data) { + return data >= min && data <= max + } }, errors: false, metaSchema: { - type: 'array', - items: [ - { type: 'number' }, - { type: 'number' } - ], - additionalItems: false - } -}); + type: "array", + items: [{type: "number"}, {type: "number"}], + additionalItems: false, + }, +}) var schema = { - "range": [2, 4], - "exclusiveRange": true -}; -var validate = ajv.compile(schema); -console.log(validate(2.01)); // true -console.log(validate(3.99)); // true -console.log(validate(2)); // false -console.log(validate(4)); // false + range: [2, 4], + exclusiveRange: true, +} +var validate = ajv.compile(schema) +console.log(validate(2.01)) // true +console.log(validate(3.99)) // true +console.log(validate(2)) // false +console.log(validate(4)) // false ``` See note on custom errors and asynchronous keywords in the previous section. - ### Define keyword with "macro" function "Macro" function is called during schema compilation. It is passed schema, parent schema and [schema compilation context](#schema-compilation-context) and it should return another schema that will be applied to the data in addition to the original schema. @@ -140,66 +139,62 @@ It is the most efficient approach (in cases when the keyword logic can be expres In addition to the errors from the expanded schema macro keyword will add its own error in case validation fails. - Example. `range` and `exclusiveRange` keywords from the previous example defined with macro: ```javascript -ajv.addKeyword('range', { - type: 'number', +ajv.addKeyword("range", { + type: "number", macro: function (schema, parentSchema) { return { minimum: schema[0], maximum: schema[1], exclusiveMinimum: !!parentSchema.exclusiveRange, - exclusiveMaximum: !!parentSchema.exclusiveRange - }; + exclusiveMaximum: !!parentSchema.exclusiveRange, + } }, metaSchema: { - type: 'array', - items: [ - { type: 'number' }, - { type: 'number' } - ], - additionalItems: false - } -}); + type: "array", + items: [{type: "number"}, {type: "number"}], + additionalItems: false, + }, +}) ``` Example. `contains` keyword from version 5 proposals that requires that the array has at least one item matching schema (see https://github.com/json-schema/json-schema/wiki/contains-(v5-proposal)): ```javascript var schema = { - "contains": { - "type": "number", - "minimum": 4, - "exclusiveMinimum": true - } -}; - -var validate = ajv.addKeyword('contains', { - type: 'array', - macro: function (schema) { - return { - "not": { - "items": { - "not": schema - } + contains: { + type: "number", + minimum: 4, + exclusiveMinimum: true, + }, +} + +var validate = ajv + .addKeyword("contains", { + type: "array", + macro: function (schema) { + return { + not: { + items: { + not: schema, + }, + }, } - }; - } -}) -.compile(schema); + }, + }) + .compile(schema) -console.log(validate([1,2,3])); // false -console.log(validate([2,3,4])); // false -console.log(validate([3,4,5])); // true, number 5 matches schema inside "contains" +console.log(validate([1, 2, 3])) // false +console.log(validate([2, 3, 4])) // false +console.log(validate([3, 4, 5])) // true, number 5 matches schema inside "contains" ``` `contains` keyword is already available in Ajv with option `v5: true`. See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/ajv-validator/ajv/blob/master/spec/custom.spec.js#L151). - ### Define keyword with "inline" compilation function Inline compilation function is called during schema compilation. It is passed four parameters: `it` (the current schema compilation context), `keyword` (added in v3.0 to allow defining multiple keywords with a single function), `schema` and `parentSchema` and it should return the code (as a string) that will be inlined in the code of compiled schema. This code can be either an expression that evaluates to the validation result (boolean) or a set of statements that assigns the validation result to a variable. @@ -211,35 +206,35 @@ While it can be more challenging to define keywords with "inline" functions, it - access to the parent data and the path to the currently validated data - access to Ajv utilities via `it.util` - Example `even` keyword: ```javascript -var schema = { "even": true }; - -var validate = ajv.addKeyword('even', { - type: 'number', - inline: function (it, keyword, schema) { - var op = schema ? '===' : '!=='; - return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; - }, - metaSchema: { type: 'boolean' } -}) -.compile(schema); - -console.log(validate(2)); // true -console.log(validate(3)); // false +var schema = {even: true} + +var validate = ajv + .addKeyword("even", { + type: "number", + inline: function (it, keyword, schema) { + var op = schema ? "===" : "!==" + return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" + }, + metaSchema: {type: "boolean"}, + }) + .compile(schema) + +console.log(validate(2)) // true +console.log(validate(3)) // false ``` `'data' + (it.dataLevel || '')` in the example above is the reference to the currently validated data. Also note that `schema` (keyword schema) is the same as `it.schema.even`, so schema is not strictly necessary here - it is passed for convenience. - Example `range` keyword defined using [doT template](https://github.com/olado/doT): ```javascript // {% raw %} -var doT = require('dot'); -var inlineRangeTemplate = doT.compile("\ +var doT = require("dot") +var inlineRangeTemplate = doT.compile( + "\ {{ \ var $data = 'data' + (it.dataLevel || '') \ , $min = it.schema.range[0] \ @@ -248,21 +243,19 @@ var inlineRangeTemplate = doT.compile("\ , $lt = it.schema.exclusiveRange ? '<' : '<='; \ }} \ var valid{{=it.level}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; \ -"); +" +) -ajv.addKeyword('range', { - type: 'number', +ajv.addKeyword("range", { + type: "number", inline: inlineRangeTemplate, statements: true, metaSchema: { - type: 'array', - items: [ - { type: 'number' }, - { type: 'number' } - ], - additionalItems: false - } -}); + type: "array", + items: [{type: "number"}, {type: "number"}], + additionalItems: false, + }, +}) // {% endraw %} ``` @@ -272,13 +265,12 @@ Property `statements` in the keyword definition should be set to `true` if the v The main challenge of defining inline keywords is that you have to write both the code that will execute during schema compilation (compile-time) and the code that will execute during data validation (validation-time - this code can be generated either using strings concatenation or using templates, see the examples below). -Ajv uses [doT templates](https://github.com/olado/doT) to generate the code of validation functions that makes it easier to separate compile-time and validation-time code because of the different syntax used in templates and in the code. Ajv also uses different variable names for compile-time and validation-time variables to make it easier to differentiate - compile-time variable names start with $ character. +Ajv uses [doT templates](https://github.com/olado/doT) to generate the code of validation functions that makes it easier to separate compile-time and validation-time code because of the different syntax used in templates and in the code. Ajv also uses different variable names for compile-time and validation-time variables to make it easier to differentiate - compile-time variable names start with \$ character. Also you have to bear in mind that while compile-time variables exist in the scope of the function you wrote to compile the keyword, so they are isolated, validation-time variables share the scope with all the variables in the scope of a single validation function. So if your keyword has subschemas you have to append the schema level (`it.level`) to the variable names. See [schema compilation context](#schema-compilation-context) for more information on which properties and utilities from the schema compilation context you can use. - ## Schema compilation context The first parameter passed to inline keyword compilation function (and the 3rd parameter passed to compile and macro keyword functions) is `it`, the schema compilation context. All the properties and functions documented here are safe to use in your keywords, they won't be renamed or change their meaning without major version change. @@ -289,7 +281,7 @@ The first parameter passed to inline keyword compilation function (and the 3rd p - _dataLevel_ - the level of the currently validated data. It can be used to access both the property names and the data on all levels from the top. See [Validation time variables](#validation-time-variables). - _schema_ - current level schema. The value of your keyword is `it.schema[keyword]`. This value is also passed as the 3rd parameter to the inline compilation function and the current level schema as the 4th parameter. - _schemaPath_ - the validation time expression that evaluates to the property name of the current schema. -- _baseId_ - the current schema base URI that should be used as the base for resolving URIs in references ($ref). +- _baseId_ - the current schema base URI that should be used as the base for resolving URIs in references (\$ref). - _async_ - truthy if the current schema is asynchronous. - _opts_ - Ajv instance option. You should not be changing them. - _formats_ - all formats available in Ajv instance, including the custom ones. @@ -298,7 +290,6 @@ The first parameter passed to inline keyword compilation function (and the 3rd p - _util_ - [Ajv utilities](#ajv-utilities) you can use in your inline compilation functions. - _self_ - Ajv instance. - ## Validation time variables There is a number of variables and expressions you can use in the generated (validation-time) code of your keywords. @@ -314,7 +305,6 @@ There is a number of variables and expressions you can use in the generated (val - `'errors'` - the number of encountered errors. See [Reporting errors in custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). - `'vErrors'` - the array with errors collected so far. See [Reporting errors in custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). - ## Ajv utilities There are sevral useful functions you can use in your inline keywords. These functions are available as properties of `it.util` object: @@ -323,33 +313,29 @@ There are sevral useful functions you can use in your inline keywords. These fun Clone or extend the object. If one object is passed, it is cloned. If two objects are passed, the second object is extended with the properties of the first. - ##### .toHash(Array arr) -> Object Converts the array of strings to the object where each string becomes the key with the value of `true`. ```javascript -it.util.toHash(['a', 'b', 'c']) // { a: true, b: true, c: true } +it.util.toHash(["a", "b", "c"]) // { a: true, b: true, c: true } ``` - ##### .equal(value1, value2) -> Boolean Performs deep equality comparison. This function is used in keywords `enum`, `constant`, `uniqueItems` and can be used in custom keywords. - ##### .getProperty(String key) -> String Converts the string that is the key/index to access the property/item to the JavaScript syntax to access the property (either "." notation or "[...]" notation). ```javascript -it.util.getProperty('a') // ".a" -it.util.getProperty('1') // "['1']" +it.util.getProperty("a") // ".a" +it.util.getProperty("1") // "['1']" it.util.getProperty("a'b") // "['a\\'b']" -it.util.getProperty(1) // "[1]" +it.util.getProperty(1) // "[1]" ``` - ##### .schemaHasRules(Object schema, Object rules) -> String Determines whether the passed schema has rules that should be validated. This function should be used before calling `it.validate` to compile subschemas. @@ -358,12 +344,10 @@ Determines whether the passed schema has rules that should be validated. This fu it.util.schemaHasRules(schema, it.RULES.all) // true or false ``` - ##### .escapeQuotes(String str) -> String Escapes single quotes in the string, so it can be inserted in the generated code inside the string constant with the single quotes. - ##### .toQuotedString(String str) -> String Converts the string to the JavaScript string constant in single quotes (using the escaped string). @@ -372,38 +356,32 @@ Converts the string to the JavaScript string constant in single quotes (using th it.util.toQuotedString("a'b") // "'a\\'b'" ``` - ##### .getData(String jsonPointer, Number dataLevel, Array paths) -> String Returns the validation-time expression to safely access data based on the passed [relative json pointer](https://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (See [examples](https://gist.github.com/geraintluff/5911303)). ```javascript -it.util.getData('2/test/1', it.dataLevel, it.dataPathArr) +it.util.getData("2/test/1", it.dataLevel, it.dataPathArr) // The result depends on the current level // if it.dataLevel is 3 the result is "data1 && data1.test && data1.test[1]" ``` - ##### .escapeJsonPointer(String str) -> String Converts the property name to the JSON-Pointer fragment. - ##### .unescapeJsonPointer (String str) -> String Converts JSON-Pointer fragment to the property name. - ##### .unescapeFragment(String str) -> String Converts the property name to the JSON-Pointer fragment that can be used in URI. - ##### .escapeFragment(String str) -> String Converts the JSON-Pointer fragment from URI to the property name. - ## Reporting errors in custom keywords All custom keywords but macro keywords can optionally create custom error messages. @@ -415,14 +393,14 @@ Inline custom keyword should increase error counter `errors` and add error to `v When inline keyword performs validation Ajv checks whether it created errors by comparing errors count before and after validation. To skip this check add option `errors` (can be `"full"`, `true` or `false`) to keyword definition: ```javascript -ajv.addKeyword('range', { - type: 'number', +ajv.addKeyword("range", { + type: "number", inline: inlineRangeTemplate, statements: true, - errors: true // keyword should create custom errors when validation fails + errors: true, // keyword should create custom errors when validation fails // errors: 'full' // created errors should have dataPath already set // errors: false // keyword never creates errors, Ajv will add a default error -}); +}) ``` Each error object should at least have properties `keyword`, `message` and `params`, other properties will be added. @@ -431,7 +409,6 @@ Inlined keywords can optionally define `dataPath` and `schemaPath` properties in If custom keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](https://github.com/ajv-validator/ajv#validation-errors)). - ## Short-circuit validation In some cases inline keyword can terminate validation and return the result as soon as it encounters the error. It is only practical if the keyword you define has many criteria to validate and you want it to be able to fail fast. You only need to do it if your keyword defines errors itself, otherwise Ajv will return when it creates the default error (if the conditions below are met). @@ -444,18 +421,18 @@ Two conditions should be checked before keyword can return the result: If these conditions are met your keyword can immediately return result. In case the current schema is synchronous (`it.async` is not `true`) you can add this to keyword's generated code when it encounters error `err`: ```javascript -if (vErrors === null) vErrors = [err]; -else vErrors.push(err); -validate.errors = vErrors; -return false; +if (vErrors === null) vErrors = [err] +else vErrors.push(err) +validate.errors = vErrors +return false ``` In case the current schema is asynchronous (it.async is truthy) to return result you need: ```javascript -if (vErrors === null) vErrors = [err]; -else vErrors.push(err); -throw new ValidationError(vErrors); // ValidationError is in the scope +if (vErrors === null) vErrors = [err] +else vErrors.push(err) +throw new ValidationError(vErrors) // ValidationError is in the scope ``` In case `allErrors` option is used the keyword should continue validation after it encounters an error trying to find as many errors as possible. diff --git a/FAQ.md b/FAQ.md index f010a51c8a..902a94f7ac 100644 --- a/FAQ.md +++ b/FAQ.md @@ -2,8 +2,6 @@ The purpose of this document is to help find answers quicker. I am happy to continue the discussion about these issues, so please comment on some of the issues mentioned below or create a new issue if it seems more appropriate. - - ## Using JSON schema Ajv implements JSON schema specification. Before submitting the issue about the behaviour of any validation keywords please review them in: @@ -12,53 +10,42 @@ Ajv implements JSON schema specification. Before submitting the issue about the - [Validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md) in Ajv documentation - [JSON Schema tutorial](https://spacetelescope.github.io/understanding-json-schema/) (for draft-04) - ##### Why Ajv validates empty object as valid? "properties" keyword does not require the presence of any properties, you need to use "required" keyword. It also doesn't require that the data is an object, so any other type of data will also be valid. To require a specific type use "type" keyword. - ##### Why Ajv validates only the first item of the array? "items" keyword support [two syntaxes](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#items) - 1) when the schema applies to all items; 2) when there is a different schema for each item in the beginning of the array. This problem means you are using the second syntax. - - ## Ajv API for returning validation errors See [#65](https://github.com/ajv-validator/ajv/issues/65), [#212](https://github.com/ajv-validator/ajv/issues/212), [#236](https://github.com/ajv-validator/ajv/issues/236), [#242](https://github.com/ajv-validator/ajv/issues/242), [#256](https://github.com/ajv-validator/ajv/issues/256). - ##### Why Ajv assigns errors as a property of validation function (or instance) instead of returning an object with validation results and errors? The reasons are history (other fast validators with the same api) and performance (returning boolean is faster). Although more code is written to process errors than to handle successful results, almost all server-side validations pass. The existing API is more efficient from the performance point of view. Ajv also supports asynchronous validation (with custom asynchronous formats and keywords) that returns a promise that either resolves to `true` or rejects with an error. - ##### Would errors get overwritten in case of "concurrent" validations? No. There is no concurrency in JavaScript - it is single-threaded. While a validation is run no other JavaScript code (that can access the same memory) can be executed. As long as the errors are used in the same execution block, the errors will not be overwritten. - ##### Can we change / extend API to add a method that would return errors (rather than assign them to `errors` property)? No. In many cases there is a module responsible for the validation in the application, usually to load schemas and to process errors. This module is the right place to introduce any custom API. Convenience is a subjective thing, changing or extending API purely because of convenience would either break backward compatibility (even if it's done in a new major version it still complicates migration) or bloat API (making it more difficult to maintain). - ##### Why don't `"additionalProperties": false` errors display the property name? Doing this would create a precedent where validated data is used in error messages, creating a vulnerability (e.g., when ajv is used to validate API data/parameters and error messages are logged). Since the property name is already in the params object, in an application you can modify messages in any way you need. ajv-errors package allows modifying messages as well. - - ## Additional properties inside compound keywords anyOf, oneOf, etc. See [#127](https://github.com/ajv-validator/ajv/issues/127), [#129](https://github.com/ajv-validator/ajv/issues/129), [#134](https://github.com/ajv-validator/ajv/issues/134), [#140](https://github.com/ajv-validator/ajv/issues/140), [#193](https://github.com/ajv-validator/ajv/issues/193), [#205](https://github.com/ajv-validator/ajv/issues/205), [#238](https://github.com/ajv-validator/ajv/issues/238), [#264](https://github.com/ajv-validator/ajv/issues/264). - ##### Why the keyword `additionalProperties: false` fails validation when some properties are "declared" inside a subschema in `anyOf`/etc.? The keyword `additionalProperties` creates the restriction on validated data based on its own value (`false` or schema object) and on the keywords `properties` and `patternProperties` in the SAME schema object. JSON Schema validators must NOT take into account properties used in other schema objects. @@ -67,11 +54,11 @@ While you can expect that the schema below would allow the objects either with p ```json { - "properties": { "foo": { "type": "number" } }, + "properties": {"foo": {"type": "number"}}, "additionalProperties": false, "oneOf": [ - { "properties": { "bar": { "type": "number" } } }, - { "properties": { "baz": { "type": "number" } } } + {"properties": {"bar": {"type": "number"}}}, + {"properties": {"baz": {"type": "number"}}} ] } ``` @@ -80,21 +67,17 @@ The reason for that is that `additionalProperties` keyword ignores properties in There are several ways to implement the described logic that would allow two properties, please see the suggestions in the issues mentioned above. - ##### Why the validation fails when I use option `removeAdditional` with the keyword `anyOf`/etc.? This problem is related to the problem explained above - properties treated as additional in the sense of `additionalProperties` keyword, based on `properties`/`patternProperties` keyword in the same schema object. See the exemple in [Filtering Data](https://github.com/ajv-validator/ajv#filtering-data) section of readme. - - -## Generating schemas with resolved references ($ref) +## Generating schemas with resolved references (\$ref) See [#22](https://github.com/ajv-validator/ajv/issues/22), [#125](https://github.com/ajv-validator/ajv/issues/125), [#146](https://github.com/ajv-validator/ajv/issues/146), [#228](https://github.com/ajv-validator/ajv/issues/228), [#336](https://github.com/ajv-validator/ajv/issues/336), [#454](https://github.com/ajv-validator/ajv/issues/454). - -##### Why Ajv does not replace references ($ref) with the actual referenced schemas as some validators do? +##### Why Ajv does not replace references (\$ref) with the actual referenced schemas as some validators do? 1. The scope of Ajv is validating data against JSON Schemas; inlining referenced schemas is not necessary for validation. When Ajv generates code for validation it either inlines the code of referenced schema or uses function calls. Doing schema manipulation is more complex and out of scope. 2. When schemas are recursive (or mutually recursive) resolving references would result in self-referencing recursive data-structures that can be difficult to process. @@ -102,7 +85,6 @@ See [#22](https://github.com/ajv-validator/ajv/issues/22), [#125](https://github There were many conversations about the meaning of `$ref` in [JSON Schema GitHub organisation](https://github.com/json-schema-org). The consensus is that while it is possible to treat `$ref` as schema inclusion with two caveats (above), this interpretation is unnecessary complex. A more efficient approach is to treat `$ref` as a delegation, i.e. a special keyword that validates the current data instance against the referenced schema. The analogy with programming languages is that `$ref` is a function call rather than a macro. See [here](https://github.com/json-schema-org/json-schema-spec/issues/279), for example. - ##### How can I generate a schema where `$ref` keywords are replaced with referenced schemas? There are two possible approaches: diff --git a/KEYWORDS.md b/KEYWORDS.md index 6601a9a1b3..fae3fb1b94 100644 --- a/KEYWORDS.md +++ b/KEYWORDS.md @@ -1,48 +1,44 @@ # JSON Schema validation keywords - In a simple way, JSON Schema is an object with validation keywords. The keywords and their values define what rules the data should satisfy to be valid. - ## Keywords - [type](#type) - [Keywords for numbers](#keywords-for-numbers) - - [maximum / minimum and exclusiveMaximum / exclusiveMinimum](#maximum--minimum-and-exclusivemaximum--exclusiveminimum) (changed in draft-06) - - [multipleOf](#multipleof) + - [maximum / minimum and exclusiveMaximum / exclusiveMinimum](#maximum--minimum-and-exclusivemaximum--exclusiveminimum) (changed in draft-06) + - [multipleOf](#multipleof) - [Keywords for strings](#keywords-for-strings) - - [maxLength/minLength](#maxlength--minlength) - - [pattern](#pattern) - - [format](#format) - - [formatMaximum / formatMinimum and formatExclusiveMaximum / formatExclusiveMinimum](#formatmaximum--formatminimum-and-formatexclusivemaximum--formatexclusiveminimum-proposed) (proposed) + - [maxLength/minLength](#maxlength--minlength) + - [pattern](#pattern) + - [format](#format) + - [formatMaximum / formatMinimum and formatExclusiveMaximum / formatExclusiveMinimum](#formatmaximum--formatminimum-and-formatexclusivemaximum--formatexclusiveminimum-proposed) (proposed) - [Keywords for arrays](#keywords-for-arrays) - - [maxItems/minItems](#maxitems--minitems) - - [uniqueItems](#uniqueitems) - - [items](#items) - - [additionalItems](#additionalitems) - - [contains](#contains) (added in draft-06) + - [maxItems/minItems](#maxitems--minitems) + - [uniqueItems](#uniqueitems) + - [items](#items) + - [additionalItems](#additionalitems) + - [contains](#contains) (added in draft-06) - [Keywords for objects](#keywords-for-objects) - - [maxProperties/minProperties](#maxproperties--minproperties) - - [required](#required) - - [properties](#properties) - - [patternProperties](#patternproperties) - - [additionalProperties](#additionalproperties) - - [dependencies](#dependencies) - - [propertyNames](#propertynames) (added in draft-06) - - [patternRequired](#patternrequired-proposed) (proposed) + - [maxProperties/minProperties](#maxproperties--minproperties) + - [required](#required) + - [properties](#properties) + - [patternProperties](#patternproperties) + - [additionalProperties](#additionalproperties) + - [dependencies](#dependencies) + - [propertyNames](#propertynames) (added in draft-06) + - [patternRequired](#patternrequired-proposed) (proposed) - [Keywords for all types](#keywords-for-all-types) - - [enum](#enum) - - [const](#const) (added in draft-06) + - [enum](#enum) + - [const](#const) (added in draft-06) - [Compound keywords](#compound-keywords) - - [not](#not) - - [oneOf](#oneof) - - [anyOf](#anyof) - - [allOf](#allof) - - [if/then/else](#ifthenelse) (NEW in draft-07) - - + - [not](#not) + - [oneOf](#oneof) + - [anyOf](#anyof) + - [allOf](#allof) + - [if/then/else](#ifthenelse) (NEW in draft-07) ## `type` @@ -50,8 +46,7 @@ The keywords and their values define what rules the data should satisfy to be va Type can be: `number`, `integer`, `string`, `boolean`, `array`, `object` or `null`. - -__Examples__ +**Examples** 1. _schema_: `{ "type": "number" }` @@ -59,30 +54,24 @@ __Examples__ _invalid_: `"abc"`, `"1"`, `[]`, `{}`, `null`, `true` - -2. _schema_: `{ "type": "integer" }` +2) _schema_: `{ "type": "integer" }` _valid_: `1`, `2` _invalid_: `"abc"`, `"1"`, `1.5`, `[]`, `{}`, `null`, `true` - 3. _schema_: `{ "type": ["number", "string"] }` _valid_: `1`, `1.5`, `"abc"`, `"1"` _invalid_: `[]`, `{}`, `null`, `true` - All examples above are JSON Schemas that only require data to be of certain type to be valid. Most other keywords apply only to a particular type of data. If the data is of different type, the keyword will not apply and the data will be considered valid. - - ## Keywords for numbers - ### `maximum` / `minimum` and `exclusiveMaximum` / `exclusiveMinimum` The value of keyword `maximum` (`minimum`) should be a number. This value is the maximum (minimum) allowed value for the data to be valid. @@ -93,8 +82,7 @@ Draft-06/07: The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should Ajv supports both draft-04 and draft-06/07 syntaxes. - -__Examples__ +**Examples** 1. _schema_: `{ "maximum": 5 }` @@ -102,30 +90,25 @@ __Examples__ _invalid_: `6`, `7` - -2. _schema_: `{ "minimum": 5 }` +2) _schema_: `{ "minimum": 5 }` _valid_: `5`, `6`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) _invalid_: `4`, `4.5` - 3. _schema_: - draft-04: `{ "minimum": 5, "exclusiveMinimum": true }` - draft-06/07: `{ "exclusiveMinimum": 5 }` + draft-04: `{ "minimum": 5, "exclusiveMinimum": true }` + draft-06/07: `{ "exclusiveMinimum": 5 }` _valid_: `6`, `7`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) _invalid_: `4.5`, `5` - - ### `multipleOf` The value of the keyword should be a number. The data to be valid should be a multiple of the keyword value (i.e. the result of division of the data on the value should be integer). - -__Examples__ +**Examples** 1. _schema_: `{ "multipleOf": 5 }` @@ -133,23 +116,19 @@ __Examples__ _invalid_: `1`, `4` - -2. _schema_: `{ "multipleOf": 2.5 }` +2) _schema_: `{ "multipleOf": 2.5 }` _valid_: `2.5`, `5`, `7.5`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) _invalid_: `1`, `4` - - ## Keywords for strings ### `maxLength` / `minLength` The value of the keywords should be a number. The data to be valid should have length satisfying this rule. Unicode pairs are counted as a single character. - -__Examples__ +**Examples** 1. _schema_: `{ "maxLength": 5 }` @@ -157,23 +136,19 @@ __Examples__ _invalid_: `"abcdef"` - -2. _schema_: `{ "minLength": 2 }` +2) _schema_: `{ "minLength": 2 }` _valid_: `"ab"`, `"😀😀"`, any non-string (`1`, `[]`, `{}`, `null`, `true`) _invalid_: `"a"`, `"😀"` - - ### `pattern` The value of the keyword should be a string. The data to be valid should match the regular expression defined by the keyword value. Ajv uses `new RegExp(value)` to create the regular expression that will be used to test data. - -__Example__ +**Example** _schema_: `{ "pattern": "[abc]+" }` @@ -181,16 +156,13 @@ _valid_: `"a"`, `"abcd"`, `"cde"`, any non-string (`1`, `[]`, `{}`, `null`, `tru _invalid_: `"def"`, `""` - - ### `format` The value of the keyword should be a string. The data to be valid should match the format with this name. Ajv defines these formats: date, date-time, uri, email, hostname, ipv4, ipv6, regex. - -__Example__ +**Example** _schema_: `{ "format": "ipv4" }` @@ -198,8 +170,6 @@ _valid_: `"192.168.0.1"`, any non-string (`1`, `[]`, `{}`, `null`, `true`) _invalid_: `"abc"` - - ### `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum` (proposed) Defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. @@ -210,16 +180,15 @@ Ajv defines comparison rules for formats `"date"`, `"time"` and `"date-time"`. The value of keyword `formatExclusiveMaximum` (`formatExclusiveMinimum`) should be a boolean value. These keyword cannot be used without `formatMaximum` (`formatMinimum`). If this keyword value is equal to `true`, the data to be valid should not be equal to the value in `formatMaximum` (`formatMinimum`) keyword. - -__Example__ +**Example** _schema_: ```json { - "format": "date", - "formatMaximum": "2016-02-06", - "formatExclusiveMaximum": true + "format": "date", + "formatMaximum": "2016-02-06", + "formatExclusiveMaximum": true } ``` @@ -227,16 +196,13 @@ _valid_: `"2015-12-31"`, `"2016-02-05"`, any non-string _invalid_: `"2016-02-06"`, `"2016-02-07"`, `"abc"` - - ## Keywords for arrays ### `maxItems` / `minItems` The value of the keywords should be a number. The data array to be valid should not have more (less) items than the keyword value. - -__Example__ +**Example** _schema_: `{ "maxItems": 3 }` @@ -244,22 +210,17 @@ _valid_: `[]`, `[1]`, `["1", 2, "3"]`, any non-array (`"abc"`, `1`, `{}`, `null` _invalid_: `[1, 2, 3, 4]` - - ### `uniqueItems` The value of the keyword should be a boolean. If the keyword value is `true`, the data array to be valid should have unique items. - -__Example__ +**Example** _schema_: `{ "uniqueItems": true }` _valid_: `[]`, `[1]`, `["1", 2, "3"]`, any non-array (`"abc"`, `1`, `{}`, `null`, `true`) -_invalid_: `[1, 2, 1]`, `[{ "a": 1, "b": 2 }, { "b": 2, "a": 1 }]` - - +_invalid_: `[1, 2, 1]`, `[{ "a": 1, "b": 2 }, { "b": 2, "a": 1 }]` ### `items` @@ -269,8 +230,7 @@ If the keyword value is an object, then for the data array to be valid each item If the keyword value is an array, then items with indices less than the number of items in the keyword should be valid according to the schemas with the same indices. Whether additional items are valid will depend on "additionalItems" keyword. - -__Examples__ +**Examples** 1. _schema_: `{ "items": { "type": "integer" } }` @@ -278,14 +238,11 @@ __Examples__ _invalid_: `[1,"abc"]` +2) _schema_: -2. _schema_: ```json { - "items": [ - { "type": "integer" }, - { "type": "string" } - ] + "items": [{"type": "integer"}, {"type": "string"}] } ``` @@ -293,8 +250,6 @@ __Examples__ _invalid_: `["abc", 1]`, `["abc"]` - - ### `additionalItems` The value of the keyword should be a boolean or an object. @@ -309,19 +264,18 @@ If the length of data array is bigger than the length of "items" keyword value t - `true`: data is valid - an object: data is valid if all additional items (i.e. items with indices greater or equal than "items" keyword value length) are valid according to the schema in "additionalItems" keyword. - -__Examples__ +**Examples** 1. _schema_: `{ "additionalItems": { "type": "integer" } }` any data is valid against such schema - "additionalItems" is ignored. +2) _schema_: -2. _schema_: ```json { - "items": { "type": "integer" }, - "additionalItems": { "type": "string" } + "items": {"type": "integer"}, + "additionalItems": {"type": "string"} } ``` @@ -329,15 +283,12 @@ __Examples__ _invalid_: `[1, "abc"]`, (any array with some items other than integers) - 3. _schema_: + ```json { - "items": [ - { "type": "integer" }, - { "type": "integer" } - ], - "additionalItems": true + "items": [{"type": "integer"}, {"type": "integer"}], + "additionalItems": true } ``` @@ -345,15 +296,12 @@ __Examples__ _invalid_: `["abc"]`, `[1, "abc", 3]` +4) _schema_: -4. _schema_: ```json { - "items": [ - { "type": "integer" }, - { "type": "integer" } - ], - "additionalItems": { "type": "string" } + "items": [{"type": "integer"}, {"type": "integer"}], + "additionalItems": {"type": "string"} } ``` @@ -361,12 +309,11 @@ __Examples__ _invalid_: `["abc"]`, `[1, 2, 3]` - ### `contains` The value of the keyword is a JSON Schema. The array is valid if it contains at least one item that is valid according to this schema. -__Example__ +**Example** _schema_: `{ "contains": { "type": "integer" } }` @@ -374,29 +321,26 @@ _valid_: `[1]`, `[1, "foo"]`, any array with at least one integer, any non-array _invalid_: `[]`, `["foo", "bar"]`, any array without integers - The schema from the example above is equivalent to: ```json { - "not": { - "type": "array", - "items": { - "not": { "type": "integer" } - } + "not": { + "type": "array", + "items": { + "not": {"type": "integer"} } + } } ``` - ## Keywords for objects ### `maxProperties` / `minProperties` The value of the keywords should be a number. The data object to be valid should have not more (less) properties than the keyword value. - -__Example__ +**Example** _schema_: `{ "maxProperties": 2 }` @@ -404,14 +348,11 @@ _valid_: `{}`, `{"a": 1}`, `{"a": "1", "b": 2}`, any non-object _invalid_: `{"a": 1, "b": 2, "c": 3}` - - ### `required` The value of the keyword should be an array of unique strings. The data object to be valid should contain all properties with names equal to the elements in the keyword value. - -__Example__ +**Example** _schema_: `{ "required": ["a", "b"] }` @@ -419,26 +360,25 @@ _valid_: `{"a": 1, "b": 2}`, `{"a": 1, "b": 2, "c": 3}`, any non-object _invalid_: `{}`, `{"a": 1}`, `{"c": 3, "d":4}` - - ### `properties` The value of the keyword should be a map with keys equal to data object properties. Each value in the map should be a JSON Schema. For data object to be valid the corresponding values in data object properties should be valid according to these schemas. -__Please note__: `properties` keyword does not require that the properties mentioned in it are present in the object (see examples). +**Please note**: `properties` keyword does not require that the properties mentioned in it are present in the object (see examples). -__Example__ +**Example** _schema_: + ```json { - "properties": { - "foo": { "type": "string" }, - "bar": { - "type": "number", - "minimum": 2 - } + "properties": { + "foo": {"type": "string"}, + "bar": { + "type": "number", + "minimum": 2 } + } } ``` @@ -446,26 +386,24 @@ _valid_: `{}`, `{"foo": "a"}`, `{"foo": "a", "bar": 2}`, any non-object _invalid_: `{"foo": 1}`, `{"foo": "a", "bar": 1}` - - ### `patternProperties` The value of this keyword should be a map where keys should be regular expressions and the values should be JSON Schemas. For data object to be valid the values in data object properties that match regular expression(s) should be valid according to the corresponding schema(s). When the value in data object property matches multiple regular expressions it should be valid according to all the schemas for all matched regular expressions. -__Please note__: `patternProperties` keyword does not require that properties matching patterns are present in the object (see examples). +**Please note**: `patternProperties` keyword does not require that properties matching patterns are present in the object (see examples). - -__Example__ +**Example** _schema_: + ```json { - "patternProperties": { - "^fo.*$": { "type": "string" }, - "^ba.*$": { "type": "number" } - } + "patternProperties": { + "^fo.*$": {"type": "string"}, + "^ba.*$": {"type": "number"} + } } ``` @@ -473,8 +411,6 @@ _valid_: `{}`, `{"foo": "a"}`, `{"foo": "a", "bar": 1}`, any non-object _invalid_: `{"foo": 1}`, `{"foo": "a", "bar": "b"}` - - ### `additionalProperties` The value of the keyword should be either a boolean or a JSON Schema. @@ -485,19 +421,19 @@ If the value is `false` the data object to be valid should not have "additional If the value is a schema for the data object to be valid the values in all "additional properties" should be valid according to this schema. - -__Examples__ +**Examples** 1. _schema_: + ```json { - "properties": { - "foo": { "type": "number" } - }, - "patternProperties": { - "^.*r$": { "type": "number" } - }, - "additionalProperties": false + "properties": { + "foo": {"type": "number"} + }, + "patternProperties": { + "^.*r$": {"type": "number"} + }, + "additionalProperties": false } ``` @@ -505,16 +441,17 @@ __Examples__ _invalid_: `{"a": 3}`, `{"foo": 1, "baz": 3}` -2. _schema_: +2. _schema_: + ```json { - "properties": { - "foo": { "type": "number" } - }, - "patternProperties": { - "^.*r$": { "type": "number" } - }, - "additionalProperties": { "type": "string" } + "properties": { + "foo": {"type": "number"} + }, + "patternProperties": { + "^.*r$": {"type": "number"} + }, + "additionalProperties": {"type": "string"} } ``` @@ -522,33 +459,33 @@ __Examples__ _invalid_: `{"a": 3}`, `{"foo": 1, "baz": 3}` -3. _schema_: +3. _schema_: + ```json { - "properties": { - "foo": { "type": "number" } + "properties": { + "foo": {"type": "number"} + }, + "additionalProperties": false, + "anyOf": [ + { + "properties": { + "bar": {"type": "number"} + } }, - "additionalProperties": false, - "anyOf": [ - { - "properties": { - "bar": { "type": "number" } - } - }, - { - "properties": { - "baz": { "type": "number" } - } - } - ] + { + "properties": { + "baz": {"type": "number"} + } + } + ] } ``` + _valid_: `{}`, `{"foo": 1}`, any non-object _invalid_: `{"bar": 2}`, `{"baz": 3}`, `{"foo": 1, "bar": 2}`, etc. - - ### `dependencies` The value of the keyword is a map with keys equal to data object properties. Each value in the map should be either an array of unique property names ("property dependency") or a JSON Schema ("schema dependency"). @@ -557,15 +494,15 @@ For property dependency, if the data object contains a property that is a key in For schema dependency, if the data object contains a property that is a key in the keyword value, then to be valid the data object itself (NOT the property value) should be valid according to the schema. - -__Examples__ +**Examples** 1. _schema (property dependency)_: + ```json { - "dependencies": { - "foo": ["bar", "baz"] - } + "dependencies": { + "foo": ["bar", "baz"] + } } ``` @@ -573,17 +510,17 @@ __Examples__ _invalid_: `{"foo": 1}`, `{"foo": 1, "bar": 2}`, `{"foo": 1, "baz": 3}` +2) _schema (schema dependency)_: -2. _schema (schema dependency)_: ```json { - "dependencies": { - "foo": { - "properties": { - "bar": { "type": "number" } - } - } + "dependencies": { + "foo": { + "properties": { + "bar": {"type": "number"} + } } + } } ``` @@ -591,22 +528,19 @@ __Examples__ _invalid_: `{"foo": 1, "bar": "a"}` - - ### `propertyNames` The value of this keyword is a JSON Schema. For data object to be valid each property name in this object should be valid according to this schema. - -__Example__ +**Example** _schema_: ```json { - "propertyNames": { "format": "email" } + "propertyNames": {"format": "email"} } ``` @@ -614,8 +548,6 @@ _valid_: `{"foo@bar.com": "any", "bar@bar.com": "any"}`, any non-object _invalid_: `{"foo": "any value"}` - - ### `patternRequired` (proposed) Defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. @@ -624,7 +556,7 @@ The value of this keyword should be an array of strings, each string being a reg If the array contains multiple regular expressions, more than one expression can match the same property name. -__Examples__ +**Examples** 1. _schema_: `{ "patternRequired": [ "f.*o" ] }` @@ -638,16 +570,13 @@ __Examples__ _invalid_: `{}`, `{ "foo": 1 }`, `{ "bar": 2 }` - - ## Keywords for all types ### `enum` The value of the keyword should be an array of unique items of any types. The data is valid if it is deeply equal to one of items in the array. - -__Example__ +**Example** _schema_: `{ "enum": [ 2, "foo", {"foo": "bar" }, [1, 2, 3] ] }` @@ -655,13 +584,11 @@ _valid_: `2`, `"foo"`, `{"foo": "bar"}`, `[1, 2, 3]` _invalid_: `1`, `"bar"`, `{"foo": "baz"}`, `[1, 2, 3, 4]`, any value not in the array - - ### `const` The value of this keyword can be anything. The data is valid if it is deeply equal to the value of the keyword. -__Example__ +**Example** _schema_: `{ "const": "foo" }` @@ -669,20 +596,18 @@ _valid_: `"foo"` _invalid_: any other value +The same can be achieved with `enum` keyword using the array with one item. But `const` keyword is more than just a syntax sugar for `enum`. In combination with the [\$data reference](https://github.com/ajv-validator/ajv#data-reference) it allows to define equality relations between different parts of the data. This cannot be achieved with `enum` keyword even with `$data` reference because `$data` cannot be used in place of one item - it can only be used in place of the whole array in `enum` keyword. -The same can be achieved with `enum` keyword using the array with one item. But `const` keyword is more than just a syntax sugar for `enum`. In combination with the [$data reference](https://github.com/ajv-validator/ajv#data-reference) it allows to define equality relations between different parts of the data. This cannot be achieved with `enum` keyword even with `$data` reference because `$data` cannot be used in place of one item - it can only be used in place of the whole array in `enum` keyword. - - -__Example__ +**Example** _schema_: ```json { - "properties": { - "foo": { "type": "number" }, - "bar": { "const": { "$data": "1/foo" } } - } + "properties": { + "foo": {"type": "number"}, + "bar": {"const": {"$data": "1/foo"}} + } } ``` @@ -690,16 +615,13 @@ _valid_: `{ "foo": 1, "bar": 1 }`, `{}` _invalid_: `{ "foo": 1 }`, `{ "bar": 1 }`, `{ "foo": 1, "bar": 2 }` - - ## Compound keywords ### `not` The value of the keyword should be a JSON Schema. The data is valid if it is invalid according to this schema. - -__Examples__ +**Examples** 1. _schema_: `{ "not": { "minimum": 3 } }` @@ -711,11 +633,11 @@ __Examples__ ```json { - "not": { - "items": { - "not": { "type": "string" } - } + "not": { + "items": { + "not": {"type": "string"} } + } } ``` @@ -723,22 +645,17 @@ __Examples__ _invalid_: `[]`, `[1]`, any non-array, any array not containing strings - - ### `oneOf` The value of the keyword should be an array of JSON Schemas. The data is valid if it matches exactly one JSON Schema from this array. Validators have to validate data against all schemas to establish validity according to this keyword. - -__Example__ +**Example** _schema_: + ```json { - "oneOf": [ - { "maximum": 3 }, - { "type": "integer" } - ] + "oneOf": [{"maximum": 3}, {"type": "integer"}] } ``` @@ -746,22 +663,17 @@ _valid_: `1.5`, `2.5`, `4`, `5`, any non-number _invalid_: `2`, `3`, `4.5`, `5.5` - - ### `anyOf` The value of the keyword should be an array of JSON Schemas. The data is valid if it is valid according to one or more JSON Schemas in this array. Validators only need to validate data against schemas in order until the first schema matches (or until all schemas have been tried). For this reason validating against this keyword is faster than against "oneOf" keyword in most cases. - -__Example__ +**Example** _schema_: + ```json { - "anyOf": [ - { "maximum": 3 }, - { "type": "integer" } - ] + "anyOf": [{"maximum": 3}, {"type": "integer"}] } ``` @@ -769,22 +681,17 @@ _valid_: `1.5`, `2`, `2.5`, `3`, `4`, `5`, any non-number _invalid_: `4.5`, `5.5` - - ### `allOf` The value of the keyword should be an array of JSON Schemas. The data is valid if it is valid according to all JSON Schemas in this array. - -__Example__ +**Example** _schema_: + ```json { - "allOf": [ - { "maximum": 3 }, - { "type": "integer" } - ] + "allOf": [{"maximum": 3}, {"type": "integer"}] } ``` @@ -792,8 +699,6 @@ _valid_: `2`, `3` _invalid_: `1.5`, `2.5`, `4`, `4.5`, `5`, `5.5`, any non-number - - ### `if`/`then`/`else` These keywords allow to implement conditional validation. Their values should be valid JSON Schemas (object or boolean). @@ -804,16 +709,15 @@ If the data is valid against the sub-schema in `if` keyword, then the validation If the data is invalid against the sub-schema in `if` keyword, then the validation result is equal to the result of data validation against the sub-schema in `else` keyword (if `else` is absent, the validation succeeds). - -__Examples__ +**Examples** 1. _schema_: ```json { - "if": { "properties": { "power": { "minimum": 9000 } } }, - "then": { "required": [ "disbelief" ] }, - "else": { "required": [ "confidence" ] } + "if": {"properties": {"power": {"minimum": 9000}}}, + "then": {"required": ["disbelief"]}, + "else": {"required": ["confidence"]} } ``` @@ -830,20 +734,19 @@ __Examples__ - `{ "power": 10000, "confidence": true }` (`disbelief` is required) - `{ "power": 1000 }` (`confidence` is required) - -2. _schema_: +2) _schema_: ```json { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "if": { "minimum": 100 }, - "then": { "multipleOf": 100 }, - "else": { - "if": { "minimum": 10 }, - "then": { "multipleOf": 10 } - } + "type": "integer", + "minimum": 1, + "maximum": 1000, + "if": {"minimum": 100}, + "then": {"multipleOf": 100}, + "else": { + "if": {"minimum": 10}, + "then": {"multipleOf": 10} + } } ``` diff --git a/README.md b/README.md index ce4507fdbb..c40b9c6c35 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ The fastest JSON Schema validator for Node.js and browser. Supports draft-04/06/ [![Gitter](https://img.shields.io/gitter/room/ajv-validator/ajv.svg)](https://gitter.im/ajv-validator/ajv) [![GitHub Sponsors](https://img.shields.io/badge/$-sponsors-brightgreen)](https://github.com/sponsors/epoberezkin) - ## Please [sponsor Ajv development](https://github.com/sponsors/epoberezkin) I will get straight to the point - I need your support to ensure that the development of Ajv continues. -I have developed Ajv for 5 years in my free time, but it is not sustainable. I'd appreciate if you consider supporting its further development with donations: +I have developed Ajv for 5 years in my free time, but it is not sustainable. I'd appreciate if you consider supporting its further development with donations: + - [GitHub sponsors page](https://github.com/sponsors/epoberezkin) (GitHub will match it) - [Ajv Open Collective️](https://opencollective.com/ajv) @@ -36,7 +36,6 @@ I believe it would benefit all Ajv users to help put together the fund that will Thank you - #### Open Collective sponsors @@ -52,29 +51,27 @@ Thank you - ## Using version 6 [JSON Schema draft-07](http://json-schema.org/latest/json-schema-validation.html) is published. [Ajv version 6.0.0](https://github.com/ajv-validator/ajv/releases/tag/v6.0.0) that supports draft-07 is released. It may require either migrating your schemas or updating your code (to continue using draft-04 and v5 schemas, draft-06 schemas will be supported without changes). -__Please note__: To use Ajv with draft-06 schemas you need to explicitly add the meta-schema to the validator instance: +**Please note**: To use Ajv with draft-06 schemas you need to explicitly add the meta-schema to the validator instance: ```javascript -ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); +ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")) ``` To use Ajv with draft-04 schemas in addition to explicitly adding meta-schema you also need to use option schemaId: ```javascript -var ajv = new Ajv({schemaId: 'id'}); +var ajv = new Ajv({schemaId: "id"}) // If you want to use both draft-04 and draft-06/07 schemas: // var ajv = new Ajv({schemaId: 'auto'}); -ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); +ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-04.json")) ``` - ## Contents - [Performance](#performance) @@ -88,8 +85,8 @@ ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); - [Keywords](#validation-keywords) - [Annotation keywords](#annotation-keywords) - [Formats](#formats) - - [Combining schemas with $ref](#ref) - - [$data reference](#data-reference) + - [Combining schemas with \$ref](#ref) + - [\$data reference](#data-reference) - NEW: [$merge and $patch keywords](#merge-and-patch-keywords) - [Defining custom keywords](#defining-custom-keywords) - [Asynchronous schema compilation](#asynchronous-schema-compilation) @@ -114,7 +111,6 @@ ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); - [Tests, Contributing, Changes history](#tests) - [Support, Code of conduct, License](#open-source-software-support) - ## Performance Ajv generates code using [doT templates](https://github.com/olado/doT) to turn JSON Schemas into super-fast validation functions that are efficient for v8 optimization. @@ -126,12 +122,10 @@ Currently Ajv is the fastest and the most standard compliant validator according - [z-schema benchmark](https://rawgit.com/zaggino/z-schema/master/benchmark/results.html) - [themis benchmark](https://cdn.rawgit.com/playlyfe/themis/master/benchmark/results.html) - Performance of different validators by [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark): [![performance](https://chart.googleapis.com/chart?chxt=x,y&cht=bhs&chco=76A4FB&chls=2.0&chbh=32,4,1&chs=600x416&chxl=-1:|djv|ajv|json-schema-validator-generator|jsen|is-my-json-valid|themis|z-schema|jsck|skeemas|json-schema-library|tv4&chd=t:100,98,72.1,66.8,50.1,15.1,6.1,3.8,1.2,0.7,0.2)](https://github.com/ebdrup/json-schema-benchmark/blob/master/README.md#performance) - ## Features - Ajv implements full JSON Schema [draft-06/07](http://json-schema.org/) and draft-04 standards: @@ -153,44 +147,41 @@ Performance of different validators by [json-schema-benchmark](https://github.co - draft-06/07 keywords `const`, `contains`, `propertyNames` and `if/then/else` - draft-06 boolean schemas (`true`/`false` as a schema to always pass/fail). - keywords `switch`, `patternRequired`, `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum` from [JSON Schema extension proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) with [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package -- [$data reference](#data-reference) to use values from the validated data as values for the schema keywords +- [\$data reference](#data-reference) to use values from the validated data as values for the schema keywords - [asynchronous validation](#asynchronous-validation) of custom formats and keywords Currently Ajv is the only validator that passes all the tests from [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) (according to [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark), apart from the test that requires that `1.0` is not an integer that is impossible to satisfy in JavaScript). - ## Install ``` npm install ajv ``` - ## Getting started Try it in the Node.js REPL: https://tonicdev.com/npm/ajv - The fastest validation call: ```javascript // Node.js require: -var Ajv = require('ajv'); +var Ajv = require("ajv") // or ESM/TypeScript import -import Ajv from 'ajv'; +import Ajv from "ajv" -var ajv = new Ajv(); // options can be passed, e.g. {allErrors: true} -var validate = ajv.compile(schema); -var valid = validate(data); -if (!valid) console.log(validate.errors); +var ajv = new Ajv() // options can be passed, e.g. {allErrors: true} +var validate = ajv.compile(schema) +var valid = validate(data) +if (!valid) console.log(validate.errors) ``` or with less code ```javascript // ... -var valid = ajv.validate(schema, data); -if (!valid) console.log(ajv.errors); +var valid = ajv.validate(schema, data) +if (!valid) console.log(ajv.errors) // ... ``` @@ -198,9 +189,8 @@ or ```javascript // ... -var valid = ajv.addSchema(schema, 'mySchema') - .validate('mySchema', data); -if (!valid) console.log(ajv.errorsText()); +var valid = ajv.addSchema(schema, "mySchema").validate("mySchema", data) +if (!valid) console.log(ajv.errorsText()) // ... ``` @@ -210,13 +200,12 @@ Ajv compiles schemas to functions and caches them in all cases (using schema ser The best performance is achieved when using compiled functions returned by `compile` or `getSchema` methods (there is no additional function call). -__Please note__: every time a validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback). See [Validation errors](#validation-errors) +**Please note**: every time a validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback). See [Validation errors](#validation-errors) -__Note for TypeScript users__: `ajv` provides its own TypeScript declarations +**Note for TypeScript users**: `ajv` provides its own TypeScript declarations out of the box, so you don't need to install the deprecated `@types/ajv` module. - ## Using in browser You can require Ajv directly from the code you browserify - in this case Ajv will be a part of your bundle. @@ -224,6 +213,7 @@ You can require Ajv directly from the code you browserify - in this case Ajv wil If you need to use Ajv in several bundles you can create a separate UMD bundle using `npm run bundle` script (thanks to [siddo420](https://github.com/siddo420)). Then you need to load Ajv in the browser: + ```html ``` @@ -236,8 +226,7 @@ Ajv is tested with these browsers: [![Sauce Test Status](https://saucelabs.com/browser-matrix/epoberezkin.svg)](https://saucelabs.com/u/epoberezkin) -__Please note__: some frameworks, e.g. Dojo, may redefine global require in such way that is not compatible with CommonJS module format. In such case Ajv bundle has to be loaded before the framework and then you can use global Ajv (see issue [#234](https://github.com/ajv-validator/ajv/issues/234)). - +**Please note**: some frameworks, e.g. Dojo, may redefine global require in such way that is not compatible with CommonJS module format. In such case Ajv bundle has to be loaded before the framework and then you can use global Ajv (see issue [#234](https://github.com/ajv-validator/ajv/issues/234)). ### Ajv and Content Security Policies (CSP) @@ -248,7 +237,6 @@ In order to make use of Ajv without easing your CSP, you can [pre-compile a sche Note that pre-compilation of schemas is performed using [ajv-pack](https://github.com/ajv-validator/ajv-pack) and there are [some limitations to the schema features it can compile](https://github.com/ajv-validator/ajv-pack#limitations). A successfully pre-compiled schema is equivalent to the same schema compiled at runtime. - ## Command line interface CLI is available as a separate npm package [ajv-cli](https://github.com/ajv-validator/ajv-cli). It supports: @@ -264,7 +252,6 @@ CLI is available as a separate npm package [ajv-cli](https://github.com/ajv-vali - all Ajv options - reporting changes in data after validation in [JSON-patch](https://tools.ietf.org/html/rfc6902) format - ## Validation keywords Ajv supports all validation keywords from draft-07 of JSON Schema standard: @@ -284,7 +271,6 @@ With [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package Ajv a See [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md) for more details. - ## Annotation keywords JSON Schema specification defines several annotation keywords that describe schema itself but do not perform any validation. @@ -294,17 +280,16 @@ JSON Schema specification defines several annotation keywords that describe sche - `default`: a default value of the data instance, see [Assigning defaults](#assigning-defaults). - `examples` (NEW in draft-06): an array of data instances. Ajv does not check the validity of these instances against the schema. - `readOnly` and `writeOnly` (NEW in draft-07): marks data-instance as read-only or write-only in relation to the source of the data (database, api, etc.). -- `contentEncoding`: [RFC 2045](https://tools.ietf.org/html/rfc2045#section-6.1 ), e.g., "base64". +- `contentEncoding`: [RFC 2045](https://tools.ietf.org/html/rfc2045#section-6.1), e.g., "base64". - `contentMediaType`: [RFC 2046](https://tools.ietf.org/html/rfc2046), e.g., "image/png". -__Please note__: Ajv does not implement validation of the keywords `examples`, `contentEncoding` and `contentMediaType` but it reserves them. If you want to create a plugin that implements some of them, it should remove these keywords from the instance. - +**Please note**: Ajv does not implement validation of the keywords `examples`, `contentEncoding` and `contentMediaType` but it reserves them. If you want to create a plugin that implements some of them, it should remove these keywords from the instance. ## Formats Ajv implements formats defined by JSON Schema specification and several other formats. It is recommended NOT to use "format" keyword implementations with untrusted data, as they use potentially unsafe regular expressions - see [ReDoS attack](#redos-attack). -__Please note__: if you need to use "format" keyword to validate untrusted data, you MUST assess their suitability and safety for your validation scenarios. +**Please note**: if you need to use "format" keyword to validate untrusted data, you MUST assess their suitability and safety for your validation scenarios. The following formats are implemented for string validation with "format" keyword: @@ -324,7 +309,7 @@ The following formats are implemented for string validation with "format" keywor - _json-pointer_: JSON-pointer according to [RFC6901](https://tools.ietf.org/html/rfc6901). - _relative-json-pointer_: relative JSON-pointer according to [this draft](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00). -__Please note__: JSON Schema draft-07 also defines formats `iri`, `iri-reference`, `idn-hostname` and `idn-email` for URLs, hostnames and emails with international characters. Ajv does not implement these formats. If you create Ajv plugin that implements them please make a PR to mention this plugin here. +**Please note**: JSON Schema draft-07 also defines formats `iri`, `iri-reference`, `idn-hostname` and `idn-email` for URLs, hostnames and emails with international characters. Ajv does not implement these formats. If you create Ajv plugin that implements them please make a PR to mention this plugin here. There are two modes of format validation: `fast` and `full`. This mode affects formats `date`, `time`, `date-time`, `uri`, `uri-reference`, and `email`. See [Options](#options) for details. @@ -334,8 +319,7 @@ The option `unknownFormats` allows changing the default behaviour when an unknow You can find regular expressions used for format validation and the sources that were used in [formats.js](https://github.com/ajv-validator/ajv/blob/master/lib/compile/formats.js). - -## Combining schemas with $ref +## Combining schemas with \$ref You can structure your validation logic across multiple schema files and have schemas reference each other using `$ref` keyword. @@ -343,102 +327,100 @@ Example: ```javascript var schema = { - "$id": "http://example.com/schemas/schema.json", - "type": "object", - "properties": { - "foo": { "$ref": "defs.json#/definitions/int" }, - "bar": { "$ref": "defs.json#/definitions/str" } - } -}; + $id: "http://example.com/schemas/schema.json", + type: "object", + properties: { + foo: {$ref: "defs.json#/definitions/int"}, + bar: {$ref: "defs.json#/definitions/str"}, + }, +} var defsSchema = { - "$id": "http://example.com/schemas/defs.json", - "definitions": { - "int": { "type": "integer" }, - "str": { "type": "string" } - } -}; + $id: "http://example.com/schemas/defs.json", + definitions: { + int: {type: "integer"}, + str: {type: "string"}, + }, +} ``` Now to compile your schema you can either pass all schemas to Ajv instance: ```javascript -var ajv = new Ajv({schemas: [schema, defsSchema]}); -var validate = ajv.getSchema('http://example.com/schemas/schema.json'); +var ajv = new Ajv({schemas: [schema, defsSchema]}) +var validate = ajv.getSchema("http://example.com/schemas/schema.json") ``` or use `addSchema` method: ```javascript -var ajv = new Ajv; -var validate = ajv.addSchema(defsSchema) - .compile(schema); +var ajv = new Ajv() +var validate = ajv.addSchema(defsSchema).compile(schema) ``` See [Options](#options) and [addSchema](#api) method. -__Please note__: -- `$ref` is resolved as the uri-reference using schema $id as the base URI (see the example). +**Please note**: + +- `$ref` is resolved as the uri-reference using schema \$id as the base URI (see the example). - References can be recursive (and mutually recursive) to implement the schemas for different data structures (such as linked lists, trees, graphs, etc.). -- You don't have to host your schema files at the URIs that you use as schema $id. These URIs are only used to identify the schemas, and according to JSON Schema specification validators should not expect to be able to download the schemas from these URIs. +- You don't have to host your schema files at the URIs that you use as schema \$id. These URIs are only used to identify the schemas, and according to JSON Schema specification validators should not expect to be able to download the schemas from these URIs. - The actual location of the schema file in the file system is not used. -- You can pass the identifier of the schema as the second parameter of `addSchema` method or as a property name in `schemas` option. This identifier can be used instead of (or in addition to) schema $id. -- You cannot have the same $id (or the schema identifier) used for more than one schema - the exception will be thrown. +- You can pass the identifier of the schema as the second parameter of `addSchema` method or as a property name in `schemas` option. This identifier can be used instead of (or in addition to) schema \$id. +- You cannot have the same \$id (or the schema identifier) used for more than one schema - the exception will be thrown. - You can implement dynamic resolution of the referenced schemas using `compileAsync` method. In this way you can store schemas in any system (files, web, database, etc.) and reference them without explicitly adding to Ajv instance. See [Asynchronous schema compilation](#asynchronous-schema-compilation). - -## $data reference +## \$data reference With `$data` option you can use values from the validated data as the values for the schema keywords. See [proposal](https://github.com/json-schema-org/json-schema-spec/issues/51) for more information about how it works. `$data` reference is supported in the keywords: const, enum, format, maximum/minimum, exclusiveMaximum / exclusiveMinimum, maxLength / minLength, maxItems / minItems, maxProperties / minProperties, formatMaximum / formatMinimum, formatExclusiveMaximum / formatExclusiveMinimum, multipleOf, pattern, required, uniqueItems. -The value of "$data" should be a [JSON-pointer](https://tools.ietf.org/html/rfc6901) to the data (the root is always the top level data object, even if the $data reference is inside a referenced subschema) or a [relative JSON-pointer](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (it is relative to the current point in data; if the $data reference is inside a referenced subschema it cannot point to the data outside of the root level for this subschema). +The value of "$data" should be a [JSON-pointer](https://tools.ietf.org/html/rfc6901) to the data (the root is always the top level data object, even if the $data reference is inside a referenced subschema) or a [relative JSON-pointer](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (it is relative to the current point in data; if the \$data reference is inside a referenced subschema it cannot point to the data outside of the root level for this subschema). Examples. This schema requires that the value in property `smaller` is less or equal than the value in the property larger: ```javascript -var ajv = new Ajv({$data: true}); +var ajv = new Ajv({$data: true}) var schema = { - "properties": { - "smaller": { - "type": "number", - "maximum": { "$data": "1/larger" } + properties: { + smaller: { + type: "number", + maximum: {$data: "1/larger"}, }, - "larger": { "type": "number" } - } -}; + larger: {type: "number"}, + }, +} var validData = { smaller: 5, - larger: 7 -}; + larger: 7, +} -ajv.validate(schema, validData); // true +ajv.validate(schema, validData) // true ``` This schema requires that the properties have the same format as their field names: ```javascript var schema = { - "additionalProperties": { - "type": "string", - "format": { "$data": "0#" } - } -}; + additionalProperties: { + type: "string", + format: {$data: "0#"}, + }, +} var validData = { - 'date-time': '1963-06-19T08:30:06.283185Z', - email: 'joe.bloggs@example.com' + "date-time": "1963-06-19T08:30:06.283185Z", + email: "joe.bloggs@example.com", } ``` `$data` reference is resolved safely - it won't throw even if some property is undefined. If `$data` resolves to `undefined` the validation succeeds (with the exclusion of `const` keyword). If `$data` resolves to incorrect type (e.g. not "number" for maximum keyword) the validation fails. - ## $merge and $patch keywords With the package [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) you can use the keywords `$merge` and `$patch` that allow extending JSON Schemas with patches using formats [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/html/rfc7396) and [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902). @@ -446,7 +428,7 @@ With the package [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-pa To add keywords `$merge` and `$patch` to Ajv instance use this code: ```javascript -require('ajv-merge-patch')(ajv); +require("ajv-merge-patch")(ajv) ``` Examples. @@ -458,11 +440,11 @@ Using `$merge`: "$merge": { "source": { "type": "object", - "properties": { "p": { "type": "string" } }, + "properties": {"p": {"type": "string"}}, "additionalProperties": false }, "with": { - "properties": { "q": { "type": "number" } } + "properties": {"q": {"type": "number"}} } } } @@ -475,11 +457,11 @@ Using `$patch`: "$patch": { "source": { "type": "object", - "properties": { "p": { "type": "string" } }, + "properties": {"p": {"type": "string"}}, "additionalProperties": false }, "with": [ - { "op": "add", "path": "/properties/q", "value": { "type": "number" } } + {"op": "add", "path": "/properties/q", "value": {"type": "number"}} ] } } @@ -491,8 +473,8 @@ The schemas above are equivalent to this schema: { "type": "object", "properties": { - "p": { "type": "string" }, - "q": { "type": "number" } + "p": {"type": "string"}, + "q": {"type": "number"} }, "additionalProperties": false } @@ -502,7 +484,6 @@ The properties `source` and `with` in the keywords `$merge` and `$patch` can use See the package [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) for more information. - ## Defining custom keywords The advantages of using custom keywords are: @@ -520,6 +501,7 @@ The concerns you have to be aware of when extending JSON Schema standard with cu You can define custom keywords with [addKeyword](#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords. Ajv allows defining keywords with: + - validation function - compilation function - macro function @@ -528,31 +510,34 @@ Ajv allows defining keywords with: Example. `range` and `exclusiveRange` keywords using compiled schema: ```javascript -ajv.addKeyword('range', { - type: 'number', +ajv.addKeyword("range", { + type: "number", compile: function (sch, parentSchema) { - var min = sch[0]; - var max = sch[1]; + var min = sch[0] + var max = sch[1] return parentSchema.exclusiveRange === true - ? function (data) { return data > min && data < max; } - : function (data) { return data >= min && data <= max; } - } -}); - -var schema = { "range": [2, 4], "exclusiveRange": true }; -var validate = ajv.compile(schema); -console.log(validate(2.01)); // true -console.log(validate(3.99)); // true -console.log(validate(2)); // false -console.log(validate(4)); // false + ? function (data) { + return data > min && data < max + } + : function (data) { + return data >= min && data <= max + } + }, +}) + +var schema = {range: [2, 4], exclusiveRange: true} +var validate = ajv.compile(schema) +console.log(validate(2.01)) // true +console.log(validate(3.99)) // true +console.log(validate(2)) // false +console.log(validate(4)) // false ``` Several custom keywords (typeof, instanceof, range and propertyNames) are defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - they can be used for your schemas and as a starting point for your own custom keywords. See [Defining custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) for more details. - ## Asynchronous schema compilation During asynchronous compilation remote references are loaded using supplied function. See `compileAsync` [method](#api-compileAsync) and `loadSchema` [option](#options). @@ -560,24 +545,23 @@ During asynchronous compilation remote references are loaded using supplied func Example: ```javascript -var ajv = new Ajv({ loadSchema: loadSchema }); +var ajv = new Ajv({loadSchema: loadSchema}) ajv.compileAsync(schema).then(function (validate) { - var valid = validate(data); + var valid = validate(data) // ... -}); +}) function loadSchema(uri) { return request.json(uri).then(function (res) { if (res.statusCode >= 400) - throw new Error('Loading error: ' + res.statusCode); - return res.body; - }); + throw new Error("Loading error: " + res.statusCode) + return res.body + }) } ``` -__Please note__: [Option](#options) `missingRefs` should NOT be set to `"ignore"` or `"fail"` for asynchronous compilation to work. - +**Please note**: [Option](#options) `missingRefs` should NOT be set to `"ignore"` or `"fail"` for asynchronous compilation to work. ## Asynchronous validation @@ -587,7 +571,7 @@ You can define custom formats and keywords that perform validation asynchronousl If your schema uses asynchronous formats/keywords or refers to some schema that contains them it should have `"$async": true` keyword so that Ajv can compile it correctly. If asynchronous format/keyword or reference to asynchronous schema is used in the schema without `$async` keyword Ajv will throw an exception during schema compilation. -__Please note__: all asynchronous subschemas that are referenced from the current or other schemas should have `"$async": true` keyword as well, otherwise the schema compilation will fail. +**Please note**: all asynchronous subschemas that are referenced from the current or other schemas should have `"$async": true` keyword as well, otherwise the schema compilation will fail. Validation function for an asynchronous custom format/keyword should return a promise that resolves with `true` or `false` (or rejects with `new Ajv.ValidationError(errors)` if you want to return custom errors from the keyword function). @@ -597,101 +581,95 @@ The compiled validation function has `$async: true` property (if the schema is a Validation result will be a promise that resolves with validated data or rejects with an exception `Ajv.ValidationError` that contains the array of validation errors in `errors` property. - Example: ```javascript -var ajv = new Ajv; +var ajv = new Ajv() // require('ajv-async')(ajv); -ajv.addKeyword('idExists', { +ajv.addKeyword("idExists", { async: true, - type: 'number', - validate: checkIdExists -}); - + type: "number", + validate: checkIdExists, +}) function checkIdExists(schema, data) { return knex(schema.table) - .select('id') - .where('id', data) - .then(function (rows) { - return !!rows.length; // true if record is found - }); + .select("id") + .where("id", data) + .then(function (rows) { + return !!rows.length // true if record is found + }) } var schema = { - "$async": true, - "properties": { - "userId": { - "type": "integer", - "idExists": { "table": "users" } + $async: true, + properties: { + userId: { + type: "integer", + idExists: {table: "users"}, }, - "postId": { - "type": "integer", - "idExists": { "table": "posts" } - } - } -}; - -var validate = ajv.compile(schema); + postId: { + type: "integer", + idExists: {table: "posts"}, + }, + }, +} -validate({ userId: 1, postId: 19 }) -.then(function (data) { - console.log('Data is valid', data); // { userId: 1, postId: 19 } -}) -.catch(function (err) { - if (!(err instanceof Ajv.ValidationError)) throw err; - // data is invalid - console.log('Validation errors:', err.errors); -}); +var validate = ajv.compile(schema) + +validate({userId: 1, postId: 19}) + .then(function (data) { + console.log("Data is valid", data) // { userId: 1, postId: 19 } + }) + .catch(function (err) { + if (!(err instanceof Ajv.ValidationError)) throw err + // data is invalid + console.log("Validation errors:", err.errors) + }) ``` ### Using transpilers with asynchronous validation functions. [ajv-async](https://github.com/ajv-validator/ajv-async) uses [nodent](https://github.com/MatAtBread/nodent) to transpile async functions. To use another transpiler you should separately install it (or load its bundle in the browser). - #### Using nodent ```javascript -var ajv = new Ajv; -require('ajv-async')(ajv); +var ajv = new Ajv() +require("ajv-async")(ajv) // in the browser if you want to load ajv-async bundle separately you can: // window.ajvAsync(ajv); -var validate = ajv.compile(schema); // transpiled es7 async function -validate(data).then(successFunc).catch(errorFunc); +var validate = ajv.compile(schema) // transpiled es7 async function +validate(data).then(successFunc).catch(errorFunc) ``` - #### Using other transpilers ```javascript -var ajv = new Ajv({ processCode: transpileFunc }); -var validate = ajv.compile(schema); // transpiled es7 async function -validate(data).then(successFunc).catch(errorFunc); +var ajv = new Ajv({processCode: transpileFunc}) +var validate = ajv.compile(schema) // transpiled es7 async function +validate(data).then(successFunc).catch(errorFunc) ``` See [Options](#options). - ## Security considerations JSON Schema, if properly used, can replace data sanitisation. It doesn't replace other API security considerations. It also introduces additional security aspects to consider. - ##### Security contact To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues. - ##### Untrusted schemas Ajv treats JSON schemas as trusted as your application code. This security model is based on the most common use case, when the schemas are static and bundled together with the application. If your schemas are received from untrusted sources (or generated from untrusted data) there are several scenarios you need to prevent: + - compiling schemas can cause stack overflow (if they are too deep) - compiling schemas can be slow (e.g. [#557](https://github.com/ajv-validator/ajv/issues/557)) - validating certain data can be slow @@ -700,14 +678,12 @@ It is difficult to predict all the scenarios, but at the very least it may help Regardless the measures you take, using untrusted schemas increases security risks. - ##### Circular references in JavaScript objects Ajv does not support schemas and validated data that have circular references in objects. See [issue #802](https://github.com/ajv-validator/ajv/issues/802). An attempt to compile such schemas or validate such data would cause stack overflow (or will not complete in case of asynchronous validation). Depending on the parser you use, untrusted data can lead to circular references. - ##### Security risks of trusted schemas Some keywords in JSON Schemas can lead to very slow validation for certain data. These keywords include (but may be not limited to): @@ -716,26 +692,27 @@ Some keywords in JSON Schemas can lead to very slow validation for certain data. - `patternProperties` for large property names - use `propertyNames` to mitigate, but some regular expressions can have exponential evaluation time as well. - `uniqueItems` for large non-scalar arrays - use `maxItems` to mitigate -__Please note__: The suggestions above to prevent slow validation would only work if you do NOT use `allErrors: true` in production code (using it would continue validation after validation errors). +**Please note**: The suggestions above to prevent slow validation would only work if you do NOT use `allErrors: true` in production code (using it would continue validation after validation errors). You can validate your JSON schemas against [this meta-schema](https://github.com/ajv-validator/ajv/blob/master/lib/refs/json-schema-secure.json) to check that these recommendations are followed: ```javascript -const isSchemaSecure = ajv.compile(require('ajv/lib/refs/json-schema-secure.json')); +const isSchemaSecure = ajv.compile( + require("ajv/lib/refs/json-schema-secure.json") +) -const schema1 = {format: 'email'}; -isSchemaSecure(schema1); // false +const schema1 = {format: "email"} +isSchemaSecure(schema1) // false -const schema2 = {format: 'email', maxLength: MAX_LENGTH}; -isSchemaSecure(schema2); // true +const schema2 = {format: "email", maxLength: MAX_LENGTH} +isSchemaSecure(schema2) // true ``` -__Please note__: following all these recommendation is not a guarantee that validation of untrusted data is safe - it can still lead to some undesirable results. - +**Please note**: following all these recommendation is not a guarantee that validation of untrusted data is safe - it can still lead to some undesirable results. ##### Content Security Policies (CSP) -See [Ajv and Content Security Policies (CSP)](#ajv-and-content-security-policies-csp) +See [Ajv and Content Security Policies (CSP)](#ajv-and-content-security-policies-csp) ## ReDoS attack @@ -743,7 +720,7 @@ Certain regular expressions can lead to the exponential evaluation time even wit Please assess the regular expressions you use in the schemas on their vulnerability to this attack - see [safe-regex](https://github.com/substack/safe-regex), for example. -__Please note__: some formats that Ajv implements use [regular expressions](https://github.com/ajv-validator/ajv/blob/master/lib/compile/formats.js) that can be vulnerable to ReDoS attack, so if you use Ajv to validate data from untrusted sources __it is strongly recommended__ to consider the following: +**Please note**: some formats that Ajv implements use [regular expressions](https://github.com/ajv-validator/ajv/blob/master/lib/compile/formats.js) that can be vulnerable to ReDoS attack, so if you use Ajv to validate data from untrusted sources **it is strongly recommended** to consider the following: - making assessment of "format" implementations in Ajv. - using `format: 'fast'` option that simplifies some of the regular expressions (although it does not guarantee that they are safe). @@ -752,7 +729,6 @@ __Please note__: some formats that Ajv implements use [regular expressions](http Whatever mitigation you choose, please assume all formats provided by Ajv as potentially unsafe and make your own assessment of their suitability for your validation scenarios. - ## Filtering data With [option `removeAdditional`](#options) (added by [andyscott](https://github.com/andyscott)) you can filter data during the validation. @@ -762,40 +738,40 @@ This option modifies original data. Example: ```javascript -var ajv = new Ajv({ removeAdditional: true }); +var ajv = new Ajv({removeAdditional: true}) var schema = { - "additionalProperties": false, - "properties": { - "foo": { "type": "number" }, - "bar": { - "additionalProperties": { "type": "number" }, - "properties": { - "baz": { "type": "string" } - } - } - } + additionalProperties: false, + properties: { + foo: {type: "number"}, + bar: { + additionalProperties: {type: "number"}, + properties: { + baz: {type: "string"}, + }, + }, + }, } var data = { - "foo": 0, - "additional1": 1, // will be removed; `additionalProperties` == false - "bar": { - "baz": "abc", - "additional2": 2 // will NOT be removed; `additionalProperties` != false + foo: 0, + additional1: 1, // will be removed; `additionalProperties` == false + bar: { + baz: "abc", + additional2: 2, // will NOT be removed; `additionalProperties` != false }, } -var validate = ajv.compile(schema); +var validate = ajv.compile(schema) -console.log(validate(data)); // true -console.log(data); // { "foo": 0, "bar": { "baz": "abc", "additional2": 2 } +console.log(validate(data)) // true +console.log(data) // { "foo": 0, "bar": { "baz": "abc", "additional2": 2 } ``` If `removeAdditional` option in the example above were `"all"` then both `additional1` and `additional2` properties would have been removed. If the option were `"failing"` then property `additional1` would have been removed regardless of its value and property `additional2` would have been removed only if its value were failing the schema in the inner `additionalProperties` (so in the example above it would have stayed because it passes the schema, but any non-number would have been removed). -__Please note__: If you use `removeAdditional` option with `additionalProperties` keyword inside `anyOf`/`oneOf` keywords your validation can fail with this schema, for example: +**Please note**: If you use `removeAdditional` option with `additionalProperties` keyword inside `anyOf`/`oneOf` keywords your validation can fail with this schema, for example: ```json { @@ -803,16 +779,16 @@ __Please note__: If you use `removeAdditional` option with `additionalProperties "oneOf": [ { "properties": { - "foo": { "type": "string" } + "foo": {"type": "string"} }, - "required": [ "foo" ], + "required": ["foo"], "additionalProperties": false }, { "properties": { - "bar": { "type": "integer" } + "bar": {"type": "integer"} }, - "required": [ "bar" ], + "required": ["bar"], "additionalProperties": false } ] @@ -829,20 +805,16 @@ While this behaviour is unexpected (issues [#129](https://github.com/ajv-validat { "type": "object", "properties": { - "foo": { "type": "string" }, - "bar": { "type": "integer" } + "foo": {"type": "string"}, + "bar": {"type": "integer"} }, "additionalProperties": false, - "oneOf": [ - { "required": [ "foo" ] }, - { "required": [ "bar" ] } - ] + "oneOf": [{"required": ["foo"]}, {"required": ["bar"]}] } ``` The schema above is also more efficient - it will compile into a faster function. - ## Assigning defaults With [option `useDefaults`](#options) Ajv will assign values from `default` keyword in the schemas of `properties` and `items` (when it is the array of schemas) to the missing properties and items. @@ -851,47 +823,43 @@ With the option value `"empty"` properties and items equal to `null` or `""` (em This option modifies original data. -__Please note__: the default value is inserted in the generated validation code as a literal, so the value inserted in the data will be the deep clone of the default in the schema. - +**Please note**: the default value is inserted in the generated validation code as a literal, so the value inserted in the data will be the deep clone of the default in the schema. Example 1 (`default` in `properties`): ```javascript -var ajv = new Ajv({ useDefaults: true }); +var ajv = new Ajv({useDefaults: true}) var schema = { - "type": "object", - "properties": { - "foo": { "type": "number" }, - "bar": { "type": "string", "default": "baz" } + type: "object", + properties: { + foo: {type: "number"}, + bar: {type: "string", default: "baz"}, }, - "required": [ "foo", "bar" ] -}; + required: ["foo", "bar"], +} -var data = { "foo": 1 }; +var data = {foo: 1} -var validate = ajv.compile(schema); +var validate = ajv.compile(schema) -console.log(validate(data)); // true -console.log(data); // { "foo": 1, "bar": "baz" } +console.log(validate(data)) // true +console.log(data) // { "foo": 1, "bar": "baz" } ``` Example 2 (`default` in `items`): ```javascript var schema = { - "type": "array", - "items": [ - { "type": "number" }, - { "type": "string", "default": "foo" } - ] + type: "array", + items: [{type: "number"}, {type: "string", default: "foo"}], } -var data = [ 1 ]; +var data = [1] -var validate = ajv.compile(schema); +var validate = ajv.compile(schema) -console.log(validate(data)); // true -console.log(data); // [ 1, "foo" ] +console.log(validate(data)) // true +console.log(data) // [ 1, "foo" ] ``` `default` keywords in other cases are ignored: @@ -903,68 +871,64 @@ console.log(data); // [ 1, "foo" ] The [`strictDefaults` option](#options) customizes Ajv's behavior for the defaults that Ajv ignores (`true` raises an error, and `"log"` outputs a warning). - ## Coercing data types When you are validating user inputs all your data properties are usually strings. The option `coerceTypes` allows you to have your data types coerced to the types specified in your schema `type` keywords, both to pass the validation and to use the correctly typed data afterwards. This option modifies original data. -__Please note__: if you pass a scalar value to the validating function its type will be coerced and it will pass the validation, but the value of the variable you pass won't be updated because scalars are passed by value. - +**Please note**: if you pass a scalar value to the validating function its type will be coerced and it will pass the validation, but the value of the variable you pass won't be updated because scalars are passed by value. Example 1: ```javascript -var ajv = new Ajv({ coerceTypes: true }); +var ajv = new Ajv({coerceTypes: true}) var schema = { - "type": "object", - "properties": { - "foo": { "type": "number" }, - "bar": { "type": "boolean" } + type: "object", + properties: { + foo: {type: "number"}, + bar: {type: "boolean"}, }, - "required": [ "foo", "bar" ] -}; + required: ["foo", "bar"], +} -var data = { "foo": "1", "bar": "false" }; +var data = {foo: "1", bar: "false"} -var validate = ajv.compile(schema); +var validate = ajv.compile(schema) -console.log(validate(data)); // true -console.log(data); // { "foo": 1, "bar": false } +console.log(validate(data)) // true +console.log(data) // { "foo": 1, "bar": false } ``` Example 2 (array coercions): ```javascript -var ajv = new Ajv({ coerceTypes: 'array' }); +var ajv = new Ajv({coerceTypes: "array"}) var schema = { - "properties": { - "foo": { "type": "array", "items": { "type": "number" } }, - "bar": { "type": "boolean" } - } -}; + properties: { + foo: {type: "array", items: {type: "number"}}, + bar: {type: "boolean"}, + }, +} -var data = { "foo": "1", "bar": ["false"] }; +var data = {foo: "1", bar: ["false"]} -var validate = ajv.compile(schema); +var validate = ajv.compile(schema) -console.log(validate(data)); // true -console.log(data); // { "foo": [1], "bar": false } +console.log(validate(data)) // true +console.log(data) // { "foo": [1], "bar": false } ``` The coercion rules, as you can see from the example, are different from JavaScript both to validate user input as expected and to have the coercion reversible (to correctly validate cases where different types are defined in subschemas of "anyOf" and other compound keywords). See [Coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.md) for details. - ## API ##### new Ajv(Object options) -> Object Create Ajv instance. - ##### .compile(Object schema) -> Function<Object data> Generate validating function and cache the compiled schema for future use. @@ -973,8 +937,7 @@ Validating function returns a boolean value. This function has properties `error The schema passed to this method will be validated against meta-schema unless `validateSchema` option is false. If schema is invalid, an error will be thrown. See [options](#options). - -##### .compileAsync(Object schema [, Boolean meta] [, Function callback]) -> Promise +##### .compileAsync(Object schema [, Boolean meta][, function callback]) -> Promise Asynchronous version of `compile` method that loads missing remote schemas using asynchronous function in `options.loadSchema`. This function returns a Promise that resolves to a validation function. An optional callback passed to `compileAsync` will be called with 2 parameters: error (or null) and validating function. The returned promise will reject (and the callback will be called with an error) when: @@ -988,7 +951,6 @@ You can asynchronously compile meta-schema by passing `true` as the second param See example in [Asynchronous compilation](#asynchronous-schema-compilation). - ##### .validate(Object schema|String key|String ref, data) -> Boolean Validate data using passed schema (it will be compiled and cached). @@ -997,11 +959,10 @@ Instead of the schema you can use the key that was previously passed to `addSche Validation errors will be available in the `errors` property of Ajv instance (`null` if there were no errors). -__Please note__: every time this method is called the errors are overwritten so you need to copy them to another variable if you want to use them later. +**Please note**: every time this method is called the errors are overwritten so you need to copy them to another variable if you want to use them later. If the schema is asynchronous (has `$async` keyword on the top level) this method returns a Promise. See [Asynchronous validation](#asynchronous-validation). - ##### .addSchema(Array<Object>|Object schema [, String key]) -> Ajv Add schema(s) to validator instance. This method does not compile schemas (but it still validates them). Because of that dependencies can be added in any order and circular dependencies are supported. It also prevents unnecessary compilation of schemas that are containers for other schemas but not used as a whole. @@ -1010,18 +971,17 @@ Array of schemas can be passed (schemas should have ids), the second parameter w Key can be passed that can be used to reference the schema and will be used as the schema id if there is no id inside the schema. If the key is not passed, the schema id will be used as the key. - Once the schema is added, it (and all the references inside it) can be referenced in other schemas and used to validate data. Although `addSchema` does not compile schemas, explicit compilation is not required - the schema will be compiled when it is used first time. By default the schema is validated against meta-schema before it is added, and if the schema does not pass validation the exception is thrown. This behaviour is controlled by `validateSchema` option. -__Please note__: Ajv uses the [method chaining syntax](https://en.wikipedia.org/wiki/Method_chaining) for all methods with the prefix `add*` and `remove*`. +**Please note**: Ajv uses the [method chaining syntax](https://en.wikipedia.org/wiki/Method_chaining) for all methods with the prefix `add*` and `remove*`. This allows you to do nice things like the following. ```javascript -var validate = new Ajv().addSchema(schema).addFormat(name, regex).getSchema(uri); +var validate = new Ajv().addSchema(schema).addFormat(name, regex).getSchema(uri) ``` ##### .addMetaSchema(Array<Object>|Object schema [, String key]) -> Ajv @@ -1030,7 +990,6 @@ Adds meta schema(s) that can be used to validate other schemas. That function sh There is no need to explicitly add draft-07 meta schema (http://json-schema.org/draft-07/schema) - it is added by default, unless option `meta` is set to `false`. You only need to use it if you have a changed meta-schema that you want to use to validate your schemas. See `validateSchema`. - ##### .validateSchema(Object schema) -> Boolean Validates schema. This method should be used to validate schemas rather than `validate` due to the inconsistency of `uri` format in JSON Schema standard. @@ -1043,17 +1002,16 @@ If schema has `$schema` property, then the schema with this id (that should be p Errors will be available at `ajv.errors`. - ##### .getSchema(String key) -> Function<Object data> Retrieve compiled schema previously added with `addSchema` by the key passed to `addSchema` or by its full reference (id). The returned validating function has `schema` property with the reference to the original schema. - ##### .removeSchema([Object schema|String key|String ref|RegExp pattern]) -> Ajv Remove added/cached schema. Even if schema is referenced by other schemas it can be safely removed as dependent schemas have local references. Schema can be removed using: + - key passed to `addSchema` - it's full reference (id) - RegExp that should match schema id or key (meta-schemas won't be removed) @@ -1061,7 +1019,6 @@ Schema can be removed using: If no parameter is passed all schemas but meta-schemas will be removed and the cache will be cleared. - ##### .addFormat(String name, String|RegExp|Function|Object format) -> Ajv Add custom format to validate strings or numbers. It can also be used to replace pre-defined formats for Ajv instance. @@ -1079,7 +1036,6 @@ If object is passed it should have properties `validate`, `compare` and `async`: Custom formats can be also added via `formats` option. - ##### .addKeyword(String keyword, Object definition) -> Ajv Add custom validation keyword to Ajv instance. @@ -1090,6 +1046,7 @@ Keyword must start with a letter, `_` or `$`, and may continue with letters, num It is recommended to use an application-specific prefix for keywords to avoid current and future name collisions. Example Keywords: + - `"xyz-example"`: valid, and uses prefix for the xyz project to avoid name collisions. - `"example"`: valid, but not recommended as it could collide with future versions of JSON Schema etc. - `"3-example"`: invalid as numbers are not allowed to be the first character in a keyword @@ -1107,30 +1064,27 @@ Keyword definition is an object with the following properties: - _modifying_: `true` MUST be passed if keyword modifies data - _statements_: `true` can be passed in case inline keyword generates statements (as opposed to expression) - _valid_: pass `true`/`false` to pre-define validation result, the result returned from validation function will be ignored. This option cannot be used with macro keywords. -- _$data_: an optional `true` value to support [$data reference](#data-reference) as the value of custom keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has validating function (as the only option or in addition to compile, macro or inline function). +- _\$data_: an optional `true` value to support [\$data reference](#data-reference) as the value of custom keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has validating function (as the only option or in addition to compile, macro or inline function). - _async_: an optional `true` value if the validation function is asynchronous (whether it is compiled or passed in _validate_ property); in this case it should return a promise that resolves with a value `true` or `false`. This option is ignored in case of "macro" and "inline" keywords. - _errors_: an optional boolean or string `"full"` indicating whether keyword returns errors. If this property is not set Ajv will determine if the errors were set in case of failed validation. -_compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. _validate_ can be used separately or in addition to them to support $data reference. +_compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. _validate_ can be used separately or in addition to them to support \$data reference. -__Please note__: If the keyword is validating data type that is different from the type(s) in its definition, the validation function will not be called (and expanded macro will not be used), so there is no need to check for data type inside validation function or inside schema returned by macro function (unless you want to enforce a specific type and for some reason do not want to use a separate `type` keyword for that). In the same way as standard keywords work, if the keyword does not apply to the data type being validated, the validation of this keyword will succeed. +**Please note**: If the keyword is validating data type that is different from the type(s) in its definition, the validation function will not be called (and expanded macro will not be used), so there is no need to check for data type inside validation function or inside schema returned by macro function (unless you want to enforce a specific type and for some reason do not want to use a separate `type` keyword for that). In the same way as standard keywords work, if the keyword does not apply to the data type being validated, the validation of this keyword will succeed. See [Defining custom keywords](#defining-custom-keywords) for more details. - ##### .getKeyword(String keyword) -> Object|Boolean Returns custom keyword definition, `true` for pre-defined keywords and `false` if the keyword is unknown. - ##### .removeKeyword(String keyword) -> Ajv Removes custom or pre-defined keyword so you can redefine them. While this method can be used to extend pre-defined keywords, it can also be used to completely change their meaning - it may lead to unexpected results. -__Please note__: schemas compiled before the keyword is removed will continue to work without changes. To recompile schemas use `removeSchema` method and compile them again. - +**Please note**: schemas compiled before the keyword is removed will continue to work without changes. To recompile schemas use `removeSchema` method and compile them again. ##### .errorsText([Array<Object> errors [, Object options]]) -> String @@ -1138,7 +1092,6 @@ Returns the text with all errors in a String. Options can have properties `separator` (string used to separate errors, ", " by default) and `dataVar` (the variable name that dataPaths are prefixed with, "data" by default). - ## Options Defaults: @@ -1154,7 +1107,7 @@ Defaults: uniqueItems: true, unicode: true, nullable: false, - format: 'fast', + format: false, formats: {}, unknownFormats: true, schemas: {}, @@ -1194,11 +1147,11 @@ Defaults: ##### Validation and reporting options -- _$data_: support [$data references](#data-reference). Draft 6 meta-schema that is added by default will be extended to allow them. If you want to use another meta-schema you need to use $dataMetaSchema method to add support for $data reference. See [API](#api). +- _\$data_: support [\$data references](#data-reference). Draft 6 meta-schema that is added by default will be extended to allow them. If you want to use another meta-schema you need to use $dataMetaSchema method to add support for $data reference. See [API](#api). - _allErrors_: check all rules collecting all errors. Default is to return after the first error. - _verbose_: include the reference to the part of the schema (`schema` and `parentSchema`) and validated data in errors (false by default). -- _$comment_ (NEW in Ajv version 6.0): log or pass the value of `$comment` keyword to a function. Option values: - - `false` (default): ignore $comment keyword. +- _\$comment_ (NEW in Ajv version 6.0): log or pass the value of `$comment` keyword to a function. Option values: + - `false` (default): ignore \$comment keyword. - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function - _jsonPointers_: set `dataPath` property of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation. @@ -1212,15 +1165,14 @@ Defaults: - _formats_: an object with custom formats. Keys and values will be passed to `addFormat` method. - _keywords_: an object with custom keywords. Keys and values will be passed to `addKeyword` method. - _unknownFormats_: handling of unknown formats. Option values: - - `true` (default) - if an unknown format is encountered the exception is thrown during schema compilation. If `format` keyword value is [$data reference](#data-reference) and it is unknown the validation will fail. - - `[String]` - an array of unknown format names that will be ignored. This option can be used to allow usage of third party schemas with format(s) for which you don't have definitions, but still fail if another unknown format is used. If `format` keyword value is [$data reference](#data-reference) and it is not in this array the validation will fail. + - `true` (default) - if an unknown format is encountered the exception is thrown during schema compilation. If `format` keyword value is [\$data reference](#data-reference) and it is unknown the validation will fail. + - `[String]` - an array of unknown format names that will be ignored. This option can be used to allow usage of third party schemas with format(s) for which you don't have definitions, but still fail if another unknown format is used. If `format` keyword value is [\$data reference](#data-reference) and it is not in this array the validation will fail. - `"ignore"` - to log warning during schema compilation and always pass validation (the default behaviour in versions before 5.0.0). This option is not recommended, as it allows to mistype format name and it won't be validated without any error message. This behaviour is required by JSON Schema specification. - _schemas_: an array or object of schemas that will be added to the instance. In case you pass the array the schemas must have IDs in them. When the object is passed the method `addSchema(value, key)` will be called for each schema in this object. - _logger_: sets the logging method. Default is the global `console` object that should have methods `log`, `warn` and `error`. See [Error logging](#error-logging). Option values: - custom logger - it should have methods `log`, `warn` and `error`. If any of these methods is missing an exception will be thrown. - `false` - logging is disabled. - ##### Referenced schema options - _schemaId_: this option defines which keywords are used as schema URI. Option value: @@ -1237,7 +1189,6 @@ Defaults: - `true` - validate all keywords in the schemas with `$ref` (the default behaviour in versions before 5.0.0). - _loadSchema_: asynchronous function that will be used to load remote schemas when `compileAsync` [method](#api-compileAsync) is used and some reference is missing (option `missingRefs` should NOT be 'fail' or 'ignore'). This function should accept remote schema uri as a parameter and return a Promise that resolves to a schema. See example in [Asynchronous compilation](#asynchronous-schema-compilation). - ##### Options to modify validated data - _removeAdditional_: remove additional properties - see example in [Filtering data](#filtering-data). This option is not used if schema is added with `addMetaSchema` method. Option values: @@ -1255,7 +1206,6 @@ Defaults: - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). - ##### Strict mode options - _strictDefaults_: report ignored `default` keywords in schemas. Option values: @@ -1277,12 +1227,11 @@ Defaults: - `true` - always transpile with nodent. - `false` - do not transpile; if async functions are not supported an exception will be thrown. - ##### Advanced options - _meta_: add [meta-schema](http://json-schema.org/documentation.html) so it can be used by other schemas (true by default). If an object is passed, it will be used as the default meta-schema for schemas that have no `$schema` keyword. This default meta-schema MUST have `$schema` keyword. - _validateSchema_: validate added/compiled schemas against meta-schema (true by default). `$schema` property in the schema can be http://json-schema.org/draft-07/schema or absent (draft-07 meta-schema will be used) or can be a reference to the schema previously added with `addMetaSchema` method. Option values: - - `true` (default) - if the validation fails, throw the exception. + - `true` (default) - if the validation fails, throw the exception. - `"log"` - if the validation fails, log error. - `false` - skip schema validation. - _addUsedSchema_: by default methods `compile` and `validate` add schemas to the instance if they have `$id` (or `id`) property that doesn't start with "#". If `$id` is present and it is not unique the exception will be thrown. Set this option to `false` to skip adding schemas to the instance and the `$id` uniqueness check when these methods are used. This option does not affect `addSchema` method. @@ -1303,12 +1252,10 @@ Defaults: - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)`, `del(key)` and `clear()`. - _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used. - ## Validation errors In case of validation failure, Ajv assigns the array of errors to `errors` property of validation function (or to `errors` property of Ajv instance when `validate` or `validateSchema` methods were called). In case of [asynchronous validation](#asynchronous-validation), the returned promise is rejected with exception `Ajv.ValidationError` that has `errors` property. - ### Error objects Each error is an object with the following properties: @@ -1322,8 +1269,7 @@ Each error is an object with the following properties: - _parentSchema_: the schema containing the keyword (added with `verbose` option) - _data_: the data validated by the keyword (added with `verbose` option). -__Please note__: `propertyNames` keyword schema validation errors have an additional property `propertyName`, `dataPath` points to the object. After schema validation for each property name, if it is invalid an additional error is added with the property `keyword` equal to `"propertyNames"`. - +**Please note**: `propertyNames` keyword schema validation errors have an additional property `propertyName`, `dataPath` points to the object. After schema validation for each property name, if it is invalid an additional error is added with the property `keyword` equal to `"propertyNames"`. ### Error parameters @@ -1355,29 +1301,28 @@ Properties of `params` object in errors depend on the keyword that failed valida - `oneOf` - property `passingSchemas` (array of indices of passing schemas, null if no schema passes). - custom keywords (in case keyword definition doesn't create errors) - property `keyword` (the keyword name). - ### Error logging Using the `logger` option when initiallizing Ajv will allow you to define custom logging. Here you can build upon the exisiting logging. The use of other logging packages is supported as long as the package or its associated wrapper exposes the required methods. If any of the required methods are missing an exception will be thrown. + - **Required Methods**: `log`, `warn`, `error` ```javascript -var otherLogger = new OtherLogger(); +var otherLogger = new OtherLogger() var ajv = new Ajv({ logger: { log: console.log.bind(console), warn: function warn() { - otherLogger.logWarn.apply(otherLogger, arguments); + otherLogger.logWarn.apply(otherLogger, arguments) }, error: function error() { - otherLogger.logError.apply(otherLogger, arguments); - console.error.apply(console, arguments); - } - } -}); + otherLogger.logError.apply(otherLogger, arguments) + console.error.apply(console, arguments) + }, + }, +}) ``` - ## Plugins Ajv can be extended with plugins that add custom keywords, formats or functions to process generated code. When such plugin is published as npm package it is recommended that it follows these conventions: @@ -1388,7 +1333,6 @@ Ajv can be extended with plugins that add custom keywords, formats or functions If you have published a useful plugin please submit a PR to add it to the next section. - ## Related packages - [ajv-async](https://github.com/ajv-validator/ajv-async) - plugin to configure async validation mode @@ -1426,7 +1370,6 @@ If you have published a useful plugin please submit a PR to add it to the next s - [gh-pages-generator](https://github.com/epoberezkin/gh-pages-generator) - multi-page site generator converting markdown files to GitHub pages - [ESLint](https://github.com/eslint/eslint) - the pluggable linting utility for JavaScript and JSX - ## Tests ``` @@ -1445,12 +1388,11 @@ All validation functions are generated using doT templates in [dot](https://gith Please see [Contributing guidelines](https://github.com/ajv-validator/ajv/blob/master/CONTRIBUTING.md) - ## Changes history See https://github.com/ajv-validator/ajv/releases -__Please note__: [Changes in version 6.0.0](https://github.com/ajv-validator/ajv/releases/tag/v6.0.0). +**Please note**: [Changes in version 6.0.0](https://github.com/ajv-validator/ajv/releases/tag/v6.0.0). [Version 5.0.0](https://github.com/ajv-validator/ajv/releases/tag/5.0.0). @@ -1460,19 +1402,16 @@ __Please note__: [Changes in version 6.0.0](https://github.com/ajv-validator/ajv [Version 2.0.0](https://github.com/ajv-validator/ajv/releases/tag/2.0.0). - ## Code of conduct Please review and follow the [Code of conduct](https://github.com/ajv-validator/ajv/blob/master/CODE_OF_CONDUCT.md). Please report any unacceptable behaviour to ajv.validator@gmail.com - it will be reviewed by the project team. - ## Open-source software support Ajv is a part of [Tidelift subscription](https://tidelift.com/subscription/pkg/npm-ajv?utm_source=npm-ajv&utm_medium=referral&utm_campaign=readme) - it provides a centralised support to open-source software users, in addition to the support provided by software maintainers. - ## License [MIT](https://github.com/ajv-validator/ajv/blob/master/LICENSE) diff --git a/bower.json b/bower.json index 507989c626..6e8d8a893a 100644 --- a/bower.json +++ b/bower.json @@ -2,24 +2,10 @@ "name": "ajv", "description": "Another JSON Schema Validator", "main": "dist/ajv.min.js", - "authors": [ - "Evgeny Poberezkin" - ], + "authors": ["Evgeny Poberezkin"], "license": "MIT", - "keywords": [ - "JSON", - "schema", - "validator" - ], + "keywords": ["JSON", "schema", "validator"], "homepage": "https://github.com/ajv-validator/ajv", - "moduleType": [ - "amd", - "globals", - "node" - ], - "ignore": [ - "node_modules", - "bower_components", - "spec" - ] + "moduleType": ["amd", "globals", "node"], + "ignore": ["node_modules", "bower_components", "spec"] } diff --git a/karma.conf.js b/karma.conf.js index 155d0874ee..5c2a3eb1c5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,50 +1,42 @@ // Karma configuration // Generated on Thu Mar 13 2014 14:12:04 GMT-0700 (PDT) -module.exports = function(config) { +module.exports = function (config) { config.set({ - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - + basePath: "", // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'], - + frameworks: ["mocha"], // list of files / patterns to load in the browser files: [ - 'dist/ajv.min.js', - 'node_modules/chai/chai.js', - 'node_modules/ajv-async/dist/ajv-async.min.js', - 'node_modules/bluebird/js/browser/bluebird.core.min.js', - '.browser/*.spec.js' + "dist/ajv.min.js", + "node_modules/chai/chai.js", + "node_modules/ajv-async/dist/ajv-async.min.js", + "node_modules/bluebird/js/browser/bluebird.core.min.js", + ".browser/*.spec.js", ], // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['dots'], - + reporters: ["dots"], // web server port port: 9876, - // enable / disable colors in the output (reporters and logs) colors: true, - // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, - // enable / disable watching file and executing tests whenever any file changes autoWatch: false, - // Start these browsers, currently available: // - Chrome // - ChromeCanary @@ -53,18 +45,18 @@ module.exports = function(config) { // - Safari (only Mac) // - PhantomJS // - IE (only Windows) - browsers: ['HeadlessChrome'], + browsers: ["HeadlessChrome"], customLaunchers: { - HeadlessChrome:{ - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - }, + HeadlessChrome: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, }, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: true, - browserNoActivityTimeout: 90000 - }); -}; + browserNoActivityTimeout: 90000, + }) +} diff --git a/karma.sauce.js b/karma.sauce.js index fac172e36f..dc54aa486d 100644 --- a/karma.sauce.js +++ b/karma.sauce.js @@ -1,104 +1,99 @@ -'use strict'; +"use strict" -var fs = require('fs'); - -module.exports = function(config) { +var fs = require("fs") +module.exports = function (config) { // Use ENV vars on Travis and sauce.json locally to get credentials if (!process.env.SAUCE_USERNAME) { - if (!fs.existsSync('sauce.json')) { - console.log('Create a sauce.json with your credentials based on the sauce-sample.json file.'); - process.exit(1); + if (!fs.existsSync("sauce.json")) { + console.log( + "Create a sauce.json with your credentials based on the sauce-sample.json file." + ) + process.exit(1) } else { - process.env.SAUCE_USERNAME = require('./sauce').username; - process.env.SAUCE_ACCESS_KEY = require('./sauce').accessKey; + process.env.SAUCE_USERNAME = require("./sauce").username + process.env.SAUCE_ACCESS_KEY = require("./sauce").accessKey } } // Browsers to run on Sauce Labs var customLaunchers = { - 'SL_Chrome_27': { - base: 'SauceLabs', - browserName: 'chrome', - version: '27' + SL_Chrome_27: { + base: "SauceLabs", + browserName: "chrome", + version: "27", }, - 'SL_Chrome': { - base: 'SauceLabs', - browserName: 'chrome' + SL_Chrome: { + base: "SauceLabs", + browserName: "chrome", }, - 'SL_InternetExplorer_10': { - base: 'SauceLabs', - browserName: 'internet explorer', - version: '10' + SL_InternetExplorer_10: { + base: "SauceLabs", + browserName: "internet explorer", + version: "10", }, - 'SL_InternetExplorer': { - base: 'SauceLabs', - browserName: 'internet explorer' + SL_InternetExplorer: { + base: "SauceLabs", + browserName: "internet explorer", }, - 'SL_MicrosoftEdge': { - base: 'SauceLabs', - browserName: 'MicrosoftEdge' + SL_MicrosoftEdge: { + base: "SauceLabs", + browserName: "MicrosoftEdge", }, - 'SL_FireFox_17': { - base: 'SauceLabs', - browserName: 'firefox', - version: '17' + SL_FireFox_17: { + base: "SauceLabs", + browserName: "firefox", + version: "17", }, - 'SL_FireFox': { - base: 'SauceLabs', - browserName: 'firefox' + SL_FireFox: { + base: "SauceLabs", + browserName: "firefox", }, - 'SL_Safari_7': { - base: 'SauceLabs', - browserName: 'safari', - version: '7' + SL_Safari_7: { + base: "SauceLabs", + browserName: "safari", + version: "7", }, - 'SL_Safari': { - base: 'SauceLabs', - browserName: 'safari' + SL_Safari: { + base: "SauceLabs", + browserName: "safari", }, - 'SL_iPhone_8': { - base: 'SauceLabs', - browserName: 'iphone', - version: '8.4' + SL_iPhone_8: { + base: "SauceLabs", + browserName: "iphone", + version: "8.4", }, - 'SL_iPhone': { - base: 'SauceLabs', - browserName: 'iphone' + SL_iPhone: { + base: "SauceLabs", + browserName: "iphone", }, - 'SL_Android': { - base: 'SauceLabs', - browserName: 'android' - } - }; - + SL_Android: { + base: "SauceLabs", + browserName: "android", + }, + } config.set({ - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - + basePath: "", // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'], - + frameworks: ["mocha"], // list of files / patterns to load in the browser files: [ - 'dist/ajv.min.js', - 'node_modules/chai/chai.js', - 'node_modules/ajv-async/dist/ajv-async.min.js', - 'node_modules/bluebird/js/browser/bluebird.core.min.js', - '.browser/*.spec.js' + "dist/ajv.min.js", + "node_modules/chai/chai.js", + "node_modules/ajv-async/dist/ajv-async.min.js", + "node_modules/bluebird/js/browser/bluebird.core.min.js", + ".browser/*.spec.js", ], - // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['dots', 'saucelabs'], - + reporters: ["dots", "saucelabs"], // web server port port: 9876, @@ -110,8 +105,8 @@ module.exports = function(config) { logLevel: config.LOG_INFO, sauceLabs: { - testName: 'Ajv', - 'idleTimeout': 900 + testName: "Ajv", + idleTimeout: 900, }, captureTimeout: 1200000, browserNoActivityTimeout: 600000, @@ -122,6 +117,6 @@ module.exports = function(config) { // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: Object.keys(customLaunchers), - singleRun: true - }); -}; + singleRun: true, + }) +} diff --git a/lib/ajv.d.ts b/lib/ajv.d.ts index cc2881b33b..57ac813893 100644 --- a/lib/ajv.d.ts +++ b/lib/ajv.d.ts @@ -1,148 +1,158 @@ declare var ajv: { - (options?: ajv.Options): ajv.Ajv; - new(options?: ajv.Options): ajv.Ajv; - ValidationError: typeof AjvErrors.ValidationError; - MissingRefError: typeof AjvErrors.MissingRefError; - $dataMetaSchema: object; + (options?: ajv.Options): ajv.Ajv + new (options?: ajv.Options): ajv.Ajv + ValidationError: typeof AjvErrors.ValidationError + MissingRefError: typeof AjvErrors.MissingRefError + $dataMetaSchema: object } declare namespace AjvErrors { class ValidationError extends Error { - constructor(errors: Array); + constructor(errors: Array) - message: string; - errors: Array; - ajv: true; - validation: true; + message: string + errors: Array + ajv: true + validation: true } class MissingRefError extends Error { - constructor(baseId: string, ref: string, message?: string); - static message: (baseId: string, ref: string) => string; + constructor(baseId: string, ref: string, message?: string) + static message: (baseId: string, ref: string) => string - message: string; - missingRef: string; - missingSchema: string; + message: string + missingRef: string + missingSchema: string } } declare namespace ajv { - type ValidationError = AjvErrors.ValidationError; + type ValidationError = AjvErrors.ValidationError - type MissingRefError = AjvErrors.MissingRefError; + type MissingRefError = AjvErrors.MissingRefError interface Ajv { /** - * Validate data using schema - * Schema will be compiled and cached (using serialized JSON as key, [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize by default). - * @param {string|object|Boolean} schemaKeyRef key, ref or schema object - * @param {Any} data to be validated - * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). - */ - validate(schemaKeyRef: object | string | boolean, data: any): boolean | PromiseLike; + * Validate data using schema + * Schema will be compiled and cached (using serialized JSON as key, [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize by default). + * @param {string|object|Boolean} schemaKeyRef key, ref or schema object + * @param {Any} data to be validated + * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). + */ + validate( + schemaKeyRef: object | string | boolean, + data: any + ): boolean | PromiseLike /** - * Create validating function for passed schema. - * @param {object|Boolean} schema schema object - * @return {Function} validating function - */ - compile(schema: object | boolean): ValidateFunction; + * Create validating function for passed schema. + * @param {object|Boolean} schema schema object + * @return {Function} validating function + */ + compile(schema: object | boolean): ValidateFunction /** - * Creates validating function for passed schema with asynchronous loading of missing schemas. - * `loadSchema` option should be a function that accepts schema uri and node-style callback. - * @this Ajv - * @param {object|Boolean} schema schema object - * @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped - * @param {Function} callback optional node-style callback, it is always called with 2 parameters: error (or null) and validating function. - * @return {PromiseLike} validating function - */ - compileAsync(schema: object | boolean, meta?: Boolean, callback?: (err: Error, validate: ValidateFunction) => any): PromiseLike; + * Creates validating function for passed schema with asynchronous loading of missing schemas. + * `loadSchema` option should be a function that accepts schema uri and node-style callback. + * @this Ajv + * @param {object|Boolean} schema schema object + * @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped + * @param {Function} callback optional node-style callback, it is always called with 2 parameters: error (or null) and validating function. + * @return {PromiseLike} validating function + */ + compileAsync( + schema: object | boolean, + meta?: Boolean, + callback?: (err: Error, validate: ValidateFunction) => any + ): PromiseLike /** - * Adds schema to the instance. - * @param {object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. - * @param {string} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. - * @return {Ajv} this for method chaining - */ - addSchema(schema: Array | object, key?: string): Ajv; + * Adds schema to the instance. + * @param {object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. + * @param {string} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. + * @return {Ajv} this for method chaining + */ + addSchema(schema: Array | object, key?: string): Ajv /** - * Add schema that will be used to validate other schemas - * options in META_IGNORE_OPTIONS are alway set to false - * @param {object} schema schema object - * @param {string} key optional schema key - * @return {Ajv} this for method chaining - */ - addMetaSchema(schema: object, key?: string): Ajv; + * Add schema that will be used to validate other schemas + * options in META_IGNORE_OPTIONS are alway set to false + * @param {object} schema schema object + * @param {string} key optional schema key + * @return {Ajv} this for method chaining + */ + addMetaSchema(schema: object, key?: string): Ajv /** - * Validate schema - * @param {object|Boolean} schema schema to validate - * @return {Boolean} true if schema is valid - */ - validateSchema(schema: object | boolean): boolean; + * Validate schema + * @param {object|Boolean} schema schema to validate + * @return {Boolean} true if schema is valid + */ + validateSchema(schema: object | boolean): boolean /** - * Get compiled schema from the instance by `key` or `ref`. - * @param {string} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id). - * @return {Function} schema validating function (with property `schema`). Returns undefined if keyRef can't be resolved to an existing schema. - */ - getSchema(keyRef: string): ValidateFunction | undefined; + * Get compiled schema from the instance by `key` or `ref`. + * @param {string} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id). + * @return {Function} schema validating function (with property `schema`). Returns undefined if keyRef can't be resolved to an existing schema. + */ + getSchema(keyRef: string): ValidateFunction | undefined /** - * Remove cached schema(s). - * If no parameter is passed all schemas but meta-schemas are removed. - * If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. - * Even if schema is referenced by other schemas it still can be removed as other schemas have local references. - * @param {string|object|RegExp|Boolean} schemaKeyRef key, ref, pattern to match key/ref or schema object - * @return {Ajv} this for method chaining - */ - removeSchema(schemaKeyRef?: object | string | RegExp | boolean): Ajv; + * Remove cached schema(s). + * If no parameter is passed all schemas but meta-schemas are removed. + * If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. + * Even if schema is referenced by other schemas it still can be removed as other schemas have local references. + * @param {string|object|RegExp|Boolean} schemaKeyRef key, ref, pattern to match key/ref or schema object + * @return {Ajv} this for method chaining + */ + removeSchema(schemaKeyRef?: object | string | RegExp | boolean): Ajv /** - * Add custom format - * @param {string} name format name - * @param {string|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) - * @return {Ajv} this for method chaining - */ - addFormat(name: string, format: FormatValidator | FormatDefinition): Ajv; + * Add custom format + * @param {string} name format name + * @param {string|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) + * @return {Ajv} this for method chaining + */ + addFormat(name: string, format: FormatValidator | FormatDefinition): Ajv /** - * Define custom keyword - * @this Ajv - * @param {string} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords. - * @param {object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. - * @return {Ajv} this for method chaining - */ - addKeyword(keyword: string, definition: KeywordDefinition): Ajv; + * Define custom keyword + * @this Ajv + * @param {string} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords. + * @param {object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. + * @return {Ajv} this for method chaining + */ + addKeyword(keyword: string, definition: KeywordDefinition): Ajv /** - * Get keyword definition - * @this Ajv - * @param {string} keyword pre-defined or custom keyword. - * @return {object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. - */ - getKeyword(keyword: string): object | boolean; + * Get keyword definition + * @this Ajv + * @param {string} keyword pre-defined or custom keyword. + * @return {object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. + */ + getKeyword(keyword: string): object | boolean /** - * Remove keyword - * @this Ajv - * @param {string} keyword pre-defined or custom keyword. - * @return {Ajv} this for method chaining - */ - removeKeyword(keyword: string): Ajv; + * Remove keyword + * @this Ajv + * @param {string} keyword pre-defined or custom keyword. + * @return {Ajv} this for method chaining + */ + removeKeyword(keyword: string): Ajv /** - * Validate keyword - * @this Ajv - * @param {object} definition keyword definition object - * @param {boolean} throwError true to throw exception if definition is invalid - * @return {boolean} validation result - */ - validateKeyword(definition: KeywordDefinition, throwError: boolean): boolean; + * Validate keyword + * @this Ajv + * @param {object} definition keyword definition object + * @param {boolean} throwError true to throw exception if definition is invalid + * @return {boolean} validation result + */ + validateKeyword(definition: KeywordDefinition, throwError: boolean): boolean /** - * Convert array of error message objects to string - * @param {Array} errors optional array of validation errors, if not passed errors from the instance are used. - * @param {object} options optional options with properties `separator` and `dataVar`. - * @return {string} human readable string with all errors descriptions - */ - errorsText(errors?: Array | null, options?: ErrorsTextOptions): string; - errors?: Array | null; + * Convert array of error message objects to string + * @param {Array} errors optional array of validation errors, if not passed errors from the instance are used. + * @param {object} options optional options with properties `separator` and `dataVar`. + * @return {string} human readable string with all errors descriptions + */ + errorsText( + errors?: Array | null, + options?: ErrorsTextOptions + ): string + errors?: Array | null } interface CustomLogger { - log(...args: any[]): any; - warn(...args: any[]): any; - error(...args: any[]): any; + log(...args: any[]): any + warn(...args: any[]): any + error(...args: any[]): any } interface ValidateFunction { @@ -152,128 +162,147 @@ declare namespace ajv { parentData?: object | Array, parentDataProperty?: string | number, rootData?: object | Array - ): boolean | PromiseLike; - schema?: object | boolean; - errors?: null | Array; - refs?: object; - refVal?: Array; - root?: ValidateFunction | object; - $async?: true; - source?: object; + ): boolean | PromiseLike + schema?: object | boolean + errors?: null | Array + refs?: object + refVal?: Array + root?: ValidateFunction | object + $async?: true + source?: object } interface Options { - $data?: boolean; - allErrors?: boolean; - verbose?: boolean; - jsonPointers?: boolean; - uniqueItems?: boolean; - unicode?: boolean; - format?: false | string; - formats?: object; - keywords?: object; - unknownFormats?: true | string[] | 'ignore'; - schemas?: Array | object; - schemaId?: '$id' | 'id' | 'auto'; - missingRefs?: true | 'ignore' | 'fail'; - extendRefs?: true | 'ignore' | 'fail'; - loadSchema?: (uri: string, cb?: (err: Error, schema: object) => void) => PromiseLike; - removeAdditional?: boolean | 'all' | 'failing'; - useDefaults?: boolean | 'empty' | 'shared'; - coerceTypes?: boolean | 'array'; - strictDefaults?: boolean | 'log'; - strictKeywords?: boolean | 'log'; - strictNumbers?: boolean; - async?: boolean | string; - transpile?: string | ((code: string) => string); - meta?: boolean | object; - validateSchema?: boolean | 'log'; - addUsedSchema?: boolean; - inlineRefs?: boolean | number; - passContext?: boolean; - loopRequired?: number; - ownProperties?: boolean; - multipleOfPrecision?: boolean | number; - errorDataPath?: string, - messages?: boolean; - sourceCode?: boolean; - processCode?: (code: string, schema: object) => string; - cache?: object; - logger?: CustomLogger | false; - nullable?: boolean; - serialize?: ((schema: object | boolean) => any) | false; + $data?: boolean + allErrors?: boolean + verbose?: boolean + jsonPointers?: boolean + uniqueItems?: boolean + unicode?: boolean + format?: false | string + formats?: object + keywords?: object + unknownFormats?: true | string[] | "ignore" + schemas?: Array | object + schemaId?: "$id" | "id" | "auto" + missingRefs?: true | "ignore" | "fail" + extendRefs?: true | "ignore" | "fail" + loadSchema?: ( + uri: string, + cb?: (err: Error, schema: object) => void + ) => PromiseLike + removeAdditional?: boolean | "all" | "failing" + useDefaults?: boolean | "empty" | "shared" + coerceTypes?: boolean | "array" + strictDefaults?: boolean | "log" + strictKeywords?: boolean | "log" + strictNumbers?: boolean + async?: boolean | string + transpile?: string | ((code: string) => string) + meta?: boolean | object + validateSchema?: boolean | "log" + addUsedSchema?: boolean + inlineRefs?: boolean | number + passContext?: boolean + loopRequired?: number + ownProperties?: boolean + multipleOfPrecision?: boolean | number + errorDataPath?: string + messages?: boolean + sourceCode?: boolean + processCode?: (code: string, schema: object) => string + cache?: object + logger?: CustomLogger | false + nullable?: boolean + serialize?: ((schema: object | boolean) => any) | false } - type FormatValidator = string | RegExp | ((data: string) => boolean | PromiseLike); - type NumberFormatValidator = ((data: number) => boolean | PromiseLike); + type FormatValidator = + | string + | RegExp + | ((data: string) => boolean | PromiseLike) + type NumberFormatValidator = (data: number) => boolean | PromiseLike interface NumberFormatDefinition { - type: "number", - validate: NumberFormatValidator; - compare?: (data1: number, data2: number) => number; - async?: boolean; + type: "number" + validate: NumberFormatValidator + compare?: (data1: number, data2: number) => number + async?: boolean } interface StringFormatDefinition { - type?: "string", - validate: FormatValidator; - compare?: (data1: string, data2: string) => number; - async?: boolean; + type?: "string" + validate: FormatValidator + compare?: (data1: string, data2: string) => number + async?: boolean } - type FormatDefinition = NumberFormatDefinition | StringFormatDefinition; + type FormatDefinition = NumberFormatDefinition | StringFormatDefinition interface KeywordDefinition { - type?: string | Array; - async?: boolean; - $data?: boolean; - errors?: boolean | string; - metaSchema?: object; + type?: string | Array + async?: boolean + $data?: boolean + errors?: boolean | string + metaSchema?: object // schema: false makes validate not to expect schema (ValidateFunction) - schema?: boolean; - statements?: boolean; - dependencies?: Array; - modifying?: boolean; - valid?: boolean; + schema?: boolean + statements?: boolean + dependencies?: Array + modifying?: boolean + valid?: boolean // one and only one of the following properties should be present - validate?: SchemaValidateFunction | ValidateFunction; - compile?: (schema: any, parentSchema: object, it: CompilationContext) => ValidateFunction; - macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean; - inline?: (it: CompilationContext, keyword: string, schema: any, parentSchema: object) => string; + validate?: SchemaValidateFunction | ValidateFunction + compile?: ( + schema: any, + parentSchema: object, + it: CompilationContext + ) => ValidateFunction + macro?: ( + schema: any, + parentSchema: object, + it: CompilationContext + ) => object | boolean + inline?: ( + it: CompilationContext, + keyword: string, + schema: any, + parentSchema: object + ) => string } interface CompilationContext { - level: number; - dataLevel: number; - dataPathArr: string[]; - schema: any; - schemaPath: string; - baseId: string; - async: boolean; - opts: Options; + level: number + dataLevel: number + dataPathArr: string[] + schema: any + schemaPath: string + baseId: string + async: boolean + opts: Options formats: { - [index: string]: FormatDefinition | undefined; - }; + [index: string]: FormatDefinition | undefined + } keywords: { - [index: string]: KeywordDefinition | undefined; - }; - compositeRule: boolean; - validate: (schema: object) => boolean; + [index: string]: KeywordDefinition | undefined + } + compositeRule: boolean + validate: (schema: object) => boolean util: { - copy(obj: any, target?: any): any; - toHash(source: string[]): { [index: string]: true | undefined }; - equal(obj: any, target: any): boolean; - getProperty(str: string): string; - schemaHasRules(schema: object, rules: any): string; - escapeQuotes(str: string): string; - toQuotedString(str: string): string; - getData(jsonPointer: string, dataLevel: number, paths: string[]): string; - escapeJsonPointer(str: string): string; - unescapeJsonPointer(str: string): string; - escapeFragment(str: string): string; - unescapeFragment(str: string): string; - }; - self: Ajv; + copy(obj: any, target?: any): any + toHash(source: string[]): {[index: string]: true | undefined} + equal(obj: any, target: any): boolean + getProperty(str: string): string + schemaHasRules(schema: object, rules: any): string + escapeQuotes(str: string): string + toQuotedString(str: string): string + getData(jsonPointer: string, dataLevel: number, paths: string[]): string + escapeJsonPointer(str: string): string + unescapeJsonPointer(str: string): string + escapeFragment(str: string): string + unescapeFragment(str: string): string + } + self: Ajv } interface SchemaValidateFunction { @@ -285,54 +314,67 @@ declare namespace ajv { parentData?: object | Array, parentDataProperty?: string | number, rootData?: object | Array - ): boolean | PromiseLike; - errors?: Array; + ): boolean | PromiseLike + errors?: Array } interface ErrorsTextOptions { - separator?: string; - dataVar?: string; + separator?: string + dataVar?: string } interface ErrorObject { - keyword: string; - dataPath: string; - schemaPath: string; - params: ErrorParameters; + keyword: string + dataPath: string + schemaPath: string + params: ErrorParameters // Added to validation errors of propertyNames keyword schema - propertyName?: string; + propertyName?: string // Excluded if messages set to false. - message?: string; + message?: string // These are added with the `verbose` option. - schema?: any; - parentSchema?: object; - data?: any; + schema?: any + parentSchema?: object + data?: any } - type ErrorParameters = RefParams | LimitParams | AdditionalPropertiesParams | - DependenciesParams | FormatParams | ComparisonParams | - MultipleOfParams | PatternParams | RequiredParams | - TypeParams | UniqueItemsParams | CustomParams | - PatternRequiredParams | PropertyNamesParams | - IfParams | SwitchParams | NoParams | EnumParams; + type ErrorParameters = + | RefParams + | LimitParams + | AdditionalPropertiesParams + | DependenciesParams + | FormatParams + | ComparisonParams + | MultipleOfParams + | PatternParams + | RequiredParams + | TypeParams + | UniqueItemsParams + | CustomParams + | PatternRequiredParams + | PropertyNamesParams + | IfParams + | SwitchParams + | NoParams + | EnumParams interface RefParams { - ref: string; + ref: string } interface LimitParams { - limit: number; + limit: number } interface AdditionalPropertiesParams { - additionalProperty: string; + additionalProperty: string } interface DependenciesParams { - property: string; - missingProperty: string; - depsCount: number; - deps: string; + property: string + missingProperty: string + depsCount: number + deps: string } interface FormatParams { @@ -340,57 +382,57 @@ declare namespace ajv { } interface ComparisonParams { - comparison: string; - limit: number | string; - exclusive: boolean; + comparison: string + limit: number | string + exclusive: boolean } interface MultipleOfParams { - multipleOf: number; + multipleOf: number } interface PatternParams { - pattern: string; + pattern: string } interface RequiredParams { - missingProperty: string; + missingProperty: string } interface TypeParams { - type: string; + type: string } interface UniqueItemsParams { - i: number; - j: number; + i: number + j: number } interface CustomParams { - keyword: string; + keyword: string } interface PatternRequiredParams { - missingPattern: string; + missingPattern: string } interface PropertyNamesParams { - propertyName: string; + propertyName: string } interface IfParams { - failingKeyword: string; + failingKeyword: string } interface SwitchParams { - caseIndex: number; + caseIndex: number } - interface NoParams { } + interface NoParams {} interface EnumParams { - allowedValues: Array; + allowedValues: Array } } -export = ajv; +export = ajv diff --git a/lib/ajv.js b/lib/ajv.js index 06a45b650b..6878427afe 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -1,46 +1,51 @@ -'use strict'; - -var compileSchema = require('./compile') - , resolve = require('./compile/resolve') - , Cache = require('./cache') - , SchemaObject = require('./compile/schema_obj') - , stableStringify = require('fast-json-stable-stringify') - , formats = require('./compile/formats') - , rules = require('./compile/rules') - , $dataMetaSchema = require('./data') - , util = require('./compile/util'); - -module.exports = Ajv; - -Ajv.prototype.validate = validate; -Ajv.prototype.compile = compile; -Ajv.prototype.addSchema = addSchema; -Ajv.prototype.addMetaSchema = addMetaSchema; -Ajv.prototype.validateSchema = validateSchema; -Ajv.prototype.getSchema = getSchema; -Ajv.prototype.removeSchema = removeSchema; -Ajv.prototype.addFormat = addFormat; -Ajv.prototype.errorsText = errorsText; - -Ajv.prototype._addSchema = _addSchema; -Ajv.prototype._compile = _compile; - -Ajv.prototype.compileAsync = require('./compile/async'); -var customKeyword = require('./keyword'); -Ajv.prototype.addKeyword = customKeyword.add; -Ajv.prototype.getKeyword = customKeyword.get; -Ajv.prototype.removeKeyword = customKeyword.remove; -Ajv.prototype.validateKeyword = customKeyword.validate; - -var errorClasses = require('./compile/error_classes'); -Ajv.ValidationError = errorClasses.Validation; -Ajv.MissingRefError = errorClasses.MissingRef; -Ajv.$dataMetaSchema = $dataMetaSchema; - -var META_SCHEMA_ID = 'http://json-schema.org/draft-07/schema'; - -var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes', 'strictDefaults' ]; -var META_SUPPORT_DATA = ['/properties']; +"use strict" + +var compileSchema = require("./compile"), + resolve = require("./compile/resolve"), + Cache = require("./cache"), + SchemaObject = require("./compile/schema_obj"), + stableStringify = require("fast-json-stable-stringify"), + formats = require("./compile/formats"), + rules = require("./compile/rules"), + $dataMetaSchema = require("./data"), + util = require("./compile/util") + +module.exports = Ajv + +Ajv.prototype.validate = validate +Ajv.prototype.compile = compile +Ajv.prototype.addSchema = addSchema +Ajv.prototype.addMetaSchema = addMetaSchema +Ajv.prototype.validateSchema = validateSchema +Ajv.prototype.getSchema = getSchema +Ajv.prototype.removeSchema = removeSchema +Ajv.prototype.addFormat = addFormat +Ajv.prototype.errorsText = errorsText + +Ajv.prototype._addSchema = _addSchema +Ajv.prototype._compile = _compile + +Ajv.prototype.compileAsync = require("./compile/async") +var customKeyword = require("./keyword") +Ajv.prototype.addKeyword = customKeyword.add +Ajv.prototype.getKeyword = customKeyword.get +Ajv.prototype.removeKeyword = customKeyword.remove +Ajv.prototype.validateKeyword = customKeyword.validate + +var errorClasses = require("./compile/error_classes") +Ajv.ValidationError = errorClasses.Validation +Ajv.MissingRefError = errorClasses.MissingRef +Ajv.$dataMetaSchema = $dataMetaSchema + +var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" + +var META_IGNORE_OPTIONS = [ + "removeAdditional", + "useDefaults", + "coerceTypes", + "strictDefaults", +] +var META_SUPPORT_DATA = ["/properties"] /** * Creates validator instance. @@ -49,35 +54,34 @@ var META_SUPPORT_DATA = ['/properties']; * @return {Object} ajv instance */ function Ajv(opts) { - if (!(this instanceof Ajv)) return new Ajv(opts); - opts = this._opts = util.copy(opts) || {}; - setLogger(this); - this._schemas = {}; - this._refs = {}; - this._fragments = {}; - this._formats = formats(opts.format); - - this._cache = opts.cache || new Cache; - this._loadingSchemas = {}; - this._compilations = []; - this.RULES = rules(); - this._getId = chooseGetId(opts); - - opts.loopRequired = opts.loopRequired || Infinity; - if (opts.errorDataPath == 'property') opts._errorDataPathProperty = true; - if (opts.serialize === undefined) opts.serialize = stableStringify; - this._metaOpts = getMetaSchemaOptions(this); - - if (opts.formats) addInitialFormats(this); - if (opts.keywords) addInitialKeywords(this); - addDefaultMetaSchema(this); - if (typeof opts.meta == 'object') this.addMetaSchema(opts.meta); - if (opts.nullable) this.addKeyword('nullable', {metaSchema: {type: 'boolean'}}); - addInitialSchemas(this); + if (!(this instanceof Ajv)) return new Ajv(opts) + opts = this._opts = util.copy(opts) || {} + setLogger(this) + this._schemas = {} + this._refs = {} + this._fragments = {} + this._formats = formats(opts.format) + + this._cache = opts.cache || new Cache() + this._loadingSchemas = {} + this._compilations = [] + this.RULES = rules() + this._getId = chooseGetId(opts) + + opts.loopRequired = opts.loopRequired || Infinity + if (opts.errorDataPath == "property") opts._errorDataPathProperty = true + if (opts.serialize === undefined) opts.serialize = stableStringify + this._metaOpts = getMetaSchemaOptions(this) + + if (opts.formats) addInitialFormats(this) + if (opts.keywords) addInitialKeywords(this) + addDefaultMetaSchema(this) + if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) + if (opts.nullable) + this.addKeyword("nullable", {metaSchema: {type: "boolean"}}) + addInitialSchemas(this) } - - /** * Validate data using schema * Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize. @@ -87,21 +91,20 @@ function Ajv(opts) { * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). */ function validate(schemaKeyRef, data) { - var v; - if (typeof schemaKeyRef == 'string') { - v = this.getSchema(schemaKeyRef); - if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"'); + var v + if (typeof schemaKeyRef == "string") { + v = this.getSchema(schemaKeyRef) + if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"') } else { - var schemaObj = this._addSchema(schemaKeyRef); - v = schemaObj.validate || this._compile(schemaObj); + var schemaObj = this._addSchema(schemaKeyRef) + v = schemaObj.validate || this._compile(schemaObj) } - var valid = v(data); - if (v.$async !== true) this.errors = v.errors; - return valid; + var valid = v(data) + if (v.$async !== true) this.errors = v.errors + return valid } - /** * Create validating function for passed schema. * @this Ajv @@ -110,11 +113,10 @@ function validate(schemaKeyRef, data) { * @return {Function} validating function */ function compile(schema, _meta) { - var schemaObj = this._addSchema(schema, undefined, _meta); - return schemaObj.validate || this._compile(schemaObj); + var schemaObj = this._addSchema(schema, undefined, _meta) + return schemaObj.validate || this._compile(schemaObj) } - /** * Adds schema to the instance. * @this Ajv @@ -125,20 +127,20 @@ function compile(schema, _meta) { * @return {Ajv} this for method chaining */ function addSchema(schema, key, _skipValidation, _meta) { - if (Array.isArray(schema)){ - for (var i=0; i%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i; +var URITEMPLATE = /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i // For the source: https://gist.github.com/dperini/729294 // For test cases: https://mathiasbynens.be/demo/url-regex // @todo Delete current URL in favour of the commented out URL rule when this issue is fixed https://github.com/eslint/eslint/issues/7983. // var URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu; -var URL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i; -var UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i; -var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/; -var JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i; -var RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/; +var URL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i +var UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i +var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/ +var JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i +var RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/ - -module.exports = formats; +module.exports = formats function formats(mode) { - mode = mode == 'full' ? 'full' : 'fast'; - return util.copy(formats[mode]); + mode = mode == "full" ? "full" : "fast" + return util.copy(formats[mode]) } - formats.fast = { // date: http://tools.ietf.org/html/rfc3339#section-5.6 date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/, // date-time: http://tools.ietf.org/html/rfc3339#section-5.6 time: /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, - 'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, + "date-time": /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js uri: /^(?:[a-z][a-z0-9+-.]*:)(?:\/?\/)?[^\s]*$/i, - 'uri-reference': /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, - 'uri-template': URITEMPLATE, + "uri-reference": /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, + "uri-template": URITEMPLATE, url: URL, // email (sources from jsen validator): // http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363 @@ -54,20 +52,19 @@ formats.fast = { uuid: UUID, // JSON-pointer: https://tools.ietf.org/html/rfc6901 // uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A - 'json-pointer': JSON_POINTER, - 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, + "json-pointer": JSON_POINTER, + "json-pointer-uri-fragment": JSON_POINTER_URI_FRAGMENT, // relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00 - 'relative-json-pointer': RELATIVE_JSON_POINTER -}; - + "relative-json-pointer": RELATIVE_JSON_POINTER, +} formats.full = { date: date, time: time, - 'date-time': date_time, + "date-time": date_time, uri: uri, - 'uri-reference': URIREF, - 'uri-template': URITEMPLATE, + "uri-reference": URIREF, + "uri-template": URITEMPLATE, url: URL, email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, hostname: HOSTNAME, @@ -75,68 +72,68 @@ formats.full = { ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, regex: regex, uuid: UUID, - 'json-pointer': JSON_POINTER, - 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, - 'relative-json-pointer': RELATIVE_JSON_POINTER -}; - + "json-pointer": JSON_POINTER, + "json-pointer-uri-fragment": JSON_POINTER_URI_FRAGMENT, + "relative-json-pointer": RELATIVE_JSON_POINTER, +} function isLeapYear(year) { // https://tools.ietf.org/html/rfc3339#appendix-C - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) } - function date(str) { // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 - var matches = str.match(DATE); - if (!matches) return false; - - var year = +matches[1]; - var month = +matches[2]; - var day = +matches[3]; - - return month >= 1 && month <= 12 && day >= 1 && - day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]); + var matches = str.match(DATE) + if (!matches) return false + + var year = +matches[1] + var month = +matches[2] + var day = +matches[3] + + return ( + month >= 1 && + month <= 12 && + day >= 1 && + day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]) + ) } - function time(str, full) { - var matches = str.match(TIME); - if (!matches) return false; - - var hour = matches[1]; - var minute = matches[2]; - var second = matches[3]; - var timeZone = matches[5]; - return ((hour <= 23 && minute <= 59 && second <= 59) || - (hour == 23 && minute == 59 && second == 60)) && - (!full || timeZone); + var matches = str.match(TIME) + if (!matches) return false + + var hour = matches[1] + var minute = matches[2] + var second = matches[3] + var timeZone = matches[5] + return ( + ((hour <= 23 && minute <= 59 && second <= 59) || + (hour == 23 && minute == 59 && second == 60)) && + (!full || timeZone) + ) } - -var DATE_TIME_SEPARATOR = /t|\s/i; +var DATE_TIME_SEPARATOR = /t|\s/i function date_time(str) { // http://tools.ietf.org/html/rfc3339#section-5.6 - var dateTime = str.split(DATE_TIME_SEPARATOR); - return dateTime.length == 2 && date(dateTime[0]) && time(dateTime[1], true); + var dateTime = str.split(DATE_TIME_SEPARATOR) + return dateTime.length == 2 && date(dateTime[0]) && time(dateTime[1], true) } - -var NOT_URI_FRAGMENT = /\/|:/; +var NOT_URI_FRAGMENT = /\/|:/ function uri(str) { // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." - return NOT_URI_FRAGMENT.test(str) && URI.test(str); + return NOT_URI_FRAGMENT.test(str) && URI.test(str) } - -var Z_ANCHOR = /[^\\]\\Z/; +var Z_ANCHOR = /[^\\]\\Z/ function regex(str) { - if (Z_ANCHOR.test(str)) return false; + if (Z_ANCHOR.test(str)) return false try { - new RegExp(str); - return true; - } catch(e) { - return false; + new RegExp(str) + return true + } catch (e) { + return false } } diff --git a/lib/compile/index.js b/lib/compile/index.js index 97518c4247..2dea7a530c 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,24 +1,23 @@ -'use strict'; +"use strict" -var resolve = require('./resolve') - , util = require('./util') - , errorClasses = require('./error_classes') - , stableStringify = require('fast-json-stable-stringify'); +var resolve = require("./resolve"), + util = require("./util"), + errorClasses = require("./error_classes"), + stableStringify = require("fast-json-stable-stringify") -var validateGenerator = require('../dotjs/validate'); +var validateGenerator = require("../dotjs/validate") /** * Functions below are used inside compiled validations function */ -var ucs2length = util.ucs2length; -var equal = require('fast-deep-equal'); +var ucs2length = util.ucs2length +var equal = require("fast-deep-equal") // this error is thrown by async schemas to return validation errors via exception -var ValidationError = errorClasses.Validation; - -module.exports = compile; +var ValidationError = errorClasses.Validation +module.exports = compile /** * Compiles schema to validation function @@ -32,58 +31,58 @@ module.exports = compile; function compile(schema, root, localRefs, baseId) { /* jshint validthis: true, evil: true */ /* eslint no-shadow: 0 */ - var self = this - , opts = this._opts - , refVal = [ undefined ] - , refs = {} - , patterns = [] - , patternsHash = {} - , defaults = [] - , defaultsHash = {} - , customRules = []; + var self = this, + opts = this._opts, + refVal = [undefined], + refs = {}, + patterns = [], + patternsHash = {}, + defaults = [], + defaultsHash = {}, + customRules = [] - root = root || { schema: schema, refVal: refVal, refs: refs }; + root = root || {schema: schema, refVal: refVal, refs: refs} - var c = checkCompiling.call(this, schema, root, baseId); - var compilation = this._compilations[c.index]; - if (c.compiling) return (compilation.callValidate = callValidate); + var c = checkCompiling.call(this, schema, root, baseId) + var compilation = this._compilations[c.index] + if (c.compiling) return (compilation.callValidate = callValidate) - var formats = this._formats; - var RULES = this.RULES; + var formats = this._formats + var RULES = this.RULES try { - var v = localCompile(schema, root, localRefs, baseId); - compilation.validate = v; - var cv = compilation.callValidate; + var v = localCompile(schema, root, localRefs, baseId) + compilation.validate = v + var cv = compilation.callValidate if (cv) { - cv.schema = v.schema; - cv.errors = null; - cv.refs = v.refs; - cv.refVal = v.refVal; - cv.root = v.root; - cv.$async = v.$async; - if (opts.sourceCode) cv.source = v.source; + cv.schema = v.schema + cv.errors = null + cv.refs = v.refs + cv.refVal = v.refVal + cv.root = v.root + cv.$async = v.$async + if (opts.sourceCode) cv.source = v.source } - return v; + return v } finally { - endCompiling.call(this, schema, root, baseId); + endCompiling.call(this, schema, root, baseId) } /* @this {*} - custom context, see passContext option */ function callValidate() { /* jshint validthis: true */ - var validate = compilation.validate; - var result = validate.apply(this, arguments); - callValidate.errors = validate.errors; - return result; + var validate = compilation.validate + var result = validate.apply(this, arguments) + callValidate.errors = validate.errors + return result } function localCompile(_schema, _root, localRefs, baseId) { - var isRoot = !_root || (_root && _root.schema == _schema); + var isRoot = !_root || (_root && _root.schema == _schema) if (_root.schema != root.schema) - return compile.call(self, _schema, _root, localRefs, baseId); + return compile.call(self, _schema, _root, localRefs, baseId) - var $async = _schema.$async === true; + var $async = _schema.$async === true var sourceCode = validateGenerator({ isTop: true, @@ -91,8 +90,8 @@ function compile(schema, root, localRefs, baseId) { isRoot: isRoot, baseId: baseId, root: _root, - schemaPath: '', - errSchemaPath: '#', + schemaPath: "", + errSchemaPath: "#", errorPath: '""', MissingRefError: errorClasses.MissingRef, RULES: RULES, @@ -106,30 +105,33 @@ function compile(schema, root, localRefs, baseId) { opts: opts, formats: formats, logger: self.logger, - self: self - }); + self: self, + }) - sourceCode = vars(refVal, refValCode) + vars(patterns, patternCode) - + vars(defaults, defaultCode) + vars(customRules, customRuleCode) - + sourceCode; + sourceCode = + vars(refVal, refValCode) + + vars(patterns, patternCode) + + vars(defaults, defaultCode) + + vars(customRules, customRuleCode) + + sourceCode - if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema); + if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) // console.log('\n\n\n *** \n', JSON.stringify(sourceCode)); - var validate; + var validate try { var makeValidate = new Function( - 'self', - 'RULES', - 'formats', - 'root', - 'refVal', - 'defaults', - 'customRules', - 'equal', - 'ucs2length', - 'ValidationError', + "self", + "RULES", + "formats", + "root", + "refVal", + "defaults", + "customRules", + "equal", + "ucs2length", + "ValidationError", sourceCode - ); + ) validate = makeValidate( self, @@ -142,168 +144,174 @@ function compile(schema, root, localRefs, baseId) { equal, ucs2length, ValidationError - ); + ) - refVal[0] = validate; - } catch(e) { - self.logger.error('Error compiling schema, function code:', sourceCode); - throw e; + refVal[0] = validate + } catch (e) { + self.logger.error("Error compiling schema, function code:", sourceCode) + throw e } - validate.schema = _schema; - validate.errors = null; - validate.refs = refs; - validate.refVal = refVal; - validate.root = isRoot ? validate : _root; - if ($async) validate.$async = true; + validate.schema = _schema + validate.errors = null + validate.refs = refs + validate.refVal = refVal + validate.root = isRoot ? validate : _root + if ($async) validate.$async = true if (opts.sourceCode === true) { validate.source = { code: sourceCode, patterns: patterns, - defaults: defaults - }; + defaults: defaults, + } } - return validate; + return validate } function resolveRef(baseId, ref, isRoot) { - ref = resolve.url(baseId, ref); - var refIndex = refs[ref]; - var _refVal, refCode; + ref = resolve.url(baseId, ref) + var refIndex = refs[ref] + var _refVal, refCode if (refIndex !== undefined) { - _refVal = refVal[refIndex]; - refCode = 'refVal[' + refIndex + ']'; - return resolvedRef(_refVal, refCode); + _refVal = refVal[refIndex] + refCode = "refVal[" + refIndex + "]" + return resolvedRef(_refVal, refCode) } if (!isRoot && root.refs) { - var rootRefId = root.refs[ref]; + var rootRefId = root.refs[ref] if (rootRefId !== undefined) { - _refVal = root.refVal[rootRefId]; - refCode = addLocalRef(ref, _refVal); - return resolvedRef(_refVal, refCode); + _refVal = root.refVal[rootRefId] + refCode = addLocalRef(ref, _refVal) + return resolvedRef(_refVal, refCode) } } - refCode = addLocalRef(ref); - var v = resolve.call(self, localCompile, root, ref); + refCode = addLocalRef(ref) + var v = resolve.call(self, localCompile, root, ref) if (v === undefined) { - var localSchema = localRefs && localRefs[ref]; + var localSchema = localRefs && localRefs[ref] if (localSchema) { v = resolve.inlineRef(localSchema, opts.inlineRefs) - ? localSchema - : compile.call(self, localSchema, root, localRefs, baseId); + ? localSchema + : compile.call(self, localSchema, root, localRefs, baseId) } } if (v === undefined) { - removeLocalRef(ref); + removeLocalRef(ref) } else { - replaceLocalRef(ref, v); - return resolvedRef(v, refCode); + replaceLocalRef(ref, v) + return resolvedRef(v, refCode) } } function addLocalRef(ref, v) { - var refId = refVal.length; - refVal[refId] = v; - refs[ref] = refId; - return 'refVal' + refId; + var refId = refVal.length + refVal[refId] = v + refs[ref] = refId + return "refVal" + refId } function removeLocalRef(ref) { - delete refs[ref]; + delete refs[ref] } function replaceLocalRef(ref, v) { - var refId = refs[ref]; - refVal[refId] = v; + var refId = refs[ref] + refVal[refId] = v } function resolvedRef(refVal, code) { - return typeof refVal == 'object' || typeof refVal == 'boolean' - ? { code: code, schema: refVal, inline: true } - : { code: code, $async: refVal && !!refVal.$async }; + return typeof refVal == "object" || typeof refVal == "boolean" + ? {code: code, schema: refVal, inline: true} + : {code: code, $async: refVal && !!refVal.$async} } function usePattern(regexStr) { - var index = patternsHash[regexStr]; + var index = patternsHash[regexStr] if (index === undefined) { - index = patternsHash[regexStr] = patterns.length; - patterns[index] = regexStr; + index = patternsHash[regexStr] = patterns.length + patterns[index] = regexStr } - return 'pattern' + index; + return "pattern" + index } function useDefault(value) { switch (typeof value) { - case 'boolean': - case 'number': - return '' + value; - case 'string': - return util.toQuotedString(value); - case 'object': - if (value === null) return 'null'; - var valueStr = stableStringify(value); - var index = defaultsHash[valueStr]; + case "boolean": + case "number": + return "" + value + case "string": + return util.toQuotedString(value) + case "object": + if (value === null) return "null" + var valueStr = stableStringify(value) + var index = defaultsHash[valueStr] if (index === undefined) { - index = defaultsHash[valueStr] = defaults.length; - defaults[index] = value; + index = defaultsHash[valueStr] = defaults.length + defaults[index] = value } - return 'default' + index; + return "default" + index } } function useCustomRule(rule, schema, parentSchema, it) { if (self._opts.validateSchema !== false) { - var deps = rule.definition.dependencies; - if (deps && !deps.every(function(keyword) { - return Object.prototype.hasOwnProperty.call(parentSchema, keyword); - })) - throw new Error('parent schema must have all required keywords: ' + deps.join(',')); - - var validateSchema = rule.definition.validateSchema; + var deps = rule.definition.dependencies + if ( + deps && + !deps.every(function (keyword) { + return Object.prototype.hasOwnProperty.call(parentSchema, keyword) + }) + ) + throw new Error( + "parent schema must have all required keywords: " + deps.join(",") + ) + + var validateSchema = rule.definition.validateSchema if (validateSchema) { - var valid = validateSchema(schema); + var valid = validateSchema(schema) if (!valid) { - var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); - if (self._opts.validateSchema == 'log') self.logger.error(message); - else throw new Error(message); + var message = + "keyword schema is invalid: " + + self.errorsText(validateSchema.errors) + if (self._opts.validateSchema == "log") self.logger.error(message) + else throw new Error(message) } } } - var compile = rule.definition.compile - , inline = rule.definition.inline - , macro = rule.definition.macro; + var compile = rule.definition.compile, + inline = rule.definition.inline, + macro = rule.definition.macro - var validate; + var validate if (compile) { - validate = compile.call(self, schema, parentSchema, it); + validate = compile.call(self, schema, parentSchema, it) } else if (macro) { - validate = macro.call(self, schema, parentSchema, it); - if (opts.validateSchema !== false) self.validateSchema(validate, true); + validate = macro.call(self, schema, parentSchema, it) + if (opts.validateSchema !== false) self.validateSchema(validate, true) } else if (inline) { - validate = inline.call(self, it, rule.keyword, schema, parentSchema); + validate = inline.call(self, it, rule.keyword, schema, parentSchema) } else { - validate = rule.definition.validate; - if (!validate) return; + validate = rule.definition.validate + if (!validate) return } if (validate === undefined) - throw new Error('custom keyword "' + rule.keyword + '"failed to compile'); + throw new Error('custom keyword "' + rule.keyword + '"failed to compile') - var index = customRules.length; - customRules[index] = validate; + var index = customRules.length + customRules[index] = validate return { - code: 'customRule' + index, - validate: validate - }; + code: "customRule" + index, + validate: validate, + } } } - /** * Checks if the schema is currently compiled * @this Ajv @@ -314,18 +322,17 @@ function compile(schema, root, localRefs, baseId) { */ function checkCompiling(schema, root, baseId) { /* jshint validthis: true */ - var index = compIndex.call(this, schema, root, baseId); - if (index >= 0) return { index: index, compiling: true }; - index = this._compilations.length; + var index = compIndex.call(this, schema, root, baseId) + if (index >= 0) return {index: index, compiling: true} + index = this._compilations.length this._compilations[index] = { schema: schema, root: root, - baseId: baseId - }; - return { index: index, compiling: false }; + baseId: baseId, + } + return {index: index, compiling: false} } - /** * Removes the schema from the currently compiled list * @this Ajv @@ -335,11 +342,10 @@ function checkCompiling(schema, root, baseId) { */ function endCompiling(schema, root, baseId) { /* jshint validthis: true */ - var i = compIndex.call(this, schema, root, baseId); - if (i >= 0) this._compilations.splice(i, 1); + var i = compIndex.call(this, schema, root, baseId) + if (i >= 0) this._compilations.splice(i, 1) } - /** * Index of schema compilation in the currently compiled list * @this Ajv @@ -350,38 +356,40 @@ function endCompiling(schema, root, baseId) { */ function compIndex(schema, root, baseId) { /* jshint validthis: true */ - for (var i=0; i= 0xD800 && value <= 0xDBFF && pos < len) { + length++ + value = str.charCodeAt(pos++) + if (value >= 0xd800 && value <= 0xdbff && pos < len) { // high surrogate, and there is a next character - value = str.charCodeAt(pos); - if ((value & 0xFC00) == 0xDC00) pos++; // low surrogate + value = str.charCodeAt(pos) + if ((value & 0xfc00) == 0xdc00) pos++ // low surrogate } } - return length; -}; + return length +} diff --git a/lib/compile/util.js b/lib/compile/util.js index ef07b8c757..31d927d03e 100644 --- a/lib/compile/util.js +++ b/lib/compile/util.js @@ -1,5 +1,4 @@ -'use strict'; - +"use strict" module.exports = { copy: copy, @@ -9,8 +8,8 @@ module.exports = { toHash: toHash, getProperty: getProperty, escapeQuotes: escapeQuotes, - equal: require('fast-deep-equal'), - ucs2length: require('./ucs2length'), + equal: require("fast-deep-equal"), + ucs2length: require("./ucs2length"), varOccurences: varOccurences, varReplace: varReplace, schemaHasRules: schemaHasRules, @@ -23,217 +22,253 @@ module.exports = { unescapeFragment: unescapeFragment, unescapeJsonPointer: unescapeJsonPointer, escapeFragment: escapeFragment, - escapeJsonPointer: escapeJsonPointer -}; - + escapeJsonPointer: escapeJsonPointer, +} function copy(o, to) { - to = to || {}; - for (var key in o) to[key] = o[key]; - return to; + to = to || {} + for (var key in o) to[key] = o[key] + return to } - function checkDataType(dataType, data, strictNumbers, negate) { - var EQUAL = negate ? ' !== ' : ' === ' - , AND = negate ? ' || ' : ' && ' - , OK = negate ? '!' : '' - , NOT = negate ? '' : '!'; + var EQUAL = negate ? " !== " : " === ", + AND = negate ? " || " : " && ", + OK = negate ? "!" : "", + NOT = negate ? "" : "!" switch (dataType) { - case 'null': return data + EQUAL + 'null'; - case 'array': return OK + 'Array.isArray(' + data + ')'; - case 'object': return '(' + OK + data + AND + - 'typeof ' + data + EQUAL + '"object"' + AND + - NOT + 'Array.isArray(' + data + '))'; - case 'integer': return '(typeof ' + data + EQUAL + '"number"' + AND + - NOT + '(' + data + ' % 1)' + - AND + data + EQUAL + data + - (strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')'; - case 'number': return '(typeof ' + data + EQUAL + '"' + dataType + '"' + - (strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')'; - default: return 'typeof ' + data + EQUAL + '"' + dataType + '"'; + case "null": + return data + EQUAL + "null" + case "array": + return OK + "Array.isArray(" + data + ")" + case "object": + return ( + "(" + + OK + + data + + AND + + "typeof " + + data + + EQUAL + + '"object"' + + AND + + NOT + + "Array.isArray(" + + data + + "))" + ) + case "integer": + return ( + "(typeof " + + data + + EQUAL + + '"number"' + + AND + + NOT + + "(" + + data + + " % 1)" + + AND + + data + + EQUAL + + data + + (strictNumbers ? AND + OK + "isFinite(" + data + ")" : "") + + ")" + ) + case "number": + return ( + "(typeof " + + data + + EQUAL + + '"' + + dataType + + '"' + + (strictNumbers ? AND + OK + "isFinite(" + data + ")" : "") + + ")" + ) + default: + return "typeof " + data + EQUAL + '"' + dataType + '"' } } - function checkDataTypes(dataTypes, data, strictNumbers) { switch (dataTypes.length) { - case 1: return checkDataType(dataTypes[0], data, strictNumbers, true); + case 1: + return checkDataType(dataTypes[0], data, strictNumbers, true) default: - var code = ''; - var types = toHash(dataTypes); + var code = "" + var types = toHash(dataTypes) if (types.array && types.object) { - code = types.null ? '(': '(!' + data + ' || '; - code += 'typeof ' + data + ' !== "object")'; - delete types.null; - delete types.array; - delete types.object; + code = types.null ? "(" : "(!" + data + " || " + code += "typeof " + data + ' !== "object")' + delete types.null + delete types.array + delete types.object } - if (types.number) delete types.integer; + if (types.number) delete types.integer for (var t in types) - code += (code ? ' && ' : '' ) + checkDataType(t, data, strictNumbers, true); + code += + (code ? " && " : "") + checkDataType(t, data, strictNumbers, true) - return code; + return code } } - -var COERCE_TO_TYPES = toHash([ 'string', 'number', 'integer', 'boolean', 'null' ]); +var COERCE_TO_TYPES = toHash(["string", "number", "integer", "boolean", "null"]) function coerceToTypes(optionCoerceTypes, dataTypes) { if (Array.isArray(dataTypes)) { - var types = []; - for (var i=0; i= lvl) throw new Error('Cannot access property/index ' + up + ' levels up, current level is ' + lvl); - return paths[lvl - up]; + matches = $data.match(RELATIVE_JSON_POINTER) + if (!matches) throw new Error("Invalid JSON-pointer: " + $data) + up = +matches[1] + jsonPointer = matches[2] + if (jsonPointer == "#") { + if (up >= lvl) + throw new Error( + "Cannot access property/index " + + up + + " levels up, current level is " + + lvl + ) + return paths[lvl - up] } - if (up > lvl) throw new Error('Cannot access data ' + up + ' levels up, current level is ' + lvl); - data = 'data' + ((lvl - up) || ''); - if (!jsonPointer) return data; + if (up > lvl) + throw new Error( + "Cannot access data " + up + " levels up, current level is " + lvl + ) + data = "data" + (lvl - up || "") + if (!jsonPointer) return data } - var expr = data; - var segments = jsonPointer.split('/'); - for (var i=0; i 0; - } - }); + describe("formats for number", function () { + it("should validate only numbers", function () { + ajv.addFormat("positive", { + type: "number", + validate: function (x) { + return x > 0 + }, + }) var validate = ajv.compile({ - format: 'positive' - }); - validate(-2) .should.equal(false); - validate(0) .should.equal(false); - validate(2) .should.equal(true); - validate('abc') .should.equal(true); - }); - - it('should validate numbers with format via $data', function() { - ajv = new Ajv({$data: true}); - ajv.addFormat('positive', { - type: 'number', - validate: function(x) { - return x > 0; - } - }); + format: "positive", + }) + validate(-2).should.equal(false) + validate(0).should.equal(false) + validate(2).should.equal(true) + validate("abc").should.equal(true) + }) + + it("should validate numbers with format via $data", function () { + ajv = new Ajv({$data: true}) + ajv.addFormat("positive", { + type: "number", + validate: function (x) { + return x > 0 + }, + }) var validate = ajv.compile({ properties: { - data: { format: { $data: '1/frmt' } }, - frmt: { type: 'string' } - } - }); - validate({data: -2, frmt: 'positive'}) .should.equal(false); - validate({data: 0, frmt: 'positive'}) .should.equal(false); - validate({data: 2, frmt: 'positive'}) .should.equal(true); - validate({data: 'abc', frmt: 'positive'}) .should.equal(true); - }); - }); - }); - - - describe('validateSchema method', function() { - it('should validate schema against meta-schema', function() { + data: {format: {$data: "1/frmt"}}, + frmt: {type: "string"}, + }, + }) + validate({data: -2, frmt: "positive"}).should.equal(false) + validate({data: 0, frmt: "positive"}).should.equal(false) + validate({data: 2, frmt: "positive"}).should.equal(true) + validate({data: "abc", frmt: "positive"}).should.equal(true) + }) + }) + }) + + describe("validateSchema method", function () { + it("should validate schema against meta-schema", function () { var valid = ajv.validateSchema({ - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'number' - }); + $schema: "http://json-schema.org/draft-07/schema#", + type: "number", + }) - valid .should.equal(true); - should.equal(ajv.errors, null); + valid.should.equal(true) + should.equal(ajv.errors, null) valid = ajv.validateSchema({ - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'wrong_type' - }); - - valid .should.equal(false); - ajv.errors.length .should.equal(3); - ajv.errors[0].keyword .should.equal('enum'); - ajv.errors[1].keyword .should.equal('type'); - ajv.errors[2].keyword .should.equal('anyOf'); - }); - - it('should throw exception if meta-schema is unknown', function() { - should.throw(function() { + $schema: "http://json-schema.org/draft-07/schema#", + type: "wrong_type", + }) + + valid.should.equal(false) + ajv.errors.length.should.equal(3) + ajv.errors[0].keyword.should.equal("enum") + ajv.errors[1].keyword.should.equal("type") + ajv.errors[2].keyword.should.equal("anyOf") + }) + + it("should throw exception if meta-schema is unknown", function () { + should.throw(function () { ajv.validateSchema({ - $schema: 'http://example.com/unknown/schema#', - type: 'number' - }); - }); - }); - - it('should throw exception if $schema is not a string', function() { - should.throw(function() { + $schema: "http://example.com/unknown/schema#", + type: "number", + }) + }) + }) + + it("should throw exception if $schema is not a string", function () { + should.throw(function () { ajv.validateSchema({ $schema: {}, - type: 'number' - }); - }); - }); + type: "number", + }) + }) + }) - describe('sub-schema validation outside of definitions during compilation', function() { - it('maximum', function() { + describe("sub-schema validation outside of definitions during compilation", function () { + it("maximum", function () { passValidationThrowCompile({ - $ref: '#/foo', - foo: {maximum: 'bar'} - }); - }); + $ref: "#/foo", + foo: {maximum: "bar"}, + }) + }) - it('exclusiveMaximum', function() { + it("exclusiveMaximum", function () { passValidationThrowCompile({ - $ref: '#/foo', - foo: {exclusiveMaximum: 'bar'} - }); - }); + $ref: "#/foo", + foo: {exclusiveMaximum: "bar"}, + }) + }) - it('maxItems', function() { + it("maxItems", function () { passValidationThrowCompile({ - $ref: '#/foo', - foo: {maxItems: 'bar'} - }); - }); + $ref: "#/foo", + foo: {maxItems: "bar"}, + }) + }) - it('maxLength', function() { + it("maxLength", function () { passValidationThrowCompile({ - $ref: '#/foo', - foo: {maxLength: 'bar'} - }); - }); + $ref: "#/foo", + foo: {maxLength: "bar"}, + }) + }) - it('maxProperties', function() { + it("maxProperties", function () { passValidationThrowCompile({ - $ref: '#/foo', - foo: {maxProperties: 'bar'} - }); - }); + $ref: "#/foo", + foo: {maxProperties: "bar"}, + }) + }) - it('multipleOf', function() { + it("multipleOf", function () { passValidationThrowCompile({ - $ref: '#/foo', - foo: {maxProperties: 'bar'} - }); - }); + $ref: "#/foo", + foo: {maxProperties: "bar"}, + }) + }) function passValidationThrowCompile(schema) { - ajv.validateSchema(schema) .should.equal(true); - should.throw(function() { - ajv.compile(schema); - }); + ajv.validateSchema(schema).should.equal(true) + should.throw(function () { + ajv.compile(schema) + }) } - }); - }); -}); + }) + }) +}) diff --git a/spec/ajv_async_instances.js b/spec/ajv_async_instances.js index 8facd36374..8a41945a45 100644 --- a/spec/ajv_async_instances.js +++ b/spec/ajv_async_instances.js @@ -1,39 +1,39 @@ -'use strict'; +"use strict" -var Ajv = require('./ajv') - , util = require('../lib/compile/util') - , setupAsync = require('./ajv-async'); +var Ajv = require("./ajv"), + util = require("../lib/compile/util"), + setupAsync = require("./ajv-async") -module.exports = getAjvInstances; - -var firstTime = true; +module.exports = getAjvInstances +var firstTime = true function getAjvInstances(opts) { - opts = opts || {}; - var instances = []; + opts = opts || {} + var instances = [] var options = [ {}, - { transpile: true }, - { allErrors: true }, - { transpile: true, allErrors: true } - ]; + {transpile: true}, + {allErrors: true}, + {transpile: true, allErrors: true}, + ] options.forEach(function (_opts) { - util.copy(opts, _opts); - var ajv = getAjv(_opts); - if (ajv) instances.push(ajv); - }); + util.copy(opts, _opts) + var ajv = getAjv(_opts) + if (ajv) instances.push(ajv) + }) if (firstTime) { - console.log('Testing', instances.length, 'ajv instances:'); - firstTime = false; + console.log("Testing", instances.length, "ajv instances:") + firstTime = false } - return instances; + return instances } - -function getAjv(opts){ - try { return setupAsync(new Ajv(opts)); } catch(e) {} +function getAjv(opts) { + try { + return setupAsync(new Ajv(opts)) + } catch (e) {} } diff --git a/spec/ajv_instances.js b/spec/ajv_instances.js index f056d5a01d..4ca9fa5385 100644 --- a/spec/ajv_instances.js +++ b/spec/ajv_instances.js @@ -1,32 +1,30 @@ -'use strict'; +"use strict" -var Ajv = require('./ajv'); - -module.exports = getAjvInstances; +var Ajv = require("./ajv") +module.exports = getAjvInstances function getAjvInstances(options, extraOpts) { - return _getAjvInstances(options, extraOpts || {}); + return _getAjvInstances(options, extraOpts || {}) } function _getAjvInstances(opts, useOpts) { - var optNames = Object.keys(opts); + var optNames = Object.keys(opts) if (optNames.length) { - opts = copy(opts); - var useOpts1 = copy(useOpts) - , optName = optNames[0]; - useOpts1[optName] = opts[optName]; - delete opts[optName]; - var instances = _getAjvInstances(opts, useOpts) - , instances1 = _getAjvInstances(opts, useOpts1); - return instances.concat(instances1); + opts = copy(opts) + var useOpts1 = copy(useOpts), + optName = optNames[0] + useOpts1[optName] = opts[optName] + delete opts[optName] + var instances = _getAjvInstances(opts, useOpts), + instances1 = _getAjvInstances(opts, useOpts1) + return instances.concat(instances1) } - return [ new Ajv(useOpts) ]; + return [new Ajv(useOpts)] } - function copy(o, to) { - to = to || {}; - for (var key in o) to[key] = o[key]; - return to; + to = to || {} + for (var key in o) to[key] = o[key] + return to } diff --git a/spec/ajv_options.js b/spec/ajv_options.js index e09e73b634..711fa9678d 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -1,20 +1,20 @@ -'use strict'; +"use strict" -var isBrowser = typeof window == 'object'; -var fullTest = isBrowser || !process.env.AJV_FAST_TEST; +var isBrowser = typeof window == "object" +var fullTest = isBrowser || !process.env.AJV_FAST_TEST var options = fullTest - ? { - allErrors: true, - verbose: true, - format: 'full', - extendRefs: 'ignore', - inlineRefs: false, - jsonPointers: true - } - : { allErrors: true }; + ? { + allErrors: true, + verbose: true, + format: "full", + extendRefs: "ignore", + inlineRefs: false, + jsonPointers: true, + } + : {allErrors: true} if (fullTest && !isBrowser) - options.processCode = require('js-beautify').js_beautify; + options.processCode = require("js-beautify").js_beautify -module.exports = options; +module.exports = options diff --git a/spec/async.spec.js b/spec/async.spec.js index 5b4c5ff6e5..691de0b202 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -1,459 +1,441 @@ -'use strict'; +"use strict" /* global Promise */ -var Ajv = require('./ajv') - , should = require('./chai').should(); +var Ajv = require("./ajv"), + should = require("./chai").should() - -describe('compileAsync method', function() { - var ajv, loadCallCount; +describe("compileAsync method", function () { + var ajv, loadCallCount var SCHEMAS = { "http://example.com/object.json": { - "$id": "http://example.com/object.json", - "properties": { - "a": { "type": "string" }, - "b": { "$ref": "int2plus.json" } - } + $id: "http://example.com/object.json", + properties: { + a: {type: "string"}, + b: {$ref: "int2plus.json"}, + }, }, "http://example.com/int2plus.json": { - "$id": "http://example.com/int2plus.json", - "type": "integer", - "minimum": 2 + $id: "http://example.com/int2plus.json", + type: "integer", + minimum: 2, }, "http://example.com/tree.json": { - "$id": "http://example.com/tree.json", - "type": "array", - "items": { "$ref": "leaf.json" } + $id: "http://example.com/tree.json", + type: "array", + items: {$ref: "leaf.json"}, }, "http://example.com/leaf.json": { - "$id": "http://example.com/leaf.json", - "properties": { - "name": { "type": "string" }, - "subtree": { "$ref": "tree.json" } - } + $id: "http://example.com/leaf.json", + properties: { + name: {type: "string"}, + subtree: {$ref: "tree.json"}, + }, }, "http://example.com/recursive.json": { - "$id": "http://example.com/recursive.json", - "properties": { - "b": { "$ref": "parent.json" } + $id: "http://example.com/recursive.json", + properties: { + b: {$ref: "parent.json"}, }, - "required": ["b"] + required: ["b"], }, "http://example.com/invalid.json": { - "$id": "http://example.com/recursive.json", - "properties": { - "invalid": { "type": "number" } + $id: "http://example.com/recursive.json", + properties: { + invalid: {type: "number"}, }, - "required": "invalid" + required: "invalid", }, "http://example.com/foobar.json": { - "$id": "http://example.com/foobar.json", - "$schema": "http://example.com/foobar_meta.json", - "myFooBar": "foo" + $id: "http://example.com/foobar.json", + $schema: "http://example.com/foobar_meta.json", + myFooBar: "foo", }, "http://example.com/foobar_meta.json": { - "$id": "http://example.com/foobar_meta.json", - "type": "object", - "properties": { - "myFooBar": { - "enum": ["foo", "bar"] - } - } + $id: "http://example.com/foobar_meta.json", + type: "object", + properties: { + myFooBar: { + enum: ["foo", "bar"], + }, + }, }, "http://example.com/foo.json": { - "$id": "http://example.com/foo.json", - "type": "object", - "properties": { - "bar": {"$ref": "bar.json"}, - "other": {"$ref": "other.json"} - } + $id: "http://example.com/foo.json", + type: "object", + properties: { + bar: {$ref: "bar.json"}, + other: {$ref: "other.json"}, + }, }, "http://example.com/bar.json": { - "$id": "http://example.com/bar.json", - "type": "object", - "properties": { - "foo": {"$ref": "foo.json"} - } + $id: "http://example.com/bar.json", + type: "object", + properties: { + foo: {$ref: "foo.json"}, + }, }, "http://example.com/other.json": { - "$id": "http://example.com/other.json" - } - }; - - beforeEach(function() { - loadCallCount = 0; - ajv = new Ajv({ loadSchema: loadSchema }); - }); + $id: "http://example.com/other.json", + }, + } + beforeEach(function () { + loadCallCount = 0 + ajv = new Ajv({loadSchema: loadSchema}) + }) - it('should compile schemas loading missing schemas with options.loadSchema function', function() { + it("should compile schemas loading missing schemas with options.loadSchema function", function () { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json" } - } - }; + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json"}, + }, + } return ajv.compileAsync(schema).then(function (validate) { - should.equal(loadCallCount, 2); - validate .should.be.a('function'); - validate({ a: { b: 2 } }) .should.equal(true); - validate({ a: { b: 1 } }) .should.equal(false); - }); - }); - - - it('should compile schemas loading missing schemas and return function via callback', function (done) { + should.equal(loadCallCount, 2) + validate.should.be.a("function") + validate({a: {b: 2}}).should.equal(true) + validate({a: {b: 1}}).should.equal(false) + }) + }) + + it("should compile schemas loading missing schemas and return function via callback", function (done) { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json" } - } - }; + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json"}, + }, + } ajv.compileAsync(schema, function (err, validate) { - should.equal(loadCallCount, 2); - should.not.exist(err); - validate .should.be.a('function'); - validate({ a: { b: 2 } }) .should.equal(true); - validate({ a: { b: 1 } }) .should.equal(false); - done(); - }); - }); - - - it('should correctly load schemas when missing reference has JSON path', function() { + should.equal(loadCallCount, 2) + should.not.exist(err) + validate.should.be.a("function") + validate({a: {b: 2}}).should.equal(true) + validate({a: {b: 1}}).should.equal(false) + done() + }) + }) + + it("should correctly load schemas when missing reference has JSON path", function () { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json#/properties/b" } - } - }; + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json#/properties/b"}, + }, + } return ajv.compileAsync(schema).then(function (validate) { - should.equal(loadCallCount, 2); - validate .should.be.a('function'); - validate({ a: 2 }) .should.equal(true); - validate({ a: 1 }) .should.equal(false); - }); - }); - - - it('should correctly compile with remote schemas that have mutual references', function() { + should.equal(loadCallCount, 2) + validate.should.be.a("function") + validate({a: 2}).should.equal(true) + validate({a: 1}).should.equal(false) + }) + }) + + it("should correctly compile with remote schemas that have mutual references", function () { var schema = { - "$id": "http://example.com/root.json", - "properties": { - "tree": { "$ref": "tree.json" } - } - }; + $id: "http://example.com/root.json", + properties: { + tree: {$ref: "tree.json"}, + }, + } return ajv.compileAsync(schema).then(function (validate) { - validate .should.be.a('function'); - var validData = { tree: [ - { name: 'a', subtree: [ { name: 'a.a' } ] }, - { name: 'b' } - ] }; - var invalidData = { tree: [ - { name: 'a', subtree: [ { name: 1 } ] } - ] }; - validate(validData) .should.equal(true); - validate(invalidData) .should.equal(false); - }); - }); - - - it('should correctly compile with remote schemas that reference the compiled schema', function() { - var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "recursive.json" } + validate.should.be.a("function") + var validData = { + tree: [{name: "a", subtree: [{name: "a.a"}]}, {name: "b"}], } - }; - return ajv.compileAsync(schema).then(function (validate) { - should.equal(loadCallCount, 1); - validate .should.be.a('function'); - var validData = { a: { b: { a: { b: {} } } } }; - var invalidData = { a: { b: { a: {} } } }; - validate(validData) .should.equal(true); - validate(invalidData) .should.equal(false); - }); - }); - + var invalidData = {tree: [{name: "a", subtree: [{name: 1}]}]} + validate(validData).should.equal(true) + validate(invalidData).should.equal(false) + }) + }) - it('should resolve reference containing "properties" segment with the same property (issue #220)', function() { + it("should correctly compile with remote schemas that reference the compiled schema", function () { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { - "$ref": "object.json#/properties/a" - } - } - }; + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "recursive.json"}, + }, + } return ajv.compileAsync(schema).then(function (validate) { - should.equal(loadCallCount, 2); - validate .should.be.a('function'); - validate({ a: 'foo' }) .should.equal(true); - validate({ a: 42 }) .should.equal(false); - }); - }); - - - describe('loading metaschemas (#334)', function() { - it('should load metaschema if not available', function() { - return test(SCHEMAS['http://example.com/foobar.json'], 1); - }); - - it('should load metaschema of referenced schema if not available', function() { - return test({ "$ref": "http://example.com/foobar.json" }, 2); - }); + should.equal(loadCallCount, 1) + validate.should.be.a("function") + var validData = {a: {b: {a: {b: {}}}}} + var invalidData = {a: {b: {a: {}}}} + validate(validData).should.equal(true) + validate(invalidData).should.equal(false) + }) + }) + + it('should resolve reference containing "properties" segment with the same property (issue #220)', function () { + var schema = { + $id: "http://example.com/parent.json", + properties: { + a: { + $ref: "object.json#/properties/a", + }, + }, + } + return ajv.compileAsync(schema).then(function (validate) { + should.equal(loadCallCount, 2) + validate.should.be.a("function") + validate({a: "foo"}).should.equal(true) + validate({a: 42}).should.equal(false) + }) + }) + + describe("loading metaschemas (#334)", function () { + it("should load metaschema if not available", function () { + return test(SCHEMAS["http://example.com/foobar.json"], 1) + }) + + it("should load metaschema of referenced schema if not available", function () { + return test({$ref: "http://example.com/foobar.json"}, 2) + }) function test(schema, expectedLoadCallCount) { - ajv.addKeyword('myFooBar', { - type: 'string', + ajv.addKeyword("myFooBar", { + type: "string", validate: function (sch, data) { - return sch == data; - } - }); + return sch == data + }, + }) return ajv.compileAsync(schema).then(function (validate) { - should.equal(loadCallCount, expectedLoadCallCount); - validate .should.be.a('function'); - validate('foo') .should.equal(true); - validate('bar') .should.equal(false); - }); + should.equal(loadCallCount, expectedLoadCallCount) + validate.should.be.a("function") + validate("foo").should.equal(true) + validate("bar").should.equal(false) + }) } - }); + }) - - it('should return compiled schema on the next tick if there are no references (#51)', function() { + it("should return compiled schema on the next tick if there are no references (#51)", function () { var schema = { - "$id": "http://example.com/int2plus.json", - "type": "integer", - "minimum": 2 - }; - var beforeCallback1; + $id: "http://example.com/int2plus.json", + type: "integer", + minimum: 2, + } + var beforeCallback1 var p1 = ajv.compileAsync(schema).then(function (validate) { - beforeCallback1 .should.equal(true); - spec(validate); - var beforeCallback2; + beforeCallback1.should.equal(true) + spec(validate) + var beforeCallback2 var p2 = ajv.compileAsync(schema).then(function (_validate) { - beforeCallback2 .should.equal(true); - spec(_validate); - }); - beforeCallback2 = true; - return p2; - }); - beforeCallback1 = true; - return p1; + beforeCallback2.should.equal(true) + spec(_validate) + }) + beforeCallback2 = true + return p2 + }) + beforeCallback1 = true + return p1 function spec(validate) { - should.equal(loadCallCount, 0); - validate .should.be.a('function'); - var validData = 2; - var invalidData = 1; - validate(validData) .should.equal(true); - validate(invalidData) .should.equal(false); + should.equal(loadCallCount, 0) + validate.should.be.a("function") + var validData = 2 + var invalidData = 1 + validate(validData).should.equal(true) + validate(invalidData).should.equal(false) } - }); - + }) - it('should queue calls so only one compileAsync executes at a time (#52)', function() { + it("should queue calls so only one compileAsync executes at a time (#52)", function () { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json" } - } - }; + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json"}, + }, + } return Promise.all([ ajv.compileAsync(schema).then(spec), ajv.compileAsync(schema).then(spec), - ajv.compileAsync(schema).then(spec) - ]); + ajv.compileAsync(schema).then(spec), + ]) function spec(validate) { - should.equal(loadCallCount, 2); - validate .should.be.a('function'); - validate({ a: { b: 2 } }) .should.equal(true); - validate({ a: { b: 1 } }) .should.equal(false); + should.equal(loadCallCount, 2) + validate.should.be.a("function") + validate({a: {b: 2}}).should.equal(true) + validate({a: {b: 1}}).should.equal(false) } - }); - + }) - it('should throw exception if loadSchema is not passed', function (done) { + it("should throw exception if loadSchema is not passed", function (done) { var schema = { - "$id": "http://example.com/int2plus.json", - "type": "integer", - "minimum": 2 - }; - ajv = new Ajv; - should.throw(function() { - ajv.compileAsync(schema, function() { - done(new Error('it should have thrown exception')); - }); - }); - setTimeout(function() { + $id: "http://example.com/int2plus.json", + type: "integer", + minimum: 2, + } + ajv = new Ajv() + should.throw(function () { + ajv.compileAsync(schema, function () { + done(new Error("it should have thrown exception")) + }) + }) + setTimeout(function () { // function is needed for the test to pass in Firefox 4 - done(); - }); - }); + done() + }) + }) - - describe('should return error via callback', function() { - it('if passed schema is invalid', function (done) { + describe("should return error via callback", function () { + it("if passed schema is invalid", function (done) { var invalidSchema = { - "$id": "http://example.com/int2plus.json", - "type": "integer", - "minimum": "invalid" - }; - ajv.compileAsync(invalidSchema, shouldFail(done)); - }); - - it('if loaded schema is invalid', function (done) { + $id: "http://example.com/int2plus.json", + type: "integer", + minimum: "invalid", + } + ajv.compileAsync(invalidSchema, shouldFail(done)) + }) + + it("if loaded schema is invalid", function (done) { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "invalid.json" } - } - }; - ajv.compileAsync(schema, shouldFail(done)); - }); + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "invalid.json"}, + }, + } + ajv.compileAsync(schema, shouldFail(done)) + }) - it('if required schema is loaded but the reference cannot be resolved', function (done) { + it("if required schema is loaded but the reference cannot be resolved", function (done) { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json#/definitions/not_found" } - } - }; - ajv.compileAsync(schema, shouldFail(done)); - }); + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json#/definitions/not_found"}, + }, + } + ajv.compileAsync(schema, shouldFail(done)) + }) - it('if loadSchema returned error', function (done) { + it("if loadSchema returned error", function (done) { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json" } - } - }; - ajv = new Ajv({ loadSchema: badLoadSchema }); - ajv.compileAsync(schema, shouldFail(done)); + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json"}, + }, + } + ajv = new Ajv({loadSchema: badLoadSchema}) + ajv.compileAsync(schema, shouldFail(done)) function badLoadSchema() { - return Promise.reject(new Error('cant load')); + return Promise.reject(new Error("cant load")) } - }); + }) - it('if schema compilation throws some other exception', function (done) { - ajv.addKeyword('badkeyword', { compile: badCompile }); - var schema = { badkeyword: true }; - ajv.compileAsync(schema, shouldFail(done)); + it("if schema compilation throws some other exception", function (done) { + ajv.addKeyword("badkeyword", {compile: badCompile}) + var schema = {badkeyword: true} + ajv.compileAsync(schema, shouldFail(done)) function badCompile(/* schema */) { - throw new Error('cant compile keyword schema'); + throw new Error("cant compile keyword schema") } - }); + }) function shouldFail(done) { return function (err, validate) { - should.exist(err); - should.not.exist(validate); - done(); - }; + should.exist(err) + should.not.exist(validate) + done() + } } - }); - + }) - describe('should return error via promise', function() { - it('if passed schema is invalid', function() { + describe("should return error via promise", function () { + it("if passed schema is invalid", function () { var invalidSchema = { - "$id": "http://example.com/int2plus.json", - "type": "integer", - "minimum": "invalid" - }; - return shouldReject(ajv.compileAsync(invalidSchema)); - }); - - it('if loaded schema is invalid', function() { + $id: "http://example.com/int2plus.json", + type: "integer", + minimum: "invalid", + } + return shouldReject(ajv.compileAsync(invalidSchema)) + }) + + it("if loaded schema is invalid", function () { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "invalid.json" } - } - }; - return shouldReject(ajv.compileAsync(schema)); - }); + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "invalid.json"}, + }, + } + return shouldReject(ajv.compileAsync(schema)) + }) - it('if required schema is loaded but the reference cannot be resolved', function() { + it("if required schema is loaded but the reference cannot be resolved", function () { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json#/definitions/not_found" } - } - }; - return shouldReject(ajv.compileAsync(schema)); - }); + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json#/definitions/not_found"}, + }, + } + return shouldReject(ajv.compileAsync(schema)) + }) - it('if loadSchema returned error', function() { + it("if loadSchema returned error", function () { var schema = { - "$id": "http://example.com/parent.json", - "properties": { - "a": { "$ref": "object.json" } - } - }; - ajv = new Ajv({ loadSchema: badLoadSchema }); - return shouldReject(ajv.compileAsync(schema)); + $id: "http://example.com/parent.json", + properties: { + a: {$ref: "object.json"}, + }, + } + ajv = new Ajv({loadSchema: badLoadSchema}) + return shouldReject(ajv.compileAsync(schema)) function badLoadSchema() { - return Promise.reject(new Error('cant load')); + return Promise.reject(new Error("cant load")) } - }); + }) - it('if schema compilation throws some other exception', function() { - ajv.addKeyword('badkeyword', { compile: badCompile }); - var schema = { badkeyword: true }; - return shouldReject(ajv.compileAsync(schema)); + it("if schema compilation throws some other exception", function () { + ajv.addKeyword("badkeyword", {compile: badCompile}) + var schema = {badkeyword: true} + return shouldReject(ajv.compileAsync(schema)) function badCompile(/* schema */) { - throw new Error('cant compile keyword schema'); + throw new Error("cant compile keyword schema") } - }); + }) function shouldReject(p) { return p.then( - function(validate) { - should.not.exist(validate); - throw new Error('Promise has resolved; it should have rejected'); + function (validate) { + should.not.exist(validate) + throw new Error("Promise has resolved; it should have rejected") }, - function(err) { - should.exist(err); + function (err) { + should.exist(err) } - ); + ) } - }); + }) - - describe('schema with multiple remote properties, the first is recursive schema (#801)', function() { - it('should validate data', function() { + describe("schema with multiple remote properties, the first is recursive schema (#801)", function () { + it("should validate data", function () { var schema = { - "$id": "http://example.com/list.json", - "type": "object", - "properties": { - "foo": {"$ref": "foo.json"} - } - }; + $id: "http://example.com/list.json", + type: "object", + properties: { + foo: {$ref: "foo.json"}, + }, + } return ajv.compileAsync(schema).then(function (validate) { - validate({foo: {}}) .should.equal(true); - }); - }); - }); - + validate({foo: {}}).should.equal(true) + }) + }) + }) function loadSchema(uri) { - loadCallCount++; + loadCallCount++ return new Promise(function (resolve, reject) { - setTimeout(function() { - if (SCHEMAS[uri]) resolve(SCHEMAS[uri]); - else reject(new Error('404')); - }, 10); - }); + setTimeout(function () { + if (SCHEMAS[uri]) resolve(SCHEMAS[uri]) + else reject(new Error("404")) + }, 10) + }) } -}); +}) diff --git a/spec/async/boolean.json b/spec/async/boolean.json index 59284c556f..db2226fb6c 100644 --- a/spec/async/boolean.json +++ b/spec/async/boolean.json @@ -10,7 +10,7 @@ "tests": [ { "description": "any data is valid", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": true } ] @@ -26,12 +26,12 @@ "tests": [ { "description": "any property is invalid", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": false }, { "description": "without property is valid", - "data": { "bar": 1 }, + "data": {"bar": 1}, "valid": true }, { @@ -80,7 +80,7 @@ "schema": { "$async": true, "properties": { - "foo": { "$ref": "#/definitions/foo" } + "foo": {"$ref": "#/definitions/foo"} }, "definitions": { "foo": true @@ -89,7 +89,7 @@ "tests": [ { "description": "any data is valid", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": true } ] @@ -99,7 +99,7 @@ "schema": { "$async": true, "properties": { - "foo": { "$ref": "#/definitions/foo" } + "foo": {"$ref": "#/definitions/foo"} }, "definitions": { "foo": false @@ -108,12 +108,12 @@ "tests": [ { "description": "any property is invalid", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": false }, { "description": "without property is valid", - "data": { "bar": 1 }, + "data": {"bar": 1}, "valid": true }, { diff --git a/spec/async/compound.json b/spec/async/compound.json index 9b9ed40bfd..23c78aee10 100644 --- a/spec/async/compound.json +++ b/spec/async/compound.json @@ -5,7 +5,7 @@ "$async": true, "allOf": [ { - "idExists": { "table": "users" } + "idExists": {"table": "users"} }, { "type": "integer", @@ -42,7 +42,7 @@ "$async": true, "anyOf": [ { - "idExists": { "table": "users" } + "idExists": {"table": "users"} }, { "type": "integer", @@ -79,7 +79,7 @@ "$async": true, "oneOf": [ { - "idExists": { "table": "users" } + "idExists": {"table": "users"} }, { "type": "integer", @@ -115,7 +115,7 @@ "schema": { "$async": true, "not": { - "idExists": { "table": "users" } + "idExists": {"table": "users"} } }, "tests": [ diff --git a/spec/async/format.json b/spec/async/format.json index d8a0cbb467..438bd05994 100644 --- a/spec/async/format.json +++ b/spec/async/format.json @@ -35,43 +35,43 @@ "$async": true, "additionalProperties": { "type": "string", - "format": { "$data": "0#" } + "format": {"$data": "0#"} } }, "tests": [ { "description": "'tomorrow' is a valid english word", - "data": { "english_word": "tomorrow" }, + "data": {"english_word": "tomorrow"}, "valid": true }, { "description": "'manana' is an invalid english word", - "data": { "english_word": "manana" }, + "data": {"english_word": "manana"}, "valid": false }, { "description": "number is invalid", - "data": { "english_word": 1 }, + "data": {"english_word": 1}, "valid": false }, { "description": "'today' throws an exception, not in the dictionary", - "data": { "english_word": "today" }, + "data": {"english_word": "today"}, "error": "unknown word" }, { "description": "valid date", - "data": { "date": "2016-01-25" }, + "data": {"date": "2016-01-25"}, "valid": true }, { "description": "invalid date", - "data": { "date": "01/25/2016" }, + "data": {"date": "01/25/2016"}, "valid": false }, { "description": "number is invalid", - "data": { "date": 1 }, + "data": {"date": 1}, "valid": false } ] diff --git a/spec/async/items.json b/spec/async/items.json index 067c7094f6..9885610237 100644 --- a/spec/async/items.json +++ b/spec/async/items.json @@ -6,41 +6,41 @@ "items": [ { "type": "integer", - "idExists": { "table": "users" } + "idExists": {"table": "users"} }, { "type": "integer" }, { "type": "integer", - "idExists": { "table": "users" } + "idExists": {"table": "users"} } ] }, "tests": [ { "description": "valid array", - "data": [ 1, 2, 5 ], + "data": [1, 2, 5], "valid": true }, { "description": "another valid array", - "data": [ 5, 2, 8 ], + "data": [5, 2, 8], "valid": true }, { "description": "invalid 1st async item", - "data": [ 9, 2, 8 ], + "data": [9, 2, 8], "valid": false }, { "description": "invalid 2nd async item", - "data": [ 1, 2, 9 ], + "data": [1, 2, 9], "valid": false }, { "description": "invalid sync item", - "data": [ 1, "abc", 5 ], + "data": [1, "abc", 5], "valid": false } ] diff --git a/spec/async/keyword.json b/spec/async/keyword.json index 34e10f3c0c..1b34fa00b7 100644 --- a/spec/async/keyword.json +++ b/spec/async/keyword.json @@ -6,43 +6,43 @@ "properties": { "userId": { "type": "integer", - "idExists": { "table": "users" } + "idExists": {"table": "users"} }, "postId": { "type": "integer", - "idExists": { "table": "posts" } + "idExists": {"table": "posts"} }, "categoryId": { "description": "will throw if present, no such table", "type": "integer", - "idExists": { "table": "categories" } + "idExists": {"table": "categories"} } } }, "tests": [ { "description": "valid object", - "data": { "userId": 1, "postId": 21 }, + "data": {"userId": 1, "postId": 21}, "valid": true }, { "description": "another valid object", - "data": { "userId": 5, "postId": 25 }, + "data": {"userId": 5, "postId": 25}, "valid": true }, { "description": "invalid - no such post", - "data": { "userId": 5, "postId": 10 }, + "data": {"userId": 5, "postId": 10}, "valid": false }, { "description": "invalid - no such user", - "data": { "userId": 9, "postId": 25 }, + "data": {"userId": 9, "postId": 25}, "valid": false }, { "description": "should throw exception during validation - no such table", - "data": { "postId": 25, "categoryId": 1 }, + "data": {"postId": 25, "categoryId": 1}, "error": "no such table" } ] @@ -54,43 +54,43 @@ "properties": { "userId": { "type": "integer", - "idExistsWithError": { "table": "users" } + "idExistsWithError": {"table": "users"} }, "postId": { "type": "integer", - "idExistsWithError": { "table": "posts" } + "idExistsWithError": {"table": "posts"} }, "categoryId": { "description": "will throw if present, no such table", "type": "integer", - "idExistsWithError": { "table": "categories" } + "idExistsWithError": {"table": "categories"} } } }, "tests": [ { "description": "valid object", - "data": { "userId": 1, "postId": 21 }, + "data": {"userId": 1, "postId": 21}, "valid": true }, { "description": "another valid object", - "data": { "userId": 5, "postId": 25 }, + "data": {"userId": 5, "postId": 25}, "valid": true }, { "description": "invalid - no such post", - "data": { "userId": 5, "postId": 10 }, + "data": {"userId": 5, "postId": 10}, "valid": false }, { "description": "invalid - no such user", - "data": { "userId": 9, "postId": 25 }, + "data": {"userId": 9, "postId": 25}, "valid": false }, { "description": "should throw exception during validation - no such table", - "data": { "postId": 25, "categoryId": 1 }, + "data": {"postId": 25, "categoryId": 1}, "error": "no such table" } ] @@ -102,33 +102,33 @@ "properties": { "userId": { "type": "integer", - "idExistsCompiled": { "table": "users" } + "idExistsCompiled": {"table": "users"} }, "postId": { "type": "integer", - "idExistsCompiled": { "table": "posts" } + "idExistsCompiled": {"table": "posts"} } } }, "tests": [ { "description": "valid object", - "data": { "userId": 1, "postId": 21 }, + "data": {"userId": 1, "postId": 21}, "valid": true }, { "description": "another valid object", - "data": { "userId": 5, "postId": 25 }, + "data": {"userId": 5, "postId": 25}, "valid": true }, { "description": "invalid - no such post", - "data": { "userId": 5, "postId": 10 }, + "data": {"userId": 5, "postId": 10}, "valid": false }, { "description": "invalid - no such user", - "data": { "userId": 9, "postId": 25 }, + "data": {"userId": 9, "postId": 25}, "valid": false } ] diff --git a/spec/async/properties.json b/spec/async/properties.json index a695062753..e905228f32 100644 --- a/spec/async/properties.json +++ b/spec/async/properties.json @@ -6,7 +6,7 @@ "properties": { "foo": { "type": "integer", - "idExists": { "table": "users" } + "idExists": {"table": "users"} }, "bar": { "type": "integer" @@ -16,22 +16,22 @@ "tests": [ { "description": "valid object", - "data": { "foo": 1, "bar": 2 }, + "data": {"foo": 1, "bar": 2}, "valid": true }, { "description": "another valid object", - "data": { "foo": 5, "bar": 2 }, + "data": {"foo": 5, "bar": 2}, "valid": true }, { "description": "invalid sync property", - "data": { "foo": 1, "bar": "abc" }, + "data": {"foo": 1, "bar": "abc"}, "valid": false }, { "description": "invalid async property", - "data": { "foo": 9, "bar": 2 }, + "data": {"foo": 9, "bar": 2}, "valid": false } ] diff --git a/spec/async_schemas.spec.js b/spec/async_schemas.spec.js index cca8202ac5..d4118a4ddc 100644 --- a/spec/async_schemas.spec.js +++ b/spec/async_schemas.spec.js @@ -1,118 +1,123 @@ -'use strict'; +"use strict" -var jsonSchemaTest = require('json-schema-test') - , Promise = require('./promise') - , getAjvInstances = require('./ajv_async_instances') - , Ajv = require('./ajv') - , suite = require('./browser_test_suite') - , after = require('./after_test'); +var jsonSchemaTest = require("json-schema-test"), + Promise = require("./promise"), + getAjvInstances = require("./ajv_async_instances"), + Ajv = require("./ajv"), + suite = require("./browser_test_suite"), + after = require("./after_test") +var instances = getAjvInstances({$data: true}) -var instances = getAjvInstances({ $data: true }); - - -instances.forEach(addAsyncFormatsAndKeywords); - +instances.forEach(addAsyncFormatsAndKeywords) jsonSchemaTest(instances, { - description: 'asynchronous schemas tests of ' + instances.length + ' ajv instances with different options', + description: + "asynchronous schemas tests of " + + instances.length + + " ajv instances with different options", suites: { - 'async schemas': - typeof window == 'object' - ? suite(require('./async/{**/,}*.json', {mode: 'list'})) - : './async/{**/,}*.json' + "async schemas": + typeof window == "object" + ? suite(require("./async/{**/,}*.json", {mode: "list"})) + : "./async/{**/,}*.json", }, async: true, - asyncValid: 'data', - assert: require('./chai').assert, + asyncValid: "data", + assert: require("./chai").assert, Promise: Promise, afterError: after.error, // afterEach: after.each, cwd: __dirname, - hideFolder: 'async/', - timeout: 90000 -}); - + hideFolder: "async/", + timeout: 90000, +}) -function addAsyncFormatsAndKeywords (ajv) { - ajv.addFormat('english_word', { +function addAsyncFormatsAndKeywords(ajv) { + ajv.addFormat("english_word", { async: true, - validate: checkWordOnServer - }); + validate: checkWordOnServer, + }) - ajv.addKeyword('idExists', { + ajv.addKeyword("idExists", { async: true, - type: 'number', + type: "number", validate: checkIdExists, - errors: false - }); + errors: false, + }) - ajv.addKeyword('idExistsWithError', { + ajv.addKeyword("idExistsWithError", { async: true, - type: 'number', + type: "number", validate: checkIdExistsWithError, - errors: true - }); + errors: true, + }) - ajv.addKeyword('idExistsCompiled', { + ajv.addKeyword("idExistsCompiled", { async: true, - type: 'number', - compile: compileCheckIdExists - }); + type: "number", + compile: compileCheckIdExists, + }) } - function checkWordOnServer(str) { - return str == 'tomorrow' ? Promise.resolve(true) - : str == 'manana' ? Promise.resolve(false) - : Promise.reject(new Error('unknown word')); + return str == "tomorrow" + ? Promise.resolve(true) + : str == "manana" + ? Promise.resolve(false) + : Promise.reject(new Error("unknown word")) } - function checkIdExists(schema, data) { switch (schema.table) { - case 'users': return check([1, 5, 8]); - case 'posts': return check([21, 25, 28]); - default: throw new Error('no such table'); + case "users": + return check([1, 5, 8]) + case "posts": + return check([21, 25, 28]) + default: + throw new Error("no such table") } function check(IDs) { - return Promise.resolve(IDs.indexOf(data) >= 0); + return Promise.resolve(IDs.indexOf(data) >= 0) } } - function checkIdExistsWithError(schema, data) { - var table = schema.table; + var table = schema.table switch (table) { - case 'users': return check(table, [1, 5, 8]); - case 'posts': return check(table, [21, 25, 28]); - default: throw new Error('no such table'); + case "users": + return check(table, [1, 5, 8]) + case "posts": + return check(table, [21, 25, 28]) + default: + throw new Error("no such table") } function check(_table, IDs) { - if (IDs.indexOf(data) >= 0) - return Promise.resolve(true); + if (IDs.indexOf(data) >= 0) return Promise.resolve(true) var error = { - keyword: 'idExistsWithError', - message: 'id not found in table ' + _table - }; - return Promise.reject(new Ajv.ValidationError([error])); + keyword: "idExistsWithError", + message: "id not found in table " + _table, + } + return Promise.reject(new Ajv.ValidationError([error])) } } - function compileCheckIdExists(schema) { switch (schema.table) { - case 'users': return compileCheck([1, 5, 8]); - case 'posts': return compileCheck([21, 25, 28]); - default: throw new Error('no such table'); + case "users": + return compileCheck([1, 5, 8]) + case "posts": + return compileCheck([21, 25, 28]) + default: + throw new Error("no such table") } function compileCheck(IDs) { return function (data) { - return Promise.resolve(IDs.indexOf(data) >= 0); - }; + return Promise.resolve(IDs.indexOf(data) >= 0) + } } } diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index 04d47cecef..dd2525e660 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -1,271 +1,277 @@ -'use strict'; +"use strict" -var Ajv = require('./ajv') - , Promise = require('./promise') - , getAjvInstances = require('./ajv_async_instances') - , should = require('./chai').should(); +var Ajv = require("./ajv"), + Promise = require("./promise"), + getAjvInstances = require("./ajv_async_instances"), + should = require("./chai").should() - -describe('async schemas, formats and keywords', function() { - this.timeout(30000); - var ajv, instances; +describe("async schemas, formats and keywords", function () { + this.timeout(30000) + var ajv, instances beforeEach(function () { - instances = getAjvInstances(); - ajv = instances[0]; - }); - + instances = getAjvInstances() + ajv = instances[0] + }) - describe('async schemas without async elements', function() { - it('should return result as promise', function() { + describe("async schemas without async elements", function () { + it("should return result as promise", function () { var schema = { $async: true, - type: 'string', - maxLength: 3 - }; + type: "string", + maxLength: 3, + } - return repeat(function() { return Promise.all(instances.map(test)); }); + return repeat(function () { + return Promise.all(instances.map(test)) + }) function test(_ajv) { - var validate = _ajv.compile(schema); + var validate = _ajv.compile(schema) return Promise.all([ - shouldBeValid( validate('abc'), 'abc' ), - shouldBeInvalid( validate('abcd') ), - shouldBeInvalid( validate(1) ), - ]); + shouldBeValid(validate("abc"), "abc"), + shouldBeInvalid(validate("abcd")), + shouldBeInvalid(validate(1)), + ]) } - }); + }) - it('should fail compilation if async schema is inside sync schema', function() { + it("should fail compilation if async schema is inside sync schema", function () { var schema = { properties: { foo: { $async: true, - type: 'string', - maxLength: 3 - } - } - }; - - shouldThrowFunc('async schema in sync schema', function() { - ajv.compile(schema); - }); - - schema.$async = true; + type: "string", + maxLength: 3, + }, + }, + } - ajv.compile(schema); - }); - }); + shouldThrowFunc("async schema in sync schema", function () { + ajv.compile(schema) + }) + schema.$async = true - describe('async formats', function() { - beforeEach(addFormatEnglishWord); + ajv.compile(schema) + }) + }) + describe("async formats", function () { + beforeEach(addFormatEnglishWord) - it('should fail compilation if async format is inside sync schema', function() { + it("should fail compilation if async format is inside sync schema", function () { instances.forEach(function (_ajv) { var schema = { - type: 'string', - format: 'english_word' - }; - - shouldThrowFunc('async format in sync schema', function() { - _ajv.compile(schema); - }); - schema.$async = true; - _ajv.compile(schema); - }); - }); - }); - - - describe('async custom keywords', function() { - beforeEach(function() { + type: "string", + format: "english_word", + } + + shouldThrowFunc("async format in sync schema", function () { + _ajv.compile(schema) + }) + schema.$async = true + _ajv.compile(schema) + }) + }) + }) + + describe("async custom keywords", function () { + beforeEach(function () { instances.forEach(function (_ajv) { - _ajv.addKeyword('idExists', { + _ajv.addKeyword("idExists", { async: true, - type: 'number', + type: "number", validate: checkIdExists, - errors: false - }); + errors: false, + }) - _ajv.addKeyword('idExistsWithError', { + _ajv.addKeyword("idExistsWithError", { async: true, - type: 'number', + type: "number", validate: checkIdExistsWithError, - errors: true - }); - }); - }); + errors: true, + }) + }) + }) - - it('should fail compilation if async keyword is inside sync schema', function() { + it("should fail compilation if async keyword is inside sync schema", function () { instances.forEach(function (_ajv) { var schema = { - type: 'object', + type: "object", properties: { userId: { - type: 'integer', - idExists: { table: 'users' } - } - } - }; - - shouldThrowFunc('async keyword in sync schema', function() { - _ajv.compile(schema); - }); + type: "integer", + idExists: {table: "users"}, + }, + }, + } - schema.$async = true; - _ajv.compile(schema); - }); - }); + shouldThrowFunc("async keyword in sync schema", function () { + _ajv.compile(schema) + }) + schema.$async = true + _ajv.compile(schema) + }) + }) - it('should return custom error', function() { - return Promise.all(instances.map(function (_ajv) { - var schema = { - $async: true, - type: 'object', - properties: { - userId: { - type: 'integer', - idExistsWithError: { table: 'users' } + it("should return custom error", function () { + return Promise.all( + instances.map(function (_ajv) { + var schema = { + $async: true, + type: "object", + properties: { + userId: { + type: "integer", + idExistsWithError: {table: "users"}, + }, + postId: { + type: "integer", + idExistsWithError: {table: "posts"}, + }, }, - postId: { - type: 'integer', - idExistsWithError: { table: 'posts' } - } } - }; - - var validate = _ajv.compile(schema); - return Promise.all([ - shouldBeInvalid( validate({ userId: 5, postId: 10 }), [ 'id not found in table posts' ] ), - shouldBeInvalid( validate({ userId: 9, postId: 25 }), [ 'id not found in table users' ] ) - ]); - })); - }); + var validate = _ajv.compile(schema) + return Promise.all([ + shouldBeInvalid(validate({userId: 5, postId: 10}), [ + "id not found in table posts", + ]), + shouldBeInvalid(validate({userId: 9, postId: 25}), [ + "id not found in table users", + ]), + ]) + }) + ) + }) function checkIdExists(schema, data) { switch (schema.table) { - case 'users': return check([1, 5, 8]); - case 'posts': return check([21, 25, 28]); - default: throw new Error('no such table'); + case "users": + return check([1, 5, 8]) + case "posts": + return check([21, 25, 28]) + default: + throw new Error("no such table") } function check(IDs) { - return Promise.resolve(IDs.indexOf(data) >= 0); + return Promise.resolve(IDs.indexOf(data) >= 0) } } function checkIdExistsWithError(schema, data) { - var table = schema.table; + var table = schema.table switch (table) { - case 'users': return check(table, [1, 5, 8]); - case 'posts': return check(table, [21, 25, 28]); - default: throw new Error('no such table'); + case "users": + return check(table, [1, 5, 8]) + case "posts": + return check(table, [21, 25, 28]) + default: + throw new Error("no such table") } function check(_table, IDs) { - if (IDs.indexOf(data) >= 0) return Promise.resolve(true); + if (IDs.indexOf(data) >= 0) return Promise.resolve(true) var error = { - keyword: 'idExistsWithError', - message: 'id not found in table ' + _table - }; - return Promise.reject(new Ajv.ValidationError([error])); + keyword: "idExistsWithError", + message: "id not found in table " + _table, + } + return Promise.reject(new Ajv.ValidationError([error])) } } - }); - + }) - describe('async referenced schemas', function() { - beforeEach(function() { - instances = getAjvInstances({ inlineRefs: false, extendRefs: 'ignore' }); - addFormatEnglishWord(); - }); + describe("async referenced schemas", function () { + beforeEach(function () { + instances = getAjvInstances({inlineRefs: false, extendRefs: "ignore"}) + addFormatEnglishWord() + }) - it('should validate referenced async schema', function() { + it("should validate referenced async schema", function () { var schema = { $async: true, definitions: { english_word: { $async: true, - type: 'string', - format: 'english_word' - } + type: "string", + format: "english_word", + }, }, properties: { - word: { $ref: '#/definitions/english_word' } - } - }; - - return repeat(function() { return Promise.all(instances.map(function (_ajv) { - var validate = _ajv.compile(schema); - var validData = { word: 'tomorrow' }; + word: {$ref: "#/definitions/english_word"}, + }, + } - return Promise.all([ - shouldBeValid( validate(validData), validData ), - shouldBeInvalid( validate({ word: 'manana' }) ), - shouldBeInvalid( validate({ word: 1 }) ), - shouldThrow( validate({ word: 'today' }), 'unknown word' ) - ]); - })); }); - }); - - it('should validate recursive async schema', function() { + return repeat(function () { + return Promise.all( + instances.map(function (_ajv) { + var validate = _ajv.compile(schema) + var validData = {word: "tomorrow"} + + return Promise.all([ + shouldBeValid(validate(validData), validData), + shouldBeInvalid(validate({word: "manana"})), + shouldBeInvalid(validate({word: 1})), + shouldThrow(validate({word: "today"}), "unknown word"), + ]) + }) + ) + }) + }) + + it("should validate recursive async schema", function () { var schema = { $async: true, definitions: { english_word: { $async: true, - type: 'string', - format: 'english_word' - } + type: "string", + format: "english_word", + }, }, - type: 'object', + type: "object", properties: { foo: { - anyOf: [ - { $ref: '#/definitions/english_word' }, - { $ref: '#' } - ] - } - } - }; + anyOf: [{$ref: "#/definitions/english_word"}, {$ref: "#"}], + }, + }, + } - return recursiveTest(schema); - }); + return recursiveTest(schema) + }) - it('should validate recursive ref to async sub-schema, issue #612', function() { + it("should validate recursive ref to async sub-schema, issue #612", function () { var schema = { $async: true, - type: 'object', + type: "object", properties: { foo: { $async: true, anyOf: [ { - type: 'string', - format: 'english_word' + type: "string", + format: "english_word", }, { - type: 'object', + type: "object", properties: { - foo: { $ref: '#/properties/foo' } - } - } - ] - } - } - }; + foo: {$ref: "#/properties/foo"}, + }, + }, + ], + }, + }, + } - return recursiveTest(schema); - }); + return recursiveTest(schema) + }) - it('should validate ref from referenced async schema to root schema', function() { + it("should validate ref from referenced async schema to root schema", function () { var schema = { $async: true, definitions: { @@ -273,184 +279,194 @@ describe('async schemas, formats and keywords', function() { $async: true, anyOf: [ { - type: 'string', - format: 'english_word' + type: "string", + format: "english_word", }, - { $ref: '#' } - ] - } + {$ref: "#"}, + ], + }, }, - type: 'object', + type: "object", properties: { - foo: { $ref: '#/definitions/wordOrRoot' } - } - }; + foo: {$ref: "#/definitions/wordOrRoot"}, + }, + } - return recursiveTest(schema); - }); + return recursiveTest(schema) + }) - it('should validate refs between two async schemas', function() { + it("should validate refs between two async schemas", function () { var schemaObj = { - $id: 'http://e.com/obj.json#', + $id: "http://e.com/obj.json#", $async: true, - type: 'object', + type: "object", properties: { - foo: { $ref: 'http://e.com/word.json#' } - } - }; + foo: {$ref: "http://e.com/word.json#"}, + }, + } var schemaWord = { - $id: 'http://e.com/word.json#', + $id: "http://e.com/word.json#", $async: true, anyOf: [ { - type: 'string', - format: 'english_word' + type: "string", + format: "english_word", }, - { $ref: 'http://e.com/obj.json#' } - ] - }; + {$ref: "http://e.com/obj.json#"}, + ], + } - return recursiveTest(schemaObj, schemaWord); - }); + return recursiveTest(schemaObj, schemaWord) + }) - it('should fail compilation if sync schema references async schema', function() { + it("should fail compilation if sync schema references async schema", function () { var schema = { - $id: 'http://e.com/obj.json#', - type: 'object', + $id: "http://e.com/obj.json#", + type: "object", properties: { - foo: { $ref: 'http://e.com/word.json#' } - } - }; + foo: {$ref: "http://e.com/word.json#"}, + }, + } var schemaWord = { - $id: 'http://e.com/word.json#', + $id: "http://e.com/word.json#", $async: true, anyOf: [ { - type: 'string', - format: 'english_word' + type: "string", + format: "english_word", }, - { $ref: 'http://e.com/obj.json#' } - ] - }; + {$ref: "http://e.com/obj.json#"}, + ], + } - ajv.addSchema(schemaWord); - ajv.addFormat('english_word', { + ajv.addSchema(schemaWord) + ajv.addFormat("english_word", { async: true, - validate: checkWordOnServer - }); + validate: checkWordOnServer, + }) - shouldThrowFunc('async schema referenced by sync schema', function() { - ajv.compile(schema); - }); + shouldThrowFunc("async schema referenced by sync schema", function () { + ajv.compile(schema) + }) - schema.$id = 'http://e.com/obj2.json#'; - schema.$async = true; + schema.$id = "http://e.com/obj2.json#" + schema.$async = true - ajv.compile(schema); - }); + ajv.compile(schema) + }) function recursiveTest(schema, refSchema) { - return repeat(function() { return Promise.all(instances.map(function (_ajv) { - if (refSchema) try { _ajv.addSchema(refSchema); } catch(e) {} - var validate = _ajv.compile(schema); - var data; - - return Promise.all([ - shouldBeValid( validate(data = { foo: 'tomorrow' }), data ), - shouldBeInvalid( validate({ foo: 'manana' }) ), - shouldBeInvalid( validate({ foo: 1 }) ), - shouldThrow( validate({ foo: 'today' }), 'unknown word' ), - shouldBeValid( validate(data = { foo: { foo: 'tomorrow' }}), data ), - shouldBeInvalid( validate({ foo: { foo: 'manana' }}) ), - shouldBeInvalid( validate({ foo: { foo: 1 }}) ), - shouldThrow( validate({ foo: { foo: 'today' }}), 'unknown word' ), - shouldBeValid( validate(data = { foo: { foo: { foo: 'tomorrow' }}}), data ), - shouldBeInvalid( validate({ foo: { foo: { foo: 'manana' }}}) ), - shouldBeInvalid( validate({ foo: { foo: { foo: 1 }}}) ), - shouldThrow( validate({ foo: { foo: { foo: 'today' }}}), 'unknown word' ) - ]); - })); }); + return repeat(function () { + return Promise.all( + instances.map(function (_ajv) { + if (refSchema) + try { + _ajv.addSchema(refSchema) + } catch (e) {} + var validate = _ajv.compile(schema) + var data + + return Promise.all([ + shouldBeValid(validate((data = {foo: "tomorrow"})), data), + shouldBeInvalid(validate({foo: "manana"})), + shouldBeInvalid(validate({foo: 1})), + shouldThrow(validate({foo: "today"}), "unknown word"), + shouldBeValid(validate((data = {foo: {foo: "tomorrow"}})), data), + shouldBeInvalid(validate({foo: {foo: "manana"}})), + shouldBeInvalid(validate({foo: {foo: 1}})), + shouldThrow(validate({foo: {foo: "today"}}), "unknown word"), + shouldBeValid( + validate((data = {foo: {foo: {foo: "tomorrow"}}})), + data + ), + shouldBeInvalid(validate({foo: {foo: {foo: "manana"}}})), + shouldBeInvalid(validate({foo: {foo: {foo: 1}}})), + shouldThrow( + validate({foo: {foo: {foo: "today"}}}), + "unknown word" + ), + ]) + }) + ) + }) } - }); - + }) function addFormatEnglishWord() { instances.forEach(function (_ajv) { - _ajv.addFormat('english_word', { + _ajv.addFormat("english_word", { async: true, - validate: checkWordOnServer - }); - }); + validate: checkWordOnServer, + }) + }) } -}); - +}) function checkWordOnServer(str) { - return str == 'tomorrow' ? Promise.resolve(true) - : str == 'manana' ? Promise.resolve(false) - : Promise.reject(new Error('unknown word')); + return str == "tomorrow" + ? Promise.resolve(true) + : str == "manana" + ? Promise.resolve(false) + : Promise.reject(new Error("unknown word")) } - function shouldThrowFunc(message, func) { - var err; - should.throw(function() { - try { func(); } - catch(e) { err = e; throw e; } - }); + var err + should.throw(function () { + try { + func() + } catch (e) { + err = e + throw e + } + }) - err.message .should.equal(message); + err.message.should.equal(message) } - function shouldBeValid(p, data) { return p.then(function (valid) { - valid .should.equal(data); - }); + valid.should.equal(data) + }) } - -var SHOULD_BE_INVALID = 'test: should be invalid'; +var SHOULD_BE_INVALID = "test: should be invalid" function shouldBeInvalid(p, expectedMessages) { - return checkNotValid(p) - .then(function (err) { - err .should.be.instanceof(Ajv.ValidationError); - err.errors .should.be.an('array'); - err.validation .should.equal(true); + return checkNotValid(p).then(function (err) { + err.should.be.instanceof(Ajv.ValidationError) + err.errors.should.be.an("array") + err.validation.should.equal(true) if (expectedMessages) { var messages = err.errors.map(function (e) { - return e.message; - }); - messages .should.eql(expectedMessages); + return e.message + }) + messages.should.eql(expectedMessages) } - }); + }) } - function shouldThrow(p, exception) { - return checkNotValid(p) - .then(function (err) { - err.message .should.equal(exception); - }); + return checkNotValid(p).then(function (err) { + err.message.should.equal(exception) + }) } - function checkNotValid(p) { - return p.then(function (/* valid */) { - throw new Error(SHOULD_BE_INVALID); - }) - .catch(function (err) { - err. should.be.instanceof(Error); - if (err.message == SHOULD_BE_INVALID) throw err; - return err; - }); + return p + .then(function (/* valid */) { + throw new Error(SHOULD_BE_INVALID) + }) + .catch(function (err) { + err.should.be.instanceof(Error) + if (err.message == SHOULD_BE_INVALID) throw err + return err + }) } - function repeat(func) { - return func(); + return func() // var promises = []; // for (var i=0; i<1000; i++) promises.push(func()); // return Promise.all(promises); diff --git a/spec/boolean.spec.js b/spec/boolean.spec.js index c4c3b3eb19..5da22820f4 100644 --- a/spec/boolean.spec.js +++ b/spec/boolean.spec.js @@ -1,476 +1,438 @@ -'use strict'; +"use strict" -var Ajv = require('./ajv'); -require('./chai').should(); +var Ajv = require("./ajv") +require("./chai").should() +describe("boolean schemas", function () { + var ajvs -describe('boolean schemas', function() { - var ajvs; + before(function () { + ajvs = [new Ajv(), new Ajv({allErrors: true}), new Ajv({inlineRefs: false})] + }) - before(function() { - ajvs = [ - new Ajv, - new Ajv({allErrors: true}), - new Ajv({inlineRefs: false}) - ]; - }); + describe("top level schema", function () { + describe("schema = true", function () { + it("should validate any data as valid", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('top level schema', function() { - describe('schema = true', function() { - it('should validate any data as valid', function() { - ajvs.forEach(test(true, true)); - }); - }); - - describe('schema = false', function() { - it('should validate any data as invalid', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should validate any data as invalid", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { - var validate = ajv.compile(boolSchema); - testSchema(validate, valid); - }; + var validate = ajv.compile(boolSchema) + testSchema(validate, valid) + } } - }); - + }) - describe('in properties / sub-properties', function() { - describe('schema = true', function() { - it('should be valid with any property value', function() { - ajvs.forEach(test(true, true)); - }); - }); + describe("in properties / sub-properties", function () { + describe("schema = true", function () { + it("should be valid with any property value", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('schema = false', function() { - it('should be invalid with any property value', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any property value", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - type: 'object', + type: "object", properties: { foo: boolSchema, bar: { - type: 'object', + type: "object", properties: { - baz: boolSchema - } - } - } - }; - - var validate = ajv.compile(schema); - validate({ foo: 1, bar: { baz: 1 }}) .should.equal(valid); - validate({ foo: '1', bar: { baz: '1' }}) .should.equal(valid); - validate({ foo: {}, bar: { baz: {} }}) .should.equal(valid); - validate({ foo: [], bar: { baz: [] }}) .should.equal(valid); - validate({ foo: true, bar: { baz: true }}) .should.equal(valid); - validate({ foo: false, bar: { baz: false }}) .should.equal(valid); - validate({ foo: null, bar: { baz: null }}) .should.equal(valid); - - validate({ bar: { quux: 1 } }) .should.equal(true); - }; + baz: boolSchema, + }, + }, + }, + } + + var validate = ajv.compile(schema) + validate({foo: 1, bar: {baz: 1}}).should.equal(valid) + validate({foo: "1", bar: {baz: "1"}}).should.equal(valid) + validate({foo: {}, bar: {baz: {}}}).should.equal(valid) + validate({foo: [], bar: {baz: []}}).should.equal(valid) + validate({foo: true, bar: {baz: true}}).should.equal(valid) + validate({foo: false, bar: {baz: false}}).should.equal(valid) + validate({foo: null, bar: {baz: null}}).should.equal(valid) + + validate({bar: {quux: 1}}).should.equal(true) + } } - }); + }) + describe("in items / sub-items", function () { + describe("schema = true", function () { + it("should be valid with any item value", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('in items / sub-items', function() { - describe('schema = true', function() { - it('should be valid with any item value', function() { - ajvs.forEach(test(true, true)); - }); - }); - - describe('schema = false', function() { - it('should be invalid with any item value', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any item value", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - type: 'array', - items: boolSchema - }; + type: "array", + items: boolSchema, + } - var validate = ajv.compile(schema); - validate([ 1 ]) .should.equal(valid); - validate([ '1' ]) .should.equal(valid); - validate([ {} ]) .should.equal(valid); - validate([ [] ]) .should.equal(valid); - validate([ true ]) .should.equal(valid); - validate([ false ]) .should.equal(valid); - validate([ null ]) .should.equal(valid); + var validate = ajv.compile(schema) + validate([1]).should.equal(valid) + validate(["1"]).should.equal(valid) + validate([{}]).should.equal(valid) + validate([[]]).should.equal(valid) + validate([true]).should.equal(valid) + validate([false]).should.equal(valid) + validate([null]).should.equal(valid) - validate([]) .should.equal(true); + validate([]).should.equal(true) schema = { - type: 'array', + type: "array", items: [ true, { - type: 'array', - items: [ - true, - boolSchema - ] + type: "array", + items: [true, boolSchema], }, - boolSchema - ] - }; - - validate = ajv.compile(schema); - validate([ 1, [ 1, 1 ], 1 ]) .should.equal(valid); - validate([ '1', [ '1', '1' ], '1' ]) .should.equal(valid); - validate([ {}, [ {}, {} ], {} ]) .should.equal(valid); - validate([ [], [ [], [] ], [] ]) .should.equal(valid); - validate([ true, [ true, true ], true ]) .should.equal(valid); - validate([ false, [ false, false ], false ]) .should.equal(valid); - validate([ null, [ null, null ], null ]) .should.equal(valid); - - validate([ 1, [ 1 ] ]) .should.equal(true); - }; + boolSchema, + ], + } + + validate = ajv.compile(schema) + validate([1, [1, 1], 1]).should.equal(valid) + validate(["1", ["1", "1"], "1"]).should.equal(valid) + validate([{}, [{}, {}], {}]).should.equal(valid) + validate([[], [[], []], []]).should.equal(valid) + validate([true, [true, true], true]).should.equal(valid) + validate([false, [false, false], false]).should.equal(valid) + validate([null, [null, null], null]).should.equal(valid) + + validate([1, [1]]).should.equal(true) + } } - }); - + }) - describe('in dependencies and sub-dependencies', function() { - describe('schema = true', function() { - it('should be valid with any property value', function() { - ajvs.forEach(test(true, true)); - }); - }); + describe("in dependencies and sub-dependencies", function () { + describe("schema = true", function () { + it("should be valid with any property value", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('schema = false', function() { - it('should be invalid with any property value', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any property value", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - type: 'object', + type: "object", dependencies: { foo: boolSchema, bar: { - type: 'object', + type: "object", dependencies: { - baz: boolSchema - } - } - } - }; - - var validate = ajv.compile(schema); - validate({ foo: 1, bar: 1, baz: 1 }) .should.equal(valid); - validate({ foo: '1', bar: '1', baz: '1' }) .should.equal(valid); - validate({ foo: {}, bar: {}, baz: {} }) .should.equal(valid); - validate({ foo: [], bar: [], baz: [] }) .should.equal(valid); - validate({ foo: true, bar: true, baz: true }) .should.equal(valid); - validate({ foo: false, bar: false, baz: false }) .should.equal(valid); - validate({ foo: null, bar: null, baz: null }) .should.equal(valid); - - validate({ bar: 1, quux: 1 }) .should.equal(true); - }; + baz: boolSchema, + }, + }, + }, + } + + var validate = ajv.compile(schema) + validate({foo: 1, bar: 1, baz: 1}).should.equal(valid) + validate({foo: "1", bar: "1", baz: "1"}).should.equal(valid) + validate({foo: {}, bar: {}, baz: {}}).should.equal(valid) + validate({foo: [], bar: [], baz: []}).should.equal(valid) + validate({foo: true, bar: true, baz: true}).should.equal(valid) + validate({foo: false, bar: false, baz: false}).should.equal(valid) + validate({foo: null, bar: null, baz: null}).should.equal(valid) + + validate({bar: 1, quux: 1}).should.equal(true) + } } - }); - + }) - describe('in patternProperties', function () { - describe('schema = true', function() { - it('should be valid with any property matching pattern', function() { - ajvs.forEach(test(true, true)); - }); - }); + describe("in patternProperties", function () { + describe("schema = true", function () { + it("should be valid with any property matching pattern", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('schema = false', function() { - it('should be invalid with any property matching pattern', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any property matching pattern", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - type: 'object', + type: "object", patternProperties: { - '^f': boolSchema, - 'r$': { - type: 'object', + "^f": boolSchema, + r$: { + type: "object", patternProperties: { - 'z$': boolSchema - } - } - } - }; - - var validate = ajv.compile(schema); - validate({ foo: 1, bar: { baz: 1 }}) .should.equal(valid); - validate({ foo: '1', bar: { baz: '1' }}) .should.equal(valid); - validate({ foo: {}, bar: { baz: {} }}) .should.equal(valid); - validate({ foo: [], bar: { baz: [] }}) .should.equal(valid); - validate({ foo: true, bar: { baz: true }}) .should.equal(valid); - validate({ foo: false, bar: { baz: false }}) .should.equal(valid); - validate({ foo: null, bar: { baz: null }}) .should.equal(valid); - - validate({ bar: { quux: 1 } }) .should.equal(true); - }; + z$: boolSchema, + }, + }, + }, + } + + var validate = ajv.compile(schema) + validate({foo: 1, bar: {baz: 1}}).should.equal(valid) + validate({foo: "1", bar: {baz: "1"}}).should.equal(valid) + validate({foo: {}, bar: {baz: {}}}).should.equal(valid) + validate({foo: [], bar: {baz: []}}).should.equal(valid) + validate({foo: true, bar: {baz: true}}).should.equal(valid) + validate({foo: false, bar: {baz: false}}).should.equal(valid) + validate({foo: null, bar: {baz: null}}).should.equal(valid) + + validate({bar: {quux: 1}}).should.equal(true) + } } - }); + }) + describe("in propertyNames", function () { + describe("schema = true", function () { + it("should be valid with any property", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('in propertyNames', function() { - describe('schema = true', function() { - it('should be valid with any property', function() { - ajvs.forEach(test(true, true)); - }); - }); - - describe('schema = false', function() { - it('should be invalid with any property', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any property", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - type: 'object', - propertyNames: boolSchema - }; + type: "object", + propertyNames: boolSchema, + } - var validate = ajv.compile(schema); - validate({ foo: 1 }) .should.equal(valid); - validate({ bar: 1 }) .should.equal(valid); + var validate = ajv.compile(schema) + validate({foo: 1}).should.equal(valid) + validate({bar: 1}).should.equal(valid) - validate({}) .should.equal(true); - }; + validate({}).should.equal(true) + } } - }); - + }) - describe('in contains', function() { - describe('schema = true', function() { - it('should be valid with any items', function() { - ajvs.forEach(test(true, true)); - }); - }); + describe("in contains", function () { + describe("schema = true", function () { + it("should be valid with any items", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('schema = false', function() { - it('should be invalid with any items', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any items", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - type: 'array', - contains: boolSchema - }; - - var validate = ajv.compile(schema); - validate([ 1 ]) .should.equal(valid); - validate([ 'foo' ]) .should.equal(valid); - validate([ {} ]) .should.equal(valid); - validate([ [] ]) .should.equal(valid); - validate([ true ]) .should.equal(valid); - validate([ false ]) .should.equal(valid); - validate([ null ]) .should.equal(valid); - - validate([]) .should.equal(false); - }; + type: "array", + contains: boolSchema, + } + + var validate = ajv.compile(schema) + validate([1]).should.equal(valid) + validate(["foo"]).should.equal(valid) + validate([{}]).should.equal(valid) + validate([[]]).should.equal(valid) + validate([true]).should.equal(valid) + validate([false]).should.equal(valid) + validate([null]).should.equal(valid) + + validate([]).should.equal(false) + } } - }); + }) + describe("in not", function () { + describe("schema = true", function () { + it("should be invalid with any data", function () { + ajvs.forEach(test(true, false)) + }) + }) - describe('in not', function() { - describe('schema = true', function() { - it('should be invalid with any data', function() { - ajvs.forEach(test(true, false)); - }); - }); - - describe('schema = false', function() { - it('should be valid with any data', function() { - ajvs.forEach(test(false, true)); - }); - }); + describe("schema = false", function () { + it("should be valid with any data", function () { + ajvs.forEach(test(false, true)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - not: boolSchema - }; + not: boolSchema, + } - var validate = ajv.compile(schema); - testSchema(validate, valid); - }; + var validate = ajv.compile(schema) + testSchema(validate, valid) + } } - }); - + }) - describe('in allOf', function() { - describe('schema = true', function() { - it('should be valid with any data', function() { - ajvs.forEach(test(true, true)); - }); - }); + describe("in allOf", function () { + describe("schema = true", function () { + it("should be valid with any data", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('schema = false', function() { - it('should be invalid with any data', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any data", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - allOf: [ - false, - boolSchema - ] - }; + allOf: [false, boolSchema], + } - var validate = ajv.compile(schema); - testSchema(validate, false); + var validate = ajv.compile(schema) + testSchema(validate, false) schema = { - allOf: [ - true, - boolSchema - ] - }; + allOf: [true, boolSchema], + } - validate = ajv.compile(schema); - testSchema(validate, valid); - }; + validate = ajv.compile(schema) + testSchema(validate, valid) + } } - }); + }) + describe("in anyOf", function () { + describe("schema = true", function () { + it("should be valid with any data", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('in anyOf', function() { - describe('schema = true', function() { - it('should be valid with any data', function() { - ajvs.forEach(test(true, true)); - }); - }); - - describe('schema = false', function() { - it('should be invalid with any data', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any data", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - anyOf: [ - false, - boolSchema - ] - }; + anyOf: [false, boolSchema], + } - var validate = ajv.compile(schema); - testSchema(validate, valid); + var validate = ajv.compile(schema) + testSchema(validate, valid) schema = { - anyOf: [ - true, - boolSchema - ] - }; + anyOf: [true, boolSchema], + } - validate = ajv.compile(schema); - testSchema(validate, true); - }; + validate = ajv.compile(schema) + testSchema(validate, true) + } } - }); + }) + describe("in oneOf", function () { + describe("schema = true", function () { + it("should be valid with any data", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('in oneOf', function() { - describe('schema = true', function() { - it('should be valid with any data', function() { - ajvs.forEach(test(true, true)); - }); - }); - - describe('schema = false', function() { - it('should be invalid with any data', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any data", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - oneOf: [ - false, - boolSchema - ] - }; + oneOf: [false, boolSchema], + } - var validate = ajv.compile(schema); - testSchema(validate, valid); + var validate = ajv.compile(schema) + testSchema(validate, valid) schema = { - oneOf: [ - true, - boolSchema - ] - }; + oneOf: [true, boolSchema], + } - validate = ajv.compile(schema); - testSchema(validate, !valid); - }; + validate = ajv.compile(schema) + testSchema(validate, !valid) + } } - }); + }) + describe("in $ref", function () { + describe("schema = true", function () { + it("should be valid with any data", function () { + ajvs.forEach(test(true, true)) + }) + }) - describe('in $ref', function() { - describe('schema = true', function() { - it('should be valid with any data', function() { - ajvs.forEach(test(true, true)); - }); - }); - - describe('schema = false', function() { - it('should be invalid with any data', function() { - ajvs.forEach(test(false, false)); - }); - }); + describe("schema = false", function () { + it("should be invalid with any data", function () { + ajvs.forEach(test(false, false)) + }) + }) function test(boolSchema, valid) { return function (ajv) { var schema = { - $ref: '#/definitions/bool', + $ref: "#/definitions/bool", definitions: { - bool: boolSchema - } - }; + bool: boolSchema, + }, + } - var validate = ajv.compile(schema); - testSchema(validate, valid); - }; + var validate = ajv.compile(schema) + testSchema(validate, valid) + } } - }); - + }) function testSchema(validate, valid) { - validate(1) .should.equal(valid); - validate('foo') .should.equal(valid); - validate({}) .should.equal(valid); - validate([]) .should.equal(valid); - validate(true) .should.equal(valid); - validate(false) .should.equal(valid); - validate(null) .should.equal(valid); + validate(1).should.equal(valid) + validate("foo").should.equal(valid) + validate({}).should.equal(valid) + validate([]).should.equal(valid) + validate(true).should.equal(valid) + validate(false).should.equal(valid) + validate(null).should.equal(valid) } -}); +}) diff --git a/spec/browser_test_suite.js b/spec/browser_test_suite.js index 40e138c7c6..f7016eb0e2 100644 --- a/spec/browser_test_suite.js +++ b/spec/browser_test_suite.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict" module.exports = function (suite) { suite.forEach(function (file) { - if (file.name.indexOf('optional/format') == 0) - file.name = file.name.replace('optional/', ''); - file.test = file.module; - }); - return suite; -}; + if (file.name.indexOf("optional/format") == 0) + file.name = file.name.replace("optional/", "") + file.test = file.module + }) + return suite +} diff --git a/spec/chai.js b/spec/chai.js index b3cd0ed78f..2bb98a87e9 100644 --- a/spec/chai.js +++ b/spec/chai.js @@ -1,3 +1,3 @@ -'use strict'; +"use strict" -module.exports = typeof window == 'object' ? window.chai : require('' + 'chai'); +module.exports = typeof window == "object" ? window.chai : require("" + "chai") diff --git a/spec/coercion.spec.js b/spec/coercion.spec.js index a9f13de5c4..64406f7ee4 100644 --- a/spec/coercion.spec.js +++ b/spec/coercion.spec.js @@ -1,507 +1,485 @@ -'use strict'; - -var Ajv = require('./ajv'); -require('./chai').should(); +"use strict" +var Ajv = require("./ajv") +require("./chai").should() var coercionRules = { - 'string': { - 'number': [ - { from: 1, to: '1' }, - { from: 1.5, to: '1.5' }, - { from: 2e100, to: '2e+100' } - ], - 'boolean': [ - { from: false, to: 'false' }, - { from: true, to: 'true' } + string: { + number: [ + {from: 1, to: "1"}, + {from: 1.5, to: "1.5"}, + {from: 2e100, to: "2e+100"}, ], - 'null': [ - { from: null, to: '' } + boolean: [ + {from: false, to: "false"}, + {from: true, to: "true"}, ], - 'object': [ - { from: {}, to: undefined } + null: [{from: null, to: ""}], + object: [{from: {}, to: undefined}], + array: [ + {from: [], to: undefined}, + {from: [1], to: undefined}, ], - 'array': [ - { from: [], to: undefined }, - { from: [1], to: undefined } - ] }, - 'number': { - 'string': [ - { from: '1', to: 1 }, - { from: '1.5', to: 1.5 }, - { from: '2e10', to: 2e10 }, - { from: '1a', to: undefined }, - { from: 'abc', to: undefined }, - { from: '', to: undefined } + number: { + string: [ + {from: "1", to: 1}, + {from: "1.5", to: 1.5}, + {from: "2e10", to: 2e10}, + {from: "1a", to: undefined}, + {from: "abc", to: undefined}, + {from: "", to: undefined}, ], - 'boolean': [ - { from: false, to: 0 }, - { from: true, to: 1 } + boolean: [ + {from: false, to: 0}, + {from: true, to: 1}, ], - 'null': [ - { from: null, to: 0 } + null: [{from: null, to: 0}], + object: [{from: {}, to: undefined}], + array: [ + {from: [], to: undefined}, + {from: [true], to: undefined}, ], - 'object': [ - { from: {}, to: undefined } - ], - 'array': [ - { from: [], to: undefined }, - { from: [true], to: undefined } - ] }, - 'integer': { - 'string': [ - { from: '1', to: 1 }, - { from: '1.5', to: undefined }, - { from: '2e10', to: 2e10 }, - { from: '1a', to: undefined }, - { from: 'abc', to: undefined }, - { from: '', to: undefined } - ], - 'boolean': [ - { from: false, to: 0 }, - { from: true, to: 1 } + integer: { + string: [ + {from: "1", to: 1}, + {from: "1.5", to: undefined}, + {from: "2e10", to: 2e10}, + {from: "1a", to: undefined}, + {from: "abc", to: undefined}, + {from: "", to: undefined}, ], - 'null': [ - { from: null, to: 0 } + boolean: [ + {from: false, to: 0}, + {from: true, to: 1}, ], - 'object': [ - { from: {}, to: undefined } + null: [{from: null, to: 0}], + object: [{from: {}, to: undefined}], + array: [ + {from: [], to: undefined}, + {from: ["1"], to: undefined}, ], - 'array': [ - { from: [], to: undefined }, - { from: ['1'], to: undefined } - ] }, - 'boolean': { - 'string': [ - { from: 'false', to: false }, - { from: 'true', to: true }, - { from: '', to: undefined }, - { from: 'abc', to: undefined } + boolean: { + string: [ + {from: "false", to: false}, + {from: "true", to: true}, + {from: "", to: undefined}, + {from: "abc", to: undefined}, ], - 'number': [ - { from: 0, to: false }, - { from: 1, to: true }, - { from: 2, to: undefined }, - { from: 2.5, to: undefined } + number: [ + {from: 0, to: false}, + {from: 1, to: true}, + {from: 2, to: undefined}, + {from: 2.5, to: undefined}, ], - 'null': [ - { from: null, to: false } + null: [{from: null, to: false}], + object: [{from: {}, to: undefined}], + array: [ + {from: [], to: undefined}, + {from: [0], to: undefined}, ], - 'object': [ - { from: {}, to: undefined } - ], - 'array': [ - { from: [], to: undefined }, - { from: [0], to: undefined } - ] }, - 'null': { - 'string': [ - { from: '', to: null }, - { from: 'abc', to: undefined }, - { from: 'null', to: undefined } + null: { + string: [ + {from: "", to: null}, + {from: "abc", to: undefined}, + {from: "null", to: undefined}, ], - 'number': [ - { from: 0, to: null }, - { from: 1, to: undefined } + number: [ + {from: 0, to: null}, + {from: 1, to: undefined}, ], - 'boolean': [ - { from: false, to: null }, - { from: true, to: undefined } + boolean: [ + {from: false, to: null}, + {from: true, to: undefined}, ], - 'object': [ - { from: {}, to: undefined } + object: [{from: {}, to: undefined}], + array: [ + {from: [], to: undefined}, + {from: [null], to: undefined}, ], - 'array': [ - { from: [], to: undefined }, - { from: [null], to: undefined } - ] }, - 'array': { - 'all': [ - { type: 'string', from: 'abc', to: undefined }, - { type: 'number', from: 1, to: undefined }, - { type: 'boolean', from: true, to: undefined }, - { type: 'null', from: null, to: undefined }, - { type: 'object', from: {}, to: undefined } - ] + array: { + all: [ + {type: "string", from: "abc", to: undefined}, + {type: "number", from: 1, to: undefined}, + {type: "boolean", from: true, to: undefined}, + {type: "null", from: null, to: undefined}, + {type: "object", from: {}, to: undefined}, + ], }, - 'object': { - 'all': [ - { type: 'string', from: 'abc', to: undefined }, - { type: 'number', from: 1, to: undefined }, - { type: 'boolean', from: true, to: undefined }, - { type: 'null', from: null, to: undefined }, - { type: 'array', from: [], to: undefined } - ] - } -}; + object: { + all: [ + {type: "string", from: "abc", to: undefined}, + {type: "number", from: 1, to: undefined}, + {type: "boolean", from: true, to: undefined}, + {type: "null", from: null, to: undefined}, + {type: "array", from: [], to: undefined}, + ], + }, +} -var coercionArrayRules = JSON.parse(JSON.stringify(coercionRules)); +var coercionArrayRules = JSON.parse(JSON.stringify(coercionRules)) coercionArrayRules.string.array = [ - { from: ['abc'], to: 'abc' }, - { from: [123], to: '123' }, - { from: ['abc', 'def'], to: undefined }, - { from: [], to: undefined } -]; + {from: ["abc"], to: "abc"}, + {from: [123], to: "123"}, + {from: ["abc", "def"], to: undefined}, + {from: [], to: undefined}, +] coercionArrayRules.number.array = [ - { from: [1.5], to: 1.5 }, - { from: ['1.5'], to: 1.5 } -]; -coercionArrayRules.integer.array = [ - { from: [1], to: 1 }, - { from: ['1'], to: 1 }, - { from: [true], to: 1 }, - { from: [null], to: 0 } -]; -coercionArrayRules.boolean.array = [ - { from: [true], to: true }, - { from: ['true'], to: true }, - { from: [1], to: true } -]; -coercionArrayRules.null.array = [ - { from: [null], to: null }, - { from: [''], to: null }, - { from: [0], to: null }, - { from: [false], to: null } -]; -coercionArrayRules.object.array = [ - { from: [{}], to: undefined } -]; + {from: [1.5], to: 1.5}, + {from: ["1.5"], to: 1.5}, +] +coercionArrayRules.integer.array = [ + {from: [1], to: 1}, + {from: ["1"], to: 1}, + {from: [true], to: 1}, + {from: [null], to: 0}, +] +coercionArrayRules.boolean.array = [ + {from: [true], to: true}, + {from: ["true"], to: true}, + {from: [1], to: true}, +] +coercionArrayRules.null.array = [ + {from: [null], to: null}, + {from: [""], to: null}, + {from: [0], to: null}, + {from: [false], to: null}, +] +coercionArrayRules.object.array = [{from: [{}], to: undefined}] coercionArrayRules.array = { - 'string': [ - {from: 'abc', to: ['abc']} - ], - 'number': [ - {from: 1, to: [1]} - ], - 'boolean': [ - {from: true, to: [true]} - ], - 'null': [ - {from: null, to: [null]} - ], - 'object': [ - {from: {}, to: undefined} - ] -}; - -describe('Type coercion', function () { - var ajv, fullAjv, instances; - - beforeEach(function() { - ajv = new Ajv({ coerceTypes: true, verbose: true }); - fullAjv = new Ajv({ coerceTypes: true, verbose: true, allErrors: true }); - instances = [ ajv, fullAjv ]; - }); - - - it('should coerce scalar values', function() { - testRules(coercionRules, function (test, schema, canCoerce/*, toType, fromType*/) { + string: [{from: "abc", to: ["abc"]}], + number: [{from: 1, to: [1]}], + boolean: [{from: true, to: [true]}], + null: [{from: null, to: [null]}], + object: [{from: {}, to: undefined}], +} + +describe("Type coercion", function () { + var ajv, fullAjv, instances + + beforeEach(function () { + ajv = new Ajv({coerceTypes: true, verbose: true}) + fullAjv = new Ajv({coerceTypes: true, verbose: true, allErrors: true}) + instances = [ajv, fullAjv] + }) + + it("should coerce scalar values", function () { + testRules(coercionRules, function ( + test, + schema, + canCoerce /*, toType, fromType*/ + ) { instances.forEach(function (_ajv) { - var valid = _ajv.validate(schema, test.from); + var valid = _ajv.validate(schema, test.from) //if (valid !== canCoerce) console.log('true', toType, fromType, test, ajv.errors); - valid. should.equal(canCoerce); - }); - }); - }); - - it('should coerce scalar values (coerceTypes = array)', function() { - ajv = new Ajv({ coerceTypes: 'array', verbose: true }); - fullAjv = new Ajv({ coerceTypes: 'array', verbose: true, allErrors: true }); - instances = [ ajv, fullAjv ]; - - testRules(coercionArrayRules, function (test, schema, canCoerce, toType, fromType) { + valid.should.equal(canCoerce) + }) + }) + }) + + it("should coerce scalar values (coerceTypes = array)", function () { + ajv = new Ajv({coerceTypes: "array", verbose: true}) + fullAjv = new Ajv({coerceTypes: "array", verbose: true, allErrors: true}) + instances = [ajv, fullAjv] + + testRules(coercionArrayRules, function ( + test, + schema, + canCoerce, + toType, + fromType + ) { instances.forEach(function (_ajv) { - var valid = _ajv.validate(schema, test.from); - if (valid !== canCoerce) console.log(toType, '.', fromType, test, schema, ajv.errors); - valid. should.equal(canCoerce); - }); - }); - }); - - it('should coerce values in objects/arrays and update properties/items', function() { - testRules(coercionRules, function (test, schema, canCoerce/*, toType, fromType*/) { + var valid = _ajv.validate(schema, test.from) + if (valid !== canCoerce) + console.log(toType, ".", fromType, test, schema, ajv.errors) + valid.should.equal(canCoerce) + }) + }) + }) + + it("should coerce values in objects/arrays and update properties/items", function () { + testRules(coercionRules, function ( + test, + schema, + canCoerce /*, toType, fromType*/ + ) { var schemaObject = { - type: 'object', + type: "object", properties: { - foo: schema - } - }; + foo: schema, + }, + } var schemaArray = { - type: 'array', - items: schema - }; + type: "array", + items: schema, + } var schemaArrObj = { - type: 'array', - items: schemaObject - }; - + type: "array", + items: schemaObject, + } instances.forEach(function (_ajv) { - testCoercion(_ajv, schemaArray, [ test.from ], [ test.to ]); - testCoercion(_ajv, schemaObject, { foo: test.from }, { foo: test.to }); - testCoercion(_ajv, schemaArrObj, [ { foo: test.from } ], [ { foo: test.to } ]); - }); + testCoercion(_ajv, schemaArray, [test.from], [test.to]) + testCoercion(_ajv, schemaObject, {foo: test.from}, {foo: test.to}) + testCoercion(_ajv, schemaArrObj, [{foo: test.from}], [{foo: test.to}]) + }) function testCoercion(_ajv, _schema, fromData, toData) { - var valid = _ajv.validate(_schema, fromData); + var valid = _ajv.validate(_schema, fromData) //if (valid !== canCoerce) console.log(schema, fromData, toData); - valid. should.equal(canCoerce); - if (valid) fromData.should.eql(toData); + valid.should.equal(canCoerce) + if (valid) fromData.should.eql(toData) } - }); - }); - + }) + }) - it('should coerce to multiple types in order with number type', function() { + it("should coerce to multiple types in order with number type", function () { var schema = { - type: 'object', + type: "object", properties: { foo: { - type: [ 'number', 'boolean', 'null' ] - } - } - }; + type: ["number", "boolean", "null"], + }, + }, + } instances.forEach(function (_ajv) { - var data; + var data - _ajv.validate(schema, data = { foo: '1' }) .should.equal(true); - data .should.eql({ foo: 1 }); + _ajv.validate(schema, (data = {foo: "1"})).should.equal(true) + data.should.eql({foo: 1}) - _ajv.validate(schema, data = { foo: '1.5' }) .should.equal(true); - data .should.eql({ foo: 1.5 }); + _ajv.validate(schema, (data = {foo: "1.5"})).should.equal(true) + data.should.eql({foo: 1.5}) - _ajv.validate(schema, data = { foo: 'false' }) .should.equal(true); - data .should.eql({ foo: false }); + _ajv.validate(schema, (data = {foo: "false"})).should.equal(true) + data.should.eql({foo: false}) - _ajv.validate(schema, data = { foo: 1 }) .should.equal(true); - data .should.eql({ foo: 1 }); // no coercion + _ajv.validate(schema, (data = {foo: 1})).should.equal(true) + data.should.eql({foo: 1}) // no coercion - _ajv.validate(schema, data = { foo: true }) .should.equal(true); - data .should.eql({ foo: true }); // no coercion + _ajv.validate(schema, (data = {foo: true})).should.equal(true) + data.should.eql({foo: true}) // no coercion - _ajv.validate(schema, data = { foo: null }) .should.equal(true); - data .should.eql({ foo: null }); // no coercion + _ajv.validate(schema, (data = {foo: null})).should.equal(true) + data.should.eql({foo: null}) // no coercion - _ajv.validate(schema, data = { foo: 'abc' }) .should.equal(false); - data .should.eql({ foo: 'abc' }); // can't coerce + _ajv.validate(schema, (data = {foo: "abc"})).should.equal(false) + data.should.eql({foo: "abc"}) // can't coerce - _ajv.validate(schema, data = { foo: {} }) .should.equal(false); - data .should.eql({ foo: {} }); // can't coerce + _ajv.validate(schema, (data = {foo: {}})).should.equal(false) + data.should.eql({foo: {}}) // can't coerce - _ajv.validate(schema, data = { foo: [] }) .should.equal(false); - data .should.eql({ foo: [] }); // can't coerce - }); - }); + _ajv.validate(schema, (data = {foo: []})).should.equal(false) + data.should.eql({foo: []}) // can't coerce + }) + }) - it('should coerce to multiple types in order with integer type', function() { + it("should coerce to multiple types in order with integer type", function () { var schema = { - type: 'object', + type: "object", properties: { foo: { - type: [ 'integer', 'boolean', 'null' ] - } - } - }; + type: ["integer", "boolean", "null"], + }, + }, + } instances.forEach(function (_ajv) { - var data; - - _ajv.validate(schema, data = { foo: '1' }) .should.equal(true); - data .should.eql({ foo: 1 }); + var data - _ajv.validate(schema, data = { foo: 'false' }) .should.equal(true); - data .should.eql({ foo: false }); + _ajv.validate(schema, (data = {foo: "1"})).should.equal(true) + data.should.eql({foo: 1}) - _ajv.validate(schema, data = { foo: 1 }) .should.equal(true); - data .should.eql({ foo: 1 }); // no coercion + _ajv.validate(schema, (data = {foo: "false"})).should.equal(true) + data.should.eql({foo: false}) - _ajv.validate(schema, data = { foo: true }) .should.equal(true); - data .should.eql({ foo: true }); // no coercion + _ajv.validate(schema, (data = {foo: 1})).should.equal(true) + data.should.eql({foo: 1}) // no coercion - _ajv.validate(schema, data = { foo: null }) .should.equal(true); - data .should.eql({ foo: null }); // no coercion + _ajv.validate(schema, (data = {foo: true})).should.equal(true) + data.should.eql({foo: true}) // no coercion - _ajv.validate(schema, data = { foo: 'abc' }) .should.equal(false); - data .should.eql({ foo: 'abc' }); // can't coerce + _ajv.validate(schema, (data = {foo: null})).should.equal(true) + data.should.eql({foo: null}) // no coercion - _ajv.validate(schema, data = { foo: {} }) .should.equal(false); - data .should.eql({ foo: {} }); // can't coerce + _ajv.validate(schema, (data = {foo: "abc"})).should.equal(false) + data.should.eql({foo: "abc"}) // can't coerce - _ajv.validate(schema, data = { foo: [] }) .should.equal(false); - data .should.eql({ foo: [] }); // can't coerce - }); - }); + _ajv.validate(schema, (data = {foo: {}})).should.equal(false) + data.should.eql({foo: {}}) // can't coerce + _ajv.validate(schema, (data = {foo: []})).should.equal(false) + data.should.eql({foo: []}) // can't coerce + }) + }) - it('should fail to coerce non-number if multiple properties/items are coerced (issue #152)', function() { + it("should fail to coerce non-number if multiple properties/items are coerced (issue #152)", function () { var schema = { - type: 'object', + type: "object", properties: { - foo: { type: 'number' }, - bar: { type: 'number' } - } - }; + foo: {type: "number"}, + bar: {type: "number"}, + }, + } var schema2 = { - type: 'array', - items: { type: 'number' } - }; - - instances.forEach(function (_ajv) { - var data = { foo: '123', bar: 'bar' }; - _ajv.validate(schema, data) .should.equal(false); - data .should.eql({ foo: 123, bar: 'bar' }); + type: "array", + items: {type: "number"}, + } - var data2 = [ '123', 'bar' ]; - _ajv.validate(schema2, data2) .should.equal(false); - data2 .should.eql([ 123, 'bar' ]); - }); - }); + instances.forEach(function (_ajv) { + var data = {foo: "123", bar: "bar"} + _ajv.validate(schema, data).should.equal(false) + data.should.eql({foo: 123, bar: "bar"}) + var data2 = ["123", "bar"] + _ajv.validate(schema2, data2).should.equal(false) + data2.should.eql([123, "bar"]) + }) + }) - it('should update data if the schema is in ref that is not inlined', function () { - instances.push(new Ajv({ coerceTypes: true, inlineRefs: false })); + it("should update data if the schema is in ref that is not inlined", function () { + instances.push(new Ajv({coerceTypes: true, inlineRefs: false})) var schema = { - type: 'object', + type: "object", definitions: { - foo: { type: 'number' } + foo: {type: "number"}, }, properties: { - foo: { $ref: '#/definitions/foo' } - } - }; + foo: {$ref: "#/definitions/foo"}, + }, + } var schema2 = { - type: 'object', + type: "object", definitions: { foo: { // allOf is needed to make sure that "foo" is compiled to a separate function // and not simply passed through (as it would be if it were only $ref) - allOf: [{ $ref: '#/definitions/bar' }] + allOf: [{$ref: "#/definitions/bar"}], }, - bar: { type: 'number' } + bar: {type: "number"}, }, properties: { - foo: { $ref: '#/definitions/foo' } - } - }; + foo: {$ref: "#/definitions/foo"}, + }, + } var schemaRecursive = { - type: [ 'object', 'number' ], + type: ["object", "number"], properties: { - foo: { $ref: '#' } - } - }; + foo: {$ref: "#"}, + }, + } var schemaRecursive2 = { - $id: 'http://e.com/schema.json#', + $id: "http://e.com/schema.json#", definitions: { foo: { - $id: 'http://e.com/foo.json#', - type: [ 'object', 'number' ], + $id: "http://e.com/foo.json#", + type: ["object", "number"], properties: { - foo: { $ref: '#' } - } - } + foo: {$ref: "#"}, + }, + }, }, properties: { - foo: { $ref: 'http://e.com/foo.json#' } - } - }; + foo: {$ref: "http://e.com/foo.json#"}, + }, + } instances.forEach(function (_ajv) { - testCoercion(schema, { foo: '1' }, { foo: 1 }); - testCoercion(schema2, { foo: '1' }, { foo: 1 }); - testCoercion(schemaRecursive, { foo: { foo: '1' } }, { foo: { foo: 1 } }); - testCoercion(schemaRecursive2, { foo: { foo: { foo: '1' } } }, - { foo: { foo: { foo: 1 } } }); + testCoercion(schema, {foo: "1"}, {foo: 1}) + testCoercion(schema2, {foo: "1"}, {foo: 1}) + testCoercion(schemaRecursive, {foo: {foo: "1"}}, {foo: {foo: 1}}) + testCoercion( + schemaRecursive2, + {foo: {foo: {foo: "1"}}}, + {foo: {foo: {foo: 1}}} + ) function testCoercion(_schema, fromData, toData) { - var valid = _ajv.validate(_schema, fromData); + var valid = _ajv.validate(_schema, fromData) // if (!valid) console.log(schema, fromData, toData); - valid. should.equal(true); - fromData .should.eql(toData); + valid.should.equal(true) + fromData.should.eql(toData) } - }); - }); - + }) + }) - it('should generate one error for type with coerceTypes option (issue #469)', function() { + it("should generate one error for type with coerceTypes option (issue #469)", function () { var schema = { - "type": "number", - "minimum": 10 - }; + type: "number", + minimum: 10, + } instances.forEach(function (_ajv) { - var validate = _ajv.compile(schema); - validate(9). should.equal(false); - validate.errors.length .should.equal(1); - - validate(11). should.equal(true); + var validate = _ajv.compile(schema) + validate(9).should.equal(false) + validate.errors.length.should.equal(1) - validate('foo'). should.equal(false); - validate.errors.length .should.equal(1); - }); - }); + validate(11).should.equal(true) + validate("foo").should.equal(false) + validate.errors.length.should.equal(1) + }) + }) - it('should check "uniqueItems" after coercion', function() { + it('should check "uniqueItems" after coercion', function () { var schema = { - items: {type: 'number'}, - uniqueItems: true - }; + items: {type: "number"}, + uniqueItems: true, + } instances.forEach(function (_ajv) { - var validate = _ajv.compile(schema); - validate([1, '2', 3]). should.equal(true); + var validate = _ajv.compile(schema) + validate([1, "2", 3]).should.equal(true) - validate([1, '2', 2]). should.equal(false); - validate.errors.length .should.equal(1); - validate.errors[0].keyword .should.equal('uniqueItems'); - }); - }); + validate([1, "2", 2]).should.equal(false) + validate.errors.length.should.equal(1) + validate.errors[0].keyword.should.equal("uniqueItems") + }) + }) - - it('should check "contains" after coercion', function() { + it('should check "contains" after coercion', function () { var schema = { - items: {type: 'number'}, - contains: {const: 2} - }; + items: {type: "number"}, + contains: {const: 2}, + } instances.forEach(function (_ajv) { - var validate = _ajv.compile(schema); - validate([1, '2', 3]). should.equal(true); - - validate([1, '3', 4]). should.equal(false); - validate.errors.pop().keyword .should.equal('contains'); - }); - }); + var validate = _ajv.compile(schema) + validate([1, "2", 3]).should.equal(true) + validate([1, "3", 4]).should.equal(false) + validate.errors.pop().keyword.should.equal("contains") + }) + }) function testRules(rules, cb) { for (var toType in rules) { for (var fromType in rules[toType]) { - var tests = rules[toType][fromType]; + var tests = rules[toType][fromType] tests.forEach(function (test) { - var canCoerce = test.to !== undefined; + var canCoerce = test.to !== undefined var schema = canCoerce - ? (Array.isArray(test.to) - ? { "type": toType, "items": { "type": fromType, "enum": [ test.to[0] ] } } - : { "type": toType, "enum": [ test.to ] }) - : { type: toType }; - cb(test, schema, canCoerce, toType, fromType); - }); + ? Array.isArray(test.to) + ? {type: toType, items: {type: fromType, enum: [test.to[0]]}} + : {type: toType, enum: [test.to]} + : {type: toType} + cb(test, schema, canCoerce, toType, fromType) + }) } } } -}); +}) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 2924fceea9..00d410a84c 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -1,263 +1,281 @@ -'use strict'; +"use strict" -var getAjvInstances = require('./ajv_instances') - , should = require('./chai').should() - , equal = require('../lib/compile/equal') - , customRules = require('./custom_rules'); +var getAjvInstances = require("./ajv_instances"), + should = require("./chai").should(), + equal = require("../lib/compile/equal"), + customRules = require("./custom_rules") +describe("Custom keywords", function () { + var ajv, instances -describe('Custom keywords', function () { - var ajv, instances; - - beforeEach(function() { + beforeEach(function () { instances = getAjvInstances({ - allErrors: true, - verbose: true, - inlineRefs: false - }); - ajv = instances[0]; - }); - - - describe('custom rules', function() { - describe('rule with "interpreted" keyword validation', function() { - it('should add and validate rule', function() { - testEvenKeyword({ type: 'number', validate: validateEven }); + allErrors: true, + verbose: true, + inlineRefs: false, + }) + ajv = instances[0] + }) + + describe("custom rules", function () { + describe('rule with "interpreted" keyword validation', function () { + it("should add and validate rule", function () { + testEvenKeyword({type: "number", validate: validateEven}) function validateEven(schema, data) { - if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); - return data % 2 ? !schema : schema; + if (typeof schema != "boolean") + throw new Error('The value of "even" keyword must be boolean') + return data % 2 ? !schema : schema } - }); + }) - it('should add, validate keyword schema and validate rule', function() { + it("should add, validate keyword schema and validate rule", function () { testEvenKeyword({ - type: 'number', + type: "number", validate: validateEven, - metaSchema: { "type": "boolean" } - }); + metaSchema: {type: "boolean"}, + }) - shouldBeInvalidSchema({ "x-even": "not_boolean" }); + shouldBeInvalidSchema({"x-even": "not_boolean"}) function validateEven(schema, data) { - return data % 2 ? !schema : schema; + return data % 2 ? !schema : schema } - }); + }) - it('should pass parent schema to "interpreted" keyword validation', function() { + it('should pass parent schema to "interpreted" keyword validation', function () { testRangeKeyword({ - type: 'number', - validate: validateRange - }); + type: "number", + validate: validateRange, + }) function validateRange(schema, data, parentSchema) { - validateRangeSchema(schema, parentSchema); + validateRangeSchema(schema, parentSchema) return parentSchema.exclusiveRange === true - ? data > schema[0] && data < schema[1] - : data >= schema[0] && data <= schema[1]; + ? data > schema[0] && data < schema[1] + : data >= schema[0] && data <= schema[1] } - }); + }) - it('should validate meta schema and pass parent schema to "interpreted" keyword validation', function() { + it('should validate meta schema and pass parent schema to "interpreted" keyword validation', function () { testRangeKeyword({ - type: 'number', + type: "number", validate: validateRange, metaSchema: { - "type": "array", - "items": [ { "type": "number" }, { "type": "number" } ], - "additionalItems": false - } - }); - shouldBeInvalidSchema({ 'x-range': [ "1", 2 ] }); - shouldBeInvalidSchema({ 'x-range': {} }); - shouldBeInvalidSchema({ 'x-range': [ 1, 2, 3 ] }); + type: "array", + items: [{type: "number"}, {type: "number"}], + additionalItems: false, + }, + }) + shouldBeInvalidSchema({"x-range": ["1", 2]}) + shouldBeInvalidSchema({"x-range": {}}) + shouldBeInvalidSchema({"x-range": [1, 2, 3]}) function validateRange(schema, data, parentSchema) { return parentSchema.exclusiveRange === true - ? data > schema[0] && data < schema[1] - : data >= schema[0] && data <= schema[1]; + ? data > schema[0] && data < schema[1] + : data >= schema[0] && data <= schema[1] } - }); + }) - it('should allow defining custom errors for "interpreted" keyword', function() { - testRangeKeyword({ type: 'number', validate: validateRange }, true); + it('should allow defining custom errors for "interpreted" keyword', function () { + testRangeKeyword({type: "number", validate: validateRange}, true) function validateRange(schema, data, parentSchema) { - validateRangeSchema(schema, parentSchema); - var min = schema[0] - , max = schema[1] - , exclusive = parentSchema.exclusiveRange === true; + validateRangeSchema(schema, parentSchema) + var min = schema[0], + max = schema[1], + exclusive = parentSchema.exclusiveRange === true - var minOk = exclusive ? data > min : data >= min; - var maxOk = exclusive ? data < max : data <= max; - var valid = minOk && maxOk; + var minOk = exclusive ? data > min : data >= min + var maxOk = exclusive ? data < max : data <= max + var valid = minOk && maxOk if (!valid) { - var err = { keyword: 'x-range' }; - validateRange.errors = [err]; - var comparison, limit; + var err = {keyword: "x-range"} + validateRange.errors = [err] + var comparison, limit if (minOk) { - comparison = exclusive ? '<' : '<='; - limit = max; + comparison = exclusive ? "<" : "<=" + limit = max } else { - comparison = exclusive ? '>' : '>='; - limit = min; + comparison = exclusive ? ">" : ">=" + limit = min } - err.message = 'should be ' + comparison + ' ' + limit; + err.message = "should be " + comparison + " " + limit err.params = { comparison: comparison, limit: limit, - exclusive: exclusive - }; + exclusive: exclusive, + } } - return valid; + return valid } - }); - }); - + }) + }) - describe('rule with "compiled" keyword validation', function() { - it('should add and validate rule', function() { - testEvenKeyword({ type: 'number', compile: compileEven }); - shouldBeInvalidSchema({ "x-even": "not_boolean" }); + describe('rule with "compiled" keyword validation', function () { + it("should add and validate rule", function () { + testEvenKeyword({type: "number", compile: compileEven}) + shouldBeInvalidSchema({"x-even": "not_boolean"}) function compileEven(schema) { - if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); - return schema ? isEven : isOdd; + if (typeof schema != "boolean") + throw new Error('The value of "even" keyword must be boolean') + return schema ? isEven : isOdd } - function isEven(data) { return data % 2 === 0; } - function isOdd(data) { return data % 2 !== 0; } - }); + function isEven(data) { + return data % 2 === 0 + } + function isOdd(data) { + return data % 2 !== 0 + } + }) - it('should add, validate keyword schema and validate rule', function() { + it("should add, validate keyword schema and validate rule", function () { testEvenKeyword({ - type: 'number', + type: "number", compile: compileEven, - metaSchema: { "type": "boolean" } - }); - shouldBeInvalidSchema({ "x-even": "not_boolean" }); + metaSchema: {type: "boolean"}, + }) + shouldBeInvalidSchema({"x-even": "not_boolean"}) function compileEven(schema) { - return schema ? isEven : isOdd; + return schema ? isEven : isOdd } - function isEven(data) { return data % 2 === 0; } - function isOdd(data) { return data % 2 !== 0; } - }); - - it('should compile keyword validating function only once per schema', function () { - testConstantKeyword({ compile: compileConstant }); - }); + function isEven(data) { + return data % 2 === 0 + } + function isOdd(data) { + return data % 2 !== 0 + } + }) - it('should allow multiple schemas for the same keyword', function () { - testMultipleConstantKeyword({ compile: compileConstant }); - }); + it("should compile keyword validating function only once per schema", function () { + testConstantKeyword({compile: compileConstant}) + }) - it('should pass parent schema to "compiled" keyword validation', function() { - testRangeKeyword({ type: 'number', compile: compileRange }); - }); + it("should allow multiple schemas for the same keyword", function () { + testMultipleConstantKeyword({compile: compileConstant}) + }) - it('should allow multiple parent schemas for the same keyword', function () { - testMultipleRangeKeyword({ type: 'number', compile: compileRange }); - }); - }); + it('should pass parent schema to "compiled" keyword validation', function () { + testRangeKeyword({type: "number", compile: compileRange}) + }) + it("should allow multiple parent schemas for the same keyword", function () { + testMultipleRangeKeyword({type: "number", compile: compileRange}) + }) + }) function compileConstant(schema) { - return typeof schema == 'object' && schema !== null - ? isDeepEqual - : isStrictEqual; + return typeof schema == "object" && schema !== null + ? isDeepEqual + : isStrictEqual - function isDeepEqual(data) { return equal(data, schema); } - function isStrictEqual(data) { return data === schema; } + function isDeepEqual(data) { + return equal(data, schema) + } + function isStrictEqual(data) { + return data === schema + } } function compileRange(schema, parentSchema) { - validateRangeSchema(schema, parentSchema); + validateRangeSchema(schema, parentSchema) - var min = schema[0]; - var max = schema[1]; + var min = schema[0] + var max = schema[1] return parentSchema.exclusiveRange === true - ? function (data) { return data > min && data < max; } - : function (data) { return data >= min && data <= max; }; + ? function (data) { + return data > min && data < max + } + : function (data) { + return data >= min && data <= max + } } - }); - + }) - describe('macro rules', function() { - it('should add and validate rule with "macro" keyword', function() { - testEvenKeyword({ type: 'number', macro: macroEven }, 2); - }); + describe("macro rules", function () { + it('should add and validate rule with "macro" keyword', function () { + testEvenKeyword({type: "number", macro: macroEven}, 2) + }) - it('should add and expand macro rule', function() { - testConstantKeyword({ macro: macroConstant }, 2); - }); + it("should add and expand macro rule", function () { + testConstantKeyword({macro: macroConstant}, 2) + }) - it('should allow multiple schemas for the same macro keyword', function () { - testMultipleConstantKeyword({ macro: macroConstant }, 2); - }); + it("should allow multiple schemas for the same macro keyword", function () { + testMultipleConstantKeyword({macro: macroConstant}, 2) + }) - it('should pass parent schema to "macro" keyword', function() { - testRangeKeyword({ type: 'number', macro: macroRange }, undefined, 2); - }); + it('should pass parent schema to "macro" keyword', function () { + testRangeKeyword({type: "number", macro: macroRange}, undefined, 2) + }) - it('should allow multiple parent schemas for the same macro keyword', function () { - testMultipleRangeKeyword({ type: 'number', macro: macroRange }, 2); - }); + it("should allow multiple parent schemas for the same macro keyword", function () { + testMultipleRangeKeyword({type: "number", macro: macroRange}, 2) + }) - it('should support resolving $ref without id or $id', function () { + it("should support resolving $ref without id or $id", function () { instances.forEach(function (_ajv) { - _ajv.addKeyword('macroRef', { + _ajv.addKeyword("macroRef", { macro: function (schema, parentSchema, it) { - it.baseId .should.equal('#'); - var ref = schema.$ref; - var validate = _ajv.getSchema(ref); - if (validate) return validate.schema; - throw new ajv.constructor.MissingRefError(it.baseId, ref); + it.baseId.should.equal("#") + var ref = schema.$ref + var validate = _ajv.getSchema(ref) + if (validate) return validate.schema + throw new ajv.constructor.MissingRefError(it.baseId, ref) }, metaSchema: { - "type": "object", - "required": [ "$ref" ], - "additionalProperties": false, - "properties": { - "$ref": { - "type": "string" - } - } - } - }); + type: "object", + required: ["$ref"], + additionalProperties: false, + properties: { + $ref: { + type: "string", + }, + }, + }, + }) var schema = { - "macroRef": { - "$ref": "#/definitions/schema" + macroRef: { + $ref: "#/definitions/schema", }, - "definitions": { - "schema": { - "type": "string" - } - } - }; - var validate; - (function compileMacroRef () { validate = _ajv.compile(schema); }).should.not.throw(); - shouldBeValid(validate, 'foo'); - shouldBeInvalid(validate, 1, 2); - }); - }); - - it('should recursively expand macro keywords', function() { + definitions: { + schema: { + type: "string", + }, + }, + } + var validate + ;(function compileMacroRef() { + validate = _ajv.compile(schema) + }.should.not.throw()) + shouldBeValid(validate, "foo") + shouldBeInvalid(validate, 1, 2) + }) + }) + + it("should recursively expand macro keywords", function () { instances.forEach(function (_ajv) { - _ajv.addKeyword('deepProperties', { type: 'object', macro: macroDeepProperties }); - _ajv.addKeyword('range', { type: 'number', macro: macroRange }); + _ajv.addKeyword("deepProperties", { + type: "object", + macro: macroDeepProperties, + }) + _ajv.addKeyword("range", {type: "number", macro: macroRange}) var schema = { - "deepProperties": { - "a.b.c": { "type": "number", "range": [2,4] }, - "d.e.f.g": { "type": "string" } - } - }; + deepProperties: { + "a.b.c": {type: "number", range: [2, 4]}, + "d.e.f.g": {type: "string"}, + }, + } /* This schema recursively expands to: { @@ -304,931 +322,1045 @@ describe('Custom keywords', function () { } */ - var validate = _ajv.compile(schema); + var validate = _ajv.compile(schema) shouldBeValid(validate, { a: {b: {c: 3}}, - d: {e: {f: {g: 'foo'}}} - }); - - shouldBeInvalid(validate, { - a: {b: {c: 5}}, // out of range - d: {e: {f: {g: 'foo'}}} - }, 5); - - shouldBeInvalid(validate, { - a: {b: {c: 'bar'}}, // not number - d: {e: {f: {g: 'foo'}}} - }, 4); + d: {e: {f: {g: "foo"}}}, + }) - shouldBeInvalid(validate, { - a: {b: {c: 3}}, - d: {e: {f: {g: 2}}} // not string - }, 5); + shouldBeInvalid( + validate, + { + a: {b: {c: 5}}, // out of range + d: {e: {f: {g: "foo"}}}, + }, + 5 + ) + + shouldBeInvalid( + validate, + { + a: {b: {c: "bar"}}, // not number + d: {e: {f: {g: "foo"}}}, + }, + 4 + ) + + shouldBeInvalid( + validate, + { + a: {b: {c: 3}}, + d: {e: {f: {g: 2}}}, // not string + }, + 5 + ) function macroDeepProperties(_schema) { - if (typeof _schema != 'object') - throw new Error('schema of deepProperty should be an object'); + if (typeof _schema != "object") + throw new Error("schema of deepProperty should be an object") - var expanded = []; + var expanded = [] for (var prop in _schema) { - var path = prop.split('.'); - var properties = {}; + var path = prop.split(".") + var properties = {} if (path.length == 1) { - properties[prop] = _schema[prop]; + properties[prop] = _schema[prop] } else { - var deepProperties = {}; - deepProperties[path.slice(1).join('.')] = _schema[prop]; - properties[path[0]] = { "deepProperties": deepProperties }; + var deepProperties = {} + deepProperties[path.slice(1).join(".")] = _schema[prop] + properties[path[0]] = {deepProperties: deepProperties} } - expanded.push({ "properties": properties }); + expanded.push({properties: properties}) } - return expanded.length == 1 ? expanded[0] : { "allOf": expanded }; + return expanded.length == 1 ? expanded[0] : {allOf: expanded} } - }); - }); + }) + }) - it('should correctly expand multiple macros on the same level', function() { + it("should correctly expand multiple macros on the same level", function () { instances.forEach(function (_ajv) { - _ajv.addKeyword('range', { type: 'number', macro: macroRange }); - _ajv.addKeyword('even', { type: 'number', macro: macroEven }); + _ajv.addKeyword("range", {type: "number", macro: macroRange}) + _ajv.addKeyword("even", {type: "number", macro: macroEven}) var schema = { - "range": [4,6], - "even": true - }; - - var validate = _ajv.compile(schema); - var numErrors = _ajv._opts.allErrors ? 4 : 2; - - shouldBeInvalid(validate, 2, 2); - shouldBeInvalid(validate, 3, numErrors); - shouldBeValid(validate, 4); - shouldBeInvalid(validate, 5, 2); - shouldBeValid(validate, 6); - shouldBeInvalid(validate, 7, numErrors); - shouldBeInvalid(validate, 8, 2); - }); - }); - - it('should validate macro keyword when it resolves to the same keyword as exists', function() { + range: [4, 6], + even: true, + } + + var validate = _ajv.compile(schema) + var numErrors = _ajv._opts.allErrors ? 4 : 2 + + shouldBeInvalid(validate, 2, 2) + shouldBeInvalid(validate, 3, numErrors) + shouldBeValid(validate, 4) + shouldBeInvalid(validate, 5, 2) + shouldBeValid(validate, 6) + shouldBeInvalid(validate, 7, numErrors) + shouldBeInvalid(validate, 8, 2) + }) + }) + + it("should validate macro keyword when it resolves to the same keyword as exists", function () { instances.forEach(function (_ajv) { - _ajv.addKeyword('range', { type: 'number', macro: macroRange }); + _ajv.addKeyword("range", {type: "number", macro: macroRange}) var schema = { - "range": [1,4], - "minimum": 2.5 - }; + range: [1, 4], + minimum: 2.5, + } - var validate = _ajv.compile(schema); + var validate = _ajv.compile(schema) - shouldBeValid(validate, 3); - shouldBeInvalid(validate, 2); - }); - }); + shouldBeValid(validate, 3) + shouldBeInvalid(validate, 2) + }) + }) - it('should correctly expand macros in subschemas', function() { + it("should correctly expand macros in subschemas", function () { instances.forEach(function (_ajv) { - _ajv.addKeyword('range', { type: 'number', macro: macroRange }); + _ajv.addKeyword("range", {type: "number", macro: macroRange}) var schema = { - "allOf": [ - { "range": [4,8] }, - { "range": [2,6] } - ] - }; + allOf: [{range: [4, 8]}, {range: [2, 6]}], + } - var validate = _ajv.compile(schema); + var validate = _ajv.compile(schema) - shouldBeInvalid(validate, 2, 2); - shouldBeInvalid(validate, 3, 2); - shouldBeValid(validate, 4); - shouldBeValid(validate, 5); - shouldBeValid(validate, 6); - shouldBeInvalid(validate, 7, 2); - shouldBeInvalid(validate, 8, 2); - }); - }); + shouldBeInvalid(validate, 2, 2) + shouldBeInvalid(validate, 3, 2) + shouldBeValid(validate, 4) + shouldBeValid(validate, 5) + shouldBeValid(validate, 6) + shouldBeInvalid(validate, 7, 2) + shouldBeInvalid(validate, 8, 2) + }) + }) - it('should correctly expand macros in macro expansions', function() { + it("should correctly expand macros in macro expansions", function () { instances.forEach(function (_ajv) { - _ajv.addKeyword('range', { type: 'number', macro: macroRange }); - _ajv.addKeyword('exclusiveRange', { metaSchema: {type: 'boolean'} }); - _ajv.addKeyword('myContains', { type: 'array', macro: macroContains }); + _ajv.addKeyword("range", {type: "number", macro: macroRange}) + _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) + _ajv.addKeyword("myContains", {type: "array", macro: macroContains}) var schema = { - "myContains": { - "type": "number", - "range": [4,7], - "exclusiveRange": true - } - }; + myContains: { + type: "number", + range: [4, 7], + exclusiveRange: true, + }, + } - var validate = _ajv.compile(schema); + var validate = _ajv.compile(schema) - shouldBeInvalid(validate, [1,2,3], 2); - shouldBeInvalid(validate, [2,3,4], 2); - shouldBeValid(validate, [3,4,5]); // only 5 is in range - shouldBeValid(validate, [6,7,8]); // only 6 is in range - shouldBeInvalid(validate, [7,8,9], 2); - shouldBeInvalid(validate, [8,9,10], 2); + shouldBeInvalid(validate, [1, 2, 3], 2) + shouldBeInvalid(validate, [2, 3, 4], 2) + shouldBeValid(validate, [3, 4, 5]) // only 5 is in range + shouldBeValid(validate, [6, 7, 8]) // only 6 is in range + shouldBeInvalid(validate, [7, 8, 9], 2) + shouldBeInvalid(validate, [8, 9, 10], 2) function macroContains(_schema) { - return { "not": { "items": { "not": _schema } } }; + return {not: {items: {not: _schema}}} } - }); - }); + }) + }) - it('should throw exception if macro expansion is an invalid schema', function() { - ajv.addKeyword('invalid', { macro: macroInvalid }); - var schema = { "invalid": true }; + it("should throw exception if macro expansion is an invalid schema", function () { + ajv.addKeyword("invalid", {macro: macroInvalid}) + var schema = {invalid: true} - should.throw(function() { - ajv.compile(schema); - }); + should.throw(function () { + ajv.compile(schema) + }) function macroInvalid(/* schema */) { - return { "type": "invalid" }; + return {type: "invalid"} } - }); + }) function macroEven(schema) { - if (schema === true) return { "multipleOf": 2 }; - if (schema === false) return { "not": { "multipleOf": 2 } }; - throw new Error('Schema for "even" keyword should be boolean'); + if (schema === true) return {multipleOf: 2} + if (schema === false) return {not: {multipleOf: 2}} + throw new Error('Schema for "even" keyword should be boolean') } - function macroConstant(schema/*, parentSchema */) { - return { "enum": [schema] }; + function macroConstant(schema /*, parentSchema */) { + return {enum: [schema]} } function macroRange(schema, parentSchema) { - validateRangeSchema(schema, parentSchema); - var exclusive = !!parentSchema.exclusiveRange; + validateRangeSchema(schema, parentSchema) + var exclusive = !!parentSchema.exclusiveRange return exclusive - ? { exclusiveMinimum: schema[0], exclusiveMaximum: schema[1] } - : { minimum: schema[0], maximum: schema[1] }; + ? {exclusiveMinimum: schema[0], exclusiveMaximum: schema[1]} + : {minimum: schema[0], maximum: schema[1]} } - }); - + }) - describe('inline rules', function() { - it('should add and validate rule with "inline" code keyword', function() { - testEvenKeyword({ type: 'number', inline: inlineEven }); - }); + describe("inline rules", function () { + it('should add and validate rule with "inline" code keyword', function () { + testEvenKeyword({type: "number", inline: inlineEven}) + }) - it('should pass parent schema to "inline" keyword', function() { - testRangeKeyword({ type: 'number', inline: inlineRange, statements: true }); - }); + it('should pass parent schema to "inline" keyword', function () { + testRangeKeyword({type: "number", inline: inlineRange, statements: true}) + }) - it('should define "inline" keyword as template', function() { - var inlineRangeTemplate = customRules.range; + it('should define "inline" keyword as template', function () { + var inlineRangeTemplate = customRules.range testRangeKeyword({ - type: 'number', + type: "number", inline: inlineRangeTemplate, - statements: true - }); - }); + statements: true, + }) + }) - it('should define "inline" keyword without errors', function() { - var inlineRangeTemplate = customRules.range; + it('should define "inline" keyword without errors', function () { + var inlineRangeTemplate = customRules.range testRangeKeyword({ - type: 'number', + type: "number", inline: inlineRangeTemplate, statements: true, - errors: false - }); - }); + errors: false, + }) + }) - it('should allow defining optional errors', function() { - var inlineRangeTemplate = customRules.rangeWithErrors; + it("should allow defining optional errors", function () { + var inlineRangeTemplate = customRules.rangeWithErrors - testRangeKeyword({ - type: 'number', - inline: inlineRangeTemplate, - statements: true - }, true); - }); + testRangeKeyword( + { + type: "number", + inline: inlineRangeTemplate, + statements: true, + }, + true + ) + }) - it('should allow defining required errors', function() { - var inlineRangeTemplate = customRules.rangeWithErrors; - - testRangeKeyword({ - type: 'number', - inline: inlineRangeTemplate, - statements: true, - errors: true - }, true); - }); + it("should allow defining required errors", function () { + var inlineRangeTemplate = customRules.rangeWithErrors + testRangeKeyword( + { + type: "number", + inline: inlineRangeTemplate, + statements: true, + errors: true, + }, + true + ) + }) function inlineEven(it, keyword, schema) { - var op = schema ? '===' : '!=='; - return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; + var op = schema ? "===" : "!==" + return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" } function inlineRange(it, keyword, schema, parentSchema) { - var min = schema[0] - , max = schema[1] - , data = 'data' + (it.dataLevel || '') - , gt = parentSchema.exclusiveRange ? ' > ' : ' >= ' - , lt = parentSchema.exclusiveRange ? ' < ' : ' <= '; - return 'var valid' + it.level + ' = ' + data + gt + min + ' && ' + data + lt + max + ';'; + var min = schema[0], + max = schema[1], + data = "data" + (it.dataLevel || ""), + gt = parentSchema.exclusiveRange ? " > " : " >= ", + lt = parentSchema.exclusiveRange ? " < " : " <= " + return ( + "var valid" + + it.level + + " = " + + data + + gt + + min + + " && " + + data + + lt + + max + + ";" + ) } - }); - + }) - describe('$data reference support with custom keywords (with $data option)', function() { - beforeEach(function() { - instances = getAjvInstances({ - allErrors: true, - verbose: true, - inlineRefs: false - }, { $data: true }); - ajv = instances[0]; - }); + describe("$data reference support with custom keywords (with $data option)", function () { + beforeEach(function () { + instances = getAjvInstances( + { + allErrors: true, + verbose: true, + inlineRefs: false, + }, + {$data: true} + ) + ajv = instances[0] + }) - it('should validate "interpreted" rule', function() { + it('should validate "interpreted" rule', function () { testEvenKeyword$data({ - type: 'number', + type: "number", $data: true, - validate: validateEven - }); + validate: validateEven, + }) function validateEven(schema, data) { - if (typeof schema != 'boolean') return false; - return data % 2 ? !schema : schema; + if (typeof schema != "boolean") return false + return data % 2 ? !schema : schema } - }); + }) - it('should validate rule with "compile" and "validate" funcs', function() { - var compileCalled; + it('should validate rule with "compile" and "validate" funcs', function () { + var compileCalled testEvenKeyword$data({ - type: 'number', + type: "number", $data: true, compile: compileEven, - validate: validateEven - }); - compileCalled .should.equal(true); + validate: validateEven, + }) + compileCalled.should.equal(true) function validateEven(schema, data) { - if (typeof schema != 'boolean') return false; - return data % 2 ? !schema : schema; + if (typeof schema != "boolean") return false + return data % 2 ? !schema : schema } function compileEven(schema) { - compileCalled = true; - if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); - return schema ? isEven : isOdd; + compileCalled = true + if (typeof schema != "boolean") + throw new Error('The value of "even" keyword must be boolean') + return schema ? isEven : isOdd } - function isEven(data) { return data % 2 === 0; } - function isOdd(data) { return data % 2 !== 0; } - }); + function isEven(data) { + return data % 2 === 0 + } + function isOdd(data) { + return data % 2 !== 0 + } + }) - it('should validate with "compile" and "validate" funcs with meta-schema', function() { - var compileCalled; + it('should validate with "compile" and "validate" funcs with meta-schema', function () { + var compileCalled testEvenKeyword$data({ - type: 'number', + type: "number", $data: true, compile: compileEven, validate: validateEven, - metaSchema: { "type": "boolean" } - }); - compileCalled .should.equal(true); - shouldBeInvalidSchema({ "x-even-$data": "false" }); + metaSchema: {type: "boolean"}, + }) + compileCalled.should.equal(true) + shouldBeInvalidSchema({"x-even-$data": "false"}) function validateEven(schema, data) { - return data % 2 ? !schema : schema; + return data % 2 ? !schema : schema } function compileEven(schema) { - compileCalled = true; - return schema ? isEven : isOdd; + compileCalled = true + return schema ? isEven : isOdd } - function isEven(data) { return data % 2 === 0; } - function isOdd(data) { return data % 2 !== 0; } - }); + function isEven(data) { + return data % 2 === 0 + } + function isOdd(data) { + return data % 2 !== 0 + } + }) - it('should validate rule with "macro" and "validate" funcs', function() { - var macroCalled; - testEvenKeyword$data({ - type: 'number', - $data: true, - macro: macroEven, - validate: validateEven - }, 2); - macroCalled .should.equal(true); + it('should validate rule with "macro" and "validate" funcs', function () { + var macroCalled + testEvenKeyword$data( + { + type: "number", + $data: true, + macro: macroEven, + validate: validateEven, + }, + 2 + ) + macroCalled.should.equal(true) function validateEven(schema, data) { - if (typeof schema != 'boolean') return false; - return data % 2 ? !schema : schema; + if (typeof schema != "boolean") return false + return data % 2 ? !schema : schema } function macroEven(schema) { - macroCalled = true; - if (schema === true) return { "multipleOf": 2 }; - if (schema === false) return { "not": { "multipleOf": 2 } }; - throw new Error('Schema for "even" keyword should be boolean'); + macroCalled = true + if (schema === true) return {multipleOf: 2} + if (schema === false) return {not: {multipleOf: 2}} + throw new Error('Schema for "even" keyword should be boolean') } - }); + }) - it('should validate with "macro" and "validate" funcs with meta-schema', function() { - var macroCalled; - testEvenKeyword$data({ - type: 'number', - $data: true, - macro: macroEven, - validate: validateEven, - metaSchema: { "type": "boolean" } - }, 2); - macroCalled .should.equal(true); - shouldBeInvalidSchema({ "x-even-$data": "false" }); + it('should validate with "macro" and "validate" funcs with meta-schema', function () { + var macroCalled + testEvenKeyword$data( + { + type: "number", + $data: true, + macro: macroEven, + validate: validateEven, + metaSchema: {type: "boolean"}, + }, + 2 + ) + macroCalled.should.equal(true) + shouldBeInvalidSchema({"x-even-$data": "false"}) function validateEven(schema, data) { - return data % 2 ? !schema : schema; + return data % 2 ? !schema : schema } function macroEven(schema) { - macroCalled = true; - if (schema === true) return { "multipleOf": 2 }; - if (schema === false) return { "not": { "multipleOf": 2 } }; + macroCalled = true + if (schema === true) return {multipleOf: 2} + if (schema === false) return {not: {multipleOf: 2}} } - }); + }) - it('should validate rule with "inline" and "validate" funcs', function() { - var inlineCalled; + it('should validate rule with "inline" and "validate" funcs', function () { + var inlineCalled testEvenKeyword$data({ - type: 'number', + type: "number", $data: true, inline: inlineEven, - validate: validateEven - }); - inlineCalled .should.equal(true); + validate: validateEven, + }) + inlineCalled.should.equal(true) function validateEven(schema, data) { - if (typeof schema != 'boolean') return false; - return data % 2 ? !schema : schema; + if (typeof schema != "boolean") return false + return data % 2 ? !schema : schema } function inlineEven(it, keyword, schema) { - inlineCalled = true; - var op = schema ? '===' : '!=='; - return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; + inlineCalled = true + var op = schema ? "===" : "!==" + return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" } - }); + }) - it('should validate with "inline" and "validate" funcs with meta-schema', function() { - var inlineCalled; + it('should validate with "inline" and "validate" funcs with meta-schema', function () { + var inlineCalled testEvenKeyword$data({ - type: 'number', + type: "number", $data: true, inline: inlineEven, validate: validateEven, - metaSchema: { "type": "boolean" } - }); - inlineCalled .should.equal(true); - shouldBeInvalidSchema({ "x-even-$data": "false" }); + metaSchema: {type: "boolean"}, + }) + inlineCalled.should.equal(true) + shouldBeInvalidSchema({"x-even-$data": "false"}) function validateEven(schema, data) { - return data % 2 ? !schema : schema; + return data % 2 ? !schema : schema } function inlineEven(it, keyword, schema) { - inlineCalled = true; - var op = schema ? '===' : '!=='; - return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; + inlineCalled = true + var op = schema ? "===" : "!==" + return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" } - }); + }) - it('should fail if keyword definition has "$data" but no "validate"', function() { - should.throw(function() { - ajv.addKeyword('even', { - type: 'number', + it('should fail if keyword definition has "$data" but no "validate"', function () { + should.throw(function () { + ajv.addKeyword("even", { + type: "number", $data: true, - macro: function() { return {}; } - }); - }); - }); - }); - + macro: function () { + return {} + }, + }) + }) + }) + }) function testEvenKeyword(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('x-even', definition); - var schema = { "x-even": true }; - var validate = _ajv.compile(schema); - - shouldBeValid(validate, 2); - shouldBeValid(validate, 'abc'); - shouldBeInvalid(validate, 2.5, numErrors); - shouldBeInvalid(validate, 3, numErrors); - }); + _ajv.addKeyword("x-even", definition) + var schema = {"x-even": true} + var validate = _ajv.compile(schema) + + shouldBeValid(validate, 2) + shouldBeValid(validate, "abc") + shouldBeInvalid(validate, 2.5, numErrors) + shouldBeInvalid(validate, 3, numErrors) + }) } function testEvenKeyword$data(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('x-even-$data', definition); + _ajv.addKeyword("x-even-$data", definition) - var schema = { "x-even-$data": true }; - var validate = _ajv.compile(schema); + var schema = {"x-even-$data": true} + var validate = _ajv.compile(schema) - shouldBeValid(validate, 2); - shouldBeValid(validate, 'abc'); - shouldBeInvalid(validate, 2.5, numErrors); - shouldBeInvalid(validate, 3, numErrors); + shouldBeValid(validate, 2) + shouldBeValid(validate, "abc") + shouldBeInvalid(validate, 2.5, numErrors) + shouldBeInvalid(validate, 3, numErrors) schema = { - "properties": { - "data": { "x-even-$data": { "$data": "1/evenValue" } }, - "evenValue": {} - } - }; - validate = _ajv.compile(schema); + properties: { + data: {"x-even-$data": {$data: "1/evenValue"}}, + evenValue: {}, + }, + } + validate = _ajv.compile(schema) - shouldBeValid(validate, { data: 2, evenValue: true }); - shouldBeInvalid(validate, { data: 2, evenValue: false }); - shouldBeValid(validate, { data: 'abc', evenValue: true }); - shouldBeValid(validate, { data: 'abc', evenValue: false }); - shouldBeInvalid(validate, { data: 2.5, evenValue: true }); - shouldBeValid(validate, { data: 2.5, evenValue: false }); - shouldBeInvalid(validate, { data: 3, evenValue: true }); - shouldBeValid(validate, { data: 3, evenValue: false }); + shouldBeValid(validate, {data: 2, evenValue: true}) + shouldBeInvalid(validate, {data: 2, evenValue: false}) + shouldBeValid(validate, {data: "abc", evenValue: true}) + shouldBeValid(validate, {data: "abc", evenValue: false}) + shouldBeInvalid(validate, {data: 2.5, evenValue: true}) + shouldBeValid(validate, {data: 2.5, evenValue: false}) + shouldBeInvalid(validate, {data: 3, evenValue: true}) + shouldBeValid(validate, {data: 3, evenValue: false}) - shouldBeInvalid(validate, { data: 2, evenValue: "true" }); + shouldBeInvalid(validate, {data: 2, evenValue: "true"}) // valid if the value of x-even-$data keyword is undefined - shouldBeValid(validate, { data: 2 }); - shouldBeValid(validate, { data: 3 }); - }); + shouldBeValid(validate, {data: 2}) + shouldBeValid(validate, {data: 3}) + }) } function testConstantKeyword(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('myConstant', definition); + _ajv.addKeyword("myConstant", definition) - var schema = { "myConstant": "abc" }; - var validate = _ajv.compile(schema); + var schema = {myConstant: "abc"} + var validate = _ajv.compile(schema) - shouldBeValid(validate, 'abc'); - shouldBeInvalid(validate, 2, numErrors); - shouldBeInvalid(validate, {}, numErrors); - }); + shouldBeValid(validate, "abc") + shouldBeInvalid(validate, 2, numErrors) + shouldBeInvalid(validate, {}, numErrors) + }) } function testMultipleConstantKeyword(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('x-constant', definition); + _ajv.addKeyword("x-constant", definition) var schema = { - "properties": { - "a": { "x-constant": 1 }, - "b": { "x-constant": 1 } + properties: { + a: {"x-constant": 1}, + b: {"x-constant": 1}, }, - "additionalProperties": { "x-constant": { "foo": "bar" } }, - "items": { "x-constant": { "foo": "bar" } } - }; - var validate = _ajv.compile(schema); + additionalProperties: {"x-constant": {foo: "bar"}}, + items: {"x-constant": {foo: "bar"}}, + } + var validate = _ajv.compile(schema) - shouldBeValid(validate, {a:1, b:1}); - shouldBeInvalid(validate, {a:2, b:1}, numErrors); + shouldBeValid(validate, {a: 1, b: 1}) + shouldBeInvalid(validate, {a: 2, b: 1}, numErrors) - shouldBeValid(validate, {a:1, c: {foo: 'bar'}}); - shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}}, numErrors); + shouldBeValid(validate, {a: 1, c: {foo: "bar"}}) + shouldBeInvalid(validate, {a: 1, c: {foo: "baz"}}, numErrors) - shouldBeValid(validate, [{foo: 'bar'}]); - shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]); + shouldBeValid(validate, [{foo: "bar"}]) + shouldBeValid(validate, [{foo: "bar"}, {foo: "bar"}]) - shouldBeInvalid(validate, [1], numErrors); - }); + shouldBeInvalid(validate, [1], numErrors) + }) } function testRangeKeyword(definition, customErrors, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('x-range', definition); - _ajv.addKeyword('exclusiveRange', {metaSchema: {type: 'boolean'}}); + _ajv.addKeyword("x-range", definition) + _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) - var schema = { "x-range": [2, 4] }; - var validate = _ajv.compile(schema); + var schema = {"x-range": [2, 4]} + var validate = _ajv.compile(schema) - shouldBeValid(validate, 2); - shouldBeValid(validate, 3); - shouldBeValid(validate, 4); - shouldBeValid(validate, 'abc'); + shouldBeValid(validate, 2) + shouldBeValid(validate, 3) + shouldBeValid(validate, 4) + shouldBeValid(validate, "abc") - shouldBeInvalid(validate, 1.99, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/x-range', '>=', 2); - shouldBeInvalid(validate, 4.01, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/x-range','<=', 4); + shouldBeInvalid(validate, 1.99, numErrors) + if (customErrors) + shouldBeRangeError(validate.errors[0], "", "#/x-range", ">=", 2) + shouldBeInvalid(validate, 4.01, numErrors) + if (customErrors) + shouldBeRangeError(validate.errors[0], "", "#/x-range", "<=", 4) schema = { - "properties": { - "foo": { + properties: { + foo: { "x-range": [2, 4], - "exclusiveRange": true - } - } - }; - validate = _ajv.compile(schema); - - shouldBeValid(validate, { foo: 2.01 }); - shouldBeValid(validate, { foo: 3 }); - shouldBeValid(validate, { foo: 3.99 }); - - shouldBeInvalid(validate, { foo: 2 }, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/x-range', '>', 2, true); - shouldBeInvalid(validate, { foo: 4 }, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/x-range', '<', 4, true); - }); + exclusiveRange: true, + }, + }, + } + validate = _ajv.compile(schema) + + shouldBeValid(validate, {foo: 2.01}) + shouldBeValid(validate, {foo: 3}) + shouldBeValid(validate, {foo: 3.99}) + + shouldBeInvalid(validate, {foo: 2}, numErrors) + if (customErrors) + shouldBeRangeError( + validate.errors[0], + ".foo", + "#/properties/foo/x-range", + ">", + 2, + true + ) + shouldBeInvalid(validate, {foo: 4}, numErrors) + if (customErrors) + shouldBeRangeError( + validate.errors[0], + ".foo", + "#/properties/foo/x-range", + "<", + 4, + true + ) + }) } function testMultipleRangeKeyword(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('x-range', definition); - _ajv.addKeyword('exclusiveRange', {metaSchema: {type: 'boolean'}}); + _ajv.addKeyword("x-range", definition) + _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) var schema = { - "properties": { - "a": { "x-range": [2, 4], "exclusiveRange": true }, - "b": { "x-range": [2, 4], "exclusiveRange": false } + properties: { + a: {"x-range": [2, 4], exclusiveRange: true}, + b: {"x-range": [2, 4], exclusiveRange: false}, }, - "additionalProperties": { "x-range": [5, 7] }, - "items": { "x-range": [5, 7] } - }; - var validate = _ajv.compile(schema); + additionalProperties: {"x-range": [5, 7]}, + items: {"x-range": [5, 7]}, + } + var validate = _ajv.compile(schema) - shouldBeValid(validate, {a:3.99, b:4}); - shouldBeInvalid(validate, {a:4, b:4}, numErrors); + shouldBeValid(validate, {a: 3.99, b: 4}) + shouldBeInvalid(validate, {a: 4, b: 4}, numErrors) - shouldBeValid(validate, {a:2.01, c: 7}); - shouldBeInvalid(validate, {a:2.01, c: 7.01}, numErrors); + shouldBeValid(validate, {a: 2.01, c: 7}) + shouldBeInvalid(validate, {a: 2.01, c: 7.01}, numErrors) - shouldBeValid(validate, [5, 6, 7]); - shouldBeInvalid(validate, [7.01], numErrors); - }); + shouldBeValid(validate, [5, 6, 7]) + shouldBeInvalid(validate, [7.01], numErrors) + }) } - function shouldBeRangeError(error, dataPath, schemaPath, comparison, limit, exclusive) { - delete error.schema; - delete error.data; - error .should.eql({ - keyword: 'x-range', + function shouldBeRangeError( + error, + dataPath, + schemaPath, + comparison, + limit, + exclusive + ) { + delete error.schema + delete error.data + error.should.eql({ + keyword: "x-range", dataPath: dataPath, schemaPath: schemaPath, - message: 'should be ' + comparison + ' ' + limit, + message: "should be " + comparison + " " + limit, params: { comparison: comparison, limit: limit, - exclusive: !!exclusive - } - }); + exclusive: !!exclusive, + }, + }) } function validateRangeSchema(schema, parentSchema) { - var schemaValid = Array.isArray(schema) && schema.length == 2 - && typeof schema[0] == 'number' - && typeof schema[1] == 'number'; - if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers'); - - var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined - || typeof parentSchema.exclusiveRange == 'boolean'; - if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean'); + var schemaValid = + Array.isArray(schema) && + schema.length == 2 && + typeof schema[0] == "number" && + typeof schema[1] == "number" + if (!schemaValid) + throw new Error( + "Invalid schema for range keyword, should be array of 2 numbers" + ) + + var exclusiveRangeSchemaValid = + parentSchema.exclusiveRange === undefined || + typeof parentSchema.exclusiveRange == "boolean" + if (!exclusiveRangeSchemaValid) + throw new Error( + "Invalid schema for exclusiveRange keyword, should be bolean" + ) } function shouldBeValid(validate, data) { - validate(data) .should.equal(true); - should.not.exist(validate.errors); + validate(data).should.equal(true) + should.not.exist(validate.errors) } function shouldBeInvalid(validate, data, numErrors) { - validate(data) .should.equal(false); - validate.errors .should.have.length(numErrors || 1); + validate(data).should.equal(false) + validate.errors.should.have.length(numErrors || 1) } function shouldBeInvalidSchema(schema) { instances.forEach(function (_ajv) { - should.throw(function() { - _ajv.compile(schema); - }); - }); + should.throw(function () { + _ajv.compile(schema) + }) + }) } - describe('addKeyword method', function() { - var TEST_TYPES = [ undefined, 'number', 'string', 'boolean', ['number', 'string']]; + describe("addKeyword method", function () { + var TEST_TYPES = [ + undefined, + "number", + "string", + "boolean", + ["number", "string"], + ] - it('should throw if defined keyword is passed', function() { - testThrow(['minimum', 'maximum', 'multipleOf', 'minLength', 'maxLength']); - testThrowDuplicate('custom'); + it("should throw if defined keyword is passed", function () { + testThrow(["minimum", "maximum", "multipleOf", "minLength", "maxLength"]) + testThrowDuplicate("custom") function testThrow(keywords) { TEST_TYPES.forEach(function (dataType, index) { - should.throw(function(){ - addKeyword(keywords[index], dataType); - }); - }); + should.throw(function () { + addKeyword(keywords[index], dataType) + }) + }) } function testThrowDuplicate(keywordPrefix) { - var index = 0; + var index = 0 TEST_TYPES.forEach(function (dataType1) { TEST_TYPES.forEach(function (dataType2) { - var keyword = keywordPrefix + (index++); - addKeyword(keyword, dataType1); - should.throw(function() { - addKeyword(keyword, dataType2); - }); - }); - }); + var keyword = keywordPrefix + index++ + addKeyword(keyword, dataType1) + should.throw(function () { + addKeyword(keyword, dataType2) + }) + }) + }) } - }); - - it('should throw if keyword is not a valid name', function() { - should.not.throw(function() { - ajv.addKeyword('mykeyword', { - validate: function() { return true; } - }); - }); - - should.not.throw(function() { - ajv.addKeyword('hyphens-are-valid', { - validate: function() { return true; } - }); - }); - - should.throw(function() { - ajv.addKeyword('3-start-with-number-not-valid`', { - validate: function() { return true; } - }); - }); - - should.throw(function() { - ajv.addKeyword('-start-with-hyphen-not-valid`', { - validate: function() { return true; } - }); - }); - - should.throw(function() { - ajv.addKeyword('spaces not valid`', { - validate: function() { return true; } - }); - }); - }); - - it('should return instance of itself', function() { - var res = ajv.addKeyword('any', { - validate: function() { return true; } - }); - res.should.equal(ajv); - }); - - it('should throw if unknown type is passed', function() { - should.throw(function() { - addKeyword('custom1', 'wrongtype'); - }); - - should.throw(function() { - addKeyword('custom2', ['number', 'wrongtype']); - }); - - should.throw(function() { - addKeyword('custom3', ['number', undefined]); - }); - }); + }) + + it("should throw if keyword is not a valid name", function () { + should.not.throw(function () { + ajv.addKeyword("mykeyword", { + validate: function () { + return true + }, + }) + }) + + should.not.throw(function () { + ajv.addKeyword("hyphens-are-valid", { + validate: function () { + return true + }, + }) + }) + + should.throw(function () { + ajv.addKeyword("3-start-with-number-not-valid`", { + validate: function () { + return true + }, + }) + }) + + should.throw(function () { + ajv.addKeyword("-start-with-hyphen-not-valid`", { + validate: function () { + return true + }, + }) + }) + + should.throw(function () { + ajv.addKeyword("spaces not valid`", { + validate: function () { + return true + }, + }) + }) + }) + + it("should return instance of itself", function () { + var res = ajv.addKeyword("any", { + validate: function () { + return true + }, + }) + res.should.equal(ajv) + }) + + it("should throw if unknown type is passed", function () { + should.throw(function () { + addKeyword("custom1", "wrongtype") + }) + + should.throw(function () { + addKeyword("custom2", ["number", "wrongtype"]) + }) + + should.throw(function () { + addKeyword("custom3", ["number", undefined]) + }) + }) function addKeyword(keyword, dataType) { ajv.addKeyword(keyword, { type: dataType, - validate: function() {} - }); + validate: function () {}, + }) } - }); - + }) - describe('getKeyword', function() { - it('should return boolean for pre-defined and unknown keywords', function() { - ajv.getKeyword('type') .should.equal(true); - ajv.getKeyword('properties') .should.equal(true); - ajv.getKeyword('additionalProperties') .should.equal(true); - ajv.getKeyword('unknown') .should.equal(false); - }); + describe("getKeyword", function () { + it("should return boolean for pre-defined and unknown keywords", function () { + ajv.getKeyword("type").should.equal(true) + ajv.getKeyword("properties").should.equal(true) + ajv.getKeyword("additionalProperties").should.equal(true) + ajv.getKeyword("unknown").should.equal(false) + }) - it('should return keyword definition for custom keywords', function() { + it("should return keyword definition for custom keywords", function () { var definition = { - validate: function() { return true; } - }; - - ajv.addKeyword('mykeyword', definition); - ajv.getKeyword('mykeyword') .should.equal(definition); - }); - }); - - - describe('removeKeyword', function() { - it('should remove and allow redefining custom keyword', function() { - ajv.addKeyword('positive', { - type: 'number', - validate: function (schema, data) { return data > 0; } - }); - - var schema = { positive: true }; - - var validate = ajv.compile(schema); - validate(0) .should.equal(false); - validate(1) .should.equal(true); - - should.throw(function() { - ajv.addKeyword('positive', { - type: 'number', - validate: function(sch, data) { return data >= 0; } - }); - }); - - ajv.removeKeyword('positive'); - ajv.removeSchema(schema); - ajv.addKeyword('positive', { - type: 'number', - validate: function (sch, data) { return data >= 0; } - }); - - validate = ajv.compile(schema); - validate(-1) .should.equal(false); - validate(0) .should.equal(true); - validate(1) .should.equal(true); - }); - - it('should remove and allow redefining standard keyword', function() { - var schema = { minimum: 1 }; - var validate = ajv.compile(schema); - validate(0) .should.equal(false); - validate(1) .should.equal(true); - validate(2) .should.equal(true); - - ajv.removeKeyword('minimum'); - ajv.removeSchema(schema); - - validate = ajv.compile(schema); - validate(0) .should.equal(true); - validate(1) .should.equal(true); - validate(2) .should.equal(true); - - ajv.addKeyword('minimum', { - type: 'number', + validate: function () { + return true + }, + } + + ajv.addKeyword("mykeyword", definition) + ajv.getKeyword("mykeyword").should.equal(definition) + }) + }) + + describe("removeKeyword", function () { + it("should remove and allow redefining custom keyword", function () { + ajv.addKeyword("positive", { + type: "number", + validate: function (schema, data) { + return data > 0 + }, + }) + + var schema = {positive: true} + + var validate = ajv.compile(schema) + validate(0).should.equal(false) + validate(1).should.equal(true) + + should.throw(function () { + ajv.addKeyword("positive", { + type: "number", + validate: function (sch, data) { + return data >= 0 + }, + }) + }) + + ajv.removeKeyword("positive") + ajv.removeSchema(schema) + ajv.addKeyword("positive", { + type: "number", + validate: function (sch, data) { + return data >= 0 + }, + }) + + validate = ajv.compile(schema) + validate(-1).should.equal(false) + validate(0).should.equal(true) + validate(1).should.equal(true) + }) + + it("should remove and allow redefining standard keyword", function () { + var schema = {minimum: 1} + var validate = ajv.compile(schema) + validate(0).should.equal(false) + validate(1).should.equal(true) + validate(2).should.equal(true) + + ajv.removeKeyword("minimum") + ajv.removeSchema(schema) + + validate = ajv.compile(schema) + validate(0).should.equal(true) + validate(1).should.equal(true) + validate(2).should.equal(true) + + ajv.addKeyword("minimum", { + type: "number", // make minimum exclusive - validate: function (sch, data) { return data > sch; } - }); - ajv.removeSchema(schema); + validate: function (sch, data) { + return data > sch + }, + }) + ajv.removeSchema(schema) - validate = ajv.compile(schema); - validate(0) .should.equal(false); - validate(1) .should.equal(false); - validate(2) .should.equal(true); - }); + validate = ajv.compile(schema) + validate(0).should.equal(false) + validate(1).should.equal(false) + validate(2).should.equal(true) + }) - it('should return instance of itself', function() { + it("should return instance of itself", function () { var res = ajv - .addKeyword('any', { - validate: function() { return true; } + .addKeyword("any", { + validate: function () { + return true + }, }) - .removeKeyword('any'); - res.should.equal(ajv); - }); - }); + .removeKeyword("any") + res.should.equal(ajv) + }) + }) + describe("custom keywords mutating data", function () { + it("should NOT update data without option modifying", function () { + should.throw(function () { + testModifying(false) + }) + }) - describe('custom keywords mutating data', function() { - it('should NOT update data without option modifying', function() { - should.throw(function() { - testModifying(false); - }); - }); - - it('should update data with option modifying', function() { - testModifying(true); - }); + it("should update data with option modifying", function () { + testModifying(true) + }) function testModifying(withOption) { var collectionFormat = { csv: function (data, dataPath, parentData, parentDataProperty) { - parentData[parentDataProperty] = data.split(','); - return true; - } - }; + parentData[parentDataProperty] = data.split(",") + return true + }, + } - ajv.addKeyword('collectionFormat', { - type: 'string', + ajv.addKeyword("collectionFormat", { + type: "string", modifying: withOption, - compile: function(schema) { return collectionFormat[schema]; }, + compile: function (schema) { + return collectionFormat[schema] + }, metaSchema: { - enum: ['csv'] - } - }); + enum: ["csv"], + }, + }) var validate = ajv.compile({ - type: 'object', + type: "object", properties: { foo: { allOf: [ - { collectionFormat: 'csv' }, + {collectionFormat: "csv"}, { - type: 'array', - items: { type: 'string' }, - } - ] - } + type: "array", + items: {type: "string"}, + }, + ], + }, }, - additionalProperties: false - }); + additionalProperties: false, + }) - var obj = { foo: 'bar,baz,quux' }; + var obj = {foo: "bar,baz,quux"} - validate(obj) .should.equal(true); - obj .should.eql({ foo: ['bar', 'baz', 'quux'] }); + validate(obj).should.equal(true) + obj.should.eql({foo: ["bar", "baz", "quux"]}) } - }); - - - describe('custom keywords with predefined validation result', function() { - it('should ignore result from validation function', function() { - ajv.addKeyword('pass', { - validate: function() { return false; }, - valid: true - }); - - ajv.addKeyword('fail', { - validate: function() { return true; }, - valid: false - }); - - ajv.validate({ pass: '' }, 1) .should.equal(true); - ajv.validate({ fail: '' }, 1) .should.equal(false); - }); - - it('should throw exception if used with macro keyword', function() { - should.throw(function() { - ajv.addKeyword('pass', { - macro: function() { return {}; }, - valid: true - }); - }); - - should.throw(function() { - ajv.addKeyword('fail', { - macro: function() { return {not:{}}; }, - valid: false - }); - }); - }); - }); - - - describe('"dependencies" in keyword definition', function() { - it("should require properties in the parent schema", function() { - ajv.addKeyword('allRequired', { - macro: function(schema, parentSchema) { - return schema ? {required: Object.keys(parentSchema.properties)} : true; + }) + + describe("custom keywords with predefined validation result", function () { + it("should ignore result from validation function", function () { + ajv.addKeyword("pass", { + validate: function () { + return false + }, + valid: true, + }) + + ajv.addKeyword("fail", { + validate: function () { + return true + }, + valid: false, + }) + + ajv.validate({pass: ""}, 1).should.equal(true) + ajv.validate({fail: ""}, 1).should.equal(false) + }) + + it("should throw exception if used with macro keyword", function () { + should.throw(function () { + ajv.addKeyword("pass", { + macro: function () { + return {} + }, + valid: true, + }) + }) + + should.throw(function () { + ajv.addKeyword("fail", { + macro: function () { + return {not: {}} + }, + valid: false, + }) + }) + }) + }) + + describe('"dependencies" in keyword definition', function () { + it("should require properties in the parent schema", function () { + ajv.addKeyword("allRequired", { + macro: function (schema, parentSchema) { + return schema + ? {required: Object.keys(parentSchema.properties)} + : true }, - metaSchema: {type: 'boolean'}, - dependencies: ['properties'] - }); + metaSchema: {type: "boolean"}, + dependencies: ["properties"], + }) var invalidSchema = { - allRequired: true - }; + allRequired: true, + } should.throw(function () { - ajv.compile(invalidSchema); - }); + ajv.compile(invalidSchema) + }) var schema = { properties: { - foo: true + foo: true, }, - allRequired: true - }; + allRequired: true, + } - var v = ajv.compile(schema); - v({foo: 1}) .should.equal(true); - v({}) .should.equal(false); - }); + var v = ajv.compile(schema) + v({foo: 1}).should.equal(true) + v({}).should.equal(false) + }) - it("'dependencies'should be array of valid strings", function() { - ajv.addKeyword('newKeyword1', { - metaSchema: {type: 'boolean'}, - dependencies: ['dep1'] - }); + it("'dependencies'should be array of valid strings", function () { + ajv.addKeyword("newKeyword1", { + metaSchema: {type: "boolean"}, + dependencies: ["dep1"], + }) should.throw(function () { - ajv.addKeyword('newKeyword2', { - metaSchema: {type: 'boolean'}, - dependencies: [1] - }); - }); - }); - }); -}); + ajv.addKeyword("newKeyword2", { + metaSchema: {type: "boolean"}, + dependencies: [1], + }) + }) + }) + }) +}) diff --git a/spec/custom_rules/index.js b/spec/custom_rules/index.js index afc02fb4b1..534fc1495f 100644 --- a/spec/custom_rules/index.js +++ b/spec/custom_rules/index.js @@ -1,9 +1,11 @@ -'use strict'; +"use strict" -var fs = require('fs') - , doT = require('dot'); +var fs = require("fs"), + doT = require("dot") module.exports = { - range: doT.compile(fs.readFileSync(__dirname + '/range.jst', 'utf8')), - rangeWithErrors: doT.compile(fs.readFileSync(__dirname + '/range_with_errors.jst', 'utf8')) -}; + range: doT.compile(fs.readFileSync(__dirname + "/range.jst", "utf8")), + rangeWithErrors: doT.compile( + fs.readFileSync(__dirname + "/range_with_errors.jst", "utf8") + ), +} diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 6291b15ae1..82b9150e2e 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -1,899 +1,1221 @@ -'use strict'; +"use strict" -var Ajv = require('./ajv') - , should = require('./chai').should(); +var Ajv = require("./ajv"), + should = require("./chai").should() +describe("Validation errors", function () { + var ajv, ajvJP, fullAjv -describe('Validation errors', function () { - var ajv, ajvJP, fullAjv; - - beforeEach(function() { - createInstances(); - }); + beforeEach(function () { + createInstances() + }) function createInstances(errorDataPath) { - ajv = new Ajv({ errorDataPath: errorDataPath, loopRequired: 21 }); - ajvJP = new Ajv({ errorDataPath: errorDataPath, jsonPointers: true, loopRequired: 21 }); - fullAjv = new Ajv({ errorDataPath: errorDataPath, allErrors: true, verbose: true, jsonPointers: true, loopRequired: 21 }); + ajv = new Ajv({errorDataPath: errorDataPath, loopRequired: 21}) + ajvJP = new Ajv({ + errorDataPath: errorDataPath, + jsonPointers: true, + loopRequired: 21, + }) + fullAjv = new Ajv({ + errorDataPath: errorDataPath, + allErrors: true, + verbose: true, + jsonPointers: true, + loopRequired: 21, + }) } - it('error should include dataPath', function() { + it("error should include dataPath", function () { var schema = { properties: { - foo: { type: 'number' } - } - }; + foo: {type: "number"}, + }, + } - testSchema1(schema); - }); + testSchema1(schema) + }) - it('"refs" error should include dataPath', function() { + it('"refs" error should include dataPath', function () { var schema = { definitions: { - num: { type: 'number' } + num: {type: "number"}, }, properties: { - foo: { $ref: '#/definitions/num' } - } - }; - - testSchema1(schema, '#/definitions/num'); - }); + foo: {$ref: "#/definitions/num"}, + }, + } + testSchema1(schema, "#/definitions/num") + }) - describe('"additionalProperties" errors', function() { - it('should include property in dataPath with option errorDataPath="property"', function() { - createInstances('property'); - testAdditional('property'); - }); + describe('"additionalProperties" errors', function () { + it('should include property in dataPath with option errorDataPath="property"', function () { + createInstances("property") + testAdditional("property") + }) - it('should NOT include property in dataPath WITHOUT option errorDataPath', function() { - testAdditional(); - }); + it("should NOT include property in dataPath WITHOUT option errorDataPath", function () { + testAdditional() + }) function testAdditional(errorDataPath) { var schema = { properties: { foo: {}, - bar: {} + bar: {}, }, - additionalProperties: false - }; - - var data = { foo: 1, bar: 2 } - , invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 }; - - var path = pathFunc(errorDataPath); - var msg = additionalFunc(errorDataPath); - - var validate = ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData); - shouldBeError(validate.errors[0], 'additionalProperties', '#/additionalProperties', path("['baz']"), msg, { additionalProperty: 'baz' }); - - var validateJP = ajvJP.compile(schema); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData); - shouldBeError(validateJP.errors[0], 'additionalProperties', '#/additionalProperties', path("/baz"), msg, { additionalProperty: 'baz' }); - - var fullValidate = fullAjv.compile(schema); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData, 2); - shouldBeError(fullValidate.errors[0], 'additionalProperties', '#/additionalProperties', path('/baz'), msg, { additionalProperty: 'baz' }); - shouldBeError(fullValidate.errors[1], 'additionalProperties', '#/additionalProperties', path('/quux'), msg, { additionalProperty: 'quux' }); + additionalProperties: false, + } - if (errorDataPath == 'property') { + var data = {foo: 1, bar: 2}, + invalidData = {foo: 1, bar: 2, baz: 3, quux: 4} + + var path = pathFunc(errorDataPath) + var msg = additionalFunc(errorDataPath) + + var validate = ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData) + shouldBeError( + validate.errors[0], + "additionalProperties", + "#/additionalProperties", + path("['baz']"), + msg, + {additionalProperty: "baz"} + ) + + var validateJP = ajvJP.compile(schema) + shouldBeValid(validateJP, data) + shouldBeInvalid(validateJP, invalidData) + shouldBeError( + validateJP.errors[0], + "additionalProperties", + "#/additionalProperties", + path("/baz"), + msg, + {additionalProperty: "baz"} + ) + + var fullValidate = fullAjv.compile(schema) + shouldBeValid(fullValidate, data) + shouldBeInvalid(fullValidate, invalidData, 2) + shouldBeError( + fullValidate.errors[0], + "additionalProperties", + "#/additionalProperties", + path("/baz"), + msg, + {additionalProperty: "baz"} + ) + shouldBeError( + fullValidate.errors[1], + "additionalProperties", + "#/additionalProperties", + path("/quux"), + msg, + {additionalProperty: "quux"} + ) + + if (errorDataPath == "property") { fullValidate.errors - .filter(function(err) { return err.keyword == 'additionalProperties'; }) - .map(function(err) { return fullAjv._opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); }) - .forEach(function(p) { delete invalidData[p]; }); - - invalidData .should.eql({ foo: 1, bar: 2 }); + .filter(function (err) { + return err.keyword == "additionalProperties" + }) + .map(function (err) { + return fullAjv._opts.jsonPointers + ? err.dataPath.substr(1) + : err.dataPath.slice(2, -2) + }) + .forEach(function (p) { + delete invalidData[p] + }) + + invalidData.should.eql({foo: 1, bar: 2}) } } - }); - + }) - describe('errors when "additionalProperties" is schema', function() { - it('should include property in dataPath with option errorDataPath="property"', function() { - createInstances('property'); - testAdditionalIsSchema('property'); - }); + describe('errors when "additionalProperties" is schema', function () { + it('should include property in dataPath with option errorDataPath="property"', function () { + createInstances("property") + testAdditionalIsSchema("property") + }) - it('should NOT include property in dataPath WITHOUT option errorDataPath', function() { - testAdditionalIsSchema(); - }); + it("should NOT include property in dataPath WITHOUT option errorDataPath", function () { + testAdditionalIsSchema() + }) function testAdditionalIsSchema() { var schema = { properties: { - foo: { type: 'integer' }, - bar: { type: 'integer' } + foo: {type: "integer"}, + bar: {type: "integer"}, }, additionalProperties: { - type: 'object', + type: "object", properties: { - quux: { type: 'string' } - } - } - }; - - var data = { foo: 1, bar: 2, baz: { quux: 'abc' } } - , invalidData = { foo: 1, bar: 2, baz: { quux: 3 }, boo: { quux: 4 } }; - - var schPath = '#/additionalProperties/properties/quux/type'; - - var validate = ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData); - shouldBeError(validate.errors[0], 'type', schPath, "['baz'].quux", 'should be string', { type: 'string' }); - - var validateJP = ajvJP.compile(schema); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData); - shouldBeError(validateJP.errors[0], 'type', schPath, "/baz/quux", 'should be string', { type: 'string' }); + quux: {type: "string"}, + }, + }, + } - var fullValidate = fullAjv.compile(schema); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData, 2); - shouldBeError(fullValidate.errors[0], 'type', schPath, '/baz/quux', 'should be string', { type: 'string' }); - shouldBeError(fullValidate.errors[1], 'type', schPath, '/boo/quux', 'should be string', { type: 'string' }); + var data = {foo: 1, bar: 2, baz: {quux: "abc"}}, + invalidData = {foo: 1, bar: 2, baz: {quux: 3}, boo: {quux: 4}} + + var schPath = "#/additionalProperties/properties/quux/type" + + var validate = ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData) + shouldBeError( + validate.errors[0], + "type", + schPath, + "['baz'].quux", + "should be string", + {type: "string"} + ) + + var validateJP = ajvJP.compile(schema) + shouldBeValid(validateJP, data) + shouldBeInvalid(validateJP, invalidData) + shouldBeError( + validateJP.errors[0], + "type", + schPath, + "/baz/quux", + "should be string", + {type: "string"} + ) + + var fullValidate = fullAjv.compile(schema) + shouldBeValid(fullValidate, data) + shouldBeInvalid(fullValidate, invalidData, 2) + shouldBeError( + fullValidate.errors[0], + "type", + schPath, + "/baz/quux", + "should be string", + {type: "string"} + ) + shouldBeError( + fullValidate.errors[1], + "type", + schPath, + "/boo/quux", + "should be string", + {type: "string"} + ) } - }); - + }) - describe('"required" errors', function() { - it('should include missing property in dataPath with option errorDataPath="property"', function() { - createInstances('property'); - testRequired('property'); - }); + describe('"required" errors', function () { + it('should include missing property in dataPath with option errorDataPath="property"', function () { + createInstances("property") + testRequired("property") + }) - it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() { - testRequired(); - }); + it("should NOT include missing property in dataPath WITHOUT option errorDataPath", function () { + testRequired() + }) function testRequired(errorDataPath) { var schema = { - required: ['foo', 'bar', 'baz'] - }; + required: ["foo", "bar", "baz"], + } - _testRequired(errorDataPath, schema, '#', '.'); + _testRequired(errorDataPath, schema, "#", ".") } + it('large data/schemas with option errorDataPath="property"', function () { + createInstances("property") + testRequiredLargeSchema("property") + }) - it('large data/schemas with option errorDataPath="property"', function() { - createInstances('property'); - testRequiredLargeSchema('property'); - }); - - it('large data/schemas WITHOUT option errorDataPath', function() { - testRequiredLargeSchema(); - }); + it("large data/schemas WITHOUT option errorDataPath", function () { + testRequiredLargeSchema() + }) function testRequiredLargeSchema(errorDataPath) { - var schema = { required: [] } - , data = {} - , invalidData1 = {} - , invalidData2 = {}; - for (var i=0; i<100; i++) { - schema.required.push(''+i); // properties from '0' to '99' are required - data[i] = invalidData1[i] = invalidData2[i] = i; + var schema = {required: []}, + data = {}, + invalidData1 = {}, + invalidData2 = {} + for (var i = 0; i < 100; i++) { + schema.required.push("" + i) // properties from '0' to '99' are required + data[i] = invalidData1[i] = invalidData2[i] = i } - delete invalidData1[1]; // property '1' will be missing - delete invalidData2[2]; // properties '2' and '198' will be missing - delete invalidData2[98]; + delete invalidData1[1] // property '1' will be missing + delete invalidData2[2] // properties '2' and '198' will be missing + delete invalidData2[98] - var path = pathFunc(errorDataPath); - var msg = requiredFunc(errorDataPath); + var path = pathFunc(errorDataPath) + var msg = requiredFunc(errorDataPath) - test(); + test() - schema = { anyOf: [ schema ] }; - test(1, '#/anyOf/0'); + schema = {anyOf: [schema]} + test(1, "#/anyOf/0") function test(extraErrors, schemaPathPrefix) { - extraErrors = extraErrors || 0; - var schPath = (schemaPathPrefix || '#') + '/required'; - var validate = ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', schPath, path("['1']"), msg('1'), { missingProperty: '1' }); - shouldBeInvalid(validate, invalidData2, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', schPath, path("['2']"), msg('2'), { missingProperty: '2' }); - - var validateJP = ajvJP.compile(schema); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', schPath, path("/1"), msg('1'), { missingProperty: '1' }); - shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', schPath, path("/2"), msg('2'), { missingProperty: '2' }); - - var fullValidate = fullAjv.compile(schema); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', schPath, path('/1'), msg('1'), { missingProperty: '1' }); - shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', schPath, path('/2'), msg('2'), { missingProperty: '2' }); - shouldBeError(fullValidate.errors[1], 'required', schPath, path('/98'), msg('98'), { missingProperty: '98' }); + extraErrors = extraErrors || 0 + var schPath = (schemaPathPrefix || "#") + "/required" + var validate = ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData1, 1 + extraErrors) + shouldBeError( + validate.errors[0], + "required", + schPath, + path("['1']"), + msg("1"), + {missingProperty: "1"} + ) + shouldBeInvalid(validate, invalidData2, 1 + extraErrors) + shouldBeError( + validate.errors[0], + "required", + schPath, + path("['2']"), + msg("2"), + {missingProperty: "2"} + ) + + var validateJP = ajvJP.compile(schema) + shouldBeValid(validateJP, data) + shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) + shouldBeError( + validateJP.errors[0], + "required", + schPath, + path("/1"), + msg("1"), + {missingProperty: "1"} + ) + shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) + shouldBeError( + validateJP.errors[0], + "required", + schPath, + path("/2"), + msg("2"), + {missingProperty: "2"} + ) + + var fullValidate = fullAjv.compile(schema) + shouldBeValid(fullValidate, data) + shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) + shouldBeError( + fullValidate.errors[0], + "required", + schPath, + path("/1"), + msg("1"), + {missingProperty: "1"} + ) + shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) + shouldBeError( + fullValidate.errors[0], + "required", + schPath, + path("/2"), + msg("2"), + {missingProperty: "2"} + ) + shouldBeError( + fullValidate.errors[1], + "required", + schPath, + path("/98"), + msg("98"), + {missingProperty: "98"} + ) } } + it('with "properties" with option errorDataPath="property"', function () { + createInstances("property") + testRequiredAndProperties("property") + }) - it('with "properties" with option errorDataPath="property"', function() { - createInstances('property'); - testRequiredAndProperties('property'); - }); - - it('with "properties" WITHOUT option errorDataPath', function() { - testRequiredAndProperties(); - }); + it('with "properties" WITHOUT option errorDataPath', function () { + testRequiredAndProperties() + }) function testRequiredAndProperties(errorDataPath) { var schema = { properties: { - 'foo': { type: 'number' }, - 'bar': { type: 'number' }, - 'baz': { type: 'number' }, + foo: {type: "number"}, + bar: {type: "number"}, + baz: {type: "number"}, }, - required: ['foo', 'bar', 'baz'] - }; + required: ["foo", "bar", "baz"], + } - _testRequired(errorDataPath, schema); + _testRequired(errorDataPath, schema) } + it('in "anyOf" with option errorDataPath="property"', function () { + createInstances("property") + testRequiredInAnyOf("property") + }) - it('in "anyOf" with option errorDataPath="property"', function() { - createInstances('property'); - testRequiredInAnyOf('property'); - }); - - it('in "anyOf" WITHOUT option errorDataPath', function() { - testRequiredInAnyOf(); - }); + it('in "anyOf" WITHOUT option errorDataPath', function () { + testRequiredInAnyOf() + }) function testRequiredInAnyOf(errorDataPath) { var schema = { - anyOf: [ - { required: ['foo', 'bar', 'baz'] } - ] - }; + anyOf: [{required: ["foo", "bar", "baz"]}], + } - _testRequired(errorDataPath, schema, '#/anyOf/0', '.', 1); + _testRequired(errorDataPath, schema, "#/anyOf/0", ".", 1) } - - it('should not validate required twice in large schemas with loopRequired option', function() { - ajv = new Ajv({ loopRequired: 1, allErrors: true }); + it("should not validate required twice in large schemas with loopRequired option", function () { + ajv = new Ajv({loopRequired: 1, allErrors: true}) var schema = { properties: { - foo: { type: 'integer' }, - bar: { type: 'integer' } + foo: {type: "integer"}, + bar: {type: "integer"}, }, - required: ['foo', 'bar'] - }; - - var validate = ajv.compile(schema); + required: ["foo", "bar"], + } - validate({}) .should.equal(false); - validate.errors .should.have.length(2); - }); + var validate = ajv.compile(schema) + validate({}).should.equal(false) + validate.errors.should.have.length(2) + }) - it('should not validate required twice with $data ref', function() { - ajv = new Ajv({ $data: true, allErrors: true }); + it("should not validate required twice with $data ref", function () { + ajv = new Ajv({$data: true, allErrors: true}) var schema = { properties: { - foo: { type: 'integer' }, - bar: { type: 'integer' } + foo: {type: "integer"}, + bar: {type: "integer"}, }, - required: { $data: '0/requiredProperties' } - }; - - var validate = ajv.compile(schema); + required: {$data: "0/requiredProperties"}, + } - validate({ requiredProperties: ['foo', 'bar'] }) .should.equal(false); - validate.errors .should.have.length(2); - }); - }); + var validate = ajv.compile(schema) + validate({requiredProperties: ["foo", "bar"]}).should.equal(false) + validate.errors.should.have.length(2) + }) + }) - describe('"dependencies" errors', function() { - it('should include missing property in dataPath with option errorDataPath="property"', function() { - createInstances('property'); - testDependencies('property'); - }); + describe('"dependencies" errors', function () { + it('should include missing property in dataPath with option errorDataPath="property"', function () { + createInstances("property") + testDependencies("property") + }) - it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() { - testDependencies(); - }); + it("should NOT include missing property in dataPath WITHOUT option errorDataPath", function () { + testDependencies() + }) function testDependencies(errorDataPath) { var schema = { dependencies: { - a: ['foo', 'bar', 'baz'] - } - }; - - var data = { a: 0, foo: 1, bar: 2, baz: 3 } - , invalidData1 = { a: 0, foo: 1, baz: 3 } - , invalidData2 = { a: 0, bar: 2 }; - - var path = pathFunc(errorDataPath); - var msg = 'should have properties foo, bar, baz when property a is present'; - - var validate = ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1); - shouldBeError(validate.errors[0], 'dependencies', '#/dependencies', path('.bar'), msg, params('.bar')); - shouldBeInvalid(validate, invalidData2); - shouldBeError(validate.errors[0], 'dependencies', '#/dependencies', path('.foo'), msg, params('.foo')); - - var validateJP = ajvJP.compile(schema); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData1); - shouldBeError(validateJP.errors[0], 'dependencies', '#/dependencies', path('/bar'), msg, params('bar')); - shouldBeInvalid(validateJP, invalidData2); - shouldBeError(validateJP.errors[0], 'dependencies', '#/dependencies', path('/foo'), msg, params('foo')); - - var fullValidate = fullAjv.compile(schema); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData1); - shouldBeError(fullValidate.errors[0], 'dependencies', '#/dependencies', path('/bar'), msg, params('bar')); - shouldBeInvalid(fullValidate, invalidData2, 2); - shouldBeError(fullValidate.errors[0], 'dependencies', '#/dependencies', path('/foo'), msg, params('foo')); - shouldBeError(fullValidate.errors[1], 'dependencies', '#/dependencies', path('/baz'), msg, params('baz')); + a: ["foo", "bar", "baz"], + }, + } + + var data = {a: 0, foo: 1, bar: 2, baz: 3}, + invalidData1 = {a: 0, foo: 1, baz: 3}, + invalidData2 = {a: 0, bar: 2} + + var path = pathFunc(errorDataPath) + var msg = + "should have properties foo, bar, baz when property a is present" + + var validate = ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData1) + shouldBeError( + validate.errors[0], + "dependencies", + "#/dependencies", + path(".bar"), + msg, + params(".bar") + ) + shouldBeInvalid(validate, invalidData2) + shouldBeError( + validate.errors[0], + "dependencies", + "#/dependencies", + path(".foo"), + msg, + params(".foo") + ) + + var validateJP = ajvJP.compile(schema) + shouldBeValid(validateJP, data) + shouldBeInvalid(validateJP, invalidData1) + shouldBeError( + validateJP.errors[0], + "dependencies", + "#/dependencies", + path("/bar"), + msg, + params("bar") + ) + shouldBeInvalid(validateJP, invalidData2) + shouldBeError( + validateJP.errors[0], + "dependencies", + "#/dependencies", + path("/foo"), + msg, + params("foo") + ) + + var fullValidate = fullAjv.compile(schema) + shouldBeValid(fullValidate, data) + shouldBeInvalid(fullValidate, invalidData1) + shouldBeError( + fullValidate.errors[0], + "dependencies", + "#/dependencies", + path("/bar"), + msg, + params("bar") + ) + shouldBeInvalid(fullValidate, invalidData2, 2) + shouldBeError( + fullValidate.errors[0], + "dependencies", + "#/dependencies", + path("/foo"), + msg, + params("foo") + ) + shouldBeError( + fullValidate.errors[1], + "dependencies", + "#/dependencies", + path("/baz"), + msg, + params("baz") + ) function params(missing) { var p = { - property: 'a', - deps: 'foo, bar, baz', - depsCount: 3 - }; - p.missingProperty = missing; - return p; + property: "a", + deps: "foo, bar, baz", + depsCount: 3, + } + p.missingProperty = missing + return p } } - }); - - - function _testRequired(errorDataPath, schema, schemaPathPrefix, prefix, extraErrors) { - var schPath = (schemaPathPrefix || '#') + '/required'; - prefix = prefix || ''; - extraErrors = extraErrors || 0; - - var data = { foo: 1, bar: 2, baz: 3 } - , invalidData1 = { foo: 1, baz: 3 } - , invalidData2 = { bar: 2 }; - - var path = pathFunc(errorDataPath); - var msg = requiredFunc(errorDataPath); - - var validate = ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', schPath, path('.bar'), msg(prefix + 'bar'), { missingProperty: prefix + 'bar' }); - shouldBeInvalid(validate, invalidData2, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', schPath, path('.foo'), msg(prefix + 'foo'), { missingProperty: prefix + 'foo' }); - - var validateJP = ajvJP.compile(schema); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', schPath, path('/bar'), msg('bar'), { missingProperty: 'bar' }); - shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', schPath, path('/foo'), msg('foo'), { missingProperty: 'foo' }); - - var fullValidate = fullAjv.compile(schema); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', schPath, path('/bar'), msg('bar'), { missingProperty: 'bar' }); - shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', schPath, path('/foo'), msg('foo'), { missingProperty: 'foo' }); - shouldBeError(fullValidate.errors[1], 'required', schPath, path('/baz'), msg('baz'), { missingProperty: 'baz' }); + }) + + function _testRequired( + errorDataPath, + schema, + schemaPathPrefix, + prefix, + extraErrors + ) { + var schPath = (schemaPathPrefix || "#") + "/required" + prefix = prefix || "" + extraErrors = extraErrors || 0 + + var data = {foo: 1, bar: 2, baz: 3}, + invalidData1 = {foo: 1, baz: 3}, + invalidData2 = {bar: 2} + + var path = pathFunc(errorDataPath) + var msg = requiredFunc(errorDataPath) + + var validate = ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData1, 1 + extraErrors) + shouldBeError( + validate.errors[0], + "required", + schPath, + path(".bar"), + msg(prefix + "bar"), + {missingProperty: prefix + "bar"} + ) + shouldBeInvalid(validate, invalidData2, 1 + extraErrors) + shouldBeError( + validate.errors[0], + "required", + schPath, + path(".foo"), + msg(prefix + "foo"), + {missingProperty: prefix + "foo"} + ) + + var validateJP = ajvJP.compile(schema) + shouldBeValid(validateJP, data) + shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) + shouldBeError( + validateJP.errors[0], + "required", + schPath, + path("/bar"), + msg("bar"), + {missingProperty: "bar"} + ) + shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) + shouldBeError( + validateJP.errors[0], + "required", + schPath, + path("/foo"), + msg("foo"), + {missingProperty: "foo"} + ) + + var fullValidate = fullAjv.compile(schema) + shouldBeValid(fullValidate, data) + shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) + shouldBeError( + fullValidate.errors[0], + "required", + schPath, + path("/bar"), + msg("bar"), + {missingProperty: "bar"} + ) + shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) + shouldBeError( + fullValidate.errors[0], + "required", + schPath, + path("/foo"), + msg("foo"), + {missingProperty: "foo"} + ) + shouldBeError( + fullValidate.errors[1], + "required", + schPath, + path("/baz"), + msg("baz"), + {missingProperty: "baz"} + ) } function pathFunc(errorDataPath) { return function (dataPath) { - return errorDataPath == 'property' ? dataPath : ''; - }; + return errorDataPath == "property" ? dataPath : "" + } } function requiredFunc(errorDataPath) { return function (prop) { - return errorDataPath == 'property' - ? 'is a required property' - : 'should have required property \'' + prop + '\''; - }; + return errorDataPath == "property" + ? "is a required property" + : "should have required property '" + prop + "'" + } } function additionalFunc(errorDataPath) { - return errorDataPath == 'property' - ? 'is an invalid additional property' - : 'should NOT have additional properties'; + return errorDataPath == "property" + ? "is an invalid additional property" + : "should NOT have additional properties" } - - it('"items" errors should include item index without quotes in dataPath (#48)', function() { + it('"items" errors should include item index without quotes in dataPath (#48)', function () { var schema1 = { - $id: 'schema1', - type: 'array', + $id: "schema1", + type: "array", items: { - type: 'integer', - minimum: 10 - } - }; - - var data = [ 10, 11, 12] - , invalidData1 = [ 1, 10 ] - , invalidData2 = [ 10, 9, 11, 8, 12]; - - var validate = ajv.compile(schema1); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1); - shouldBeError(validate.errors[0], 'minimum', '#/items/minimum', '[0]', 'should be >= 10'); - shouldBeInvalid(validate, invalidData2); - shouldBeError(validate.errors[0], 'minimum', '#/items/minimum', '[1]', 'should be >= 10'); - - var validateJP = ajvJP.compile(schema1); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData1); - shouldBeError(validateJP.errors[0], 'minimum', '#/items/minimum', '/0', 'should be >= 10'); - shouldBeInvalid(validateJP, invalidData2); - shouldBeError(validateJP.errors[0], 'minimum', '#/items/minimum', '/1', 'should be >= 10'); - - var fullValidate = fullAjv.compile(schema1); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData1); - shouldBeError(fullValidate.errors[0], 'minimum', '#/items/minimum', '/0', 'should be >= 10'); - shouldBeInvalid(fullValidate, invalidData2, 2); - shouldBeError(fullValidate.errors[0], 'minimum', '#/items/minimum', '/1', 'should be >= 10'); - shouldBeError(fullValidate.errors[1], 'minimum', '#/items/minimum', '/3', 'should be >= 10'); - - var schema2 = { - $id: 'schema2', - type: 'array', - items: [{ minimum: 10 }, { minimum: 9 }, { minimum: 12 }] - }; + type: "integer", + minimum: 10, + }, + } - validate = ajv.compile(schema2); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1); - shouldBeError(validate.errors[0], 'minimum', '#/items/0/minimum', '[0]', 'should be >= 10'); - shouldBeInvalid(validate, invalidData2); - shouldBeError(validate.errors[0], 'minimum', '#/items/2/minimum', '[2]', 'should be >= 12'); - }); + var data = [10, 11, 12], + invalidData1 = [1, 10], + invalidData2 = [10, 9, 11, 8, 12] + + var validate = ajv.compile(schema1) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData1) + shouldBeError( + validate.errors[0], + "minimum", + "#/items/minimum", + "[0]", + "should be >= 10" + ) + shouldBeInvalid(validate, invalidData2) + shouldBeError( + validate.errors[0], + "minimum", + "#/items/minimum", + "[1]", + "should be >= 10" + ) + + var validateJP = ajvJP.compile(schema1) + shouldBeValid(validateJP, data) + shouldBeInvalid(validateJP, invalidData1) + shouldBeError( + validateJP.errors[0], + "minimum", + "#/items/minimum", + "/0", + "should be >= 10" + ) + shouldBeInvalid(validateJP, invalidData2) + shouldBeError( + validateJP.errors[0], + "minimum", + "#/items/minimum", + "/1", + "should be >= 10" + ) + + var fullValidate = fullAjv.compile(schema1) + shouldBeValid(fullValidate, data) + shouldBeInvalid(fullValidate, invalidData1) + shouldBeError( + fullValidate.errors[0], + "minimum", + "#/items/minimum", + "/0", + "should be >= 10" + ) + shouldBeInvalid(fullValidate, invalidData2, 2) + shouldBeError( + fullValidate.errors[0], + "minimum", + "#/items/minimum", + "/1", + "should be >= 10" + ) + shouldBeError( + fullValidate.errors[1], + "minimum", + "#/items/minimum", + "/3", + "should be >= 10" + ) + var schema2 = { + $id: "schema2", + type: "array", + items: [{minimum: 10}, {minimum: 9}, {minimum: 12}], + } - it('should have correct schema path for additionalItems', function() { + validate = ajv.compile(schema2) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData1) + shouldBeError( + validate.errors[0], + "minimum", + "#/items/0/minimum", + "[0]", + "should be >= 10" + ) + shouldBeInvalid(validate, invalidData2) + shouldBeError( + validate.errors[0], + "minimum", + "#/items/2/minimum", + "[2]", + "should be >= 12" + ) + }) + + it("should have correct schema path for additionalItems", function () { var schema = { - type: 'array', - items: [ { type: 'integer' }, { type: 'integer' } ], - additionalItems: false - }; + type: "array", + items: [{type: "integer"}, {type: "integer"}], + additionalItems: false, + } - var data = [ 1, 2 ] - , invalidData = [ 1, 2, 3 ]; + var data = [1, 2], + invalidData = [1, 2, 3] - test(ajv); - test(ajvJP); - test(fullAjv); + test(ajv) + test(ajvJP) + test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData); - shouldBeError(validate.errors[0], 'additionalItems', '#/additionalItems', '', 'should NOT have more than 2 items'); + var validate = _ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData) + shouldBeError( + validate.errors[0], + "additionalItems", + "#/additionalItems", + "", + "should NOT have more than 2 items" + ) } - }); - + }) - describe('"propertyNames" errors', function() { - it('should add propertyName to errors', function() { + describe('"propertyNames" errors', function () { + it("should add propertyName to errors", function () { var schema = { - type: 'object', - propertyNames: { format: 'email' } - }; + type: "object", + propertyNames: {format: "email"}, + } var data = { - 'bar.baz@email.example.com': {} - }; + "bar.baz@email.example.com": {}, + } var invalidData = { - 'foo': {}, - 'bar': {}, - 'bar.baz@email.example.com': {} - }; + foo: {}, + bar: {}, + "bar.baz@email.example.com": {}, + } - test(ajv, 2); - test(ajvJP, 2); - test(fullAjv, 4); + test(ajv, 2) + test(ajvJP, 2) + test(fullAjv, 4) function test(_ajv, numErrors) { - var validate = _ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData, numErrors); - shouldBeError(validate.errors[0], 'format', '#/propertyNames/format', '', 'should match format "email"'); - shouldBeError(validate.errors[1], 'propertyNames', '#/propertyNames', '', 'property name \'foo\' is invalid'); + var validate = _ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData, numErrors) + shouldBeError( + validate.errors[0], + "format", + "#/propertyNames/format", + "", + 'should match format "email"' + ) + shouldBeError( + validate.errors[1], + "propertyNames", + "#/propertyNames", + "", + "property name 'foo' is invalid" + ) if (numErrors == 4) { - shouldBeError(validate.errors[2], 'format', '#/propertyNames/format', '', 'should match format "email"'); - shouldBeError(validate.errors[3], 'propertyNames', '#/propertyNames', '', 'property name \'bar\' is invalid'); + shouldBeError( + validate.errors[2], + "format", + "#/propertyNames/format", + "", + 'should match format "email"' + ) + shouldBeError( + validate.errors[3], + "propertyNames", + "#/propertyNames", + "", + "property name 'bar' is invalid" + ) } } - }); - }); + }) + }) - - describe('oneOf errors', function() { - it('should have errors from inner schemas', function() { + describe("oneOf errors", function () { + it("should have errors from inner schemas", function () { var schema = { - oneOf: [ - { type: 'number' }, - { type: 'integer' } - ] - }; + oneOf: [{type: "number"}, {type: "integer"}], + } - test(ajv); - test(fullAjv); + test(ajv) + test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema); - validate('foo') .should.equal(false); - validate.errors.length .should.equal(3); - validate(1) .should.equal(false); - validate.errors.length .should.equal(1); - validate(1.5) .should.equal(true); + var validate = _ajv.compile(schema) + validate("foo").should.equal(false) + validate.errors.length.should.equal(3) + validate(1).should.equal(false) + validate.errors.length.should.equal(1) + validate(1.5).should.equal(true) } - }); + }) - it('should return passing schemas in error params', function() { + it("should return passing schemas in error params", function () { var schema = { - oneOf: [ - { type: 'number' }, - { type: 'integer' }, - { const: 1.5 } - ] - }; + oneOf: [{type: "number"}, {type: "integer"}, {const: 1.5}], + } - test(ajv); - test(fullAjv); + test(ajv) + test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema); - validate(1) .should.equal(false); - var err = validate.errors.pop(); - err.keyword .should.equal('oneOf'); - err.params .should.eql({passingSchemas: [0, 1]}); - - validate(1.5) .should.equal(false); - err = validate.errors.pop(); - err.keyword .should.equal('oneOf'); - err.params .should.eql({passingSchemas: [0, 2]}); - - validate(2.5) .should.equal(true); - - validate('foo') .should.equal(false); - err = validate.errors.pop(); - err.keyword .should.equal('oneOf'); - err.params .should.eql({passingSchemas: null}); + var validate = _ajv.compile(schema) + validate(1).should.equal(false) + var err = validate.errors.pop() + err.keyword.should.equal("oneOf") + err.params.should.eql({passingSchemas: [0, 1]}) + + validate(1.5).should.equal(false) + err = validate.errors.pop() + err.keyword.should.equal("oneOf") + err.params.should.eql({passingSchemas: [0, 2]}) + + validate(2.5).should.equal(true) + + validate("foo").should.equal(false) + err = validate.errors.pop() + err.keyword.should.equal("oneOf") + err.params.should.eql({passingSchemas: null}) } - }); - }); + }) + }) - - describe('anyOf errors', function() { - it('should have errors from inner schemas', function() { + describe("anyOf errors", function () { + it("should have errors from inner schemas", function () { var schema = { - anyOf: [ - { type: 'number' }, - { type: 'integer' } - ] - }; + anyOf: [{type: "number"}, {type: "integer"}], + } - test(ajv); - test(fullAjv); + test(ajv) + test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema); - validate('foo') .should.equal(false); - validate.errors.length .should.equal(3); - validate(1) .should.equal(true); - validate(1.5) .should.equal(true); + var validate = _ajv.compile(schema) + validate("foo").should.equal(false) + validate.errors.length.should.equal(3) + validate(1).should.equal(true) + validate(1.5).should.equal(true) } - }); - }); - + }) + }) - describe('type errors', function() { - describe('integer', function() { - it('should have only one error in {allErrors: false} mode', function() { - test(ajv); - }); + describe("type errors", function () { + describe("integer", function () { + it("should have only one error in {allErrors: false} mode", function () { + test(ajv) + }) - it('should return all errors in {allErrors: true} mode', function() { - test(fullAjv, 2); - }); + it("should return all errors in {allErrors: true} mode", function () { + test(fullAjv, 2) + }) function test(_ajv, numErrors) { var schema = { - type: 'integer', - minimum: 5 - }; - + type: "integer", + minimum: 5, + } - var validate = _ajv.compile(schema); - shouldBeValid(validate, 5); - shouldBeInvalid(validate, 5.5); - shouldBeInvalid(validate, 4); - shouldBeInvalid(validate, '4'); - shouldBeInvalid(validate, 4.5, numErrors); + var validate = _ajv.compile(schema) + shouldBeValid(validate, 5) + shouldBeInvalid(validate, 5.5) + shouldBeInvalid(validate, 4) + shouldBeInvalid(validate, "4") + shouldBeInvalid(validate, 4.5, numErrors) } - }); + }) - describe('keyword for another type', function() { - it('should have only one error in {allErrors: false} mode', function() { - test(ajv); - }); + describe("keyword for another type", function () { + it("should have only one error in {allErrors: false} mode", function () { + test(ajv) + }) - it('should return all errors in {allErrors: true} mode', function() { - test(fullAjv, 2); - }); + it("should return all errors in {allErrors: true} mode", function () { + test(fullAjv, 2) + }) function test(_ajv, numErrors) { var schema = { - type: 'array', + type: "array", minItems: 2, - minimum: 5 - }; - + minimum: 5, + } - var validate = _ajv.compile(schema); - shouldBeValid(validate, [1, 2]); - shouldBeInvalid(validate, [1]); - shouldBeInvalid(validate, 5); - shouldBeInvalid(validate, 4, numErrors); + var validate = _ajv.compile(schema) + shouldBeValid(validate, [1, 2]) + shouldBeInvalid(validate, [1]) + shouldBeInvalid(validate, 5) + shouldBeInvalid(validate, 4, numErrors) } - }); + }) - describe('array of types', function() { - it('should have only one error in {allErrors: false} mode', function() { - test(ajv); - }); + describe("array of types", function () { + it("should have only one error in {allErrors: false} mode", function () { + test(ajv) + }) - it('should return all errors in {allErrors: true} mode', function() { - test(fullAjv, 2); - }); + it("should return all errors in {allErrors: true} mode", function () { + test(fullAjv, 2) + }) function test(_ajv, numErrors) { var schema = { - type: ['array', 'object'], + type: ["array", "object"], minItems: 2, minProperties: 2, - minimum: 5 - }; - + minimum: 5, + } - var validate = _ajv.compile(schema); - shouldBeValid(validate, [1, 2]); - shouldBeValid(validate, {foo: 1, bar: 2}); - shouldBeInvalid(validate, [1]); - shouldBeInvalid(validate, {foo: 1}); - shouldBeInvalid(validate, 5); - shouldBeInvalid(validate, 4, numErrors); + var validate = _ajv.compile(schema) + shouldBeValid(validate, [1, 2]) + shouldBeValid(validate, {foo: 1, bar: 2}) + shouldBeInvalid(validate, [1]) + shouldBeInvalid(validate, {foo: 1}) + shouldBeInvalid(validate, 5) + shouldBeInvalid(validate, 4, numErrors) } - }); - }); + }) + }) - - describe('exclusiveMaximum/Minimum errors', function() { - it('should include limits in error message', function() { + describe("exclusiveMaximum/Minimum errors", function () { + it("should include limits in error message", function () { var schema = { - type: 'integer', + type: "integer", exclusiveMinimum: 2, - exclusiveMaximum: 5 - }; - - [ajv, fullAjv].forEach(function (_ajv) { - var validate = _ajv.compile(schema); - shouldBeValid(validate, 3); - shouldBeValid(validate, 4); - - shouldBeInvalid(validate, 2); - testError('exclusiveMinimum', 'should be > 2', {comparison: '>', limit: 2, exclusive: true}); + exclusiveMaximum: 5, + } - shouldBeInvalid(validate, 5); - testError('exclusiveMaximum', 'should be < 5', {comparison: '<', limit: 5, exclusive: true}); + ;[ajv, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema) + shouldBeValid(validate, 3) + shouldBeValid(validate, 4) + + shouldBeInvalid(validate, 2) + testError("exclusiveMinimum", "should be > 2", { + comparison: ">", + limit: 2, + exclusive: true, + }) + + shouldBeInvalid(validate, 5) + testError("exclusiveMaximum", "should be < 5", { + comparison: "<", + limit: 5, + exclusive: true, + }) function testError(keyword, message, params) { - var err = validate.errors[0]; - shouldBeError(err, keyword, '#/' + keyword, '', message, params); + var err = validate.errors[0] + shouldBeError(err, keyword, "#/" + keyword, "", message, params) } - }); - }); + }) + }) - it('should include limits in error message with $data', function() { + it("should include limits in error message with $data", function () { var schema = { - "properties": { - "smaller": { - "type": "number", - "exclusiveMaximum": { "$data": "1/larger" } + properties: { + smaller: { + type: "number", + exclusiveMaximum: {$data: "1/larger"}, }, - "larger": { "type": "number" } - } - }; - - ajv = new Ajv({$data: true}); - fullAjv = new Ajv({$data: true, allErrors: true, verbose: true, jsonPointers: true}); + larger: {type: "number"}, + }, + } - [ajv, fullAjv].forEach(function (_ajv) { - var validate = _ajv.compile(schema); - shouldBeValid(validate, {smaller: 2, larger: 4}); - shouldBeValid(validate, {smaller: 3, larger: 4}); + ajv = new Ajv({$data: true}) + fullAjv = new Ajv({ + $data: true, + allErrors: true, + verbose: true, + jsonPointers: true, + }) + ;[ajv, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema) + shouldBeValid(validate, {smaller: 2, larger: 4}) + shouldBeValid(validate, {smaller: 3, larger: 4}) - shouldBeInvalid(validate, {smaller: 4, larger: 4}); - testError(); + shouldBeInvalid(validate, {smaller: 4, larger: 4}) + testError() - shouldBeInvalid(validate, {smaller: 5, larger: 4}); - testError(); + shouldBeInvalid(validate, {smaller: 5, larger: 4}) + testError() function testError() { - var err = validate.errors[0]; - shouldBeError(err, 'exclusiveMaximum', - '#/properties/smaller/exclusiveMaximum', - _ajv._opts.jsonPointers ? '/smaller' : '.smaller', - 'should be < 4', - {comparison: '<', limit: 4, exclusive: true}); + var err = validate.errors[0] + shouldBeError( + err, + "exclusiveMaximum", + "#/properties/smaller/exclusiveMaximum", + _ajv._opts.jsonPointers ? "/smaller" : ".smaller", + "should be < 4", + {comparison: "<", limit: 4, exclusive: true} + ) } - }); - }); - }); + }) + }) + }) + describe("if/then/else errors", function () { + var validate, numErrors - describe('if/then/else errors', function() { - var validate, numErrors; - - it('if/then/else should include failing keyword in message and params', function() { + it("if/then/else should include failing keyword in message and params", function () { var schema = { - 'if': { maximum: 10 }, - 'then': { multipleOf: 2 }, - 'else': { multipleOf: 5 } - }; + if: {maximum: 10}, + then: {multipleOf: 2}, + else: {multipleOf: 5}, + } - [ajv, fullAjv].forEach(function (_ajv) { - prepareTest(_ajv, schema); - shouldBeValid(validate, 8); - shouldBeValid(validate, 15); + ;[ajv, fullAjv].forEach(function (_ajv) { + prepareTest(_ajv, schema) + shouldBeValid(validate, 8) + shouldBeValid(validate, 15) - shouldBeInvalid(validate, 7, numErrors); - testIfError('then', 2); + shouldBeInvalid(validate, 7, numErrors) + testIfError("then", 2) - shouldBeInvalid(validate, 17, numErrors); - testIfError('else', 5); - }); - }); + shouldBeInvalid(validate, 17, numErrors) + testIfError("else", 5) + }) + }) - it('if/then should include failing keyword in message and params', function() { + it("if/then should include failing keyword in message and params", function () { var schema = { - 'if': { maximum: 10 }, - 'then': { multipleOf: 2 } - }; - - [ajv, fullAjv].forEach(function (_ajv) { - prepareTest(_ajv, schema); - shouldBeValid(validate, 8); - shouldBeValid(validate, 11); - shouldBeValid(validate, 12); - - shouldBeInvalid(validate, 7, numErrors); - testIfError('then', 2); - }); - }); - - it('if/else should include failing keyword in message and params', function() { + if: {maximum: 10}, + then: {multipleOf: 2}, + } + + ;[ajv, fullAjv].forEach(function (_ajv) { + prepareTest(_ajv, schema) + shouldBeValid(validate, 8) + shouldBeValid(validate, 11) + shouldBeValid(validate, 12) + + shouldBeInvalid(validate, 7, numErrors) + testIfError("then", 2) + }) + }) + + it("if/else should include failing keyword in message and params", function () { var schema = { - 'if': { maximum: 10 }, - 'else': { multipleOf: 5 } - }; + if: {maximum: 10}, + else: {multipleOf: 5}, + } - [ajv, fullAjv].forEach(function (_ajv) { - prepareTest(_ajv, schema); - shouldBeValid(validate, 7); - shouldBeValid(validate, 8); - shouldBeValid(validate, 15); + ;[ajv, fullAjv].forEach(function (_ajv) { + prepareTest(_ajv, schema) + shouldBeValid(validate, 7) + shouldBeValid(validate, 8) + shouldBeValid(validate, 15) - shouldBeInvalid(validate, 17, numErrors); - testIfError('else', 5); - }); - }); + shouldBeInvalid(validate, 17, numErrors) + testIfError("else", 5) + }) + }) function prepareTest(_ajv, schema) { - validate = _ajv.compile(schema); - numErrors = _ajv._opts.allErrors ? 2 : 1; + validate = _ajv.compile(schema) + numErrors = _ajv._opts.allErrors ? 2 : 1 } function testIfError(ifClause, multipleOf) { - var err = validate.errors[0]; - shouldBeError(err, 'multipleOf', '#/' + ifClause + '/multipleOf', '', - 'should be multiple of ' + multipleOf, {multipleOf: multipleOf}); + var err = validate.errors[0] + shouldBeError( + err, + "multipleOf", + "#/" + ifClause + "/multipleOf", + "", + "should be multiple of " + multipleOf, + {multipleOf: multipleOf} + ) if (numErrors == 2) { - err = validate.errors[1]; - shouldBeError(err, 'if', '#/if', '', - 'should match "' + ifClause + '" schema', {failingKeyword: ifClause}); + err = validate.errors[1] + shouldBeError( + err, + "if", + "#/if", + "", + 'should match "' + ifClause + '" schema', + {failingKeyword: ifClause} + ) } } - }); - + }) - describe('uniqueItems errors', function() { - it('should not return uniqueItems error when non-unique items are of a different type than required', function() { + describe("uniqueItems errors", function () { + it("should not return uniqueItems error when non-unique items are of a different type than required", function () { var schema = { - items: {type: 'number'}, - uniqueItems: true - }; - - [ajvJP, fullAjv].forEach(function (_ajv) { - var validate = _ajv.compile(schema); - shouldBeValid(validate, [1, 2, 3]); - - shouldBeInvalid(validate, [1, 2, 2]); - shouldBeError(validate.errors[0], 'uniqueItems', '#/uniqueItems', '', - 'should NOT have duplicate items (items ## 2 and 1 are identical)', - {i: 1, j: 2}); + items: {type: "number"}, + uniqueItems: true, + } - var expectedErrors = _ajv._opts.allErrors ? 2 : 1; - shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors); - testTypeError(0, '/1'); - if (expectedErrors == 2) testTypeError(1, '/2'); + ;[ajvJP, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema) + shouldBeValid(validate, [1, 2, 3]) + + shouldBeInvalid(validate, [1, 2, 2]) + shouldBeError( + validate.errors[0], + "uniqueItems", + "#/uniqueItems", + "", + "should NOT have duplicate items (items ## 2 and 1 are identical)", + {i: 1, j: 2} + ) + + var expectedErrors = _ajv._opts.allErrors ? 2 : 1 + shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors) + testTypeError(0, "/1") + if (expectedErrors == 2) testTypeError(1, "/2") function testTypeError(i, dataPath) { - var err = validate.errors[i]; - shouldBeError(err, 'type', '#/items/type', dataPath, 'should be number'); + var err = validate.errors[i] + shouldBeError( + err, + "type", + "#/items/type", + dataPath, + "should be number" + ) } - }); - }); - }); - + }) + }) + }) function testSchema1(schema, schemaPathPrefix) { - _testSchema1(ajv, schema, schemaPathPrefix); - _testSchema1(ajvJP, schema, schemaPathPrefix); - _testSchema1(fullAjv, schema, schemaPathPrefix); + _testSchema1(ajv, schema, schemaPathPrefix) + _testSchema1(ajvJP, schema, schemaPathPrefix) + _testSchema1(fullAjv, schema, schemaPathPrefix) } - function _testSchema1(_ajv, schema, schemaPathPrefix) { - var schPath = (schemaPathPrefix || '#/properties/foo') + '/type'; - - var data = { foo: 1 } - , invalidData = { foo: 'bar' }; - - var validate = _ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData); - shouldBeError(validate.errors[0], 'type', schPath, _ajv._opts.jsonPointers ? '/foo' : '.foo'); + var schPath = (schemaPathPrefix || "#/properties/foo") + "/type" + + var data = {foo: 1}, + invalidData = {foo: "bar"} + + var validate = _ajv.compile(schema) + shouldBeValid(validate, data) + shouldBeInvalid(validate, invalidData) + shouldBeError( + validate.errors[0], + "type", + schPath, + _ajv._opts.jsonPointers ? "/foo" : ".foo" + ) } - function shouldBeValid(validate, data) { - validate(data) .should.equal(true); - should.equal(validate.errors, null); + validate(data).should.equal(true) + should.equal(validate.errors, null) } - function shouldBeInvalid(validate, data, numErrors) { - validate(data) .should.equal(false); - should.equal(validate.errors.length, numErrors || 1); + validate(data).should.equal(false) + should.equal(validate.errors.length, numErrors || 1) } - - function shouldBeError(error, keyword, schemaPath, dataPath, message, params) { - error.keyword .should.equal(keyword); - error.schemaPath .should.equal(schemaPath); - error.dataPath .should.equal(dataPath); - error.message .should.be.a('string'); - if (message !== undefined) error.message .should.equal(message); - if (params !== undefined) error.params .should.eql(params); + function shouldBeError( + error, + keyword, + schemaPath, + dataPath, + message, + params + ) { + error.keyword.should.equal(keyword) + error.schemaPath.should.equal(schemaPath) + error.dataPath.should.equal(dataPath) + error.message.should.be.a("string") + if (message !== undefined) error.message.should.equal(message) + if (params !== undefined) error.params.should.eql(params) } -}); +}) diff --git a/spec/extras.spec.js b/spec/extras.spec.js index 15c51a6bcf..1b5c338d52 100644 --- a/spec/extras.spec.js +++ b/spec/extras.spec.js @@ -1,28 +1,31 @@ -'use strict'; +"use strict" -var jsonSchemaTest = require('json-schema-test') - , getAjvInstances = require('./ajv_instances') - , options = require('./ajv_options') - , suite = require('./browser_test_suite') - , after = require('./after_test'); +var jsonSchemaTest = require("json-schema-test"), + getAjvInstances = require("./ajv_instances"), + options = require("./ajv_options"), + suite = require("./browser_test_suite"), + after = require("./after_test") var instances = getAjvInstances(options, { $data: true, - unknownFormats: ['allowedUnknown'] -}); - + unknownFormats: ["allowedUnknown"], +}) jsonSchemaTest(instances, { - description: 'Extra keywords schemas tests of ' + instances.length + ' ajv instances with different options', + description: + "Extra keywords schemas tests of " + + instances.length + + " ajv instances with different options", suites: { - 'extras': typeof window == 'object' - ? suite(require('./extras/{**/,}*.json', {mode: 'list'})) - : './extras/{**/,}*.json' + extras: + typeof window == "object" + ? suite(require("./extras/{**/,}*.json", {mode: "list"})) + : "./extras/{**/,}*.json", }, - assert: require('./chai').assert, + assert: require("./chai").assert, afterError: after.error, afterEach: after.each, cwd: __dirname, - hideFolder: 'extras/', - timeout: 90000 -}); + hideFolder: "extras/", + timeout: 90000, +}) diff --git a/spec/extras/$data/absolute_ref.json b/spec/extras/$data/absolute_ref.json index 38b3fe5d50..74968ae6d6 100644 --- a/spec/extras/$data/absolute_ref.json +++ b/spec/extras/$data/absolute_ref.json @@ -61,11 +61,7 @@ "schema": { "properties": { "arr": { - "items": [ - {}, - {}, - {} - ], + "items": [{}, {}, {}], "additionalItems": false }, "sameArr": { @@ -155,9 +151,7 @@ "description": "1 item array containing property is valid", "data": { "name": "foo", - "list": [ - "foo" - ] + "list": ["foo"] }, "valid": true }, @@ -165,10 +159,7 @@ "description": "2 item array containing property is valid", "data": { "name": "foo", - "list": [ - "foo", - "bar" - ] + "list": ["foo", "bar"] }, "valid": true }, @@ -176,9 +167,7 @@ "description": "array not containing property is invalid", "data": { "name": "foo", - "list": [ - "bar" - ] + "list": ["bar"] }, "valid": false }, @@ -208,11 +197,7 @@ { "description": "one of the enum is valid", "data": { - "allowedValues": [ - 1, - 2, - 3 - ], + "allowedValues": [1, 2, 3], "value": 1 }, "valid": true @@ -220,11 +205,7 @@ { "description": "something else is invalid", "data": { - "allowedValues": [ - 1, - 2, - 3 - ], + "allowedValues": [1, 2, 3], "value": 4 }, "valid": false @@ -280,10 +261,7 @@ { "description": "properties are valid", "data": { - "allowedValues": [ - "foo", - "bar" - ], + "allowedValues": ["foo", "bar"], "a": "foo", "b": "bar" }, @@ -292,10 +270,7 @@ { "description": "properties are invalid", "data": { - "allowedValues": [ - "foo", - "bar" - ], + "allowedValues": ["foo", "bar"], "a": "foo", "b": "baz" }, @@ -320,9 +295,7 @@ "description": "present required property is valid", "data": { "foo": 1, - "requiredProperties": [ - "foo" - ] + "requiredProperties": ["foo"] }, "valid": true }, @@ -330,9 +303,7 @@ "description": "non-present required property is invalid", "data": { "bar": 2, - "requiredProperties": [ - "foo" - ] + "requiredProperties": ["foo"] }, "valid": false }, @@ -340,10 +311,7 @@ "description": "non-present second required property is invalid", "data": { "foo": 1, - "requiredProperties": [ - "foo", - "bar" - ] + "requiredProperties": ["foo", "bar"] }, "valid": false }, @@ -352,10 +320,7 @@ "data": { "foo": 1, "bar": 2, - "requiredProperties": [ - "foo", - "bar" - ] + "requiredProperties": ["foo", "bar"] }, "valid": true }, @@ -433,4 +398,4 @@ } ] } -] \ No newline at end of file +] diff --git a/spec/extras/$data/const.json b/spec/extras/$data/const.json index b6143c4482..44c917dd88 100644 --- a/spec/extras/$data/const.json +++ b/spec/extras/$data/const.json @@ -3,7 +3,7 @@ "description": "property is equal to another property", "schema": { "properties": { - "sameAs": { "const": { "$data": "1/thisOne" } }, + "sameAs": {"const": {"$data": "1/thisOne"}}, "thisOne": {} } }, @@ -19,16 +19,16 @@ { "description": "same object is valid", "data": { - "sameAs": { "foo": 1, "bar": 2 }, - "thisOne": { "bar": 2, "foo": 1 } + "sameAs": {"foo": 1, "bar": 2}, + "thisOne": {"bar": 2, "foo": 1} }, "valid": true }, { "description": "another value is invalid", "data": { - "sameAs": { "foo": 1 }, - "thisOne": { "foo": 2 } + "sameAs": {"foo": 1}, + "thisOne": {"foo": 2} }, "valid": false }, @@ -46,18 +46,18 @@ "description": "property values are equal to property names", "schema": { "additionalProperties": { - "const": { "$data": "0#" } + "const": {"$data": "0#"} } }, "tests": [ { "description": "valid object", - "data": { "foo": "foo", "bar": "bar", "baz": "baz" }, + "data": {"foo": "foo", "bar": "bar", "baz": "baz"}, "valid": true }, { "description": "invalid object", - "data": { "foo": "bar" }, + "data": {"foo": "bar"}, "valid": false } ] @@ -66,18 +66,18 @@ "description": "items are equal to their indeces", "schema": { "items": { - "const": { "$data": "0#" } + "const": {"$data": "0#"} } }, "tests": [ { "description": "valid array", - "data": [ 0, 1, 2, 3 ], + "data": [0, 1, 2, 3], "valid": true }, { "description": "invalid array", - "data": [ 0, 2 ], + "data": [0, 2], "valid": false } ] @@ -87,14 +87,14 @@ "schema": { "properties": { "arr": { - "items": [{},{},{}], + "items": [{}, {}, {}], "additionalItems": false }, "sameArr": { "items": [ - { "const": { "$data": "2/arr/0" } }, - { "const": { "$data": "2/arr/1" } }, - { "const": { "$data": "2/arr/2" } } + {"const": {"$data": "2/arr/0"}}, + {"const": {"$data": "2/arr/1"}}, + {"const": {"$data": "2/arr/2"}} ], "additionalItems": false } @@ -104,16 +104,16 @@ { "description": "equal arrays are valid", "data": { - "arr": [ 1, "abc", {"foo": "bar"} ], - "sameArr": [ 1, "abc", {"foo": "bar"} ] + "arr": [1, "abc", {"foo": "bar"}], + "sameArr": [1, "abc", {"foo": "bar"}] }, "valid": true }, { "description": "different arrays are invalid", "data": { - "arr": [ 1, "abc", {"foo": "bar"} ], - "sameArr": [ 1, "abc", {"foo": "foo"} ] + "arr": [1, "abc", {"foo": "bar"}], + "sameArr": [1, "abc", {"foo": "foo"}] }, "valid": false } @@ -122,7 +122,7 @@ { "description": "any data is equal to itself", "schema": { - "const": { "$data": "0" } + "const": {"$data": "0"} }, "tests": [ { @@ -137,12 +137,12 @@ }, { "description": "object is equal to itself", - "data": { "foo": "bar" }, + "data": {"foo": "bar"}, "valid": true }, { "description": "array is equal to itself", - "data": [ 1, 2, 3 ], + "data": [1, 2, 3], "valid": true } ] @@ -151,10 +151,10 @@ "description": "property value is contained in array", "schema": { "properties": { - "name": { "type": "string" }, + "name": {"type": "string"}, "list": { "type": "array", - "contains": { "const": { "$data": "2/name" } } + "contains": {"const": {"$data": "2/name"}} } } }, @@ -163,7 +163,7 @@ "description": "1 item array containing property is valid", "data": { "name": "foo", - "list": [ "foo" ] + "list": ["foo"] }, "valid": true }, @@ -171,7 +171,7 @@ "description": "2 item array containing property is valid", "data": { "name": "foo", - "list": [ "foo", "bar" ] + "list": ["foo", "bar"] }, "valid": true }, @@ -179,7 +179,7 @@ "description": "array not containing property is invalid", "data": { "name": "foo", - "list": [ "bar" ] + "list": ["bar"] }, "valid": false }, diff --git a/spec/extras/$data/enum.json b/spec/extras/$data/enum.json index 1aab4bd210..b2f895af93 100644 --- a/spec/extras/$data/enum.json +++ b/spec/extras/$data/enum.json @@ -4,7 +4,7 @@ "schema": { "properties": { "allowedValues": {}, - "value": { "enum": { "$data": "1/allowedValues" } } + "value": {"enum": {"$data": "1/allowedValues"}} } }, "tests": [ @@ -56,7 +56,7 @@ "allowedValues": {} }, "additionalProperties": { - "enum": { "$data": "1/allowedValues" } + "enum": {"$data": "1/allowedValues"} } }, "tests": [ diff --git a/spec/extras/$data/exclusiveMaximum.json b/spec/extras/$data/exclusiveMaximum.json index a33e1918c6..a1010571c9 100644 --- a/spec/extras/$data/exclusiveMaximum.json +++ b/spec/extras/$data/exclusiveMaximum.json @@ -5,7 +5,7 @@ "properties": { "larger": {}, "smaller": { - "exclusiveMaximum": { "$data": "1/larger" } + "exclusiveMaximum": {"$data": "1/larger"} } } }, @@ -59,7 +59,7 @@ } ] }, - + { "description": "exclusiveMaximum as number and maximum as $data, exclusiveMaximum > maximum", "schema": { @@ -67,7 +67,7 @@ "larger": {}, "smaller": { "exclusiveMaximum": 3.5, - "maximum": { "$data": "1/larger" } + "maximum": {"$data": "1/larger"} } } }, @@ -105,7 +105,7 @@ "larger": {}, "smaller": { "exclusiveMaximum": 3, - "maximum": { "$data": "1/larger" } + "maximum": {"$data": "1/larger"} } } }, @@ -143,7 +143,7 @@ "larger": {}, "smaller": { "exclusiveMaximum": 2.5, - "maximum": { "$data": "1/larger" } + "maximum": {"$data": "1/larger"} } } }, @@ -182,8 +182,8 @@ "larger": {}, "largerExclusive": {}, "smaller": { - "exclusiveMaximum": { "$data": "1/largerExclusive" }, - "maximum": { "$data": "1/larger" } + "exclusiveMaximum": {"$data": "1/largerExclusive"}, + "maximum": {"$data": "1/larger"} } } }, @@ -224,8 +224,8 @@ "larger": {}, "largerExclusive": {}, "smaller": { - "exclusiveMaximum": { "$data": "1/largerExclusive" }, - "maximum": { "$data": "1/larger" } + "exclusiveMaximum": {"$data": "1/largerExclusive"}, + "maximum": {"$data": "1/larger"} } } }, @@ -266,8 +266,8 @@ "larger": {}, "largerExclusive": {}, "smaller": { - "exclusiveMaximum": { "$data": "1/largerExclusive" }, - "maximum": { "$data": "1/larger" } + "exclusiveMaximum": {"$data": "1/largerExclusive"}, + "maximum": {"$data": "1/larger"} } } }, @@ -306,7 +306,7 @@ "description": "items in array are < than their indeces", "schema": { "items": { - "exclusiveMaximum": { "$data": "0#" } + "exclusiveMaximum": {"$data": "0#"} } }, "tests": [ diff --git a/spec/extras/$data/exclusiveMinimum.json b/spec/extras/$data/exclusiveMinimum.json index abb5ade814..6700c06901 100644 --- a/spec/extras/$data/exclusiveMinimum.json +++ b/spec/extras/$data/exclusiveMinimum.json @@ -5,7 +5,7 @@ "properties": { "smaller": {}, "larger": { - "exclusiveMinimum": { "$data": "1/smaller" } + "exclusiveMinimum": {"$data": "1/smaller"} } } }, @@ -60,7 +60,7 @@ "smaller": {}, "larger": { "exclusiveMinimum": 2.5, - "minimum": { "$data": "1/smaller" } + "minimum": {"$data": "1/smaller"} } } }, @@ -98,7 +98,7 @@ "smaller": {}, "larger": { "exclusiveMinimum": 3, - "minimum": { "$data": "1/smaller" } + "minimum": {"$data": "1/smaller"} } } }, @@ -136,7 +136,7 @@ "smaller": {}, "larger": { "exclusiveMinimum": 3.5, - "minimum": { "$data": "1/smaller" } + "minimum": {"$data": "1/smaller"} } } }, @@ -175,8 +175,8 @@ "smaller": {}, "smallerExclusive": {}, "larger": { - "exclusiveMinimum": { "$data": "1/smallerExclusive" }, - "minimum": { "$data": "1/smaller" } + "exclusiveMinimum": {"$data": "1/smallerExclusive"}, + "minimum": {"$data": "1/smaller"} } } }, @@ -217,8 +217,8 @@ "smaller": {}, "smallerExclusive": {}, "larger": { - "exclusiveMinimum": { "$data": "1/smallerExclusive" }, - "minimum": { "$data": "1/smaller" } + "exclusiveMinimum": {"$data": "1/smallerExclusive"}, + "minimum": {"$data": "1/smaller"} } } }, @@ -259,8 +259,8 @@ "smaller": {}, "smallerExclusive": {}, "larger": { - "exclusiveMinimum": { "$data": "1/smallerExclusive" }, - "minimum": { "$data": "1/smaller" } + "exclusiveMinimum": {"$data": "1/smallerExclusive"}, + "minimum": {"$data": "1/smaller"} } } }, @@ -299,7 +299,7 @@ "description": "items in array are > than their indeces", "schema": { "items": { - "exclusiveMinimum": { "$data": "0#" } + "exclusiveMinimum": {"$data": "0#"} } }, "tests": [ diff --git a/spec/extras/$data/format.json b/spec/extras/$data/format.json index 97c7fa7977..b6b8dccb0e 100644 --- a/spec/extras/$data/format.json +++ b/spec/extras/$data/format.json @@ -121,4 +121,4 @@ } ] } -] \ No newline at end of file +] diff --git a/spec/extras/$data/maxItems.json b/spec/extras/$data/maxItems.json index 03d599264c..377ca2b315 100644 --- a/spec/extras/$data/maxItems.json +++ b/spec/extras/$data/maxItems.json @@ -4,7 +4,7 @@ "schema": { "properties": { "maxArrayLength": {}, - "array": { "maxItems": { "$data": "1/maxArrayLength" } } + "array": {"maxItems": {"$data": "1/maxArrayLength"}} } }, "tests": [ diff --git a/spec/extras/$data/maxLength.json b/spec/extras/$data/maxLength.json index 9edd20fa1f..3b6a4a613a 100644 --- a/spec/extras/$data/maxLength.json +++ b/spec/extras/$data/maxLength.json @@ -4,7 +4,7 @@ "schema": { "properties": { "maximumLength": {}, - "string": { "maxLength": { "$data": "1/maximumLength" } } + "string": {"maxLength": {"$data": "1/maximumLength"}} } }, "tests": [ diff --git a/spec/extras/$data/maxProperties.json b/spec/extras/$data/maxProperties.json index 44469e038b..1b653aa26e 100644 --- a/spec/extras/$data/maxProperties.json +++ b/spec/extras/$data/maxProperties.json @@ -4,7 +4,7 @@ "schema": { "properties": { "maxKeys": {}, - "object": { "maxProperties": { "$data": "1/maxKeys" } } + "object": {"maxProperties": {"$data": "1/maxKeys"}} } }, "tests": [ diff --git a/spec/extras/$data/maximum.json b/spec/extras/$data/maximum.json index 9d99ead7e1..e9f9807c55 100644 --- a/spec/extras/$data/maximum.json +++ b/spec/extras/$data/maximum.json @@ -5,7 +5,7 @@ "properties": { "larger": {}, "smallerOrEqual": { - "maximum": { "$data": "1/larger" } + "maximum": {"$data": "1/larger"} } } }, @@ -65,7 +65,7 @@ "properties": { "number": { "maximum": 3, - "exclusiveMaximum": { "$data": "1/maxIsExclusive" } + "exclusiveMaximum": {"$data": "1/maxIsExclusive"} }, "maxIsExclusive": {} } @@ -156,8 +156,8 @@ "properties": { "larger": {}, "smallerOrEqual": { - "maximum": { "$data": "1/larger" }, - "exclusiveMaximum": { "$data": "1/maxIsExclusive" } + "maximum": {"$data": "1/larger"}, + "exclusiveMaximum": {"$data": "1/maxIsExclusive"} }, "maxIsExclusive": {} } @@ -256,7 +256,7 @@ "description": "items in array are <= than their indeces", "schema": { "items": { - "maximum": { "$data": "0#" } + "maximum": {"$data": "0#"} } }, "tests": [ diff --git a/spec/extras/$data/minItems.json b/spec/extras/$data/minItems.json index bae4d0dac3..fec3a7d593 100644 --- a/spec/extras/$data/minItems.json +++ b/spec/extras/$data/minItems.json @@ -4,7 +4,7 @@ "schema": { "properties": { "minArrayLength": {}, - "array": { "minItems": { "$data": "1/minArrayLength" } } + "array": {"minItems": {"$data": "1/minArrayLength"}} } }, "tests": [ diff --git a/spec/extras/$data/minLength.json b/spec/extras/$data/minLength.json index 71e36624f6..d4b338215d 100644 --- a/spec/extras/$data/minLength.json +++ b/spec/extras/$data/minLength.json @@ -4,7 +4,7 @@ "schema": { "properties": { "minimumLength": {}, - "string": { "minLength": { "$data": "1/minimumLength" } } + "string": {"minLength": {"$data": "1/minimumLength"}} } }, "tests": [ diff --git a/spec/extras/$data/minProperties.json b/spec/extras/$data/minProperties.json index d1393b6e4f..f8e990c6b2 100644 --- a/spec/extras/$data/minProperties.json +++ b/spec/extras/$data/minProperties.json @@ -4,7 +4,7 @@ "schema": { "properties": { "minKeys": {}, - "object": { "minProperties": { "$data": "1/minKeys" } } + "object": {"minProperties": {"$data": "1/minKeys"}} } }, "tests": [ diff --git a/spec/extras/$data/minimum.json b/spec/extras/$data/minimum.json index dc96f3a08c..00ddc30a7c 100644 --- a/spec/extras/$data/minimum.json +++ b/spec/extras/$data/minimum.json @@ -5,7 +5,7 @@ "properties": { "smaller": {}, "largerOrEqual": { - "minimum": { "$data": "1/smaller" } + "minimum": {"$data": "1/smaller"} } } }, @@ -58,7 +58,7 @@ "properties": { "number": { "minimum": 3, - "exclusiveMinimum": { "$data": "1/minIsExclusive" } + "exclusiveMinimum": {"$data": "1/minIsExclusive"} }, "minIsExclusive": {} } @@ -149,8 +149,8 @@ "properties": { "smaller": {}, "largerOrEqual": { - "minimum": { "$data": "1/smaller" }, - "exclusiveMinimum": { "$data": "1/minIsExclusive" } + "minimum": {"$data": "1/smaller"}, + "exclusiveMinimum": {"$data": "1/minIsExclusive"} }, "minIsExclusive": {} } @@ -249,7 +249,7 @@ "description": "items in array are >= than their indeces", "schema": { "items": { - "minimum": { "$data": "0#" } + "minimum": {"$data": "0#"} } }, "tests": [ diff --git a/spec/extras/$data/multipleOf.json b/spec/extras/$data/multipleOf.json index e463911d10..525e331c20 100644 --- a/spec/extras/$data/multipleOf.json +++ b/spec/extras/$data/multipleOf.json @@ -4,7 +4,7 @@ "schema": { "properties": { "divider": {}, - "multiple": { "multipleOf": { "$data": "1/divider" } } + "multiple": {"multipleOf": {"$data": "1/divider"}} } }, "tests": [ @@ -69,8 +69,8 @@ "description": "one property is multiple of another property with escaped characters", "schema": { "properties": { - "/divider~": { "type": "number" }, - "/multiple~": { "multipleOf": { "$data": "1/~1divider~0" } } + "/divider~": {"type": "number"}, + "/multiple~": {"multipleOf": {"$data": "1/~1divider~0"}} } }, "tests": [ @@ -98,12 +98,12 @@ "properties": { "divider": { "properties": { - "value": { "type": "number" } + "value": {"type": "number"} } }, "multiple": { "properties": { - "value": { "multipleOf": { "$data": "2/divider/value" } } + "value": {"multipleOf": {"$data": "2/divider/value"}} } } } @@ -112,16 +112,16 @@ { "description": "int by int valid", "data": { - "divider": { "value": 3 }, - "multiple": { "value": 12 } + "divider": {"value": 3}, + "multiple": {"value": 12} }, "valid": true }, { "description": "int by int invalid", "data": { - "divider": { "value": 3 }, - "multiple": { "value": 10 } + "divider": {"value": 3}, + "multiple": {"value": 10} }, "valid": false } @@ -130,9 +130,9 @@ { "description": "item is a multiple of its index", "schema": { - "items": [ {} ], + "items": [{}], "additionalItems": { - "multipleOf": { "$data": "0#" } + "multipleOf": {"$data": "0#"} } }, "tests": [ @@ -151,11 +151,11 @@ { "description": "item property is a multiple of item index", "schema": { - "items": [ {} ], + "items": [{}], "additionalItems": { "properties": { "value": { - "multipleOf": { "$data": "1#" } + "multipleOf": {"$data": "1#"} } } } diff --git a/spec/extras/$data/pattern.json b/spec/extras/$data/pattern.json index 76912485be..4b93aac7ab 100644 --- a/spec/extras/$data/pattern.json +++ b/spec/extras/$data/pattern.json @@ -4,7 +4,7 @@ "schema": { "properties": { "shouldMatch": {}, - "string": { "pattern": { "$data": "1/shouldMatch" } } + "string": {"pattern": {"$data": "1/shouldMatch"}} } }, "tests": [ @@ -53,18 +53,18 @@ "description": "property values should contain their names", "schema": { "additionalProperties": { - "pattern": { "$data": "0#" } + "pattern": {"$data": "0#"} } }, "tests": [ { "description": "valid property values", - "data": { "foo": "1foo", "bar": "bar2", "baz": "3baz4" }, + "data": {"foo": "1foo", "bar": "bar2", "baz": "3baz4"}, "valid": true }, { "description": "invalid property values", - "data": { "foo": "fo" }, + "data": {"foo": "fo"}, "valid": false } ] diff --git a/spec/extras/$data/required.json b/spec/extras/$data/required.json index f6c6200b28..0c29b1259d 100644 --- a/spec/extras/$data/required.json +++ b/spec/extras/$data/required.json @@ -7,7 +7,7 @@ "bar": {}, "requiredProperties": {} }, - "required": { "$data": "0/requiredProperties" } + "required": {"$data": "0/requiredProperties"} }, "tests": [ { diff --git a/spec/extras/$data/uniqueItems.json b/spec/extras/$data/uniqueItems.json index ab9486b3ae..ac97f8b768 100644 --- a/spec/extras/$data/uniqueItems.json +++ b/spec/extras/$data/uniqueItems.json @@ -4,7 +4,7 @@ "schema": { "properties": { "list": { - "uniqueItems": { "$data": "1/unique" } + "uniqueItems": {"$data": "1/unique"} }, "unique": {} } diff --git a/spec/extras/const.json b/spec/extras/const.json index a58429fcf2..c8ed9c629d 100644 --- a/spec/extras/const.json +++ b/spec/extras/const.json @@ -1,7 +1,7 @@ [ { "description": "const keyword requires the value to be equal to some constant", - "schema": { "const": 2 }, + "schema": {"const": 2}, "tests": [ { "description": "same value is valid", @@ -22,33 +22,33 @@ }, { "description": "const keyword requires the value to be equal to some object", - "schema": { "const": { "foo": "bar", "baz": "bax" } }, + "schema": {"const": {"foo": "bar", "baz": "bax"}}, "tests": [ { "description": "same object is valid", - "data": { "foo": "bar", "baz": "bax" }, + "data": {"foo": "bar", "baz": "bax"}, "valid": true }, { "description": "same object with different property order is valid", - "data": { "baz": "bax", "foo": "bar" }, + "data": {"baz": "bax", "foo": "bar"}, "valid": true }, { "description": "another object is invalid", - "data": { "foo": "bar" }, + "data": {"foo": "bar"}, "valid": false }, { "description": "another type is invalid", - "data": [ 1, 2 ], + "data": [1, 2], "valid": false } ] }, { "description": "const keyword with null", - "schema": { "const": null }, + "schema": {"const": null}, "tests": [ { "description": "null is valid", diff --git a/spec/extras/contains.json b/spec/extras/contains.json index 369c10aa83..cbcd1d323f 100644 --- a/spec/extras/contains.json +++ b/spec/extras/contains.json @@ -2,7 +2,7 @@ { "description": "contains keyword requires the item matching schema to be present", "schema": { - "contains": { "minimum": 5 } + "contains": {"minimum": 5} }, "tests": [ { @@ -35,7 +35,7 @@ { "description": "contains keyword with const keyword requires a specific item to be present", "schema": { - "contains": { "const": 5 } + "contains": {"const": 5} }, "tests": [ { diff --git a/spec/extras/propertyNames.json b/spec/extras/propertyNames.json index 745530f8e6..0be77ef07d 100644 --- a/spec/extras/propertyNames.json +++ b/spec/extras/propertyNames.json @@ -3,7 +3,7 @@ "description": "propertyNames validation", "schema": { "type": "object", - "propertyNames": { "format": "email" } + "propertyNames": {"format": "email"} }, "tests": [ { diff --git a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js index bc3d0d7d0c..3c00e17f04 100644 --- a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js +++ b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js @@ -1,20 +1,19 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #1001: addKeyword breaks schema without ID', function() { - it('should allow using schemas without ID with addKeyword', function() { +describe("issue #1001: addKeyword breaks schema without ID", function () { + it("should allow using schemas without ID with addKeyword", function () { var schema = { definitions: { - foo: {} - } - }; + foo: {}, + }, + } - var ajv = new Ajv(); - ajv.addSchema(schema); - ajv.addKeyword('myKeyword', {}); - ajv.getSchema('#/definitions/foo') .should.be.a('function'); - }); -}); + var ajv = new Ajv() + ajv.addSchema(schema) + ajv.addKeyword("myKeyword", {}) + ajv.getSchema("#/definitions/foo").should.be.a("function") + }) +}) diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js index aa734aa153..e79fa641b0 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js @@ -1,58 +1,57 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #181, custom keyword is not validated in allErrors mode if there were previous error', function() { - it('should validate custom keyword that doesn\'t create errors', function() { +describe("issue #181, custom keyword is not validated in allErrors mode if there were previous error", function () { + it("should validate custom keyword that doesn't create errors", function () { testCustomKeywordErrors({ - type:'object', + type: "object", errors: true, validate: function v(/* value */) { - return false; - } - }); - }); + return false + }, + }) + }) - it('should validate custom keyword that creates errors', function() { + it("should validate custom keyword that creates errors", function () { testCustomKeywordErrors({ - type:'object', + type: "object", errors: true, validate: function v(/* value */) { - v.errors = v.errors || []; + v.errors = v.errors || [] v.errors.push({ - keyword: 'alwaysFails', - message: 'alwaysFails error', + keyword: "alwaysFails", + message: "alwaysFails error", params: { - keyword: 'alwaysFails' - } - }); + keyword: "alwaysFails", + }, + }) - return false; - } - }); - }); + return false + }, + }) + }) function testCustomKeywordErrors(def) { - var ajv = new Ajv({ allErrors: true }); + var ajv = new Ajv({allErrors: true}) - ajv.addKeyword('alwaysFails', def); + ajv.addKeyword("alwaysFails", def) var schema = { - required: ['foo'], - alwaysFails: true - }; + required: ["foo"], + alwaysFails: true, + } - var validate = ajv.compile(schema); + var validate = ajv.compile(schema) - validate({ foo: 1 }) .should.equal(false); - validate.errors .should.have.length(1); - validate.errors[0].keyword .should.equal('alwaysFails'); + validate({foo: 1}).should.equal(false) + validate.errors.should.have.length(1) + validate.errors[0].keyword.should.equal("alwaysFails") - validate({}) .should.equal(false); - validate.errors .should.have.length(2); - validate.errors[0].keyword .should.equal('required'); - validate.errors[1].keyword .should.equal('alwaysFails'); + validate({}).should.equal(false) + validate.errors.should.have.length(2) + validate.errors[0].keyword.should.equal("required") + validate.errors[1].keyword.should.equal("alwaysFails") } -}); +}) diff --git a/spec/issues/182_nan_validation.spec.js b/spec/issues/182_nan_validation.spec.js index 4d341a3c95..cdb756265f 100644 --- a/spec/issues/182_nan_validation.spec.js +++ b/spec/issues/182_nan_validation.spec.js @@ -1,26 +1,25 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() +describe("issue #182, NaN validation", function () { + it("should not pass minimum/maximum validation", function () { + testNaN({minimum: 1}, false) + testNaN({maximum: 1}, false) + }) -describe('issue #182, NaN validation', function() { - it('should not pass minimum/maximum validation', function() { - testNaN({ minimum: 1 }, false); - testNaN({ maximum: 1 }, false); - }); + it("should pass type: number validation", function () { + testNaN({type: "number"}, true) + }) - it('should pass type: number validation', function() { - testNaN({ type: 'number' }, true); - }); - - it('should not pass type: integer validation', function() { - testNaN({ type: 'integer' }, false); - }); + it("should not pass type: integer validation", function () { + testNaN({type: "integer"}, false) + }) function testNaN(schema, NaNisValid) { - var ajv = new Ajv; - var validate = ajv.compile(schema); - validate(NaN) .should.equal(NaNisValid); + var ajv = new Ajv() + var validate = ajv.compile(schema) + validate(NaN).should.equal(NaNisValid) } -}); +}) diff --git a/spec/issues/204_options_schemas_data_together.spec.js b/spec/issues/204_options_schemas_data_together.spec.js index 73746c17eb..1863993343 100644 --- a/spec/issues/204_options_schemas_data_together.spec.js +++ b/spec/issues/204_options_schemas_data_together.spec.js @@ -1,23 +1,22 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #204, options schemas and $data used together', function() { - it('should use v5 metaschemas by default', function() { +describe("issue #204, options schemas and $data used together", function () { + it("should use v5 metaschemas by default", function () { var ajv = new Ajv({ - schemas: [{$id: 'str', type: 'string'}], - $data: true - }); + schemas: [{$id: "str", type: "string"}], + $data: true, + }) - var schema = { const: 42 }; - var validate = ajv.compile(schema); + var schema = {const: 42} + var validate = ajv.compile(schema) - validate(42) .should.equal(true); - validate(43) .should.equal(false); + validate(42).should.equal(true) + validate(43).should.equal(false) - ajv.validate('str', 'foo') .should.equal(true); - ajv.validate('str', 42) .should.equal(false); - }); -}); + ajv.validate("str", "foo").should.equal(true) + ajv.validate("str", 42).should.equal(false) + }) +}) diff --git a/spec/issues/210_mutual_recur_frags.spec.js b/spec/issues/210_mutual_recur_frags.spec.js index 58847f017a..4da6c4cfe7 100644 --- a/spec/issues/210_mutual_recur_frags.spec.js +++ b/spec/issues/210_mutual_recur_frags.spec.js @@ -1,79 +1,72 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #210, mutual recursive $refs that are schema fragments', function() { - it('should compile and validate schema when one ref is fragment', function() { - var ajv = new Ajv; +describe("issue #210, mutual recursive $refs that are schema fragments", function () { + it("should compile and validate schema when one ref is fragment", function () { + var ajv = new Ajv() ajv.addSchema({ - "$id" : "foo", - "definitions": { - "bar": { - "properties": { - "baz": { - "anyOf": [ - { "enum": [42] }, - { "$ref": "boo" } - ] - } - } - } - } - }); + $id: "foo", + definitions: { + bar: { + properties: { + baz: { + anyOf: [{enum: [42]}, {$ref: "boo"}], + }, + }, + }, + }, + }) ajv.addSchema({ - "$id" : "boo", - "type": "object", - "required": ["quux"], - "properties": { - "quux": { "$ref": "foo#/definitions/bar" } - } - }); + $id: "boo", + type: "object", + required: ["quux"], + properties: { + quux: {$ref: "foo#/definitions/bar"}, + }, + }) - var validate = ajv.compile({ "$ref": "foo#/definitions/bar" }); + var validate = ajv.compile({$ref: "foo#/definitions/bar"}) - validate({ baz: { quux: { baz: 42 } } }) .should.equal(true); - validate({ baz: { quux: { baz: "foo" } } }) .should.equal(false); - }); + validate({baz: {quux: {baz: 42}}}).should.equal(true) + validate({baz: {quux: {baz: "foo"}}}).should.equal(false) + }) - it('should compile and validate schema when both refs are fragments', function() { - var ajv = new Ajv; + it("should compile and validate schema when both refs are fragments", function () { + var ajv = new Ajv() ajv.addSchema({ - "$id" : "foo", - "definitions": { - "bar": { - "properties": { - "baz": { - "anyOf": [ - { "enum": [42] }, - { "$ref": "boo#/definitions/buu" } - ] - } - } - } - } - }); + $id: "foo", + definitions: { + bar: { + properties: { + baz: { + anyOf: [{enum: [42]}, {$ref: "boo#/definitions/buu"}], + }, + }, + }, + }, + }) ajv.addSchema({ - "$id" : "boo", - "definitions": { - "buu": { - "type": "object", - "required": ["quux"], - "properties": { - "quux": { "$ref": "foo#/definitions/bar" } - } - } - } - }); + $id: "boo", + definitions: { + buu: { + type: "object", + required: ["quux"], + properties: { + quux: {$ref: "foo#/definitions/bar"}, + }, + }, + }, + }) - var validate = ajv.compile({ "$ref": "foo#/definitions/bar" }); + var validate = ajv.compile({$ref: "foo#/definitions/bar"}) - validate({ baz: { quux: { baz: 42 } } }) .should.equal(true); - validate({ baz: { quux: { baz: "foo" } } }) .should.equal(false); - }); -}); + validate({baz: {quux: {baz: 42}}}).should.equal(true) + validate({baz: {quux: {baz: "foo"}}}).should.equal(false) + }) +}) diff --git a/spec/issues/240_mutual_recur_frags_common_ref.spec.js b/spec/issues/240_mutual_recur_frags_common_ref.spec.js index d9e40241dd..9bcbc076d4 100644 --- a/spec/issues/240_mutual_recur_frags_common_ref.spec.js +++ b/spec/issues/240_mutual_recur_frags_common_ref.spec.js @@ -1,210 +1,210 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #240, mutually recursive fragment refs reference a common schema', function() { +describe("issue #240, mutually recursive fragment refs reference a common schema", function () { var apiSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'schema://api.schema#', + $schema: "http://json-schema.org/draft-07/schema#", + $id: "schema://api.schema#", resource: { - $id: '#resource', + $id: "#resource", properties: { - id: { type: 'string' } - } + id: {type: "string"}, + }, }, resourceIdentifier: { - $id: '#resource_identifier', + $id: "#resource_identifier", properties: { - id: { type: 'string' }, - type: { type: 'string' } - } - } - }; + id: {type: "string"}, + type: {type: "string"}, + }, + }, + } var domainSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'schema://domain.schema#', + $schema: "http://json-schema.org/draft-07/schema#", + $id: "schema://domain.schema#", properties: { data: { oneOf: [ - { $ref: 'schema://library.schema#resource_identifier' }, - { $ref: 'schema://catalog_item.schema#resource_identifier' }, - ] - } - } - }; + {$ref: "schema://library.schema#resource_identifier"}, + {$ref: "schema://catalog_item.schema#resource_identifier"}, + ], + }, + }, + } - it('should compile and validate schema when one ref is fragment', function() { - var ajv = new Ajv; + it("should compile and validate schema when one ref is fragment", function () { + var ajv = new Ajv() var librarySchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'schema://library.schema#', + $schema: "http://json-schema.org/draft-07/schema#", + $id: "schema://library.schema#", properties: { - name: { type: 'string' }, + name: {type: "string"}, links: { properties: { catalogItems: { - type: 'array', - items: { $ref: 'schema://catalog_item_resource_identifier.schema#' } - } - } - } + type: "array", + items: { + $ref: "schema://catalog_item_resource_identifier.schema#", + }, + }, + }, + }, }, definitions: { resource_identifier: { - $id: '#resource_identifier', + $id: "#resource_identifier", allOf: [ { properties: { type: { - type: 'string', - 'enum': ['Library'] - } - } + type: "string", + enum: ["Library"], + }, + }, }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; + {$ref: "schema://api.schema#resource_identifier"}, + ], + }, + }, + } var catalogItemSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'schema://catalog_item.schema#', + $schema: "http://json-schema.org/draft-07/schema#", + $id: "schema://catalog_item.schema#", properties: { - name: { type: 'string' }, + name: {type: "string"}, links: { properties: { - library: { $ref: 'schema://library.schema#resource_identifier' } - } - } + library: {$ref: "schema://library.schema#resource_identifier"}, + }, + }, }, definitions: { resource_identifier: { - $id: '#resource_identifier', + $id: "#resource_identifier", allOf: [ { properties: { type: { - type: 'string', - 'enum': ['CatalogItem'] - } - } + type: "string", + enum: ["CatalogItem"], + }, + }, }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; + {$ref: "schema://api.schema#resource_identifier"}, + ], + }, + }, + } var catalogItemResourceIdentifierSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'schema://catalog_item_resource_identifier.schema#', + $schema: "http://json-schema.org/draft-07/schema#", + $id: "schema://catalog_item_resource_identifier.schema#", allOf: [ { properties: { type: { - type: 'string', - enum: ['CatalogItem'] - } - } + type: "string", + enum: ["CatalogItem"], + }, + }, }, { - $ref: 'schema://api.schema#resource_identifier' - } - ] - }; + $ref: "schema://api.schema#resource_identifier", + }, + ], + } - ajv.addSchema(librarySchema); - ajv.addSchema(catalogItemSchema); - ajv.addSchema(catalogItemResourceIdentifierSchema); - ajv.addSchema(apiSchema); + ajv.addSchema(librarySchema) + ajv.addSchema(catalogItemSchema) + ajv.addSchema(catalogItemResourceIdentifierSchema) + ajv.addSchema(apiSchema) - var validate = ajv.compile(domainSchema); - testSchema(validate); - }); + var validate = ajv.compile(domainSchema) + testSchema(validate) + }) - it('should compile and validate schema when both refs are fragments', function() { - var ajv = new Ajv; + it("should compile and validate schema when both refs are fragments", function () { + var ajv = new Ajv() var librarySchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'schema://library.schema#', + $schema: "http://json-schema.org/draft-07/schema#", + $id: "schema://library.schema#", properties: { - name: { type: 'string' }, + name: {type: "string"}, links: { properties: { catalogItems: { - type: 'array', - items: { $ref: 'schema://catalog_item.schema#resource_identifier' } - } - } - } + type: "array", + items: {$ref: "schema://catalog_item.schema#resource_identifier"}, + }, + }, + }, }, definitions: { resource_identifier: { - $id: '#resource_identifier', + $id: "#resource_identifier", allOf: [ { properties: { type: { - type: 'string', - 'enum': ['Library'] - } - } + type: "string", + enum: ["Library"], + }, + }, }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; + {$ref: "schema://api.schema#resource_identifier"}, + ], + }, + }, + } var catalogItemSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'schema://catalog_item.schema#', + $schema: "http://json-schema.org/draft-07/schema#", + $id: "schema://catalog_item.schema#", properties: { - name: { type: 'string' }, + name: {type: "string"}, links: { properties: { - library: { $ref: 'schema://library.schema#resource_identifier' } - } - } + library: {$ref: "schema://library.schema#resource_identifier"}, + }, + }, }, definitions: { resource_identifier: { - $id: '#resource_identifier', + $id: "#resource_identifier", allOf: [ { properties: { type: { - type: 'string', - 'enum': ['CatalogItem'] - } - } + type: "string", + enum: ["CatalogItem"], + }, + }, }, - { $ref: 'schema://api.schema#resource_identifier' } - ] - } - } - }; - - ajv.addSchema(librarySchema); - ajv.addSchema(catalogItemSchema); - ajv.addSchema(apiSchema); + {$ref: "schema://api.schema#resource_identifier"}, + ], + }, + }, + } - var validate = ajv.compile(domainSchema); - testSchema(validate); - }); + ajv.addSchema(librarySchema) + ajv.addSchema(catalogItemSchema) + ajv.addSchema(apiSchema) + var validate = ajv.compile(domainSchema) + testSchema(validate) + }) function testSchema(validate) { - validate({ data: { type: 'Library', id: '123' } }) .should.equal(true); - validate({ data: { type: 'Library', id: 123 } }) .should.equal(false); - validate({ data: { type: 'CatalogItem', id: '123' } }) .should.equal(true); - validate({ data: { type: 'CatalogItem', id: 123 } }) .should.equal(false); - validate({ data: { type: 'Foo', id: '123' } }) .should.equal(false); + validate({data: {type: "Library", id: "123"}}).should.equal(true) + validate({data: {type: "Library", id: 123}}).should.equal(false) + validate({data: {type: "CatalogItem", id: "123"}}).should.equal(true) + validate({data: {type: "CatalogItem", id: 123}}).should.equal(false) + validate({data: {type: "Foo", id: "123"}}).should.equal(false) } -}); +}) diff --git a/spec/issues/259_validate_meta_against_itself.spec.js b/spec/issues/259_validate_meta_against_itself.spec.js index d99fc21efd..ff9cffd6a6 100644 --- a/spec/issues/259_validate_meta_against_itself.spec.js +++ b/spec/issues/259_validate_meta_against_itself.spec.js @@ -1,13 +1,12 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #259, support validating [meta-]schemas against themselves', function() { - it('should add schema before validation if "id" is the same as "$schema"', function() { - var ajv = new Ajv; - var hyperSchema = require('../remotes/hyper-schema.json'); - ajv.addMetaSchema(hyperSchema); - }); -}); +describe("issue #259, support validating [meta-]schemas against themselves", function () { + it('should add schema before validation if "id" is the same as "$schema"', function () { + var ajv = new Ajv() + var hyperSchema = require("../remotes/hyper-schema.json") + ajv.addMetaSchema(hyperSchema) + }) +}) diff --git a/spec/issues/273_error_schemaPath_refd_schema.spec.js b/spec/issues/273_error_schemaPath_refd_schema.spec.js index 9271640872..f8242643c2 100644 --- a/spec/issues/273_error_schemaPath_refd_schema.spec.js +++ b/spec/issues/273_error_schemaPath_refd_schema.spec.js @@ -1,31 +1,30 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe.skip('issue #273, schemaPath in error in referenced schema', function() { - it('should have canonic reference with hash after file name', function() { - test(new Ajv); - test(new Ajv({inlineRefs: false})); +describe.skip("issue #273, schemaPath in error in referenced schema", function () { + it("should have canonic reference with hash after file name", function () { + test(new Ajv()) + test(new Ajv({inlineRefs: false})) function test(ajv) { var schema = { - "properties": { - "a": { "$ref": "int" } - } - }; + properties: { + a: {$ref: "int"}, + }, + } var referencedSchema = { - "id": "int", - "type": "integer" - }; + id: "int", + type: "integer", + } - ajv.addSchema(referencedSchema); - var validate = ajv.compile(schema); + ajv.addSchema(referencedSchema) + var validate = ajv.compile(schema) - validate({ "a": "foo" }) .should.equal(false); - validate.errors[0].schemaPath .should.equal('int#/type'); + validate({a: "foo"}).should.equal(false) + validate.errors[0].schemaPath.should.equal("int#/type") } - }); -}); + }) +}) diff --git a/spec/issues/342_uniqueItems_non-json_objects.spec.js b/spec/issues/342_uniqueItems_non-json_objects.spec.js index ae7eee0cf1..81ca66bad5 100644 --- a/spec/issues/342_uniqueItems_non-json_objects.spec.js +++ b/spec/issues/342_uniqueItems_non-json_objects.spec.js @@ -1,33 +1,36 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() +describe("issue #342, support uniqueItems with some non-JSON objects", function () { + var validate -describe('issue #342, support uniqueItems with some non-JSON objects', function() { - var validate; + before(function () { + var ajv = new Ajv() + validate = ajv.compile({uniqueItems: true}) + }) - before(function() { - var ajv = new Ajv; - validate = ajv.compile({ uniqueItems: true }); - }); + it("should allow different RegExps", function () { + validate([/foo/, /bar/]).should.equal(true) + validate([/foo/gi, /foo/gi]).should.equal(false) + validate([/foo/, {}]).should.equal(true) + }) - it('should allow different RegExps', function() { - validate([/foo/, /bar/]) .should.equal(true); - validate([/foo/ig, /foo/gi]) .should.equal(false); - validate([/foo/, {}]) .should.equal(true); - }); + it("should allow different Dates", function () { + validate([new Date("2016-11-11"), new Date("2016-11-12")]).should.equal( + true + ) + validate([new Date("2016-11-11"), new Date("2016-11-11")]).should.equal( + false + ) + validate([new Date("2016-11-11"), {}]).should.equal(true) + }) - it('should allow different Dates', function() { - validate([new Date('2016-11-11'), new Date('2016-11-12')]) .should.equal(true); - validate([new Date('2016-11-11'), new Date('2016-11-11')]) .should.equal(false); - validate([new Date('2016-11-11'), {}]) .should.equal(true); - }); - - it('should allow undefined properties', function() { - validate([{}, {foo: undefined}]) .should.equal(true); - validate([{foo: undefined}, {}]) .should.equal(true); - validate([{foo: undefined}, {bar: undefined}]) .should.equal(true); - validate([{foo: undefined}, {foo: undefined}]) .should.equal(false); - }); -}); + it("should allow undefined properties", function () { + validate([{}, {foo: undefined}]).should.equal(true) + validate([{foo: undefined}, {}]).should.equal(true) + validate([{foo: undefined}, {bar: undefined}]).should.equal(true) + validate([{foo: undefined}, {foo: undefined}]).should.equal(false) + }) +}) diff --git a/spec/issues/485_type_validation_priority.spec.js b/spec/issues/485_type_validation_priority.spec.js index eb2b1e9df3..a05b28c151 100644 --- a/spec/issues/485_type_validation_priority.spec.js +++ b/spec/issues/485_type_validation_priority.spec.js @@ -1,32 +1,31 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #485, order of type validation', function() { - it('should validate types before keywords', function() { - var ajv = new Ajv({allErrors: true}); +describe("issue #485, order of type validation", function () { + it("should validate types before keywords", function () { + var ajv = new Ajv({allErrors: true}) var validate = ajv.compile({ - type: ['integer', 'string'], - required: ['foo'], - minimum: 2 - }); + type: ["integer", "string"], + required: ["foo"], + minimum: 2, + }) - validate(2) .should.equal(true); - validate('foo') .should.equal(true); + validate(2).should.equal(true) + validate("foo").should.equal(true) - validate(1.5) .should.equal(false); - checkErrors(['type', 'minimum']); + validate(1.5).should.equal(false) + checkErrors(["type", "minimum"]) - validate({}) .should.equal(false); - checkErrors(['type', 'required']); + validate({}).should.equal(false) + checkErrors(["type", "required"]) function checkErrors(expectedErrs) { - validate.errors .should.have.length(expectedErrs.length); + validate.errors.should.have.length(expectedErrs.length) expectedErrs.forEach(function (keyword, i) { - validate.errors[i].keyword .should.equal(keyword); - }); + validate.errors[i].keyword.should.equal(keyword) + }) } - }); -}); + }) +}) diff --git a/spec/issues/50_refs_with_definitions.spec.js b/spec/issues/50_refs_with_definitions.spec.js index 26b84a8cc2..4750d9101b 100644 --- a/spec/issues/50_refs_with_definitions.spec.js +++ b/spec/issues/50_refs_with_definitions.spec.js @@ -1,49 +1,48 @@ -'use strict'; - -var Ajv = require('../ajv'); -var should = require('../chai').should(); +"use strict" +var Ajv = require("../ajv") +var should = require("../chai").should() describe('issue #50: references with "definitions"', function () { - it('should be supported by addSchema', spec('addSchema')); + it("should be supported by addSchema", spec("addSchema")) - it('should be supported by compile', spec('addSchema')); + it("should be supported by compile", spec("addSchema")) function spec(method) { - return function() { - var result; + return function () { + var result - var ajv = new Ajv; + var ajv = new Ajv() ajv[method]({ - $id: 'http://example.com/test/person.json#', + $id: "http://example.com/test/person.json#", definitions: { - name: { type: 'string' } + name: {type: "string"}, }, - type: 'object', + type: "object", properties: { - name: { $ref: '#/definitions/name'} - } - }); + name: {$ref: "#/definitions/name"}, + }, + }) ajv[method]({ - $id: 'http://example.com/test/employee.json#', - type: 'object', + $id: "http://example.com/test/employee.json#", + type: "object", properties: { - person: { $ref: '/test/person.json#' }, - role: { type: 'string' } - } - }); + person: {$ref: "/test/person.json#"}, + role: {type: "string"}, + }, + }) - result = ajv.validate('http://example.com/test/employee.json#', { + result = ajv.validate("http://example.com/test/employee.json#", { person: { - name: 'Alice' + name: "Alice", }, - role: 'Programmer' - }); + role: "Programmer", + }) - result. should.equal(true); - should.equal(ajv.errors, null); - }; + result.should.equal(true) + should.equal(ajv.errors, null) + } } -}); +}) diff --git a/spec/issues/521_wrong_warning_id_property.spec.js b/spec/issues/521_wrong_warning_id_property.spec.js index 79fbbce7ed..308ebe662b 100644 --- a/spec/issues/521_wrong_warning_id_property.spec.js +++ b/spec/issues/521_wrong_warning_id_property.spec.js @@ -1,28 +1,27 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #521, incorrect warning with "id" property', function() { - it('should not log warning', function() { - var ajv = new Ajv({schemaId: '$id'}); - var consoleWarn = console.warn; - console.warn = function() { - throw new Error('should not log warning'); - }; +describe('issue #521, incorrect warning with "id" property', function () { + it("should not log warning", function () { + var ajv = new Ajv({schemaId: "$id"}) + var consoleWarn = console.warn + console.warn = function () { + throw new Error("should not log warning") + } try { ajv.compile({ - "$id": "http://example.com/schema.json", - "type": "object", - "properties": { - "id": {"type": "string"}, + $id: "http://example.com/schema.json", + type: "object", + properties: { + id: {type: "string"}, }, - "required": [ "id"] - }); + required: ["id"], + }) } finally { - console.warn = consoleWarn; + console.warn = consoleWarn } - }); -}); + }) +}) diff --git a/spec/issues/533_missing_ref_error_when_ignore.spec.js b/spec/issues/533_missing_ref_error_when_ignore.spec.js index 4e77d5fc65..933544f574 100644 --- a/spec/issues/533_missing_ref_error_when_ignore.spec.js +++ b/spec/issues/533_missing_ref_error_when_ignore.spec.js @@ -1,29 +1,28 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', function() { +describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', function () { var schema = { - "type": "object", - "properties": { - "foo": {"$ref": "#/definitions/missing"}, - "bar": {"$ref": "#/definitions/missing"} - } - }; + type: "object", + properties: { + foo: {$ref: "#/definitions/missing"}, + bar: {$ref: "#/definitions/missing"}, + }, + } - it('should pass validation without throwing exception', function() { - var ajv = new Ajv({missingRefs: 'ignore'}); - var validate = ajv.compile(schema); - validate({foo: 'anything'}) .should.equal(true); - validate({foo: 'anything', bar: 'whatever'}) .should.equal(true); - }); + it("should pass validation without throwing exception", function () { + var ajv = new Ajv({missingRefs: "ignore"}) + var validate = ajv.compile(schema) + validate({foo: "anything"}).should.equal(true) + validate({foo: "anything", bar: "whatever"}).should.equal(true) + }) - it('should throw exception during schema compilation with option missingRefs: true', function() { - var ajv = new Ajv; - should.throw(function() { - ajv.compile(schema); - }); - }); -}); + it("should throw exception during schema compilation with option missingRefs: true", function () { + var ajv = new Ajv() + should.throw(function () { + ajv.compile(schema) + }) + }) +}) diff --git a/spec/issues/617_full_format_leap_year.spec.js b/spec/issues/617_full_format_leap_year.spec.js index 995c4ba13c..71c375fdfb 100644 --- a/spec/issues/617_full_format_leap_year.spec.js +++ b/spec/issues/617_full_format_leap_year.spec.js @@ -1,47 +1,46 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() +describe("PR #617, full date format validation should understand leap years", function () { + it("should handle non leap year affected dates with date-time", function () { + var ajv = new Ajv({format: "full"}) -describe('PR #617, full date format validation should understand leap years', function () { - it('should handle non leap year affected dates with date-time', function() { - var ajv = new Ajv({ format: 'full' }); + var schema = {format: "date-time"} + var validDateTime = "2016-01-31T00:00:00Z" - var schema = { format: 'date-time' }; - var validDateTime = '2016-01-31T00:00:00Z'; + ajv.validate(schema, validDateTime).should.equal(true) + }) - ajv.validate(schema, validDateTime).should.equal(true); - }); + it("should handle non leap year affected dates with date", function () { + var ajv = new Ajv({format: "full"}) - it('should handle non leap year affected dates with date', function () { - var ajv = new Ajv({ format: 'full' }); + var schema = {format: "date"} + var validDate = "2016-11-30" - var schema = { format: 'date' }; - var validDate = '2016-11-30'; + ajv.validate(schema, validDate).should.equal(true) + }) - ajv.validate(schema, validDate).should.equal(true); - }); + it("should handle year leaps as date-time", function () { + var ajv = new Ajv({format: "full"}) - it('should handle year leaps as date-time', function() { - var ajv = new Ajv({ format: 'full' }); + var schema = {format: "date-time"} + var validDateTime = "2016-02-29T00:00:00Z" + var invalidDateTime = "2017-02-29T00:00:00Z" - var schema = { format: 'date-time' }; - var validDateTime = '2016-02-29T00:00:00Z'; - var invalidDateTime = '2017-02-29T00:00:00Z'; + ajv.validate(schema, validDateTime).should.equal(true) + ajv.validate(schema, invalidDateTime).should.equal(false) + }) - ajv.validate(schema, validDateTime) .should.equal(true); - ajv.validate(schema, invalidDateTime) .should.equal(false); - }); + it("should handle year leaps as date", function () { + var ajv = new Ajv({format: "full"}) - it('should handle year leaps as date', function() { - var ajv = new Ajv({ format: 'full' }); + var schema = {format: "date"} + var validDate = "2016-02-29" + var invalidDate = "2017-02-29" - var schema = { format: 'date' }; - var validDate = '2016-02-29'; - var invalidDate = '2017-02-29'; - - ajv.validate(schema, validDate) .should.equal(true); - ajv.validate(schema, invalidDate) .should.equal(false); - }); -}); + ajv.validate(schema, validDate).should.equal(true) + ajv.validate(schema, invalidDate).should.equal(false) + }) +}) diff --git a/spec/issues/743_removeAdditional_to_remove_proto.spec.js b/spec/issues/743_removeAdditional_to_remove_proto.spec.js index f5a01926de..6f8b5c0385 100644 --- a/spec/issues/743_removeAdditional_to_remove_proto.spec.js +++ b/spec/issues/743_removeAdditional_to_remove_proto.spec.js @@ -1,41 +1,40 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('issue #743, property __proto__ should be removed with removeAdditional option', function() { - it('should remove additional properties', function() { - var ajv = new Ajv({removeAdditional: true}); +describe("issue #743, property __proto__ should be removed with removeAdditional option", function () { + it("should remove additional properties", function () { + var ajv = new Ajv({removeAdditional: true}) var schema = { properties: { obj: { additionalProperties: false, properties: { - a: { type: 'string' }, - b: { type: 'string' }, - c: { type: 'string' }, - d: { type: 'string' }, - e: { type: 'string' }, - f: { type: 'string' }, - g: { type: 'string' }, - h: { type: 'string' }, - i: { type: 'string' } - } - } - } - }; + a: {type: "string"}, + b: {type: "string"}, + c: {type: "string"}, + d: {type: "string"}, + e: {type: "string"}, + f: {type: "string"}, + g: {type: "string"}, + h: {type: "string"}, + i: {type: "string"}, + }, + }, + }, + } - var obj= Object.create(null); - obj.__proto__ = null; // should be removed - obj.additional = 'will be removed'; - obj.a = 'valid'; - obj.b = 'valid'; + var obj = Object.create(null) + obj.__proto__ = null // should be removed + obj.additional = "will be removed" + obj.a = "valid" + obj.b = "valid" - var data = {obj: obj}; + var data = {obj: obj} - ajv.validate(schema, data) .should.equal(true); - Object.keys(data.obj) .should.eql(['a', 'b']); - }); -}); + ajv.validate(schema, data).should.equal(true) + Object.keys(data.obj).should.eql(["a", "b"]) + }) +}) diff --git a/spec/issues/768_passContext_recursive_ref.spec.js b/spec/issues/768_passContext_recursive_ref.spec.js index 3410f74626..c820f34581 100644 --- a/spec/issues/768_passContext_recursive_ref.spec.js +++ b/spec/issues/768_passContext_recursive_ref.spec.js @@ -1,114 +1,112 @@ -'use strict'; - -var Ajv = require('../ajv'); -require('../chai').should(); - - -describe('issue #768, fix passContext in recursive $ref', function() { - var ajv, contexts; - - beforeEach(function() { - contexts = []; - }); - - describe('passContext = true', function() { - it('should pass this value as context to custom keyword validation function', function() { - var validate = getValidate(true); - var self = {}; - validate.call(self, { bar: 'a', baz: { bar: 'b' } }); - contexts .should.have.length(2); - contexts.forEach(function(ctx) { - ctx .should.equal(self); - }); - }); - }); - - describe('passContext = false', function() { - it('should pass ajv instance as context to custom keyword validation function', function() { - var validate = getValidate(false); - validate({ bar: 'a', baz: { bar: 'b' } }); - contexts .should.have.length(2); - contexts.forEach(function(ctx) { - ctx .should.equal(ajv); - }); - }); - }); - - describe('ref is fragment and passContext = true', function() { - it('should pass this value as context to custom keyword validation function', function() { - var validate = getValidateFragments(true); - var self = {}; - validate.call(self, { baz: { corge: 'a', quux: { baz: { corge: 'b' } } } }); - contexts .should.have.length(2); - contexts.forEach(function(ctx) { - ctx .should.equal(self); - }); - }); - }); - - describe('ref is fragment and passContext = false', function() { - it('should pass ajv instance as context to custom keyword validation function', function() { - var validate = getValidateFragments(false); - validate({ baz: { corge: 'a', quux: { baz: { corge: 'b' } } } }); - contexts .should.have.length(2); - contexts.forEach(function(ctx) { - ctx .should.equal(ajv); - }); - }); - }); +"use strict" + +var Ajv = require("../ajv") +require("../chai").should() + +describe("issue #768, fix passContext in recursive $ref", function () { + var ajv, contexts + + beforeEach(function () { + contexts = [] + }) + + describe("passContext = true", function () { + it("should pass this value as context to custom keyword validation function", function () { + var validate = getValidate(true) + var self = {} + validate.call(self, {bar: "a", baz: {bar: "b"}}) + contexts.should.have.length(2) + contexts.forEach(function (ctx) { + ctx.should.equal(self) + }) + }) + }) + + describe("passContext = false", function () { + it("should pass ajv instance as context to custom keyword validation function", function () { + var validate = getValidate(false) + validate({bar: "a", baz: {bar: "b"}}) + contexts.should.have.length(2) + contexts.forEach(function (ctx) { + ctx.should.equal(ajv) + }) + }) + }) + + describe("ref is fragment and passContext = true", function () { + it("should pass this value as context to custom keyword validation function", function () { + var validate = getValidateFragments(true) + var self = {} + validate.call(self, {baz: {corge: "a", quux: {baz: {corge: "b"}}}}) + contexts.should.have.length(2) + contexts.forEach(function (ctx) { + ctx.should.equal(self) + }) + }) + }) + + describe("ref is fragment and passContext = false", function () { + it("should pass ajv instance as context to custom keyword validation function", function () { + var validate = getValidateFragments(false) + validate({baz: {corge: "a", quux: {baz: {corge: "b"}}}}) + contexts.should.have.length(2) + contexts.forEach(function (ctx) { + ctx.should.equal(ajv) + }) + }) + }) function getValidate(passContext) { - ajv = new Ajv({ passContext: passContext }); - ajv.addKeyword('testValidate', { validate: storeContext }); + ajv = new Ajv({passContext: passContext}) + ajv.addKeyword("testValidate", {validate: storeContext}) var schema = { - "$id" : "foo", - "type": "object", - "required": ["bar"], - "properties": { - "bar": { "testValidate": true }, - "baz": { - "$ref": "foo" - } - } - }; - - return ajv.compile(schema); + $id: "foo", + type: "object", + required: ["bar"], + properties: { + bar: {testValidate: true}, + baz: { + $ref: "foo", + }, + }, + } + + return ajv.compile(schema) } - function getValidateFragments(passContext) { - ajv = new Ajv({ passContext: passContext }); - ajv.addKeyword('testValidate', { validate: storeContext }); + ajv = new Ajv({passContext: passContext}) + ajv.addKeyword("testValidate", {validate: storeContext}) ajv.addSchema({ - "$id" : "foo", - "definitions": { - "bar": { - "properties": { - "baz": { - "$ref": "boo" - } - } - } - } - }); + $id: "foo", + definitions: { + bar: { + properties: { + baz: { + $ref: "boo", + }, + }, + }, + }, + }) ajv.addSchema({ - "$id" : "boo", - "type": "object", - "required": ["corge"], - "properties": { - "quux": { "$ref": "foo#/definitions/bar" }, - "corge": { "testValidate": true } - } - }); - - return ajv.compile({ "$ref": "foo#/definitions/bar" }); + $id: "boo", + type: "object", + required: ["corge"], + properties: { + quux: {$ref: "foo#/definitions/bar"}, + corge: {testValidate: true}, + }, + }) + + return ajv.compile({$ref: "foo#/definitions/bar"}) } function storeContext() { - contexts.push(this); - return true; + contexts.push(this) + return true } -}); +}) diff --git a/spec/issues/8_shared_refs.spec.js b/spec/issues/8_shared_refs.spec.js index 2208f452e6..9b13c31057 100644 --- a/spec/issues/8_shared_refs.spec.js +++ b/spec/issues/8_shared_refs.spec.js @@ -1,40 +1,39 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() +describe("issue #8: schema with shared references", function () { + it("should be supported by addSchema", spec("addSchema")) -describe('issue #8: schema with shared references', function() { - it('should be supported by addSchema', spec('addSchema')); - - it('should be supported by compile', spec('compile')); + it("should be supported by compile", spec("compile")) function spec(method) { - return function() { - var ajv = new Ajv; + return function () { + var ajv = new Ajv() var propertySchema = { - type: 'string', - maxLength: 4 - }; + type: "string", + maxLength: 4, + } var schema = { - $id: 'obj.json#', - type: 'object', + $id: "obj.json#", + type: "object", properties: { foo: propertySchema, - bar: propertySchema - } - }; + bar: propertySchema, + }, + } - ajv[method](schema); + ajv[method](schema) - var result = ajv.validate('obj.json#', { foo: 'abc', bar: 'def' }); - result .should.equal(true); + var result = ajv.validate("obj.json#", {foo: "abc", bar: "def"}) + result.should.equal(true) - result = ajv.validate('obj.json#', { foo: 'abcde', bar: 'fghg' }); - result .should.equal(false); - ajv.errors .should.have.length(1); - }; + result = ajv.validate("obj.json#", {foo: "abcde", bar: "fghg"}) + result.should.equal(false) + ajv.errors.should.have.length(1) + } } -}); +}) diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js index 4ef949d608..7735a590f7 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -1,48 +1,47 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() +describe("issue #955: option removeAdditional breaks custom keywords", function () { + it("should support custom keywords with option removeAdditional", function () { + var ajv = new Ajv({removeAdditional: "all"}) -describe('issue #955: option removeAdditional breaks custom keywords', function() { - it('should support custom keywords with option removeAdditional', function() { - var ajv = new Ajv({removeAdditional: 'all'}); - - ajv.addKeyword('minTrimmedLength', { - type: 'string', - compile: function(schema) { - return function(str) { - return str.trim().length >= schema; - }; + ajv.addKeyword("minTrimmedLength", { + type: "string", + compile: function (schema) { + return function (str) { + return str.trim().length >= schema + } }, - metaSchema: {type: 'integer'} - }); + metaSchema: {type: "integer"}, + }) var schema = { - type: 'object', + type: "object", properties: { foo: { - type: 'string', - minTrimmedLength: 3 - } + type: "string", + minTrimmedLength: 3, + }, }, - required: ['foo'] - }; + required: ["foo"], + } - var validate = ajv.compile(schema); + var validate = ajv.compile(schema) var data = { - foo: ' bar ', - baz: '' - }; - validate(data) .should.equal(true); - data .should.not.have.property('baz'); + foo: " bar ", + baz: "", + } + validate(data).should.equal(true) + data.should.not.have.property("baz") data = { - foo: ' ba ', - baz: '' - }; - validate(data) .should.equal(false); - data .should.not.have.property('baz'); - }); -}); + foo: " ba ", + baz: "", + } + validate(data).should.equal(false) + data.should.not.have.property("baz") + }) +}) diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 48bce96188..0cb64741a0 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -1,68 +1,95 @@ -'use strict'; +"use strict" -var jsonSchemaTest = require('json-schema-test') - , getAjvInstances = require('./ajv_instances') - , options = require('./ajv_options') - , suite = require('./browser_test_suite') - , after = require('./after_test'); +var jsonSchemaTest = require("json-schema-test"), + getAjvInstances = require("./ajv_instances"), + options = require("./ajv_options"), + suite = require("./browser_test_suite"), + after = require("./after_test") var remoteRefs = { - 'http://localhost:1234/integer.json': require('./JSON-Schema-Test-Suite/remotes/integer.json'), - 'http://localhost:1234/subSchemas.json': require('./JSON-Schema-Test-Suite/remotes/subSchemas.json'), - 'http://localhost:1234/folder/folderInteger.json': require('./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json'), - 'http://localhost:1234/name.json': require('./JSON-Schema-Test-Suite/remotes/name.json') -}; + "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), + "http://localhost:1234/subSchemas.json": require("./JSON-Schema-Test-Suite/remotes/subSchemas.json"), + "http://localhost:1234/folder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json"), + "http://localhost:1234/name.json": require("./JSON-Schema-Test-Suite/remotes/name.json"), +} var SKIP = { - 4: ['optional/zeroTerminatedFloats'], + 4: ["optional/zeroTerminatedFloats"], 7: [ - 'optional/content', - 'format/idn-email', - 'format/idn-hostname', - 'format/iri', - 'format/iri-reference' - ] -}; - - -runTest(getAjvInstances(options, {meta: false, schemaId: 'id'}), 4, typeof window == 'object' - ? suite(require('./JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json', {mode: 'list'})) - : './JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json'); + "optional/content", + "format/idn-email", + "format/idn-hostname", + "format/iri", + "format/iri-reference", + ], +} -runTest(getAjvInstances(options, {meta: false}), 6, typeof window == 'object' - ? suite(require('./JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json', {mode: 'list'})) - : './JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json'); +runTest( + getAjvInstances(options, {meta: false, schemaId: "id"}), + 4, + typeof window == "object" + ? suite( + require("./JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json", { + mode: "list", + }) + ) + : "./JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json" +) -runTest(getAjvInstances(options), 7, typeof window == 'object' - ? suite(require('./JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json', {mode: 'list'})) - : './JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json'); +runTest( + getAjvInstances(options, {meta: false}), + 6, + typeof window == "object" + ? suite( + require("./JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json", { + mode: "list", + }) + ) + : "./JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json" +) +runTest( + getAjvInstances(options), + 7, + typeof window == "object" + ? suite( + require("./JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json", { + mode: "list", + }) + ) + : "./JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json" +) function runTest(instances, draft, tests) { instances.forEach(function (ajv) { switch (draft) { case 4: - ajv.addMetaSchema(require('../lib/refs/json-schema-draft-04.json')); - ajv._opts.defaultMeta = 'http://json-schema.org/draft-04/schema#'; - break; + ajv.addMetaSchema(require("../lib/refs/json-schema-draft-04.json")) + ajv._opts.defaultMeta = "http://json-schema.org/draft-04/schema#" + break case 6: - ajv.addMetaSchema(require('../lib/refs/json-schema-draft-06.json')); - ajv._opts.defaultMeta = 'http://json-schema.org/draft-06/schema#'; - break; + ajv.addMetaSchema(require("../lib/refs/json-schema-draft-06.json")) + ajv._opts.defaultMeta = "http://json-schema.org/draft-06/schema#" + break } - for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id); - }); + for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id) + }) jsonSchemaTest(instances, { - description: 'JSON-Schema Test Suite draft-0' + draft + ': ' + instances.length + ' ajv instances with different options', + description: + "JSON-Schema Test Suite draft-0" + + draft + + ": " + + instances.length + + " ajv instances with different options", suites: {tests: tests}, only: [], skip: SKIP[draft], - assert: require('./chai').assert, + assert: require("./chai").assert, afterError: after.error, afterEach: after.each, cwd: __dirname, - hideFolder: 'draft' + draft + '/', - timeout: 120000 - }); + hideFolder: "draft" + draft + "/", + timeout: 120000, + }) } diff --git a/spec/options/comment.spec.js b/spec/options/comment.spec.js index efdc98b5ef..154626c64e 100644 --- a/spec/options/comment.spec.js +++ b/spec/options/comment.spec.js @@ -1,92 +1,95 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('$comment option', function() { - describe('= true', function() { - var logCalls, consoleLog; +describe("$comment option", function () { + describe("= true", function () { + var logCalls, consoleLog beforeEach(function () { - consoleLog = console.log; - console.log = log; - }); + consoleLog = console.log + console.log = log + }) afterEach(function () { - console.log = consoleLog; - }); + console.log = consoleLog + }) function log() { - logCalls.push(Array.prototype.slice.call(arguments)); + logCalls.push(Array.prototype.slice.call(arguments)) } - it('should log the text from $comment keyword', function() { + it("should log the text from $comment keyword", function () { var schema = { properties: { - foo: {$comment: 'property foo'}, - bar: {$comment: 'property bar', type: 'integer'} - } - }; + foo: {$comment: "property foo"}, + bar: {$comment: "property bar", type: "integer"}, + }, + } - var ajv = new Ajv({$comment: true}); - var fullAjv = new Ajv({allErrors: true, $comment: true}); + var ajv = new Ajv({$comment: true}) + var fullAjv = new Ajv({allErrors: true, $comment: true}) - [ajv, fullAjv].forEach(function (_ajv) { - var validate = _ajv.compile(schema); + ;[ajv, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema) - test({}, true, []); - test({foo: 1}, true, [['property foo']]); - test({foo: 1, bar: 2}, true, [['property foo'], ['property bar']]); - test({foo: 1, bar: 'baz'}, false, [['property foo'], ['property bar']]); + test({}, true, []) + test({foo: 1}, true, [["property foo"]]) + test({foo: 1, bar: 2}, true, [["property foo"], ["property bar"]]) + test({foo: 1, bar: "baz"}, false, [["property foo"], ["property bar"]]) function test(data, valid, expectedLogCalls) { - logCalls = []; - validate(data) .should.equal(valid); - logCalls .should.eql(expectedLogCalls); + logCalls = [] + validate(data).should.equal(valid) + logCalls.should.eql(expectedLogCalls) } - }); + }) - console.log = consoleLog; - }); - }); + console.log = consoleLog + }) + }) - describe('function hook', function() { - var hookCalls; + describe("function hook", function () { + var hookCalls function hook() { - hookCalls.push(Array.prototype.slice.call(arguments)); + hookCalls.push(Array.prototype.slice.call(arguments)) } - it('should pass the text from $comment keyword to the hook', function() { + it("should pass the text from $comment keyword to the hook", function () { var schema = { properties: { - foo: {$comment: 'property foo'}, - bar: {$comment: 'property bar', type: 'integer'} - } - }; - - var ajv = new Ajv({$comment: hook}); - var fullAjv = new Ajv({allErrors: true, $comment: hook}); - - [ajv, fullAjv].forEach(function (_ajv) { - var validate = _ajv.compile(schema); - - test({}, true, []); - test({foo: 1}, true, [['property foo', '#/properties/foo/$comment', schema]]); - test({foo: 1, bar: 2}, true, - [['property foo', '#/properties/foo/$comment', schema], - ['property bar', '#/properties/bar/$comment', schema]]); - test({foo: 1, bar: 'baz'}, false, - [['property foo', '#/properties/foo/$comment', schema], - ['property bar', '#/properties/bar/$comment', schema]]); + foo: {$comment: "property foo"}, + bar: {$comment: "property bar", type: "integer"}, + }, + } + + var ajv = new Ajv({$comment: hook}) + var fullAjv = new Ajv({allErrors: true, $comment: hook}) + + ;[ajv, fullAjv].forEach(function (_ajv) { + var validate = _ajv.compile(schema) + + test({}, true, []) + test({foo: 1}, true, [ + ["property foo", "#/properties/foo/$comment", schema], + ]) + test({foo: 1, bar: 2}, true, [ + ["property foo", "#/properties/foo/$comment", schema], + ["property bar", "#/properties/bar/$comment", schema], + ]) + test({foo: 1, bar: "baz"}, false, [ + ["property foo", "#/properties/foo/$comment", schema], + ["property bar", "#/properties/bar/$comment", schema], + ]) function test(data, valid, expectedHookCalls) { - hookCalls = []; - validate(data) .should.equal(valid); - hookCalls .should.eql(expectedHookCalls); + hookCalls = [] + validate(data).should.equal(valid) + hookCalls.should.eql(expectedHookCalls) } - }); - }); - }); -}); + }) + }) + }) +}) diff --git a/spec/options/meta_validateSchema.spec.js b/spec/options/meta_validateSchema.spec.js index 2e287da75f..8229c67288 100644 --- a/spec/options/meta_validateSchema.spec.js +++ b/spec/options/meta_validateSchema.spec.js @@ -1,79 +1,101 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('meta and validateSchema options', function() { - it('should add draft-7 meta schema by default', function() { - testOptionMeta(new Ajv); - testOptionMeta(new Ajv({ meta: true })); +describe("meta and validateSchema options", function () { + it("should add draft-7 meta schema by default", function () { + testOptionMeta(new Ajv()) + testOptionMeta(new Ajv({meta: true})) function testOptionMeta(ajv) { - ajv.getSchema('http://json-schema.org/draft-07/schema') .should.be.a('function'); - ajv.validateSchema({ type: 'integer' }) .should.equal(true); - ajv.validateSchema({ type: 123 }) .should.equal(false); - should.not.throw(function() { ajv.addSchema({ type: 'integer' }); }); - should.throw(function() { ajv.addSchema({ type: 123 }); }); + ajv + .getSchema("http://json-schema.org/draft-07/schema") + .should.be.a("function") + ajv.validateSchema({type: "integer"}).should.equal(true) + ajv.validateSchema({type: 123}).should.equal(false) + should.not.throw(function () { + ajv.addSchema({type: "integer"}) + }) + should.throw(function () { + ajv.addSchema({type: 123}) + }) } - }); + }) - it('should throw if meta: false and validateSchema: true', function() { - var ajv = new Ajv({ meta: false }); - should.not.exist(ajv.getSchema('http://json-schema.org/draft-07/schema')); - should.not.throw(function() { ajv.addSchema({ type: 'wrong_type' }, 'integer'); }); - }); + it("should throw if meta: false and validateSchema: true", function () { + var ajv = new Ajv({meta: false}) + should.not.exist(ajv.getSchema("http://json-schema.org/draft-07/schema")) + should.not.throw(function () { + ajv.addSchema({type: "wrong_type"}, "integer") + }) + }) - it('should skip schema validation with validateSchema: false', function() { - var ajv = new Ajv; - should.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); + it("should skip schema validation with validateSchema: false", function () { + var ajv = new Ajv() + should.throw(function () { + ajv.addSchema({type: 123}, "integer") + }) - ajv = new Ajv({ validateSchema: false }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); + ajv = new Ajv({validateSchema: false}) + should.not.throw(function () { + ajv.addSchema({type: 123}, "integer") + }) - ajv = new Ajv({ validateSchema: false, meta: false }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - }); + ajv = new Ajv({validateSchema: false, meta: false}) + should.not.throw(function () { + ajv.addSchema({type: 123}, "integer") + }) + }) - it('should not throw on invalid schema with validateSchema: "log"', function() { - var logError = console.error; - var loggedError = false; - console.error = function() { loggedError = true; logError.apply(console, arguments); }; + it('should not throw on invalid schema with validateSchema: "log"', function () { + var logError = console.error + var loggedError = false + console.error = function () { + loggedError = true + logError.apply(console, arguments) + } - var ajv = new Ajv({ validateSchema: 'log' }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - loggedError .should.equal(true); + var ajv = new Ajv({validateSchema: "log"}) + should.not.throw(function () { + ajv.addSchema({type: 123}, "integer") + }) + loggedError.should.equal(true) - loggedError = false; - ajv = new Ajv({ validateSchema: 'log', meta: false }); - should.not.throw(function() { ajv.addSchema({ type: 123 }, 'integer'); }); - loggedError .should.equal(false); - console.error = logError; - }); + loggedError = false + ajv = new Ajv({validateSchema: "log", meta: false}) + should.not.throw(function () { + ajv.addSchema({type: 123}, "integer") + }) + loggedError.should.equal(false) + console.error = logError + }) - it('should validate v6 schema', function() { - var ajv = new Ajv; - ajv.validateSchema({ contains: { minimum: 2 } }) .should.equal(true); - ajv.validateSchema({ contains: 2 }). should.equal(false); - }); + it("should validate v6 schema", function () { + var ajv = new Ajv() + ajv.validateSchema({contains: {minimum: 2}}).should.equal(true) + ajv.validateSchema({contains: 2}).should.equal(false) + }) - it('should use option meta as default meta schema', function() { + it("should use option meta as default meta schema", function () { var meta = { - $schema: 'http://json-schema.org/draft-07/schema', + $schema: "http://json-schema.org/draft-07/schema", properties: { - myKeyword: { type: 'boolean' } - } - }; - var ajv = new Ajv({ meta: meta }); - ajv.validateSchema({ myKeyword: true }) .should.equal(true); - ajv.validateSchema({ myKeyword: 2 }) .should.equal(false); - ajv.validateSchema({ - $schema: 'http://json-schema.org/draft-07/schema', - myKeyword: 2 - }) .should.equal(true); + myKeyword: {type: "boolean"}, + }, + } + var ajv = new Ajv({meta: meta}) + ajv.validateSchema({myKeyword: true}).should.equal(true) + ajv.validateSchema({myKeyword: 2}).should.equal(false) + ajv + .validateSchema({ + $schema: "http://json-schema.org/draft-07/schema", + myKeyword: 2, + }) + .should.equal(true) - ajv = new Ajv; - ajv.validateSchema({ myKeyword: true }) .should.equal(true); - ajv.validateSchema({ myKeyword: 2 }) .should.equal(true); - }); -}); + ajv = new Ajv() + ajv.validateSchema({myKeyword: true}).should.equal(true) + ajv.validateSchema({myKeyword: 2}).should.equal(true) + }) +}) diff --git a/spec/options/nullable.spec.js b/spec/options/nullable.spec.js index df1bda5e44..9fb3013b8a 100644 --- a/spec/options/nullable.spec.js +++ b/spec/options/nullable.spec.js @@ -1,97 +1,96 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() +describe("nullable option", function () { + var ajv -describe('nullable option', function() { - var ajv; - - describe('= true', function() { + describe("= true", function () { beforeEach(function () { ajv = new Ajv({ - nullable: true - }); - }); + nullable: true, + }) + }) - it('should add keyword "nullable"', function() { + it('should add keyword "nullable"', function () { testNullable({ - type: 'number', - nullable: true - }); + type: "number", + nullable: true, + }) testNullable({ - type: ['number'], - nullable: true - }); + type: ["number"], + nullable: true, + }) testNullable({ - type: ['number', 'null'] - }); + type: ["number", "null"], + }) testNullable({ - type: ['number', 'null'], - nullable: true - }); + type: ["number", "null"], + nullable: true, + }) - testNotNullable({type: 'number'}); + testNotNullable({type: "number"}) - testNotNullable({type: ['number']}); - }); + testNotNullable({type: ["number"]}) + }) - it('should respect "nullable" == false with opts.nullable == true', function() { + it('should respect "nullable" == false with opts.nullable == true', function () { testNotNullable({ - type: 'number', - nullable: false - }); + type: "number", + nullable: false, + }) testNotNullable({ - type: ['number'], - nullable: false - }); - }); - }); + type: ["number"], + nullable: false, + }) + }) + }) - describe('without option "nullable"', function() { - it('should ignore keyword nullable', function() { - ajv = new Ajv; + describe('without option "nullable"', function () { + it("should ignore keyword nullable", function () { + ajv = new Ajv() testNotNullable({ - type: 'number', - nullable: true - }); + type: "number", + nullable: true, + }) testNotNullable({ - type: ['number'], - nullable: true - }); + type: ["number"], + nullable: true, + }) testNullable({ - type: ['number', 'null'], - }); + type: ["number", "null"], + }) testNullable({ - type: ['number', 'null'], - nullable: true - }); + type: ["number", "null"], + nullable: true, + }) should.not.throw(function () { - ajv.compile({nullable: false}); - }); - }); - }); + ajv.compile({nullable: false}) + }) + }) + }) function testNullable(schema) { - var validate = ajv.compile(schema); - validate(1) .should.equal(true); - validate(null) .should.equal(true); - validate('1') .should.equal(false); + var validate = ajv.compile(schema) + validate(1).should.equal(true) + validate(null).should.equal(true) + validate("1").should.equal(false) } function testNotNullable(schema) { - var validate = ajv.compile(schema); - validate(1) .should.equal(true); - validate(null) .should.equal(false); - validate('1') .should.equal(false); + var validate = ajv.compile(schema) + validate(1).should.equal(true) + validate(null).should.equal(false) + validate("1").should.equal(false) } -}); +}) diff --git a/spec/options/options_add_schemas.spec.js b/spec/options/options_add_schemas.spec.js index e1a59f2365..1574839a80 100644 --- a/spec/options/options_add_schemas.spec.js +++ b/spec/options/options_add_schemas.spec.js @@ -1,130 +1,130 @@ -'use strict'; - -var Ajv = require('../ajv'); -var should = require('../chai').should(); - - -describe('options to add schemas', function() { - describe('schemas', function() { - it('should add schemas from object', function() { - var ajv = new Ajv({ schemas: { - int: { type: 'integer' }, - str: { type: 'string' } - }}); - - ajv.validate('int', 123) .should.equal(true); - ajv.validate('int', 'foo') .should.equal(false); - ajv.validate('str', 'foo') .should.equal(true); - ajv.validate('str', 123) .should.equal(false); - }); - - it('should add schemas from array', function() { - var ajv = new Ajv({ schemas: [ - { $id: 'int', type: 'integer' }, - { $id: 'str', type: 'string' }, - { $id: 'obj', properties: { int: { $ref: 'int' }, str: { $ref: 'str' } } } - ]}); - - ajv.validate('obj', { int: 123, str: 'foo' }) .should.equal(true); - ajv.validate('obj', { int: 'foo', str: 'bar' }) .should.equal(false); - ajv.validate('obj', { int: 123, str: 456 }) .should.equal(false); - }); - }); - - - describe('addUsedSchema', function() { - [true, undefined].forEach(function (optionValue) { - describe('= ' + optionValue, function() { - var ajv; - - beforeEach(function() { - ajv = new Ajv({ addUsedSchema: optionValue }); - }); - - describe('compile and validate', function() { - it('should add schema', function() { - var schema = { $id: 'str', type: 'string' }; - var validate = ajv.compile(schema); - validate('abc') .should.equal(true); - validate(1) .should.equal(false); - ajv.getSchema('str') .should.equal(validate); - - schema = { $id: 'int', type: 'integer' }; - ajv.validate(schema, 1) .should.equal(true); - ajv.validate(schema, 'abc') .should.equal(false); - ajv.getSchema('int') .should.be.a('function'); - }); - - it('should throw with duplicate ID', function() { - ajv.compile({ $id: 'str', type: 'string' }); - should.throw(function() { - ajv.compile({ $id: 'str', minLength: 2 }); - }); - - var schema = { $id: 'int', type: 'integer' }; - var schema2 = { $id: 'int', minimum: 0 }; - ajv.validate(schema, 1) .should.equal(true); - should.throw(function() { - ajv.validate(schema2, 1); - }); - }); - }); - }); - }); - - describe('= false', function() { - var ajv; - - beforeEach(function() { - ajv = new Ajv({ addUsedSchema: false }); - }); - - - describe('compile and validate', function() { - it('should NOT add schema', function() { - var schema = { $id: 'str', type: 'string' }; - var validate = ajv.compile(schema); - validate('abc') .should.equal(true); - validate(1) .should.equal(false); - should.equal(ajv.getSchema('str'), undefined); - - schema = { $id: 'int', type: 'integer' }; - ajv.validate(schema, 1) .should.equal(true); - ajv.validate(schema, 'abc') .should.equal(false); - should.equal(ajv.getSchema('int'), undefined); - }); - - it('should NOT throw with duplicate ID', function() { - ajv.compile({ $id: 'str', type: 'string' }); - should.not.throw(function() { - ajv.compile({ $id: 'str', minLength: 2 }); - }); - - var schema = { $id: 'int', type: 'integer' }; - var schema2 = { $id: 'int', minimum: 0 }; - ajv.validate(schema, 1) .should.equal(true); - should.not.throw(function() { - ajv.validate(schema2, 1) .should.equal(true); - }); - }); - }); - }); - }); - - - describe('serialize', function() { - var serializeCalled; - - it('should use custom function to serialize schema to string', function() { - serializeCalled = undefined; - var ajv = new Ajv({ serialize: serialize }); - ajv.addSchema({ type: 'string' }); - should.equal(serializeCalled, true); - }); +"use strict" + +var Ajv = require("../ajv") +var should = require("../chai").should() + +describe("options to add schemas", function () { + describe("schemas", function () { + it("should add schemas from object", function () { + var ajv = new Ajv({ + schemas: { + int: {type: "integer"}, + str: {type: "string"}, + }, + }) + + ajv.validate("int", 123).should.equal(true) + ajv.validate("int", "foo").should.equal(false) + ajv.validate("str", "foo").should.equal(true) + ajv.validate("str", 123).should.equal(false) + }) + + it("should add schemas from array", function () { + var ajv = new Ajv({ + schemas: [ + {$id: "int", type: "integer"}, + {$id: "str", type: "string"}, + {$id: "obj", properties: {int: {$ref: "int"}, str: {$ref: "str"}}}, + ], + }) + + ajv.validate("obj", {int: 123, str: "foo"}).should.equal(true) + ajv.validate("obj", {int: "foo", str: "bar"}).should.equal(false) + ajv.validate("obj", {int: 123, str: 456}).should.equal(false) + }) + }) + + describe("addUsedSchema", function () { + ;[true, undefined].forEach(function (optionValue) { + describe("= " + optionValue, function () { + var ajv + + beforeEach(function () { + ajv = new Ajv({addUsedSchema: optionValue}) + }) + + describe("compile and validate", function () { + it("should add schema", function () { + var schema = {$id: "str", type: "string"} + var validate = ajv.compile(schema) + validate("abc").should.equal(true) + validate(1).should.equal(false) + ajv.getSchema("str").should.equal(validate) + + schema = {$id: "int", type: "integer"} + ajv.validate(schema, 1).should.equal(true) + ajv.validate(schema, "abc").should.equal(false) + ajv.getSchema("int").should.be.a("function") + }) + + it("should throw with duplicate ID", function () { + ajv.compile({$id: "str", type: "string"}) + should.throw(function () { + ajv.compile({$id: "str", minLength: 2}) + }) + + var schema = {$id: "int", type: "integer"} + var schema2 = {$id: "int", minimum: 0} + ajv.validate(schema, 1).should.equal(true) + should.throw(function () { + ajv.validate(schema2, 1) + }) + }) + }) + }) + }) + + describe("= false", function () { + var ajv + + beforeEach(function () { + ajv = new Ajv({addUsedSchema: false}) + }) + + describe("compile and validate", function () { + it("should NOT add schema", function () { + var schema = {$id: "str", type: "string"} + var validate = ajv.compile(schema) + validate("abc").should.equal(true) + validate(1).should.equal(false) + should.equal(ajv.getSchema("str"), undefined) + + schema = {$id: "int", type: "integer"} + ajv.validate(schema, 1).should.equal(true) + ajv.validate(schema, "abc").should.equal(false) + should.equal(ajv.getSchema("int"), undefined) + }) + + it("should NOT throw with duplicate ID", function () { + ajv.compile({$id: "str", type: "string"}) + should.not.throw(function () { + ajv.compile({$id: "str", minLength: 2}) + }) + + var schema = {$id: "int", type: "integer"} + var schema2 = {$id: "int", minimum: 0} + ajv.validate(schema, 1).should.equal(true) + should.not.throw(function () { + ajv.validate(schema2, 1).should.equal(true) + }) + }) + }) + }) + }) + + describe("serialize", function () { + var serializeCalled + + it("should use custom function to serialize schema to string", function () { + serializeCalled = undefined + var ajv = new Ajv({serialize: serialize}) + ajv.addSchema({type: "string"}) + should.equal(serializeCalled, true) + }) function serialize(schema) { - serializeCalled = true; - return JSON.stringify(schema); + serializeCalled = true + return JSON.stringify(schema) } - }); -}); + }) +}) diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js index 8884c8c4c0..742b0d1b2b 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.js @@ -1,88 +1,85 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('code generation options', function () { - describe('sourceCode', function() { - describe('= true', function() { - it('should add source.code property', function() { - test(new Ajv({sourceCode: true})); +describe("code generation options", function () { + describe("sourceCode", function () { + describe("= true", function () { + it("should add source.code property", function () { + test(new Ajv({sourceCode: true})) function test(ajv) { - var validate = ajv.compile({ "type": "number" }); - validate.source.code .should.be.a('string'); + var validate = ajv.compile({type: "number"}) + validate.source.code.should.be.a("string") } - }); - }); + }) + }) - describe('= false and default', function() { - it('should not add source and sourceCode properties', function() { - test(new Ajv); - test(new Ajv({sourceCode: false})); + describe("= false and default", function () { + it("should not add source and sourceCode properties", function () { + test(new Ajv()) + test(new Ajv({sourceCode: false})) function test(ajv) { - var validate = ajv.compile({ "type": "number" }); - should.not.exist(validate.source); - should.not.exist(validate.sourceCode); + var validate = ajv.compile({type: "number"}) + should.not.exist(validate.source) + should.not.exist(validate.sourceCode) } - }); - }); - }); - - - describe('processCode', function() { - it('should process generated code', function() { - var ajv = new Ajv; - var validate = ajv.compile({type: 'string'}); - validate.toString().split('\n').length .should.equal(1); - - var beautify = require('js-beautify').js_beautify; - var ajvPC = new Ajv({processCode: beautify}); - validate = ajvPC.compile({type: 'string'}); - validate.toString().split('\n').length .should.be.above(1); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); - }); - }); - - - describe('passContext option', function() { - var ajv, contexts; - - beforeEach(function() { - contexts = []; - }); - - describe('= true', function() { - it('should pass this value as context to custom keyword validation function', function() { - var validate = getValidate(true); - var self = {}; - validate.call(self, {}); - contexts .should.have.length(4); - contexts.forEach(function(ctx) { - ctx .should.equal(self); - }); - }); - }); - - describe('= false', function() { - it('should pass ajv instance as context to custom keyword validation function', function() { - var validate = getValidate(false); - var self = {}; - validate.call(self, {}); - contexts .should.have.length(4); - contexts.forEach(function(ctx) { - ctx .should.equal(ajv); - }); - }); - }); + }) + }) + }) + + describe("processCode", function () { + it("should process generated code", function () { + var ajv = new Ajv() + var validate = ajv.compile({type: "string"}) + validate.toString().split("\n").length.should.equal(1) + + var beautify = require("js-beautify").js_beautify + var ajvPC = new Ajv({processCode: beautify}) + validate = ajvPC.compile({type: "string"}) + validate.toString().split("\n").length.should.be.above(1) + validate("foo").should.equal(true) + validate(1).should.equal(false) + }) + }) + + describe("passContext option", function () { + var ajv, contexts + + beforeEach(function () { + contexts = [] + }) + + describe("= true", function () { + it("should pass this value as context to custom keyword validation function", function () { + var validate = getValidate(true) + var self = {} + validate.call(self, {}) + contexts.should.have.length(4) + contexts.forEach(function (ctx) { + ctx.should.equal(self) + }) + }) + }) + + describe("= false", function () { + it("should pass ajv instance as context to custom keyword validation function", function () { + var validate = getValidate(false) + var self = {} + validate.call(self, {}) + contexts.should.have.length(4) + contexts.forEach(function (ctx) { + ctx.should.equal(ajv) + }) + }) + }) function getValidate(passContext) { - ajv = new Ajv({ passContext: passContext, inlineRefs: false }); - ajv.addKeyword('testValidate', { validate: storeContext }); - ajv.addKeyword('testCompile', { compile: compileTestValidate }); + ajv = new Ajv({passContext: passContext, inlineRefs: false}) + ajv.addKeyword("testValidate", {validate: storeContext}) + ajv.addKeyword("testCompile", {compile: compileTestValidate}) var schema = { definitions: { @@ -91,25 +88,22 @@ describe('code generation options', function () { testCompile: true, }, test2: { - allOf: [ { $ref: '#/definitions/test1' } ] - } + allOf: [{$ref: "#/definitions/test1"}], + }, }, - allOf: [ - { $ref: '#/definitions/test1' }, - { $ref: '#/definitions/test2' } - ] - }; + allOf: [{$ref: "#/definitions/test1"}, {$ref: "#/definitions/test2"}], + } - return ajv.compile(schema); + return ajv.compile(schema) } function storeContext() { - contexts.push(this); - return true; + contexts.push(this) + return true } function compileTestValidate() { - return storeContext; + return storeContext } - }); -}); + }) +}) diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js index 1f1d20a757..a5a68f8427 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.js @@ -1,167 +1,158 @@ -'use strict'; - -var Ajv = require('../ajv'); -var should = require('../chai').should(); - - -describe('referenced schema options', function() { - describe('extendRefs', function() { - describe('= true', function() { - it('should allow extending $ref with other keywords', function() { - test(new Ajv({ extendRefs: true }), true); - }); - - it('should NOT log warning if extendRefs is true', function() { - testWarning(new Ajv({ extendRefs: true })); - }); - }); - - describe('= "ignore" and default', function() { - it('should ignore other keywords when $ref is used', function() { - test(new Ajv); - test(new Ajv({ extendRefs: 'ignore' }), false); - }); - - it('should log warning when other keywords are used with $ref', function() { - testWarning(new Ajv, /keywords\signored/); - testWarning(new Ajv({ extendRefs: 'ignore' }), /keywords\signored/); - }); - }); - - describe('= "fail"', function() { - it('should fail schema compilation if other keywords are used with $ref', function() { - testFail(new Ajv({ extendRefs: 'fail' })); +"use strict" + +var Ajv = require("../ajv") +var should = require("../chai").should() + +describe("referenced schema options", function () { + describe("extendRefs", function () { + describe("= true", function () { + it("should allow extending $ref with other keywords", function () { + test(new Ajv({extendRefs: true}), true) + }) + + it("should NOT log warning if extendRefs is true", function () { + testWarning(new Ajv({extendRefs: true})) + }) + }) + + describe('= "ignore" and default', function () { + it("should ignore other keywords when $ref is used", function () { + test(new Ajv()) + test(new Ajv({extendRefs: "ignore"}), false) + }) + + it("should log warning when other keywords are used with $ref", function () { + testWarning(new Ajv(), /keywords\signored/) + testWarning(new Ajv({extendRefs: "ignore"}), /keywords\signored/) + }) + }) + + describe('= "fail"', function () { + it("should fail schema compilation if other keywords are used with $ref", function () { + testFail(new Ajv({extendRefs: "fail"})) function testFail(ajv) { - should.throw(function() { + should.throw(function () { var schema = { - "definitions": { - "int": { "type": "integer" } + definitions: { + int: {type: "integer"}, }, - "$ref": "#/definitions/int", - "minimum": 10 - }; - ajv.compile(schema); - }); + $ref: "#/definitions/int", + minimum: 10, + } + ajv.compile(schema) + }) - should.not.throw(function() { + should.not.throw(function () { var schema = { - "definitions": { - "int": { "type": "integer" } + definitions: { + int: {type: "integer"}, }, - "allOf": [ - { "$ref": "#/definitions/int" }, - { "minimum": 10 } - ] - }; - ajv.compile(schema); - }); + allOf: [{$ref: "#/definitions/int"}, {minimum: 10}], + } + ajv.compile(schema) + }) } - }); - }); + }) + }) function test(ajv, shouldExtendRef) { var schema = { - "definitions": { - "int": { "type": "integer" } + definitions: { + int: {type: "integer"}, }, - "$ref": "#/definitions/int", - "minimum": 10 - }; + $ref: "#/definitions/int", + minimum: 10, + } - var validate = ajv.compile(schema); - validate(10) .should.equal(true); - validate(1) .should.equal(!shouldExtendRef); + var validate = ajv.compile(schema) + validate(10).should.equal(true) + validate(1).should.equal(!shouldExtendRef) schema = { - "definitions": { - "int": { "type": "integer" } + definitions: { + int: {type: "integer"}, }, - "type": "object", - "properties": { - "foo": { - "$ref": "#/definitions/int", - "minimum": 10 + type: "object", + properties: { + foo: { + $ref: "#/definitions/int", + minimum: 10, }, - "bar": { - "allOf": [ - { "$ref": "#/definitions/int" }, - { "minimum": 10 } - ] - } - } - }; + bar: { + allOf: [{$ref: "#/definitions/int"}, {minimum: 10}], + }, + }, + } - validate = ajv.compile(schema); - validate({ foo: 10, bar: 10 }) .should.equal(true); - validate({ foo: 1, bar: 10 }) .should.equal(!shouldExtendRef); - validate({ foo: 10, bar: 1 }) .should.equal(false); + validate = ajv.compile(schema) + validate({foo: 10, bar: 10}).should.equal(true) + validate({foo: 1, bar: 10}).should.equal(!shouldExtendRef) + validate({foo: 10, bar: 1}).should.equal(false) } function testWarning(ajv, msgPattern) { - var oldConsole; + var oldConsole try { - oldConsole = console.warn; - var consoleMsg; - console.warn = function() { - consoleMsg = Array.prototype.join.call(arguments, ' '); - }; + oldConsole = console.warn + var consoleMsg + console.warn = function () { + consoleMsg = Array.prototype.join.call(arguments, " ") + } var schema = { - "definitions": { - "int": { "type": "integer" } + definitions: { + int: {type: "integer"}, }, - "$ref": "#/definitions/int", - "minimum": 10 - }; + $ref: "#/definitions/int", + minimum: 10, + } - ajv.compile(schema); - if (msgPattern) consoleMsg .should.match(msgPattern); - else should.not.exist(consoleMsg); + ajv.compile(schema) + if (msgPattern) consoleMsg.should.match(msgPattern) + else should.not.exist(consoleMsg) } finally { - console.warn = oldConsole; + console.warn = oldConsole } } - }); + }) + describe("missingRefs", function () { + it("should throw if ref is missing without this option", function () { + var ajv = new Ajv() + should.throw(function () { + ajv.compile({$ref: "missing_reference"}) + }) + }) - describe('missingRefs', function() { - it('should throw if ref is missing without this option', function() { - var ajv = new Ajv; - should.throw(function() { - ajv.compile({ $ref: 'missing_reference' }); - }); - }); - - it('should not throw and pass validation with missingRef == "ignore"', function() { - testMissingRefsIgnore(new Ajv({ missingRefs: 'ignore' })); - testMissingRefsIgnore(new Ajv({ missingRefs: 'ignore', allErrors: true })); + it('should not throw and pass validation with missingRef == "ignore"', function () { + testMissingRefsIgnore(new Ajv({missingRefs: "ignore"})) + testMissingRefsIgnore(new Ajv({missingRefs: "ignore", allErrors: true})) function testMissingRefsIgnore(ajv) { - var validate = ajv.compile({ $ref: 'missing_reference' }); - validate({}) .should.equal(true); + var validate = ajv.compile({$ref: "missing_reference"}) + validate({}).should.equal(true) } - }); + }) - it('should not throw and fail validation with missingRef == "fail" if the ref is used', function() { - testMissingRefsFail(new Ajv({ missingRefs: 'fail' })); - testMissingRefsFail(new Ajv({ missingRefs: 'fail', verbose: true })); - testMissingRefsFail(new Ajv({ missingRefs: 'fail', allErrors: true })); - testMissingRefsFail(new Ajv({ missingRefs: 'fail', allErrors: true, verbose: true })); + it('should not throw and fail validation with missingRef == "fail" if the ref is used', function () { + testMissingRefsFail(new Ajv({missingRefs: "fail"})) + testMissingRefsFail(new Ajv({missingRefs: "fail", verbose: true})) + testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true})) + testMissingRefsFail( + new Ajv({missingRefs: "fail", allErrors: true, verbose: true}) + ) function testMissingRefsFail(ajv) { var validate = ajv.compile({ - anyOf: [ - { type: 'number' }, - { $ref: 'missing_reference' } - ] - }); - validate(123) .should.equal(true); - validate('foo') .should.equal(false); - - validate = ajv.compile({ $ref: 'missing_reference' }); - validate({}) .should.equal(false); + anyOf: [{type: "number"}, {$ref: "missing_reference"}], + }) + validate(123).should.equal(true) + validate("foo").should.equal(false) + + validate = ajv.compile({$ref: "missing_reference"}) + validate({}).should.equal(false) } - }); - }); -}); + }) + }) +}) diff --git a/spec/options/options_reporting.spec.js b/spec/options/options_reporting.spec.js index 578611e807..fcae23338c 100644 --- a/spec/options/options_reporting.spec.js +++ b/spec/options/options_reporting.spec.js @@ -1,158 +1,155 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('reporting options', function () { - describe('verbose', function() { - it('should add schema, parentSchema and data to errors with verbose option == true', function() { - testVerbose(new Ajv({ verbose: true })); - testVerbose(new Ajv({ verbose: true, allErrors: true })); +describe("reporting options", function () { + describe("verbose", function () { + it("should add schema, parentSchema and data to errors with verbose option == true", function () { + testVerbose(new Ajv({verbose: true})) + testVerbose(new Ajv({verbose: true, allErrors: true})) function testVerbose(ajv) { - var schema = { properties: { foo: { minimum: 5 } } }; - var validate = ajv.compile(schema); - - var data = { foo: 3 }; - validate(data) .should.equal(false); - validate.errors .should.have.length(1); - var err = validate.errors[0]; - - should.equal(err.schema, 5); - err.parentSchema .should.eql({ minimum: 5 }); - err.parentSchema .should.equal(schema.properties.foo); // by reference - should.equal(err.data, 3); + var schema = {properties: {foo: {minimum: 5}}} + var validate = ajv.compile(schema) + + var data = {foo: 3} + validate(data).should.equal(false) + validate.errors.should.have.length(1) + var err = validate.errors[0] + + should.equal(err.schema, 5) + err.parentSchema.should.eql({minimum: 5}) + err.parentSchema.should.equal(schema.properties.foo) // by reference + should.equal(err.data, 3) } - }); - }); - + }) + }) - describe('allErrors', function() { - it('should be disabled inside "not" keyword', function() { - test(new Ajv, false); - test(new Ajv({ allErrors: true }), true); + describe("allErrors", function () { + it('should be disabled inside "not" keyword', function () { + test(new Ajv(), false) + test(new Ajv({allErrors: true}), true) function test(ajv, allErrors) { - var format1called = false - , format2called = false; + var format1called = false, + format2called = false - ajv.addFormat('format1', function() { - format1called = true; - return false; - }); + ajv.addFormat("format1", function () { + format1called = true + return false + }) - ajv.addFormat('format2', function() { - format2called = true; - return false; - }); + ajv.addFormat("format2", function () { + format2called = true + return false + }) var schema1 = { - allOf: [ - { format: 'format1' }, - { format: 'format2' } - ] - }; + allOf: [{format: "format1"}, {format: "format2"}], + } - ajv.validate(schema1, 'abc') .should.equal(false); - ajv.errors .should.have.length(allErrors ? 2 : 1); - format1called .should.equal(true); - format2called .should.equal(allErrors); + ajv.validate(schema1, "abc").should.equal(false) + ajv.errors.should.have.length(allErrors ? 2 : 1) + format1called.should.equal(true) + format2called.should.equal(allErrors) var schema2 = { - not: schema1 - }; - - format1called = format2called = false; - ajv.validate(schema2, 'abc') .should.equal(true); - should.equal(ajv.errors, null); - format1called .should.equal(true); - format2called .should.equal(false); + not: schema1, + } + + format1called = format2called = false + ajv.validate(schema2, "abc").should.equal(true) + should.equal(ajv.errors, null) + format1called.should.equal(true) + format2called.should.equal(false) } - }); - }); - + }) + }) - describe('logger', function() { + describe("logger", function () { /** * The logger option tests are based on the meta scenario which writes into the logger.warn */ - var origConsoleWarn = console.warn; - var consoleCalled; + var origConsoleWarn = console.warn + var consoleCalled - beforeEach(function() { - consoleCalled = false; - console.warn = function() { - consoleCalled = true; - }; - }); + beforeEach(function () { + consoleCalled = false + console.warn = function () { + consoleCalled = true + } + }) - afterEach(function() { - console.warn = origConsoleWarn; - }); + afterEach(function () { + console.warn = origConsoleWarn + }) - it('no custom logger is given - global console should be used', function() { + it("no custom logger is given - global console should be used", function () { var ajv = new Ajv({ - meta: false - }); + meta: false, + }) ajv.compile({ - type: 'number', - minimum: 1 - }); + type: "number", + minimum: 1, + }) - should.equal(consoleCalled, true); - }); + should.equal(consoleCalled, true) + }) - it('custom logger is an object - logs should only report to it', function() { - var loggerCalled = false; + it("custom logger is an object - logs should only report to it", function () { + var loggerCalled = false var logger = { warn: log, log: log, - error: log - }; + error: log, + } var ajv = new Ajv({ meta: false, - logger: logger - }); + logger: logger, + }) ajv.compile({ - type: 'number', - minimum: 1 - }); + type: "number", + minimum: 1, + }) - should.equal(loggerCalled, true); - should.equal(consoleCalled, false); + should.equal(loggerCalled, true) + should.equal(consoleCalled, false) function log() { - loggerCalled = true; + loggerCalled = true } - }); + }) - it('logger option is false - no logs should be reported', function() { + it("logger option is false - no logs should be reported", function () { var ajv = new Ajv({ meta: false, - logger: false - }); + logger: false, + }) ajv.compile({ - type: 'number', - minimum: 1 - }); + type: "number", + minimum: 1, + }) - should.equal(consoleCalled, false); - }); + should.equal(consoleCalled, false) + }) - it('logger option is an object without required methods - an error should be thrown', function() { - (function(){ + it("logger option is an object without required methods - an error should be thrown", function () { + ;(function () { new Ajv({ meta: false, - logger: {} - }); - }).should.throw(Error, /logger must implement log, warn and error methods/); - }); - }); -}); + logger: {}, + }) + }.should.throw( + Error, + /logger must implement log, warn and error methods/ + )) + }) + }) +}) diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index 950a0c2894..1abb6ba338 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -1,116 +1,115 @@ -'use strict'; - -var Ajv = require('../ajv'); -require('../chai').should(); - - -describe('validation options', function() { - describe('format', function() { - it('should not validate formats if option format == false', function() { - var ajv = new Ajv - , ajvFF = new Ajv({ format: false }); - - var schema = { format: 'date-time' }; - var invalideDateTime = '06/19/1963 08:30:06 PST'; - - ajv.validate(schema, invalideDateTime) .should.equal(false); - ajvFF.validate(schema, invalideDateTime) .should.equal(true); - }); - }); - - - describe('formats', function() { - it('should add formats from options', function() { - var ajv = new Ajv({ formats: { - identifier: /^[a-z_$][a-z0-9_$]*$/i - }}); - - var validate = ajv.compile({ format: 'identifier' }); - - validate('Abc1') .should.equal(true); - validate('foo bar') .should.equal(false); - validate('123') .should.equal(false); - validate(123) .should.equal(true); - }); - }); - - describe('keywords', function() { - it('should add keywords from options', function() { - var ajv = new Ajv({ keywords: { - identifier: { - type: 'string', - validate: function (schema, data ) { - return /^[a-z_$][a-z0-9_$]*$/i.test(data); - } - } - }}); - - var validate = ajv.compile({ identifier: true }); - - validate('Abc1') .should.equal(true); - validate('foo bar') .should.equal(false); - validate('123') .should.equal(false); - validate(123) .should.equal(true); - }); - }); - - - describe('uniqueItems', function() { - it('should not validate uniqueItems with uniqueItems option == false', function() { - testUniqueItems(new Ajv({ uniqueItems: false })); - testUniqueItems(new Ajv({ uniqueItems: false, allErrors: true })); +"use strict" + +var Ajv = require("../ajv") +require("../chai").should() + +describe("validation options", function () { + describe("format", function () { + it("should not validate formats if option format == false", function () { + var ajv = new Ajv(), + ajvFF = new Ajv({format: false}) + + var schema = {format: "date-time"} + var invalideDateTime = "06/19/1963 08:30:06 PST" + + ajv.validate(schema, invalideDateTime).should.equal(false) + ajvFF.validate(schema, invalideDateTime).should.equal(true) + }) + }) + + describe("formats", function () { + it("should add formats from options", function () { + var ajv = new Ajv({ + formats: { + identifier: /^[a-z_$][a-z0-9_$]*$/i, + }, + }) + + var validate = ajv.compile({format: "identifier"}) + + validate("Abc1").should.equal(true) + validate("foo bar").should.equal(false) + validate("123").should.equal(false) + validate(123).should.equal(true) + }) + }) + + describe("keywords", function () { + it("should add keywords from options", function () { + var ajv = new Ajv({ + keywords: { + identifier: { + type: "string", + validate: function (schema, data) { + return /^[a-z_$][a-z0-9_$]*$/i.test(data) + }, + }, + }, + }) + + var validate = ajv.compile({identifier: true}) + + validate("Abc1").should.equal(true) + validate("foo bar").should.equal(false) + validate("123").should.equal(false) + validate(123).should.equal(true) + }) + }) + + describe("uniqueItems", function () { + it("should not validate uniqueItems with uniqueItems option == false", function () { + testUniqueItems(new Ajv({uniqueItems: false})) + testUniqueItems(new Ajv({uniqueItems: false, allErrors: true})) function testUniqueItems(ajv) { - var validate = ajv.compile({ uniqueItems: true }); - validate([1,2,3]) .should.equal(true); - validate([1,1,1]) .should.equal(true); + var validate = ajv.compile({uniqueItems: true}) + validate([1, 2, 3]).should.equal(true) + validate([1, 1, 1]).should.equal(true) } - }); - }); + }) + }) - - describe('unicode', function() { - it('should use String.prototype.length with unicode option == false', function() { - var ajvUnicode = new Ajv; - testUnicode(new Ajv({ unicode: false })); - testUnicode(new Ajv({ unicode: false, allErrors: true })); + describe("unicode", function () { + it("should use String.prototype.length with unicode option == false", function () { + var ajvUnicode = new Ajv() + testUnicode(new Ajv({unicode: false})) + testUnicode(new Ajv({unicode: false, allErrors: true})) function testUnicode(ajv) { - var validateWithUnicode = ajvUnicode.compile({ minLength: 2 }); - var validate = ajv.compile({ minLength: 2 }); + var validateWithUnicode = ajvUnicode.compile({minLength: 2}) + var validate = ajv.compile({minLength: 2}) - validateWithUnicode('😀') .should.equal(false); - validate('😀') .should.equal(true); + validateWithUnicode("😀").should.equal(false) + validate("😀").should.equal(true) - validateWithUnicode = ajvUnicode.compile({ maxLength: 1 }); - validate = ajv.compile({ maxLength: 1 }); + validateWithUnicode = ajvUnicode.compile({maxLength: 1}) + validate = ajv.compile({maxLength: 1}) - validateWithUnicode('😀') .should.equal(true); - validate('😀') .should.equal(false); + validateWithUnicode("😀").should.equal(true) + validate("😀").should.equal(false) } - }); - }); - + }) + }) - describe('multipleOfPrecision', function() { - it('should allow for some deviation from 0 when validating multipleOf with value < 1', function() { - test(new Ajv({ multipleOfPrecision: 7 })); - test(new Ajv({ multipleOfPrecision: 7, allErrors: true })); + describe("multipleOfPrecision", function () { + it("should allow for some deviation from 0 when validating multipleOf with value < 1", function () { + test(new Ajv({multipleOfPrecision: 7})) + test(new Ajv({multipleOfPrecision: 7, allErrors: true})) function test(ajv) { - var schema = { multipleOf: 0.01 }; - var validate = ajv.compile(schema); + var schema = {multipleOf: 0.01} + var validate = ajv.compile(schema) - validate(4.18) .should.equal(true); - validate(4.181) .should.equal(false); + validate(4.18).should.equal(true) + validate(4.181).should.equal(false) - schema = { multipleOf: 0.0000001 }; - validate = ajv.compile(schema); + schema = {multipleOf: 0.0000001} + validate = ajv.compile(schema) - validate(53.198098) .should.equal(true); - validate(53.1980981) .should.equal(true); - validate(53.19809811) .should.equal(false); + validate(53.198098).should.equal(true) + validate(53.1980981).should.equal(true) + validate(53.19809811).should.equal(false) } - }); - }); -}); + }) + }) +}) diff --git a/spec/options/ownProperties.spec.js b/spec/options/ownProperties.spec.js index 312579f96e..708e6fc19c 100644 --- a/spec/options/ownProperties.spec.js +++ b/spec/options/ownProperties.spec.js @@ -1,178 +1,177 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() +describe("ownProperties option", function () { + var ajv, ajvOP, ajvOP1 -describe('ownProperties option', function() { - var ajv, ajvOP, ajvOP1; + beforeEach(function () { + ajv = new Ajv({allErrors: true}) + ajvOP = new Ajv({ownProperties: true, allErrors: true}) + ajvOP1 = new Ajv({ownProperties: true}) + }) - beforeEach(function() { - ajv = new Ajv({ allErrors: true }); - ajvOP = new Ajv({ ownProperties: true, allErrors: true }); - ajvOP1 = new Ajv({ ownProperties: true }); - }); - - it('should only validate own properties with additionalProperties', function() { + it("should only validate own properties with additionalProperties", function () { var schema = { - properties: { a: { type: 'number' } }, - additionalProperties: false - }; + properties: {a: {type: "number"}}, + additionalProperties: false, + } - var obj = { a: 1 }; - var proto = { b: 2 }; - test(schema, obj, proto); - }); + var obj = {a: 1} + var proto = {b: 2} + test(schema, obj, proto) + }) - it('should only validate own properties with properties keyword', function() { + it("should only validate own properties with properties keyword", function () { var schema = { properties: { - a: { type: 'number' }, - b: { type: 'number' } - } - }; + a: {type: "number"}, + b: {type: "number"}, + }, + } - var obj = { a: 1 }; - var proto = { b: 'not a number' }; - test(schema, obj, proto); - }); + var obj = {a: 1} + var proto = {b: "not a number"} + test(schema, obj, proto) + }) - it('should only validate own properties with required keyword', function() { + it("should only validate own properties with required keyword", function () { var schema = { - required: ['a', 'b'] - }; + required: ["a", "b"], + } - var obj = { a: 1 }; - var proto = { b: 2 }; - test(schema, obj, proto, 1, true); - }); + var obj = {a: 1} + var proto = {b: 2} + test(schema, obj, proto, 1, true) + }) - it('should only validate own properties with required keyword - many properties', function() { - ajv = new Ajv({ allErrors: true, loopRequired: 1 }); - ajvOP = new Ajv({ ownProperties: true, allErrors: true, loopRequired: 1 }); - ajvOP1 = new Ajv({ ownProperties: true, loopRequired: 1 }); + it("should only validate own properties with required keyword - many properties", function () { + ajv = new Ajv({allErrors: true, loopRequired: 1}) + ajvOP = new Ajv({ownProperties: true, allErrors: true, loopRequired: 1}) + ajvOP1 = new Ajv({ownProperties: true, loopRequired: 1}) var schema = { - required: ['a', 'b', 'c', 'd'] - }; + required: ["a", "b", "c", "d"], + } - var obj = { a: 1, b: 2 }; - var proto = { c: 3, d: 4 }; - test(schema, obj, proto, 2, true); - }); + var obj = {a: 1, b: 2} + var proto = {c: 3, d: 4} + test(schema, obj, proto, 2, true) + }) - it('should only validate own properties with required keyword as $data', function() { - ajv = new Ajv({ allErrors: true, $data: true }); - ajvOP = new Ajv({ ownProperties: true, allErrors: true, $data: true }); - ajvOP1 = new Ajv({ ownProperties: true, $data: true }); + it("should only validate own properties with required keyword as $data", function () { + ajv = new Ajv({allErrors: true, $data: true}) + ajvOP = new Ajv({ownProperties: true, allErrors: true, $data: true}) + ajvOP1 = new Ajv({ownProperties: true, $data: true}) var schema = { - required: { $data: '0/req' }, + required: {$data: "0/req"}, properties: { req: { - type: 'array', - items: { type: 'string' } - } - } - }; + type: "array", + items: {type: "string"}, + }, + }, + } var obj = { - req: ['a', 'b'], - a: 1 - }; - var proto = { b: 2 }; - test(schema, obj, proto, 1, true); - }); - - it('should only validate own properties with properties and required keyword', function() { + req: ["a", "b"], + a: 1, + } + var proto = {b: 2} + test(schema, obj, proto, 1, true) + }) + + it("should only validate own properties with properties and required keyword", function () { var schema = { properties: { - a: { type: 'number' }, - b: { type: 'number' } + a: {type: "number"}, + b: {type: "number"}, }, - required: ['a', 'b'] - }; + required: ["a", "b"], + } - var obj = { a: 1 }; - var proto = { b: 2 }; - test(schema, obj, proto, 1, true); - }); + var obj = {a: 1} + var proto = {b: 2} + test(schema, obj, proto, 1, true) + }) - it('should only validate own properties with dependencies keyword', function() { + it("should only validate own properties with dependencies keyword", function () { var schema = { dependencies: { - a: ['c'], - b: ['d'] - } - }; + a: ["c"], + b: ["d"], + }, + } - var obj = { a: 1, c: 3 }; - var proto = { b: 2 }; - test(schema, obj, proto); + var obj = {a: 1, c: 3} + var proto = {b: 2} + test(schema, obj, proto) - obj = { a: 1, b: 2, c: 3 }; - proto = { d: 4 }; - test(schema, obj, proto, 1, true); - }); + obj = {a: 1, b: 2, c: 3} + proto = {d: 4} + test(schema, obj, proto, 1, true) + }) - it('should only validate own properties with schema dependencies', function() { + it("should only validate own properties with schema dependencies", function () { var schema = { dependencies: { - a: { not: { required: ['c'] } }, - b: { not: { required: ['d'] } } - } - }; + a: {not: {required: ["c"]}}, + b: {not: {required: ["d"]}}, + }, + } - var obj = { a: 1, d: 3 }; - var proto = { b: 2 }; - test(schema, obj, proto); + var obj = {a: 1, d: 3} + var proto = {b: 2} + test(schema, obj, proto) - obj = { a: 1, b: 2 }; - proto = { d: 4 }; - test(schema, obj, proto); - }); + obj = {a: 1, b: 2} + proto = {d: 4} + test(schema, obj, proto) + }) - it('should only validate own properties with patternProperties', function() { + it("should only validate own properties with patternProperties", function () { var schema = { - patternProperties: { 'f.*o': { type: 'integer' } }, - }; + patternProperties: {"f.*o": {type: "integer"}}, + } - var obj = { fooo: 1 }; - var proto = { foo: 'not a number' }; - test(schema, obj, proto); - }); + var obj = {fooo: 1} + var proto = {foo: "not a number"} + test(schema, obj, proto) + }) - it('should only validate own properties with propertyNames', function() { + it("should only validate own properties with propertyNames", function () { var schema = { propertyNames: { - format: 'email' - } - }; + format: "email", + }, + } - var obj = { 'e@example.com': 2 }; - var proto = { 'not email': 1 }; - test(schema, obj, proto, 2); - }); + var obj = {"e@example.com": 2} + var proto = {"not email": 1} + test(schema, obj, proto, 2) + }) function test(schema, obj, proto, errors, reverse) { - errors = errors || 1; - var validate = ajv.compile(schema); - var validateOP = ajvOP.compile(schema); - var validateOP1 = ajvOP1.compile(schema); - var data = Object.create(proto); - for (var key in obj) data[key] = obj[key]; + errors = errors || 1 + var validate = ajv.compile(schema) + var validateOP = ajvOP.compile(schema) + var validateOP1 = ajvOP1.compile(schema) + var data = Object.create(proto) + for (var key in obj) data[key] = obj[key] if (reverse) { - validate(data) .should.equal(true); - validateOP(data) .should.equal(false); - validateOP.errors .should.have.length(errors); - validateOP1(data) .should.equal(false); - validateOP1.errors .should.have.length(1); + validate(data).should.equal(true) + validateOP(data).should.equal(false) + validateOP.errors.should.have.length(errors) + validateOP1(data).should.equal(false) + validateOP1.errors.should.have.length(1) } else { - validate(data) .should.equal(false); - validate.errors .should.have.length(errors); - validateOP(data) .should.equal(true); - validateOP1(data) .should.equal(true); + validate(data).should.equal(false) + validate.errors.should.have.length(errors) + validateOP(data).should.equal(true) + validateOP1(data).should.equal(true) } } -}); +}) diff --git a/spec/options/removeAdditional.spec.js b/spec/options/removeAdditional.spec.js index 1eef0b7959..017f9b908b 100644 --- a/spec/options/removeAdditional.spec.js +++ b/spec/options/removeAdditional.spec.js @@ -1,122 +1,129 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -require('../chai').should(); +var Ajv = require("../ajv") +require("../chai").should() - -describe('removeAdditional option', function() { - it('should remove all additional properties', function() { - var ajv = new Ajv({ removeAdditional: 'all' }); +describe("removeAdditional option", function () { + it("should remove all additional properties", function () { + var ajv = new Ajv({removeAdditional: "all"}) ajv.addSchema({ - $id: '//test/fooBar', - properties: { foo: { type: 'string' }, bar: { type: 'string' } } - }); + $id: "//test/fooBar", + properties: {foo: {type: "string"}, bar: {type: "string"}}, + }) var object = { - foo: 'foo', bar: 'bar', baz: 'baz-to-be-removed' - }; - - ajv.validate('//test/fooBar', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.not.have.property('baz'); - }); + foo: "foo", + bar: "bar", + baz: "baz-to-be-removed", + } + ajv.validate("//test/fooBar", object).should.equal(true) + object.should.have.property("foo") + object.should.have.property("bar") + object.should.not.have.property("baz") + }) - it('should remove properties that would error when `additionalProperties = false`', function() { - var ajv = new Ajv({ removeAdditional: true }); + it("should remove properties that would error when `additionalProperties = false`", function () { + var ajv = new Ajv({removeAdditional: true}) ajv.addSchema({ - $id: '//test/fooBar', - properties: { foo: { type: 'string' }, bar: { type: 'string' } }, - additionalProperties: false - }); + $id: "//test/fooBar", + properties: {foo: {type: "string"}, bar: {type: "string"}}, + additionalProperties: false, + }) var object = { - foo: 'foo', bar: 'bar', baz: 'baz-to-be-removed' - }; - - ajv.validate('//test/fooBar', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.not.have.property('baz'); - }); + foo: "foo", + bar: "bar", + baz: "baz-to-be-removed", + } + ajv.validate("//test/fooBar", object).should.equal(true) + object.should.have.property("foo") + object.should.have.property("bar") + object.should.not.have.property("baz") + }) - it('should remove properties that would error when `additionalProperties = false` (many properties, boolean schema)', function() { - var ajv = new Ajv({removeAdditional: true}); + it("should remove properties that would error when `additionalProperties = false` (many properties, boolean schema)", function () { + var ajv = new Ajv({removeAdditional: true}) var schema = { properties: { obj: { additionalProperties: false, properties: { - a: { type: 'string' }, + a: {type: "string"}, b: false, - c: { type: 'string' }, - d: { type: 'string' }, - e: { type: 'string' }, - f: { type: 'string' }, - g: { type: 'string' }, - h: { type: 'string' }, - i: { type: 'string' } - } - } - } - }; + c: {type: "string"}, + d: {type: "string"}, + e: {type: "string"}, + f: {type: "string"}, + g: {type: "string"}, + h: {type: "string"}, + i: {type: "string"}, + }, + }, + }, + } var data = { obj: { - a: 'valid', - b: 'should not be removed', - additional: 'will be removed' - } - }; - - ajv.validate(schema, data) .should.equal(false); - data .should.eql({ + a: "valid", + b: "should not be removed", + additional: "will be removed", + }, + } + + ajv.validate(schema, data).should.equal(false) + data.should.eql({ obj: { - a: 'valid', - b: 'should not be removed' - } - }); - }); + a: "valid", + b: "should not be removed", + }, + }) + }) - - it('should remove properties that would error when `additionalProperties` is a schema', function() { - var ajv = new Ajv({ removeAdditional: 'failing' }); + it("should remove properties that would error when `additionalProperties` is a schema", function () { + var ajv = new Ajv({removeAdditional: "failing"}) ajv.addSchema({ - $id: '//test/fooBar', - properties: { foo: { type: 'string' }, bar: { type: 'string' } }, - additionalProperties: { type: 'string' } - }); + $id: "//test/fooBar", + properties: {foo: {type: "string"}, bar: {type: "string"}}, + additionalProperties: {type: "string"}, + }) var object = { - foo: 'foo', bar: 'bar', baz: 'baz-to-be-kept', fizz: 1000 - }; - - ajv.validate('//test/fooBar', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.have.property('baz'); - object.should.not.have.property('fizz'); + foo: "foo", + bar: "bar", + baz: "baz-to-be-kept", + fizz: 1000, + } + + ajv.validate("//test/fooBar", object).should.equal(true) + object.should.have.property("foo") + object.should.have.property("bar") + object.should.have.property("baz") + object.should.not.have.property("fizz") ajv.addSchema({ - $id: '//test/fooBar2', - properties: { foo: { type: 'string' }, bar: { type: 'string' } }, - additionalProperties: { type: 'string', pattern: '^to-be-', maxLength: 10 } - }); + $id: "//test/fooBar2", + properties: {foo: {type: "string"}, bar: {type: "string"}}, + additionalProperties: {type: "string", pattern: "^to-be-", maxLength: 10}, + }) object = { - foo: 'foo', bar: 'bar', baz: 'to-be-kept', quux: 'to-be-removed', fizz: 1000 - }; - - ajv.validate('//test/fooBar2', object).should.equal(true); - object.should.have.property('foo'); - object.should.have.property('bar'); - object.should.have.property('baz'); - object.should.not.have.property('fizz'); - }); -}); + foo: "foo", + bar: "bar", + baz: "to-be-kept", + quux: "to-be-removed", + fizz: 1000, + } + + ajv.validate("//test/fooBar2", object).should.equal(true) + object.should.have.property("foo") + object.should.have.property("bar") + object.should.have.property("baz") + object.should.not.have.property("fizz") + }) +}) diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index f8871034f2..7600f24fcd 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -1,72 +1,71 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('schemaId option', function() { - describe('= "$id" (default)', function() { - it('should use $id and ignore id', function() { - test(new Ajv); - test(new Ajv({schemaId: '$id'})); +describe("schemaId option", function () { + describe('= "$id" (default)', function () { + it("should use $id and ignore id", function () { + test(new Ajv()) + test(new Ajv({schemaId: "$id"})) function test(ajv) { - ajv.addSchema({ $id: 'mySchema1', type: 'string' }); - var validate = ajv.getSchema('mySchema1'); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); + ajv.addSchema({$id: "mySchema1", type: "string"}) + var validate = ajv.getSchema("mySchema1") + validate("foo").should.equal(true) + validate(1).should.equal(false) - validate = ajv.compile({ id: 'mySchema2', type: 'string' }); - should.not.exist(ajv.getSchema('mySchema2')); + validate = ajv.compile({id: "mySchema2", type: "string"}) + should.not.exist(ajv.getSchema("mySchema2")) } - }); - }); + }) + }) - describe('= "id"', function() { - it('should use id and ignore $id', function() { - var ajv = new Ajv({schemaId: 'id', meta: false}); - ajv.addMetaSchema(require('../../lib/refs/json-schema-draft-04.json')); - ajv._opts.defaultMeta = 'http://json-schema.org/draft-04/schema#'; + describe('= "id"', function () { + it("should use id and ignore $id", function () { + var ajv = new Ajv({schemaId: "id", meta: false}) + ajv.addMetaSchema(require("../../lib/refs/json-schema-draft-04.json")) + ajv._opts.defaultMeta = "http://json-schema.org/draft-04/schema#" - ajv.addSchema({ id: 'mySchema1', type: 'string' }); - var validate = ajv.getSchema('mySchema1'); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); + ajv.addSchema({id: "mySchema1", type: "string"}) + var validate = ajv.getSchema("mySchema1") + validate("foo").should.equal(true) + validate(1).should.equal(false) - validate = ajv.compile({ $id: 'mySchema2', type: 'string' }); - should.not.exist(ajv.getSchema('mySchema2')); - }); - }); + validate = ajv.compile({$id: "mySchema2", type: "string"}) + should.not.exist(ajv.getSchema("mySchema2")) + }) + }) - describe('= "auto"', function() { - it('should use both id and $id', function() { - var ajv = new Ajv({schemaId: 'auto'}); + describe('= "auto"', function () { + it("should use both id and $id", function () { + var ajv = new Ajv({schemaId: "auto"}) - ajv.addSchema({ $id: 'mySchema1', type: 'string' }); - var validate = ajv.getSchema('mySchema1'); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); + ajv.addSchema({$id: "mySchema1", type: "string"}) + var validate = ajv.getSchema("mySchema1") + validate("foo").should.equal(true) + validate(1).should.equal(false) - ajv.addSchema({ id: 'mySchema2', type: 'string' }); - validate = ajv.getSchema('mySchema2'); - validate('foo') .should.equal(true); - validate(1) .should.equal(false); - }); + ajv.addSchema({id: "mySchema2", type: "string"}) + validate = ajv.getSchema("mySchema2") + validate("foo").should.equal(true) + validate(1).should.equal(false) + }) - it('should throw if both id and $id are available and different', function() { - var ajv = new Ajv({schemaId: 'auto'}); + it("should throw if both id and $id are available and different", function () { + var ajv = new Ajv({schemaId: "auto"}) ajv.compile({ - id: 'mySchema', - $id: 'mySchema' - }); + id: "mySchema", + $id: "mySchema", + }) - should.throw(function() { + should.throw(function () { ajv.compile({ - id: 'mySchema1', - $id: 'mySchema2' - }); - }); - }); - }); -}); + id: "mySchema1", + $id: "mySchema2", + }) + }) + }) + }) +}) diff --git a/spec/options/strictDefaults.spec.js b/spec/options/strictDefaults.spec.js index 1f1093a69d..7fc6f633d5 100644 --- a/spec/options/strictDefaults.spec.js +++ b/spec/options/strictDefaults.spec.js @@ -1,165 +1,170 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('strictDefaults option', function() { - describe('useDefaults = true', function() { - describe('strictDefaults = false', function() { - it('should NOT throw an error or log a warning given an ignored default', function() { - var output = {}; +describe("strictDefaults option", function () { + describe("useDefaults = true", function () { + describe("strictDefaults = false", function () { + it("should NOT throw an error or log a warning given an ignored default", function () { + var output = {} var ajv = new Ajv({ useDefaults: true, strictDefaults: false, - logger: getLogger(output) - }); + logger: getLogger(output), + }) var schema = { default: 5, - properties: {} - }; + properties: {}, + } - ajv.compile(schema); - should.not.exist(output.warning); - }); + ajv.compile(schema) + should.not.exist(output.warning) + }) - it('should NOT throw an error or log a warning given an ignored default', function() { - var output = {}; + it("should NOT throw an error or log a warning given an ignored default", function () { + var output = {} var ajv = new Ajv({ useDefaults: true, strictDefaults: false, - logger: getLogger(output) - }); + logger: getLogger(output), + }) var schema = { oneOf: [ - { enum: ['foo', 'bar'] }, + {enum: ["foo", "bar"]}, { properties: { foo: { - default: true - } - } - } - ] - }; + default: true, + }, + }, + }, + ], + } - ajv.compile(schema); - should.not.exist(output.warning); - }); - }); + ajv.compile(schema) + should.not.exist(output.warning) + }) + }) - describe('strictDefaults = true', function() { - it('should throw an error given an ignored default in the schema root when strictDefaults is true', function() { - var ajv = new Ajv({useDefaults: true, strictDefaults: true}); + describe("strictDefaults = true", function () { + it("should throw an error given an ignored default in the schema root when strictDefaults is true", function () { + var ajv = new Ajv({useDefaults: true, strictDefaults: true}) var schema = { default: 5, - properties: {} - }; - should.throw(function() { ajv.compile(schema); }); - }); + properties: {}, + } + should.throw(function () { + ajv.compile(schema) + }) + }) - it('should throw an error given an ignored default in oneOf when strictDefaults is true', function() { - var ajv = new Ajv({useDefaults: true, strictDefaults: true}); + it("should throw an error given an ignored default in oneOf when strictDefaults is true", function () { + var ajv = new Ajv({useDefaults: true, strictDefaults: true}) var schema = { oneOf: [ - { enum: ['foo', 'bar'] }, + {enum: ["foo", "bar"]}, { properties: { foo: { - default: true - } - } - } - ] - }; - should.throw(function() { ajv.compile(schema); }); - }); - }); + default: true, + }, + }, + }, + ], + } + should.throw(function () { + ajv.compile(schema) + }) + }) + }) - describe('strictDefaults = "log"', function() { - it('should log a warning given an ignored default in the schema root when strictDefaults is "log"', function() { - var output = {}; + describe('strictDefaults = "log"', function () { + it('should log a warning given an ignored default in the schema root when strictDefaults is "log"', function () { + var output = {} var ajv = new Ajv({ useDefaults: true, - strictDefaults: 'log', - logger: getLogger(output) - }); + strictDefaults: "log", + logger: getLogger(output), + }) var schema = { default: 5, - properties: {} - }; - ajv.compile(schema); - should.equal(output.warning, 'default is ignored in the schema root'); - }); + properties: {}, + } + ajv.compile(schema) + should.equal(output.warning, "default is ignored in the schema root") + }) - it('should log a warning given an ignored default in oneOf when strictDefaults is "log"', function() { - var output = {}; + it('should log a warning given an ignored default in oneOf when strictDefaults is "log"', function () { + var output = {} var ajv = new Ajv({ useDefaults: true, - strictDefaults: 'log', - logger: getLogger(output) - }); + strictDefaults: "log", + logger: getLogger(output), + }) var schema = { oneOf: [ - { enum: ['foo', 'bar'] }, + {enum: ["foo", "bar"]}, { properties: { foo: { - default: true - } - } - } - ] - }; - ajv.compile(schema); - should.equal(output.warning, 'default is ignored for: data.foo'); - }); - }); - }); - + default: true, + }, + }, + }, + ], + } + ajv.compile(schema) + should.equal(output.warning, "default is ignored for: data.foo") + }) + }) + }) - describe('useDefaults = false', function() { - describe('strictDefaults = true', function() { - it('should NOT throw an error given an ignored default in the schema root when useDefaults is false', function() { - var ajv = new Ajv({useDefaults: false, strictDefaults: true}); + describe("useDefaults = false", function () { + describe("strictDefaults = true", function () { + it("should NOT throw an error given an ignored default in the schema root when useDefaults is false", function () { + var ajv = new Ajv({useDefaults: false, strictDefaults: true}) var schema = { default: 5, - properties: {} - }; - should.not.throw(function() { ajv.compile(schema); }); - }); + properties: {}, + } + should.not.throw(function () { + ajv.compile(schema) + }) + }) - it('should NOT throw an error given an ignored default in oneOf when useDefaults is false', function() { - var ajv = new Ajv({useDefaults: false, strictDefaults: true}); + it("should NOT throw an error given an ignored default in oneOf when useDefaults is false", function () { + var ajv = new Ajv({useDefaults: false, strictDefaults: true}) var schema = { oneOf: [ - { enum: ['foo', 'bar'] }, + {enum: ["foo", "bar"]}, { properties: { foo: { - default: true - } - } - } - ] - }; - should.not.throw(function() { ajv.compile(schema); }); - }); - }); - }); - + default: true, + }, + }, + }, + ], + } + should.not.throw(function () { + ajv.compile(schema) + }) + }) + }) + }) function getLogger(output) { return { - log: function() { - throw new Error('log should not be called'); + log: function () { + throw new Error("log should not be called") + }, + warn: function (warning) { + output.warning = warning }, - warn: function(warning) { - output.warning = warning; + error: function () { + throw new Error("error should not be called") }, - error: function() { - throw new Error('error should not be called'); - } - }; + } } -}); +}) diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js index 4895b78e46..9143d74e6f 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.js @@ -1,79 +1,82 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('strictKeywords option', function() { - describe('strictKeywords = false', function() { - it('should NOT throw an error or log a warning given an unknown keyword', function() { - var output = {}; +describe("strictKeywords option", function () { + describe("strictKeywords = false", function () { + it("should NOT throw an error or log a warning given an unknown keyword", function () { + var output = {} var ajv = new Ajv({ strictKeywords: false, - logger: getLogger(output) - }); + logger: getLogger(output), + }) var schema = { properties: {}, - unknownKeyword: 1 - }; + unknownKeyword: 1, + } - ajv.compile(schema); - should.not.exist(output.warning); - }); - }); + ajv.compile(schema) + should.not.exist(output.warning) + }) + }) - describe('strictKeywords = true', function() { - it('should throw an error given an unknown keyword in the schema root when strictKeywords is true', function() { - var ajv = new Ajv({strictKeywords: true}); + describe("strictKeywords = true", function () { + it("should throw an error given an unknown keyword in the schema root when strictKeywords is true", function () { + var ajv = new Ajv({strictKeywords: true}) var schema = { properties: {}, - unknownKeyword: 1 - }; - should.throw(function() { ajv.compile(schema); }); - }); - }); + unknownKeyword: 1, + } + should.throw(function () { + ajv.compile(schema) + }) + }) + }) - describe('strictKeywords = "log"', function() { - it('should log a warning given an unknown keyword in the schema root when strictKeywords is "log"', function() { - var output = {}; + describe('strictKeywords = "log"', function () { + it('should log a warning given an unknown keyword in the schema root when strictKeywords is "log"', function () { + var output = {} var ajv = new Ajv({ - strictKeywords: 'log', - logger: getLogger(output) - }); + strictKeywords: "log", + logger: getLogger(output), + }) var schema = { properties: {}, - unknownKeyword: 1 - }; - ajv.compile(schema); - should.equal(output.warning, 'unknown keyword: unknownKeyword'); - }); - }); + unknownKeyword: 1, + } + ajv.compile(schema) + should.equal(output.warning, "unknown keyword: unknownKeyword") + }) + }) - describe('unknown keyword inside schema that has no known keyword in compound keyword', function() { - it('should throw an error given an unknown keyword when strictKeywords is true', function() { - var ajv = new Ajv({strictKeywords: true}); + describe("unknown keyword inside schema that has no known keyword in compound keyword", function () { + it("should throw an error given an unknown keyword when strictKeywords is true", function () { + var ajv = new Ajv({strictKeywords: true}) var schema = { anyOf: [ { - unknownKeyword: 1 - } - ] - }; - should.throw(function() { ajv.compile(schema); }); - }); - }); + unknownKeyword: 1, + }, + ], + } + should.throw(function () { + ajv.compile(schema) + }) + }) + }) function getLogger(output) { return { - log: function() { - throw new Error('log should not be called'); + log: function () { + throw new Error("log should not be called") }, - warn: function(warning) { - output.warning = warning; + warn: function (warning) { + output.warning = warning }, - error: function() { - throw new Error('error should not be called'); - } - }; + error: function () { + throw new Error("error should not be called") + }, + } } -}); +}) diff --git a/spec/options/strictNumbers.spec.js b/spec/options/strictNumbers.spec.js index adf63b026d..9f939b2138 100644 --- a/spec/options/strictNumbers.spec.js +++ b/spec/options/strictNumbers.spec.js @@ -1,55 +1,57 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); +var Ajv = require("../ajv") -describe('structNumbers option', function() { - var ajv; - describe('strictNumbers default', testWithoutStrictNumbers(new Ajv())); - describe('strictNumbers = false', testWithoutStrictNumbers(new Ajv({strictNumbers: false}))); - describe('strictNumbers = true', function() { +describe("structNumbers option", function () { + var ajv + describe("strictNumbers default", testWithoutStrictNumbers(new Ajv())) + describe( + "strictNumbers = false", + testWithoutStrictNumbers(new Ajv({strictNumbers: false})) + ) + describe("strictNumbers = true", function () { beforeEach(function () { - ajv = new Ajv({strictNumbers: true}); - }); + ajv = new Ajv({strictNumbers: true}) + }) - it('should fail validation for NaN/Infinity as type number', function() { - var validate = ajv.compile({type: 'number'}); - validate("1.1").should.equal(false); - validate(1.1).should.equal(true); - validate(1).should.equal(true); - validate(NaN).should.equal(false); - validate(Infinity).should.equal(false); - }); - - it('should fail validation for NaN as type integer', function() { - var validate = ajv.compile({type: 'integer'}); - validate("1.1").should.equal(false); - validate(1.1).should.equal(false); - validate(1).should.equal(true); - validate(NaN).should.equal(false); - validate(Infinity).should.equal(false); - }); - }); -}); + it("should fail validation for NaN/Infinity as type number", function () { + var validate = ajv.compile({type: "number"}) + validate("1.1").should.equal(false) + validate(1.1).should.equal(true) + validate(1).should.equal(true) + validate(NaN).should.equal(false) + validate(Infinity).should.equal(false) + }) + it("should fail validation for NaN as type integer", function () { + var validate = ajv.compile({type: "integer"}) + validate("1.1").should.equal(false) + validate(1.1).should.equal(false) + validate(1).should.equal(true) + validate(NaN).should.equal(false) + validate(Infinity).should.equal(false) + }) + }) +}) function testWithoutStrictNumbers(_ajv) { return function () { - it('should NOT fail validation for NaN/Infinity as type number', function() { - var validate = _ajv.compile({type: 'number'}); - validate("1.1").should.equal(false); - validate(1.1).should.equal(true); - validate(1).should.equal(true); - validate(NaN).should.equal(true); - validate(Infinity).should.equal(true); - }); + it("should NOT fail validation for NaN/Infinity as type number", function () { + var validate = _ajv.compile({type: "number"}) + validate("1.1").should.equal(false) + validate(1.1).should.equal(true) + validate(1).should.equal(true) + validate(NaN).should.equal(true) + validate(Infinity).should.equal(true) + }) - it('should NOT fail validation for NaN/Infinity as type integer', function() { - var validate = _ajv.compile({type: 'integer'}); - validate("1.1").should.equal(false); - validate(1.1).should.equal(false); - validate(1).should.equal(true); - validate(NaN).should.equal(false); - validate(Infinity).should.equal(true); - }); - }; + it("should NOT fail validation for NaN/Infinity as type integer", function () { + var validate = _ajv.compile({type: "integer"}) + validate("1.1").should.equal(false) + validate(1.1).should.equal(false) + validate(1).should.equal(true) + validate(NaN).should.equal(false) + validate(Infinity).should.equal(true) + }) + } } diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.js index 6e6dfde3d8..af2b4d7059 100644 --- a/spec/options/unknownFormats.spec.js +++ b/spec/options/unknownFormats.spec.js @@ -1,108 +1,107 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var should = require('../chai').should(); +var Ajv = require("../ajv") +var should = require("../chai").should() - -describe('unknownFormats option', function() { - describe('= true (default)', function() { - it('should fail schema compilation if unknown format is used', function() { - test(new Ajv); - test(new Ajv({unknownFormats: true})); +describe("unknownFormats option", function () { + describe("= true (default)", function () { + it("should fail schema compilation if unknown format is used", function () { + test(new Ajv()) + test(new Ajv({unknownFormats: true})) function test(ajv) { - should.throw(function() { - ajv.compile({ format: 'unknown' }); - }); + should.throw(function () { + ajv.compile({format: "unknown"}) + }) } - }); + }) - it('should fail validation if unknown format is used via $data', function() { - test(new Ajv({$data: true})); - test(new Ajv({$data: true, unknownFormats: true})); + it("should fail validation if unknown format is used via $data", function () { + test(new Ajv({$data: true})) + test(new Ajv({$data: true, unknownFormats: true})) function test(ajv) { var validate = ajv.compile({ properties: { - foo: { format: { $data: '1/bar' } }, - bar: { type: 'string' } - } - }); + foo: {format: {$data: "1/bar"}}, + bar: {type: "string"}, + }, + }) - validate({foo: 1, bar: 'unknown'}) .should.equal(false); - validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); - validate({foo: '20161016', bar: 'date'}) .should.equal(false); - validate({foo: '20161016'}) .should.equal(true); + validate({foo: 1, bar: "unknown"}).should.equal(false) + validate({foo: "2016-10-16", bar: "date"}).should.equal(true) + validate({foo: "20161016", bar: "date"}).should.equal(false) + validate({foo: "20161016"}).should.equal(true) - validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(false); + validate({foo: "2016-10-16", bar: "unknown"}).should.equal(false) } - }); - }); + }) + }) - describe('= "ignore (default before 5.0.0)"', function() { - it('should pass schema compilation and be valid if unknown format is used', function() { - test(new Ajv({unknownFormats: 'ignore'})); + describe('= "ignore (default before 5.0.0)"', function () { + it("should pass schema compilation and be valid if unknown format is used", function () { + test(new Ajv({unknownFormats: "ignore"})) function test(ajv) { - var validate = ajv.compile({ format: 'unknown' }); - validate('anything') .should.equal(true); + var validate = ajv.compile({format: "unknown"}) + validate("anything").should.equal(true) } - }); + }) - it('should be valid if unknown format is used via $data', function() { - test(new Ajv({$data: true, unknownFormats: 'ignore'})); + it("should be valid if unknown format is used via $data", function () { + test(new Ajv({$data: true, unknownFormats: "ignore"})) function test(ajv) { var validate = ajv.compile({ properties: { - foo: { format: { $data: '1/bar' } }, - bar: { type: 'string' } - } - }); - - validate({foo: 1, bar: 'unknown'}) .should.equal(true); - validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); - validate({foo: '20161016', bar: 'date'}) .should.equal(false); - validate({foo: '20161016'}) .should.equal(true); - validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(true); + foo: {format: {$data: "1/bar"}}, + bar: {type: "string"}, + }, + }) + + validate({foo: 1, bar: "unknown"}).should.equal(true) + validate({foo: "2016-10-16", bar: "date"}).should.equal(true) + validate({foo: "20161016", bar: "date"}).should.equal(false) + validate({foo: "20161016"}).should.equal(true) + validate({foo: "2016-10-16", bar: "unknown"}).should.equal(true) } - }); - }); + }) + }) - describe('= [String]', function() { - it('should pass schema compilation and be valid if allowed unknown format is used', function() { - test(new Ajv({unknownFormats: ['allowed']})); + describe("= [String]", function () { + it("should pass schema compilation and be valid if allowed unknown format is used", function () { + test(new Ajv({unknownFormats: ["allowed"]})) function test(ajv) { - var validate = ajv.compile({ format: 'allowed' }); - validate('anything') .should.equal(true); + var validate = ajv.compile({format: "allowed"}) + validate("anything").should.equal(true) - should.throw(function() { - ajv.compile({ format: 'unknown' }); - }); + should.throw(function () { + ajv.compile({format: "unknown"}) + }) } - }); + }) - it('should be valid if allowed unknown format is used via $data', function() { - test(new Ajv({$data: true, unknownFormats: ['allowed']})); + it("should be valid if allowed unknown format is used via $data", function () { + test(new Ajv({$data: true, unknownFormats: ["allowed"]})) function test(ajv) { var validate = ajv.compile({ properties: { - foo: { format: { $data: '1/bar' } }, - bar: { type: 'string' } - } - }); - - validate({foo: 1, bar: 'allowed'}) .should.equal(true); - validate({foo: 1, bar: 'unknown'}) .should.equal(false); - validate({foo: '2016-10-16', bar: 'date'}) .should.equal(true); - validate({foo: '20161016', bar: 'date'}) .should.equal(false); - validate({foo: '20161016'}) .should.equal(true); - - validate({foo: '2016-10-16', bar: 'allowed'}) .should.equal(true); - validate({foo: '2016-10-16', bar: 'unknown'}) .should.equal(false); + foo: {format: {$data: "1/bar"}}, + bar: {type: "string"}, + }, + }) + + validate({foo: 1, bar: "allowed"}).should.equal(true) + validate({foo: 1, bar: "unknown"}).should.equal(false) + validate({foo: "2016-10-16", bar: "date"}).should.equal(true) + validate({foo: "20161016", bar: "date"}).should.equal(false) + validate({foo: "20161016"}).should.equal(true) + + validate({foo: "2016-10-16", bar: "allowed"}).should.equal(true) + validate({foo: "2016-10-16", bar: "unknown"}).should.equal(false) } - }); - }); -}); + }) + }) +}) diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index 7a12e8423b..b660a0c5ba 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -1,223 +1,229 @@ -'use strict'; +"use strict" -var Ajv = require('../ajv'); -var getAjvInstances = require('../ajv_instances'); -require('../chai').should(); +var Ajv = require("../ajv") +var getAjvInstances = require("../ajv_instances") +require("../chai").should() +describe("useDefaults options", function () { + it("should replace undefined property with default value", function () { + var instances = getAjvInstances( + { + allErrors: true, + loopRequired: 3, + }, + {useDefaults: true} + ) -describe('useDefaults options', function() { - it('should replace undefined property with default value', function() { - var instances = getAjvInstances({ - allErrors: true, - loopRequired: 3 - }, { useDefaults: true }); - - instances.forEach(test); - + instances.forEach(test) function test(ajv) { var schema = { properties: { - foo: { type: 'string', default: 'abc' }, - bar: { type: 'number', default: 1 }, - baz: { type: 'boolean', default: false }, - nil: { type: 'null', default: null }, - obj: { type: 'object', default: {} }, - arr: { type: 'array', default: [] } + foo: {type: "string", default: "abc"}, + bar: {type: "number", default: 1}, + baz: {type: "boolean", default: false}, + nil: {type: "null", default: null}, + obj: {type: "object", default: {}}, + arr: {type: "array", default: []}, }, - required: ['foo', 'bar', 'baz', 'nil', 'obj', 'arr'], - minProperties: 6 - }; - - var validate = ajv.compile(schema); - - var data = {}; - validate(data) .should.equal(true); - data .should.eql({ foo: 'abc', bar: 1, baz: false, nil: null, obj: {}, arr:[] }); + required: ["foo", "bar", "baz", "nil", "obj", "arr"], + minProperties: 6, + } - data = { foo: 'foo', bar: 2, obj: { test: true } }; - validate(data) .should.equal(true); - data .should.eql({ foo: 'foo', bar: 2, baz: false, nil: null, obj: { test: true }, arr:[] }); + var validate = ajv.compile(schema) + + var data = {} + validate(data).should.equal(true) + data.should.eql({ + foo: "abc", + bar: 1, + baz: false, + nil: null, + obj: {}, + arr: [], + }) + + data = {foo: "foo", bar: 2, obj: {test: true}} + validate(data).should.equal(true) + data.should.eql({ + foo: "foo", + bar: 2, + baz: false, + nil: null, + obj: {test: true}, + arr: [], + }) } - }); + }) - it('should replace undefined item with default value', function() { - test(new Ajv({ useDefaults: true })); - test(new Ajv({ useDefaults: true, allErrors: true })); + it("should replace undefined item with default value", function () { + test(new Ajv({useDefaults: true})) + test(new Ajv({useDefaults: true, allErrors: true})) function test(ajv) { var schema = { items: [ - { type: 'string', default: 'abc' }, - { type: 'number', default: 1 }, - { type: 'boolean', default: false } + {type: "string", default: "abc"}, + {type: "number", default: 1}, + {type: "boolean", default: false}, ], - minItems: 3 - }; + minItems: 3, + } - var validate = ajv.compile(schema); + var validate = ajv.compile(schema) - var data = []; - validate(data) .should.equal(true); - data .should.eql([ 'abc', 1, false ]); + var data = [] + validate(data).should.equal(true) + data.should.eql(["abc", 1, false]) - data = [ 'foo' ]; - validate(data) .should.equal(true); - data .should.eql([ 'foo', 1, false ]); + data = ["foo"] + validate(data).should.equal(true) + data.should.eql(["foo", 1, false]) - data = ['foo', 2,'false']; - validate(data) .should.equal(false); - validate.errors .should.have.length(1); - data .should.eql([ 'foo', 2, 'false' ]); + data = ["foo", 2, "false"] + validate(data).should.equal(false) + validate.errors.should.have.length(1) + data.should.eql(["foo", 2, "false"]) } - }); + }) - it('should apply default in "then" subschema (issue #635)', function() { - test(new Ajv({ useDefaults: true })); - test(new Ajv({ useDefaults: true, allErrors: true })); + it('should apply default in "then" subschema (issue #635)', function () { + test(new Ajv({useDefaults: true})) + test(new Ajv({useDefaults: true, allErrors: true})) function test(ajv) { var schema = { - if: { required: ['foo'] }, + if: {required: ["foo"]}, then: { properties: { - bar: { default: 2 } - } + bar: {default: 2}, + }, }, else: { properties: { - foo: { default: 1 } - } - } - }; + foo: {default: 1}, + }, + }, + } - var validate = ajv.compile(schema); + var validate = ajv.compile(schema) - var data = {}; - validate(data) .should.equal(true); - data .should.eql({foo: 1}); + var data = {} + validate(data).should.equal(true) + data.should.eql({foo: 1}) - data = {foo: 1}; - validate(data) .should.equal(true); - data .should.eql({foo: 1, bar: 2}); + data = {foo: 1} + validate(data).should.equal(true) + data.should.eql({foo: 1, bar: 2}) } - }); - - - describe('useDefaults: by value / by reference', function() { - describe('using by value', function() { - it('should NOT modify underlying defaults when modifying validated data', function() { - test('value', new Ajv({ useDefaults: true })); - test('value', new Ajv({ useDefaults: true, allErrors: true })); - }); - }); - - describe('using by reference', function() { - it('should modify underlying defaults when modifying validated data', function() { - test('reference', new Ajv({ useDefaults: 'shared' })); - test('reference', new Ajv({ useDefaults: 'shared', allErrors: true })); - }); - }); + }) + + describe("useDefaults: by value / by reference", function () { + describe("using by value", function () { + it("should NOT modify underlying defaults when modifying validated data", function () { + test("value", new Ajv({useDefaults: true})) + test("value", new Ajv({useDefaults: true, allErrors: true})) + }) + }) + + describe("using by reference", function () { + it("should modify underlying defaults when modifying validated data", function () { + test("reference", new Ajv({useDefaults: "shared"})) + test("reference", new Ajv({useDefaults: "shared", allErrors: true})) + }) + }) function test(useDefaultsMode, ajv) { var schema = { properties: { items: { - type: 'array', - default: ['a-default'] - } - } - }; - - var validate = ajv.compile(schema); - - var data = {}; - validate(data) .should.equal(true); - data.items .should.eql([ 'a-default' ]); - - data.items.push('another-value'); - data.items .should.eql([ 'a-default', 'another-value' ]); - - var data2 = {}; - validate(data2) .should.equal(true); - - if (useDefaultsMode == 'reference') - data2.items .should.eql([ 'a-default', 'another-value' ]); - else if (useDefaultsMode == 'value') - data2.items .should.eql([ 'a-default' ]); - else - throw new Error('unknown useDefaults mode'); - } - }); + type: "array", + default: ["a-default"], + }, + }, + } + var validate = ajv.compile(schema) - describe('defaults with "empty" values', function() { - var schema, data; + var data = {} + validate(data).should.equal(true) + data.items.should.eql(["a-default"]) - beforeEach(function() { + data.items.push("another-value") + data.items.should.eql(["a-default", "another-value"]) + + var data2 = {} + validate(data2).should.equal(true) + + if (useDefaultsMode == "reference") + data2.items.should.eql(["a-default", "another-value"]) + else if (useDefaultsMode == "value") data2.items.should.eql(["a-default"]) + else throw new Error("unknown useDefaults mode") + } + }) + + describe('defaults with "empty" values', function () { + var schema, data + + beforeEach(function () { schema = { properties: { obj: { properties: { - str: {default: 'foo'}, + str: {default: "foo"}, n1: {default: 1}, n2: {default: 2}, - n3: {default: 3} - } + n3: {default: 3}, + }, }, arr: { - items: [ - {default: 'foo'}, - {default: 1}, - {default: 2}, - {default: 3} - ] - } - } - }; + items: [{default: "foo"}, {default: 1}, {default: 2}, {default: 3}], + }, + }, + } data = { obj: { - str: '', + str: "", n1: null, - n2: undefined + n2: undefined, }, - arr: ['', null, undefined] - }; - }); + arr: ["", null, undefined], + } + }) - it('should NOT assign defaults when useDefaults is true/"shared"', function() { - test(new Ajv({useDefaults: true})); - test(new Ajv({useDefaults: 'shared'})); + it('should NOT assign defaults when useDefaults is true/"shared"', function () { + test(new Ajv({useDefaults: true})) + test(new Ajv({useDefaults: "shared"})) function test(ajv) { - var validate = ajv.compile(schema); - validate(data) .should.equal(true); - data .should.eql({ + var validate = ajv.compile(schema) + validate(data).should.equal(true) + data.should.eql({ obj: { - str: '', + str: "", n1: null, n2: 2, - n3: 3 + n3: 3, }, - arr: ['', null, 2, 3] - }); + arr: ["", null, 2, 3], + }) } - }); + }) - it('should assign defaults when useDefaults = "empty"', function() { - var ajv = new Ajv({useDefaults: 'empty'}); - var validate = ajv.compile(schema); - validate(data) .should.equal(true); - data .should.eql({ + it('should assign defaults when useDefaults = "empty"', function () { + var ajv = new Ajv({useDefaults: "empty"}) + var validate = ajv.compile(schema) + validate(data).should.equal(true) + data.should.eql({ obj: { - str: 'foo', + str: "foo", n1: 1, n2: 2, - n3: 3 + n3: 3, }, - arr: ['foo', 1, 2, 3] - }); - }); - }); -}); + arr: ["foo", 1, 2, 3], + }) + }) + }) +}) diff --git a/spec/promise.js b/spec/promise.js index 8c55c8750d..98caf040ee 100644 --- a/spec/promise.js +++ b/spec/promise.js @@ -1,11 +1,11 @@ -'use strict'; +"use strict" -var g = typeof global == 'object' ? global : - typeof window == 'object' ? window : this; +var g = + typeof global == "object" ? global : typeof window == "object" ? window : this if (!g.Promise) { - g.Promise = require('' + 'bluebird'); - g.Promise.config({ warnings: false }); + g.Promise = require("" + "bluebird") + g.Promise.config({warnings: false}) } -module.exports = g.Promise; +module.exports = g.Promise diff --git a/spec/remotes/buu.json b/spec/remotes/buu.json index f3d905c4a1..df6af16b09 100644 --- a/spec/remotes/buu.json +++ b/spec/remotes/buu.json @@ -4,7 +4,7 @@ "buu": { "type": "object", "properties": { - "bar": { "$ref": "bar.json" } + "bar": {"$ref": "bar.json"} } } } diff --git a/spec/remotes/first.json b/spec/remotes/first.json index 9fdb8d486f..0ddfe2a3fe 100644 --- a/spec/remotes/first.json +++ b/spec/remotes/first.json @@ -1,4 +1,4 @@ { - "$id": "http://localhost:1234/first.json", - "type": "string" + "$id": "http://localhost:1234/first.json", + "type": "string" } diff --git a/spec/remotes/foo.json b/spec/remotes/foo.json index 9e565666f1..2ea24f33b4 100644 --- a/spec/remotes/foo.json +++ b/spec/remotes/foo.json @@ -2,6 +2,6 @@ "$id": "http://localhost:1234/foo.json", "type": "object", "properties": { - "bar": { "$ref": "bar.json" } + "bar": {"$ref": "bar.json"} } } diff --git a/spec/remotes/hyper-schema.json b/spec/remotes/hyper-schema.json index 349ee2de95..59787c54df 100644 --- a/spec/remotes/hyper-schema.json +++ b/spec/remotes/hyper-schema.json @@ -1,69 +1,65 @@ { - "$schema": "http://json-schema.org/draft-07/hyper-schema#", - "$id": "http://json-schema.org/draft-07/hyper-schema#", - "title": "JSON Hyper-Schema", - "definitions": { - "schemaArray": { - "allOf": [ - { "$ref": "http://json-schema.org/draft-07/schema#/definitions/schemaArray" }, - { - "items": { "$ref": "#" } - } - ] + "$schema": "http://json-schema.org/draft-07/hyper-schema#", + "$id": "http://json-schema.org/draft-07/hyper-schema#", + "title": "JSON Hyper-Schema", + "definitions": { + "schemaArray": { + "allOf": [ + { + "$ref": "http://json-schema.org/draft-07/schema#/definitions/schemaArray" + }, + { + "items": {"$ref": "#"} } + ] + } + }, + "allOf": [{"$ref": "http://json-schema.org/draft-07/schema#"}], + "properties": { + "additionalItems": {"$ref": "#"}, + "additionalProperties": {"$ref": "#"}, + "dependencies": { + "additionalProperties": { + "anyOf": [{"$ref": "#"}, {"type": "array"}] + } + }, + "items": { + "anyOf": [{"$ref": "#"}, {"$ref": "#/definitions/schemaArray"}] + }, + "definitions": { + "additionalProperties": {"$ref": "#"} + }, + "patternProperties": { + "additionalProperties": {"$ref": "#"} }, - "allOf": [ { "$ref": "http://json-schema.org/draft-07/schema#" } ], "properties": { - "additionalItems": { "$ref": "#" }, - "additionalProperties": { "$ref": "#"}, - "dependencies": { - "additionalProperties": { - "anyOf": [ - { "$ref": "#" }, - { "type": "array" } - ] - } - }, - "items": { - "anyOf": [ - { "$ref": "#" }, - { "$ref": "#/definitions/schemaArray" } - ] - }, - "definitions": { - "additionalProperties": { "$ref": "#" } - }, - "patternProperties": { - "additionalProperties": { "$ref": "#" } - }, - "properties": { - "additionalProperties": { "$ref": "#" } - }, - "if": {"$ref": "#"}, - "then": {"$ref": "#"}, - "else": {"$ref": "#"}, - "allOf": { "$ref": "#/definitions/schemaArray" }, - "anyOf": { "$ref": "#/definitions/schemaArray" }, - "oneOf": { "$ref": "#/definitions/schemaArray" }, - "not": { "$ref": "#" }, - "contains": { "$ref": "#" }, - "propertyNames": { "$ref": "#" }, + "additionalProperties": {"$ref": "#"} + }, + "if": {"$ref": "#"}, + "then": {"$ref": "#"}, + "else": {"$ref": "#"}, + "allOf": {"$ref": "#/definitions/schemaArray"}, + "anyOf": {"$ref": "#/definitions/schemaArray"}, + "oneOf": {"$ref": "#/definitions/schemaArray"}, + "not": {"$ref": "#"}, + "contains": {"$ref": "#"}, + "propertyNames": {"$ref": "#"}, - "base": { - "type": "string", - "format": "uri-template" - }, - "links": { - "type": "array", - "items": { - "$ref": "http://json-schema.org/draft-07/hyper-schema#/links" - } - } + "base": { + "type": "string", + "format": "uri-template" }, - "links": [ - { - "rel": "self", - "href": "{+%24id}" - } - ] + "links": { + "type": "array", + "items": { + "$ref": "http://json-schema.org/draft-07/hyper-schema#/links" + } + } + }, + "links": [ + { + "rel": "self", + "href": "{+%24id}" + } + ] } diff --git a/spec/remotes/name.json b/spec/remotes/name.json index 3fa219a6eb..96ad9227a8 100644 --- a/spec/remotes/name.json +++ b/spec/remotes/name.json @@ -1,10 +1,7 @@ { "definitions": { "orNull": { - "anyOf": [ - { "type": "null" }, - { "$ref": "#" } - ] + "anyOf": [{"type": "null"}, {"$ref": "#"}] } }, "type": "string" diff --git a/spec/remotes/node.json b/spec/remotes/node.json index 41120c1efe..b2a09868e7 100644 --- a/spec/remotes/node.json +++ b/spec/remotes/node.json @@ -3,8 +3,8 @@ "description": "node", "type": "object", "properties": { - "value": { "type": "number" }, - "subtree": { "$ref": "tree.json" } + "value": {"type": "number"}, + "subtree": {"$ref": "tree.json"} }, "required": ["value"] } diff --git a/spec/remotes/scope_change.json b/spec/remotes/scope_change.json index 67991f9716..612b54725a 100644 --- a/spec/remotes/scope_change.json +++ b/spec/remotes/scope_change.json @@ -12,9 +12,9 @@ "baz": { "$id": "folder/", "type": "array", - "items": { "$ref": "folderInteger.json" }, + "items": {"$ref": "folderInteger.json"}, "bar": { - "items": { "$ref": "folderInteger.json" } + "items": {"$ref": "folderInteger.json"} } } } diff --git a/spec/remotes/second.json b/spec/remotes/second.json index 56e32ebfd2..2bce3a6e1e 100644 --- a/spec/remotes/second.json +++ b/spec/remotes/second.json @@ -1,7 +1,7 @@ { - "$id": "http://localhost:1234/second.json", - "type": "object", - "properties": { - "first": { "$ref": "first.json" } - } + "$id": "http://localhost:1234/second.json", + "type": "object", + "properties": { + "first": {"$ref": "first.json"} + } } diff --git a/spec/remotes/tree.json b/spec/remotes/tree.json index 39df56141c..daf3657b72 100644 --- a/spec/remotes/tree.json +++ b/spec/remotes/tree.json @@ -3,10 +3,10 @@ "description": "tree of nodes", "type": "object", "properties": { - "meta": { "type": "string" }, + "meta": {"type": "string"}, "nodes": { "type": "array", - "items": { "$ref": "node.json"} + "items": {"$ref": "node.json"} } }, "required": ["meta", "nodes"] diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index f04eabe96f..e8aac93382 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -1,251 +1,237 @@ -'use strict'; +"use strict" -var Ajv = require('./ajv') - , should = require('./chai').should() - , getAjvInstances = require('./ajv_instances'); +var Ajv = require("./ajv"), + should = require("./chai").should(), + getAjvInstances = require("./ajv_instances") +describe("resolve", function () { + var instances -describe('resolve', function () { - var instances; - - beforeEach(function() { + beforeEach(function () { instances = getAjvInstances({ - allErrors: true, - verbose: true, - inlineRefs: false - }); - }); - - describe('resolve.ids method', function() { - it('should resolve ids in schema', function() { + allErrors: true, + verbose: true, + inlineRefs: false, + }) + }) + + describe("resolve.ids method", function () { + it("should resolve ids in schema", function () { // Example from http://json-schema.org/latest/json-schema-core.html#anchor29 var schema = { - "$id": "http://x.y.z/rootschema.json#", - "schema1": { - "$id": "#foo", - "description": "schema1", - "type": "integer" + $id: "http://x.y.z/rootschema.json#", + schema1: { + $id: "#foo", + description: "schema1", + type: "integer", }, - "schema2": { - "$id": "otherschema.json", - "description": "schema2", - "nested": { - "$id": "#bar", - "description": "nested", - "type": "string" + schema2: { + $id: "otherschema.json", + description: "schema2", + nested: { + $id: "#bar", + description: "nested", + type: "string", + }, + alsonested: { + $id: "t/inner.json#a", + description: "alsonested", + type: "boolean", }, - "alsonested": { - "$id": "t/inner.json#a", - "description": "alsonested", - "type": "boolean" - } }, - "schema3": { - "$id": "some://where.else/completely#", - "description": "schema3", - "type": "null" + schema3: { + $id: "some://where.else/completely#", + description: "schema3", + type: "null", }, - "properties": { - "foo": { "$ref": "#foo" }, - "bar": { "$ref": "otherschema.json#bar" }, - "baz": { "$ref": "t/inner.json#a" }, - "bax": { "$ref": "some://where.else/completely#" } + properties: { + foo: {$ref: "#foo"}, + bar: {$ref: "otherschema.json#bar"}, + baz: {$ref: "t/inner.json#a"}, + bax: {$ref: "some://where.else/completely#"}, }, - "required": [ "foo", "bar", "baz", "bax" ] - }; + required: ["foo", "bar", "baz", "bax"], + } instances.forEach(function (ajv) { - var validate = ajv.compile(schema); - var data = { foo: 1, bar: 'abc', baz: true, bax: null }; - validate(data) .should.equal(true); - }); - }); + var validate = ajv.compile(schema) + var data = {foo: 1, bar: "abc", baz: true, bax: null} + validate(data).should.equal(true) + }) + }) - - it('should throw if the same id resolves to two different schemas', function() { + it("should throw if the same id resolves to two different schemas", function () { instances.forEach(function (ajv) { ajv.compile({ - "$id": "http://example.com/1.json", - "type": "integer" - }); - should.throw(function() { + $id: "http://example.com/1.json", + type: "integer", + }) + should.throw(function () { ajv.compile({ - "additionalProperties": { - "$id": "http://example.com/1.json", - "type": "string" - } - }); - }); - - should.throw(function() { + additionalProperties: { + $id: "http://example.com/1.json", + type: "string", + }, + }) + }) + + should.throw(function () { ajv.compile({ - "items": { - "$id": "#int", - "type": "integer" + items: { + $id: "#int", + type: "integer", + }, + additionalProperties: { + $id: "#int", + type: "string", }, - "additionalProperties": { - "$id": "#int", - "type": "string" - } - }); - }); - }); - }); - - it('should resolve ids defined as urn\'s (issue #423)', function() { + }) + }) + }) + }) + + it("should resolve ids defined as urn's (issue #423)", function () { var schema = { - "type": "object", - "properties": { - "ip1": { - "$id": "urn:some:ip:prop", - "type": "string", - "format": "ipv4" + type: "object", + properties: { + ip1: { + $id: "urn:some:ip:prop", + type: "string", + format: "ipv4", + }, + ip2: { + $ref: "urn:some:ip:prop", }, - "ip2": { - "$ref": "urn:some:ip:prop" - } }, - "required": [ - "ip1", - "ip2" - ] - }; + required: ["ip1", "ip2"], + } var data = { - "ip1": "0.0.0.0", - "ip2": "0.0.0.0" - }; + ip1: "0.0.0.0", + ip2: "0.0.0.0", + } + instances.forEach(function (ajv) { + var validate = ajv.compile(schema) + validate(data).should.equal(true) + }) + }) + }) + + describe("protocol-relative URIs", function () { + it("should resolve fragment", function () { instances.forEach(function (ajv) { - var validate = ajv.compile(schema); - validate(data) .should.equal(true); - }); - }); - }); - - - describe('protocol-relative URIs', function() { - it('should resolve fragment', function() { - instances.forEach(function(ajv) { var schema = { - "$id": "//e.com/types", - "definitions": { - "int": { "type": "integer" } - } - }; - - ajv.addSchema(schema); - var validate = ajv.compile({ $ref: '//e.com/types#/definitions/int' }); - validate(1) .should.equal(true); - validate('foo') .should.equal(false); - }); - }); - }); + $id: "//e.com/types", + definitions: { + int: {type: "integer"}, + }, + } + ajv.addSchema(schema) + var validate = ajv.compile({$ref: "//e.com/types#/definitions/int"}) + validate(1).should.equal(true) + validate("foo").should.equal(false) + }) + }) + }) - describe('missing schema error', function() { - this.timeout(4000); + describe("missing schema error", function () { + this.timeout(4000) - it('should contain missingRef and missingSchema', function() { + it("should contain missingRef and missingSchema", function () { testMissingSchemaError({ - baseId: 'http://example.com/1.json', - ref: 'http://another.com/int.json', - expectedMissingRef: 'http://another.com/int.json', - expectedMissingSchema: 'http://another.com/int.json' - }); - }); - - it('should resolve missingRef and missingSchema relative to base id', function() { + baseId: "http://example.com/1.json", + ref: "http://another.com/int.json", + expectedMissingRef: "http://another.com/int.json", + expectedMissingSchema: "http://another.com/int.json", + }) + }) + + it("should resolve missingRef and missingSchema relative to base id", function () { testMissingSchemaError({ - baseId: 'http://example.com/folder/1.json', - ref: 'int.json', - expectedMissingRef: 'http://example.com/folder/int.json', - expectedMissingSchema: 'http://example.com/folder/int.json' - }); - }); - - it('should resolve missingRef and missingSchema relative to base id from root', function() { + baseId: "http://example.com/folder/1.json", + ref: "int.json", + expectedMissingRef: "http://example.com/folder/int.json", + expectedMissingSchema: "http://example.com/folder/int.json", + }) + }) + + it("should resolve missingRef and missingSchema relative to base id from root", function () { testMissingSchemaError({ - baseId: 'http://example.com/folder/1.json', - ref: '/int.json', - expectedMissingRef: 'http://example.com/int.json', - expectedMissingSchema: 'http://example.com/int.json' - }); - }); - - it('missingRef should and missingSchema should NOT include JSON path (hash fragment)', function() { + baseId: "http://example.com/folder/1.json", + ref: "/int.json", + expectedMissingRef: "http://example.com/int.json", + expectedMissingSchema: "http://example.com/int.json", + }) + }) + + it("missingRef should and missingSchema should NOT include JSON path (hash fragment)", function () { testMissingSchemaError({ - baseId: 'http://example.com/1.json', - ref: 'int.json#/definitions/positive', - expectedMissingRef: 'http://example.com/int.json#/definitions/positive', - expectedMissingSchema: 'http://example.com/int.json' - }); - }); - - it('should throw missing schema error if same path exist in the current schema but id is different (issue #220)', function() { + baseId: "http://example.com/1.json", + ref: "int.json#/definitions/positive", + expectedMissingRef: "http://example.com/int.json#/definitions/positive", + expectedMissingSchema: "http://example.com/int.json", + }) + }) + + it("should throw missing schema error if same path exist in the current schema but id is different (issue #220)", function () { testMissingSchemaError({ - baseId: 'http://example.com/parent.json', - ref: 'object.json#/properties/a', - expectedMissingRef: 'http://example.com/object.json#/properties/a', - expectedMissingSchema: 'http://example.com/object.json' - }); - }); - + baseId: "http://example.com/parent.json", + ref: "object.json#/properties/a", + expectedMissingRef: "http://example.com/object.json#/properties/a", + expectedMissingSchema: "http://example.com/object.json", + }) + }) function testMissingSchemaError(opts) { instances.forEach(function (ajv) { try { ajv.compile({ - "$id": opts.baseId, - "properties": { "a": { "$ref": opts.ref } } - }); - } catch(e) { - e.missingRef .should.equal(opts.expectedMissingRef); - e.missingSchema .should.equal(opts.expectedMissingSchema); + $id: opts.baseId, + properties: {a: {$ref: opts.ref}}, + }) + } catch (e) { + e.missingRef.should.equal(opts.expectedMissingRef) + e.missingSchema.should.equal(opts.expectedMissingSchema) } - }); + }) } - }); + }) - - describe('inline referenced schemas without refs in them', function() { + describe("inline referenced schemas without refs in them", function () { var schemas = [ - { $id: 'http://e.com/obj.json#', - properties: { a: { $ref: 'int.json#' } } }, - { $id: 'http://e.com/int.json#', - type: 'integer', minimum: 2, maximum: 4 }, - { $id: 'http://e.com/obj1.json#', - definitions: { int: { type: 'integer', minimum: 2, maximum: 4 } }, - properties: { a: { $ref: '#/definitions/int' } } }, - { $id: 'http://e.com/list.json#', - items: { $ref: 'obj.json#' } } - ]; - - it('by default should inline schema if it doesn\'t contain refs', function() { - var ajv = new Ajv({ schemas: schemas }); - testSchemas(ajv, true); - }); - - - it('should NOT inline schema if option inlineRefs == false', function() { - var ajv = new Ajv({ schemas: schemas, inlineRefs: false }); - testSchemas(ajv, false); - }); - - - it('should inline schema if option inlineRefs is bigger than number of keys in referenced schema', function() { - var ajv = new Ajv({ schemas: schemas, inlineRefs: 3 }); - testSchemas(ajv, true); - }); - - - it('should NOT inline schema if option inlineRefs is less than number of keys in referenced schema', function() { - var ajv = new Ajv({ schemas: schemas, inlineRefs: 2 }); - testSchemas(ajv, false); - }); - - - it('should avoid schema substitution when refs are inlined (issue #77)', function() { - var ajv = new Ajv({ verbose: true }); + {$id: "http://e.com/obj.json#", properties: {a: {$ref: "int.json#"}}}, + {$id: "http://e.com/int.json#", type: "integer", minimum: 2, maximum: 4}, + { + $id: "http://e.com/obj1.json#", + definitions: {int: {type: "integer", minimum: 2, maximum: 4}}, + properties: {a: {$ref: "#/definitions/int"}}, + }, + {$id: "http://e.com/list.json#", items: {$ref: "obj.json#"}}, + ] + + it("by default should inline schema if it doesn't contain refs", function () { + var ajv = new Ajv({schemas: schemas}) + testSchemas(ajv, true) + }) + + it("should NOT inline schema if option inlineRefs == false", function () { + var ajv = new Ajv({schemas: schemas, inlineRefs: false}) + testSchemas(ajv, false) + }) + + it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", function () { + var ajv = new Ajv({schemas: schemas, inlineRefs: 3}) + testSchemas(ajv, true) + }) + + it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", function () { + var ajv = new Ajv({schemas: schemas, inlineRefs: 2}) + testSchemas(ajv, false) + }) + + it("should avoid schema substitution when refs are inlined (issue #77)", function () { + var ajv = new Ajv({verbose: true}) var schemaMessage = { $schema: "http://json-schema.org/draft-07/schema#", @@ -255,12 +241,12 @@ describe('resolve', function () { properties: { header: { allOf: [ - { $ref: "header.json" }, - { properties: { msgType: { "enum": [0] } } } - ] - } - } - }; + {$ref: "header.json"}, + {properties: {msgType: {enum: [0]}}}, + ], + }, + }, + } // header schema var schemaHeader = { @@ -270,73 +256,72 @@ describe('resolve', function () { properties: { version: { type: "integer", - maximum: 5 + maximum: 5, }, - msgType: { type: "integer" } + msgType: {type: "integer"}, }, - required: ["version", "msgType"] - }; + required: ["version", "msgType"], + } // a good message var validMessage = { header: { version: 4, - msgType: 0 - } - }; + msgType: 0, + }, + } // a bad message var invalidMessage = { header: { version: 6, - msgType: 0 - } - }; + msgType: 0, + }, + } // add schemas and get validator function - ajv.addSchema(schemaHeader); - ajv.addSchema(schemaMessage); - var v = ajv.getSchema('http://e.com/message.json#'); - - v(validMessage) .should.equal(true); - v.schema.$id .should.equal('http://e.com/message.json#'); + ajv.addSchema(schemaHeader) + ajv.addSchema(schemaMessage) + var v = ajv.getSchema("http://e.com/message.json#") - v(invalidMessage) .should.equal(false); - v.errors .should.have.length(1); - v.schema.$id .should.equal('http://e.com/message.json#'); + v(validMessage).should.equal(true) + v.schema.$id.should.equal("http://e.com/message.json#") - v(validMessage) .should.equal(true); - v.schema.$id .should.equal('http://e.com/message.json#'); - }); + v(invalidMessage).should.equal(false) + v.errors.should.have.length(1) + v.schema.$id.should.equal("http://e.com/message.json#") + v(validMessage).should.equal(true) + v.schema.$id.should.equal("http://e.com/message.json#") + }) function testSchemas(ajv, expectedInlined) { - var v1 = ajv.getSchema('http://e.com/obj.json') - , v2 = ajv.getSchema('http://e.com/obj1.json') - , vl = ajv.getSchema('http://e.com/list.json'); - testObjSchema(v1); - testObjSchema(v2); - testListSchema(vl); - testInlined(v1, expectedInlined); - testInlined(v2, expectedInlined); - testInlined(vl, false); + var v1 = ajv.getSchema("http://e.com/obj.json"), + v2 = ajv.getSchema("http://e.com/obj1.json"), + vl = ajv.getSchema("http://e.com/list.json") + testObjSchema(v1) + testObjSchema(v2) + testListSchema(vl) + testInlined(v1, expectedInlined) + testInlined(v2, expectedInlined) + testInlined(vl, false) } function testObjSchema(validate) { - validate({a:3}) .should.equal(true); - validate({a:1}) .should.equal(false); - validate({a:5}) .should.equal(false); + validate({a: 3}).should.equal(true) + validate({a: 1}).should.equal(false) + validate({a: 5}).should.equal(false) } function testListSchema(validate) { - validate([{a:3}]) .should.equal(true); - validate([{a:1}]) .should.equal(false); - validate([{a:5}]) .should.equal(false); + validate([{a: 3}]).should.equal(true) + validate([{a: 1}]).should.equal(false) + validate([{a: 5}]).should.equal(false) } function testInlined(validate, expectedInlined) { - var inlined = !(/refVal/.test(validate.toString())); - inlined .should.equal(expectedInlined); + var inlined = !/refVal/.test(validate.toString()) + inlined.should.equal(expectedInlined) } - }); -}); + }) +}) diff --git a/spec/schema-tests.spec.js b/spec/schema-tests.spec.js index a076057106..c1c26dd70c 100644 --- a/spec/schema-tests.spec.js +++ b/spec/schema-tests.spec.js @@ -1,51 +1,52 @@ -'use strict'; +"use strict" -var jsonSchemaTest = require('json-schema-test') - , getAjvInstances = require('./ajv_instances') - , options = require('./ajv_options') - , suite = require('./browser_test_suite') - , after = require('./after_test'); +var jsonSchemaTest = require("json-schema-test"), + getAjvInstances = require("./ajv_instances"), + options = require("./ajv_options"), + suite = require("./browser_test_suite"), + after = require("./after_test") -var instances = getAjvInstances(options, {unknownFormats: ['allowedUnknown']}); +var instances = getAjvInstances(options, {unknownFormats: ["allowedUnknown"]}) var remoteRefs = { - 'http://localhost:1234/integer.json': require('./JSON-Schema-Test-Suite/remotes/integer.json'), - 'http://localhost:1234/folder/folderInteger.json': require('./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json'), - 'http://localhost:1234/name.json': require('./remotes/name.json') -}; + "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), + "http://localhost:1234/folder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json"), + "http://localhost:1234/name.json": require("./remotes/name.json"), +} var remoteRefsWithIds = [ - require('./remotes/bar.json'), - require('./remotes/foo.json'), - require('./remotes/buu.json'), - require('./remotes/tree.json'), - require('./remotes/node.json'), - require('./remotes/second.json'), - require('./remotes/first.json'), - require('./remotes/scope_change.json'), -]; - -instances.forEach(addRemoteRefs); - + require("./remotes/bar.json"), + require("./remotes/foo.json"), + require("./remotes/buu.json"), + require("./remotes/tree.json"), + require("./remotes/node.json"), + require("./remotes/second.json"), + require("./remotes/first.json"), + require("./remotes/scope_change.json"), +] + +instances.forEach(addRemoteRefs) jsonSchemaTest(instances, { - description: 'Schema tests of ' + instances.length + ' ajv instances with different options', + description: + "Schema tests of " + + instances.length + + " ajv instances with different options", suites: { - 'Advanced schema tests': - typeof window == 'object' - ? suite(require('./tests/{**/,}*.json', {mode: 'list'})) - : './tests/{**/,}*.json' + "Advanced schema tests": + typeof window == "object" + ? suite(require("./tests/{**/,}*.json", {mode: "list"})) + : "./tests/{**/,}*.json", }, only: [], - assert: require('./chai').assert, + assert: require("./chai").assert, afterError: after.error, afterEach: after.each, cwd: __dirname, - timeout: 120000 -}); - + timeout: 120000, +}) function addRemoteRefs(ajv) { - for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id); - ajv.addSchema(remoteRefsWithIds); + for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id) + ajv.addSchema(remoteRefsWithIds) } diff --git a/spec/security.spec.js b/spec/security.spec.js index 182e8b89f1..0b3436073e 100644 --- a/spec/security.spec.js +++ b/spec/security.spec.js @@ -1,27 +1,30 @@ -'use strict'; +"use strict" -var jsonSchemaTest = require('json-schema-test') - , getAjvInstances = require('./ajv_instances') - , options = require('./ajv_options') - , suite = require('./browser_test_suite') - , after = require('./after_test'); +var jsonSchemaTest = require("json-schema-test"), + getAjvInstances = require("./ajv_instances"), + options = require("./ajv_options"), + suite = require("./browser_test_suite"), + after = require("./after_test") var instances = getAjvInstances(options, { - schemas: [require('../lib/refs/json-schema-secure.json')] -}); - + schemas: [require("../lib/refs/json-schema-secure.json")], +}) jsonSchemaTest(instances, { - description: 'Secure schemas tests of ' + instances.length + ' ajv instances with different options', + description: + "Secure schemas tests of " + + instances.length + + " ajv instances with different options", suites: { - 'security': typeof window == 'object' - ? suite(require('./security/{**/,}*.json', {mode: 'list'})) - : './security/{**/,}*.json' + security: + typeof window == "object" + ? suite(require("./security/{**/,}*.json", {mode: "list"})) + : "./security/{**/,}*.json", }, - assert: require('./chai').assert, + assert: require("./chai").assert, afterError: after.error, afterEach: after.each, cwd: __dirname, - hideFolder: 'security/', - timeout: 90000 -}); + hideFolder: "security/", + timeout: 90000, +}) diff --git a/spec/security/array.json b/spec/security/array.json index 25b655f955..449cf89ee4 100644 --- a/spec/security/array.json +++ b/spec/security/array.json @@ -1,7 +1,9 @@ [ { "description": "uniqueItems without type keyword should be used together with maxItems", - "schema": {"$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#"}, + "schema": { + "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" + }, "tests": [ { "description": "uniqueItems keyword used without maxItems is unsafe", @@ -29,7 +31,9 @@ }, { "description": "uniqueItems with scalar type(s) is safe to use without maxItems", - "schema": {"$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#"}, + "schema": { + "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" + }, "tests": [ { "description": "uniqueItems keyword with a single scalar type is safe", @@ -55,7 +59,9 @@ }, { "description": "uniqueItems with compound type(s) should be used together with maxItems", - "schema": {"$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#"}, + "schema": { + "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" + }, "tests": [ { "description": "uniqueItems keyword with a single compound type and without maxItems is unsafe", @@ -83,7 +89,7 @@ "data": { "uniqueItems": true, "items": { - "type": ["array","number"] + "type": ["array", "number"] } }, "valid": false @@ -94,7 +100,7 @@ "uniqueItems": true, "maxItems": "10", "items": { - "type": ["array","number"] + "type": ["array", "number"] } }, "valid": true diff --git a/spec/security/object.json b/spec/security/object.json index 9de069b8a3..d23561a41d 100644 --- a/spec/security/object.json +++ b/spec/security/object.json @@ -1,7 +1,9 @@ [ { "description": "patternProperties keyword should be used together with propertyNames", - "schema": { "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" }, + "schema": { + "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" + }, "tests": [ { "description": "patternProperties keyword used without propertyNames is unsafe", diff --git a/spec/security/string.json b/spec/security/string.json index c6fb55292b..5890aa288d 100644 --- a/spec/security/string.json +++ b/spec/security/string.json @@ -1,7 +1,9 @@ [ { "description": "pattern keyword should be used together with maxLength", - "schema": { "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" }, + "schema": { + "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" + }, "tests": [ { "description": "pattern keyword used without maxLength is unsafe", @@ -22,7 +24,9 @@ }, { "description": "format keyword should be used together with maxLength", - "schema": { "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" }, + "schema": { + "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" + }, "tests": [ { "description": "format keyword used without maxLength is unsafe", diff --git a/spec/tests/issues/12_restoring_root_after_resolve.json b/spec/tests/issues/12_restoring_root_after_resolve.json index cc1d615595..57375ee80e 100644 --- a/spec/tests/issues/12_restoring_root_after_resolve.json +++ b/spec/tests/issues/12_restoring_root_after_resolve.json @@ -3,13 +3,10 @@ "description": "restoring root after ref resolution (#12)", "schema": { "definitions": { - "int": { "$ref": "http://localhost:1234/integer.json" }, - "str": { "type": "string" } + "int": {"$ref": "http://localhost:1234/integer.json"}, + "str": {"type": "string"} }, - "anyOf": [ - { "$ref": "#/definitions/int" }, - { "$ref": "#/definitions/str" } - ] + "anyOf": [{"$ref": "#/definitions/int"}, {"$ref": "#/definitions/str"}] }, "tests": [ { @@ -33,13 +30,10 @@ "description": "all refs are in the same place", "schema": { "definitions": { - "int": { "type": "integer" }, - "str": { "type": "string" } + "int": {"type": "integer"}, + "str": {"type": "string"} }, - "anyOf": [ - { "$ref": "#/definitions/int" }, - { "$ref": "#/definitions/str" } - ] + "anyOf": [{"$ref": "#/definitions/int"}, {"$ref": "#/definitions/str"}] }, "tests": [ { diff --git a/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json b/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json index a55625abb0..fc77ffed70 100644 --- a/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json +++ b/spec/tests/issues/13_root_ref_in_ref_in_remote_ref.json @@ -5,7 +5,7 @@ "$id": "http://localhost:1234/issue13", "type": "object", "properties": { - "name": { "$ref": "name.json#/definitions/orNull" } + "name": {"$ref": "name.json#/definitions/orNull"} } }, "tests": [ diff --git a/spec/tests/issues/14_ref_in_remote_ref_with_id.json b/spec/tests/issues/14_ref_in_remote_ref_with_id.json index 57b474a4b4..ae4cbeac18 100644 --- a/spec/tests/issues/14_ref_in_remote_ref_with_id.json +++ b/spec/tests/issues/14_ref_in_remote_ref_with_id.json @@ -4,7 +4,7 @@ "schema": { "$id": "http://localhost:1234/issue14a.json", "type": "array", - "items": { "$ref": "foo.json" } + "items": {"$ref": "foo.json"} }, "tests": [ { @@ -32,7 +32,7 @@ "schema": { "$id": "http://localhost:1234/issue14b.json", "type": "array", - "items": { "$ref": "buu.json#/definitions/buu" } + "items": {"$ref": "buu.json#/definitions/buu"} }, "tests": [ { diff --git a/spec/tests/issues/170_ref_and_id_in_sibling.json b/spec/tests/issues/170_ref_and_id_in_sibling.json index bb08d82944..afdabbebe9 100644 --- a/spec/tests/issues/170_ref_and_id_in_sibling.json +++ b/spec/tests/issues/170_ref_and_id_in_sibling.json @@ -11,10 +11,10 @@ "$id": "http://example.com/title", "type": "string" }, - "file": { "$ref": "#/definitions/file-entry" } + "file": {"$ref": "#/definitions/file-entry"} }, "definitions": { - "file-entry": { "type": "string" } + "file-entry": {"type": "string"} } }, { @@ -26,10 +26,10 @@ "$id": "http://example.com/title", "type": "string" }, - "file": { "$ref": "#/definitions/file-entry" } + "file": {"$ref": "#/definitions/file-entry"} }, "definitions": { - "file-entry": { "type": "string" } + "file-entry": {"type": "string"} } } ], @@ -64,10 +64,10 @@ "$id": "http://example.com/0", "type": "string" }, - { "$ref": "#/definitions/file-entry" } + {"$ref": "#/definitions/file-entry"} ], "definitions": { - "file-entry": { "type": "string" } + "file-entry": {"type": "string"} } }, { @@ -79,22 +79,22 @@ "$id": "http://example.com/0", "type": "string" }, - { "$ref": "#/definitions/file-entry" } + {"$ref": "#/definitions/file-entry"} ], "definitions": { - "file-entry": { "type": "string" } + "file-entry": {"type": "string"} } } ], "tests": [ { "description": "valid array", - "data": [ "foo", "bar" ], + "data": ["foo", "bar"], "valid": true }, { "description": "invalid array", - "data": [ "foo", 2 ], + "data": ["foo", 2], "valid": false } ] @@ -110,10 +110,10 @@ "$id": "http://example.com/0", "type": "number" }, - { "$ref": "#/definitions/def" } + {"$ref": "#/definitions/def"} ], "definitions": { - "def": { "type": "string" } + "def": {"type": "string"} } }, { @@ -124,10 +124,10 @@ "$id": "http://example.com/0", "type": "number" }, - { "$ref": "#/definitions/def" } + {"$ref": "#/definitions/def"} ], "definitions": { - "def": { "type": "string" } + "def": {"type": "string"} } } ], @@ -160,10 +160,10 @@ "$id": "http://example.com/0", "type": "number" }, - { "$ref": "#/definitions/def" } + {"$ref": "#/definitions/def"} ], "definitions": { - "def": { "type": "string" } + "def": {"type": "string"} } }, { @@ -174,10 +174,10 @@ "$id": "http://example.com/0", "type": "number" }, - { "$ref": "#/definitions/def" } + {"$ref": "#/definitions/def"} ], "definitions": { - "def": { "type": "string" } + "def": {"type": "string"} } } ], @@ -211,10 +211,10 @@ "type": "string", "maxLength": 3 }, - { "$ref": "#/definitions/def" } + {"$ref": "#/definitions/def"} ], "definitions": { - "def": { "type": "string" } + "def": {"type": "string"} } }, { @@ -226,10 +226,10 @@ "type": "string", "maxLength": 3 }, - { "$ref": "#/definitions/def" } + {"$ref": "#/definitions/def"} ], "definitions": { - "def": { "type": "string" } + "def": {"type": "string"} } } ], @@ -256,12 +256,12 @@ "dependencies": { "foo": { "$id": "http://example.com/foo", - "required": [ "bar" ] + "required": ["bar"] }, - "bar": { "$ref": "#/definitions/def" } + "bar": {"$ref": "#/definitions/def"} }, "definitions": { - "def": { "required": [ "baz" ] } + "def": {"required": ["baz"]} } }, { @@ -271,29 +271,29 @@ "dependencies": { "foo": { "$id": "http://example.com/foo", - "required": [ "bar" ] + "required": ["bar"] }, - "bar": { "$ref": "#/definitions/def" } + "bar": {"$ref": "#/definitions/def"} }, "definitions": { - "def": { "required": [ "baz" ] } + "def": {"required": ["baz"]} } } ], "tests": [ { "description": "valid object", - "data": { "foo": 1, "bar": 2, "baz": 3 }, + "data": {"foo": 1, "bar": 2, "baz": 3}, "valid": true }, { "description": "invalid object 2", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": false }, { "description": "invalid object 2", - "data": { "foo": 1, "bar": 2 }, + "data": {"foo": 1, "bar": 2}, "valid": false } ] diff --git a/spec/tests/issues/17_escaping_pattern_property.json b/spec/tests/issues/17_escaping_pattern_property.json index edc70f5e49..b632c08995 100644 --- a/spec/tests/issues/17_escaping_pattern_property.json +++ b/spec/tests/issues/17_escaping_pattern_property.json @@ -2,14 +2,14 @@ { "description": "escaping pattern property (#17)", "schema": { - "type" : "object", + "type": "object", "patternProperties": { - "^.+$" : { - "type" : "object", - "required" : ["unit"] + "^.+$": { + "type": "object", + "required": ["unit"] } }, - "additionalProperties" : false + "additionalProperties": false }, "tests": [ { diff --git a/spec/tests/issues/1_ids_in_refs.json b/spec/tests/issues/1_ids_in_refs.json index 579e7d072a..2db0cac81f 100644 --- a/spec/tests/issues/1_ids_in_refs.json +++ b/spec/tests/issues/1_ids_in_refs.json @@ -22,8 +22,8 @@ } ], "tests": [ - { "description": "valid", "data": 1, "valid": true }, - { "description": "invalid", "data": "foo", "valid": false } + {"description": "valid", "data": 1, "valid": true}, + {"description": "invalid", "data": "foo", "valid": false} ] }, { @@ -51,8 +51,8 @@ } ], "tests": [ - { "description": "valid", "data": 1, "valid": true }, - { "description": "invalid", "data": "foo", "valid": false } + {"description": "valid", "data": 1, "valid": true}, + {"description": "invalid", "data": "foo", "valid": false} ] }, { @@ -66,8 +66,8 @@ "$ref": "#/definitions/int" }, "tests": [ - { "description": "valid", "data": 1, "valid": true }, - { "description": "invalid", "data": "foo", "valid": false } + {"description": "valid", "data": 1, "valid": true}, + {"description": "invalid", "data": "foo", "valid": false} ] } ] diff --git a/spec/tests/issues/20_failing_to_parse_schema.json b/spec/tests/issues/20_failing_to_parse_schema.json index b86a3a904e..259936da75 100644 --- a/spec/tests/issues/20_failing_to_parse_schema.json +++ b/spec/tests/issues/20_failing_to_parse_schema.json @@ -2,8 +2,8 @@ { "description": "Failing to parse schema with required property that is not an identifier (#20)", "schema": { - "type": "object", - "required": [ "a-b", "a'", "a\"" ] + "type": "object", + "required": ["a-b", "a'", "a\""] }, "tests": [ { @@ -25,33 +25,33 @@ { "description": "Failing to parse schema with required property that is not an identifier for many properties (#20)", "schema": { - "type": "object", - "required": [ - "a-1", - "a-2", - "a-3", - "a-4", - "a-5", - "a-6", - "a-7", - "a-8", - "a-9", - "a-10", - "a-11", - "a-12", - "a-13", - "a-14", - "a-15", - "a-16", - "a-17", - "a-18", - "a-19", - "a-20", - "a-21", - "a-22", - "'", - "\"" - ] + "type": "object", + "required": [ + "a-1", + "a-2", + "a-3", + "a-4", + "a-5", + "a-6", + "a-7", + "a-8", + "a-9", + "a-10", + "a-11", + "a-12", + "a-13", + "a-14", + "a-15", + "a-16", + "a-17", + "a-18", + "a-19", + "a-20", + "a-21", + "a-22", + "'", + "\"" + ] }, "tests": [ { diff --git a/spec/tests/issues/226_json_with_control_chars.json b/spec/tests/issues/226_json_with_control_chars.json index 5006d2293e..3e56dd8b2b 100644 --- a/spec/tests/issues/226_json_with_control_chars.json +++ b/spec/tests/issues/226_json_with_control_chars.json @@ -3,12 +3,12 @@ "description": "JSON with control characters - 'properties' (#226)", "schema": { "properties": { - "foo\nbar": { "type": "number" }, - "foo\"bar": { "type": "number" }, - "foo\\bar": { "type": "number" }, - "foo\rbar": { "type": "number" }, - "foo\tbar": { "type": "number" }, - "foo\fbar": { "type": "number" } + "foo\nbar": {"type": "number"}, + "foo\"bar": {"type": "number"}, + "foo\\bar": {"type": "number"}, + "foo\rbar": {"type": "number"}, + "foo\tbar": {"type": "number"}, + "foo\fbar": {"type": "number"} } }, "tests": [ @@ -76,7 +76,7 @@ { "description": "JSON with control characters - 'enum'", "schema": { - "enum": [ "foo\nbar", "foo\rbar" ] + "enum": ["foo\nbar", "foo\rbar"] }, "tests": [ { @@ -100,7 +100,7 @@ "description": "JSON with control characters - 'dependencies'", "schema": { "dependencies": { - "foo\nbar": [ "foo\rbar" ], + "foo\nbar": ["foo\rbar"], "foo\tbar": { "minProperties": 4 } diff --git a/spec/tests/issues/27_1_recursive_raml_schema.json b/spec/tests/issues/27_1_recursive_raml_schema.json index e2146b2d95..3869ddfdb0 100644 --- a/spec/tests/issues/27_1_recursive_raml_schema.json +++ b/spec/tests/issues/27_1_recursive_raml_schema.json @@ -5,9 +5,7 @@ "title": "A JSON Schema for a standard RAML object", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "required": [ - "title" - ], + "required": ["title"], "properties": { "title": { "type": "string", @@ -184,9 +182,7 @@ "oneOf": [ { "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { "type": { "enum": ["OAuth 1.0"] @@ -217,9 +213,7 @@ }, { "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { "type": { "enum": ["OAuth 2.0"] @@ -258,17 +252,12 @@ }, { "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { "type": { "oneOf": [ { - "enum": [ - "Basic Authentication", - "Digest Authentication" - ] + "enum": ["Basic Authentication", "Digest Authentication"] }, { "type": "string", diff --git a/spec/tests/issues/27_recursive_reference.json b/spec/tests/issues/27_recursive_reference.json index 6da686979c..77d2ac788f 100644 --- a/spec/tests/issues/27_recursive_reference.json +++ b/spec/tests/issues/27_recursive_reference.json @@ -11,21 +11,15 @@ "$id": "layout", "type": "object", "properties": { - "layout": { "type": "string" }, + "layout": {"type": "string"}, "panels": { "type": "array", "items": { - "anyOf": [ - { "type": "string" }, - { "$ref": "layout" } - ] + "anyOf": [{"type": "string"}, {"$ref": "layout"}] } } }, - "required": [ - "layout", - "panels" - ] + "required": ["layout", "panels"] } } }, @@ -38,21 +32,15 @@ "$id": "layout", "type": "object", "properties": { - "layout": { "type": "string" }, + "layout": {"type": "string"}, "panels": { "type": "array", "items": { - "anyOf": [ - { "type": "string" }, - { "$ref": "layout" } - ] + "anyOf": [{"type": "string"}, {"$ref": "layout"}] } } }, - "required": [ - "layout", - "panels" - ] + "required": ["layout", "panels"] } } } @@ -76,9 +64,7 @@ "panel2", { "layout": "test3", - "panels": [ - "panel3" - ] + "panels": ["panel3"] } ] } @@ -100,9 +86,7 @@ "panel2", { "layout": "test3", - "panels": [ - 3 - ] + "panels": [3] } ] } diff --git a/spec/tests/issues/28_escaping_pattern_error.json b/spec/tests/issues/28_escaping_pattern_error.json index 2f8a8fa718..d07dd1bbbd 100644 --- a/spec/tests/issues/28_escaping_pattern_error.json +++ b/spec/tests/issues/28_escaping_pattern_error.json @@ -2,7 +2,7 @@ { "description": "escaping pattern error (#28)", "schema": { - "type" : "object", + "type": "object", "properties": { "mediaType": { "type": "string", diff --git a/spec/tests/issues/2_root_ref_in_ref.json b/spec/tests/issues/2_root_ref_in_ref.json index 341cce07ab..cc50ffe4f8 100644 --- a/spec/tests/issues/2_root_ref_in_ref.json +++ b/spec/tests/issues/2_root_ref_in_ref.json @@ -4,14 +4,14 @@ "schema": { "definitions": { "arr": { - "type": "array", - "items": { "$ref": "#" } + "type": "array", + "items": {"$ref": "#"} } }, "type": "object", "properties": { - "name": { "type": "string" }, - "children": { "$ref": "#/definitions/arr" } + "name": {"type": "string"}, + "children": {"$ref": "#/definitions/arr"} } }, "tests": [ @@ -19,10 +19,7 @@ "description": "valid", "data": { "name": "foo", - "children": [ - { "name": "bar" }, - { "name": "baz" } - ] + "children": [{"name": "bar"}, {"name": "baz"}] }, "valid": true }, @@ -30,10 +27,7 @@ "description": "child numbers are invalid", "data": { "name": "foo", - "children": [ - { "name": 1 }, - { "name": 2 } - ] + "children": [{"name": 1}, {"name": 2}] }, "valid": false }, @@ -41,10 +35,7 @@ "description": "child arrays are invalid", "data": { "name": "foo", - "children": [ - [ ], - [ ] - ] + "children": [[], []] }, "valid": false } @@ -55,16 +46,13 @@ "schema": { "definitions": { "orNull": { - "anyOf": [ - { "type": "null" }, - { "$ref": "#" } - ] + "anyOf": [{"type": "null"}, {"$ref": "#"}] } }, "type": "object", "properties": { - "name": { "type": "string" }, - "parent": { "$ref": "#/definitions/orNull" } + "name": {"type": "string"}, + "parent": {"$ref": "#/definitions/orNull"} } }, "tests": [ diff --git a/spec/tests/issues/311_quotes_in_refs.json b/spec/tests/issues/311_quotes_in_refs.json index 04de75d339..d534524750 100644 --- a/spec/tests/issues/311_quotes_in_refs.json +++ b/spec/tests/issues/311_quotes_in_refs.json @@ -3,10 +3,10 @@ "description": "quotes in refs (#311)", "schema": { "properties": { - "foo\"bar": { "$ref": "#/definitions/foo\"bar" } + "foo\"bar": {"$ref": "#/definitions/foo\"bar"} }, "definitions": { - "foo\"bar": { "type": "number" } + "foo\"bar": {"type": "number"} } }, "tests": [ diff --git a/spec/tests/issues/413_dependencies_with_quote.json b/spec/tests/issues/413_dependencies_with_quote.json index 1b1cee3395..bf724cb96b 100644 --- a/spec/tests/issues/413_dependencies_with_quote.json +++ b/spec/tests/issues/413_dependencies_with_quote.json @@ -4,7 +4,7 @@ "schema": { "dependencies": { "foo'bar": { - "not": { "required": ["bar"] } + "not": {"required": ["bar"]} } } }, diff --git a/spec/tests/issues/502_contains_empty_array_with_ref_in_another_property.json b/spec/tests/issues/502_contains_empty_array_with_ref_in_another_property.json index 1c931a2414..6068c71027 100644 --- a/spec/tests/issues/502_contains_empty_array_with_ref_in_another_property.json +++ b/spec/tests/issues/502_contains_empty_array_with_ref_in_another_property.json @@ -4,14 +4,14 @@ "schema": { "type": "object", "properties": { - "str": { "$ref": "#/definitions/str" }, + "str": {"$ref": "#/definitions/str"}, "arr": { "type": "array", - "contains": { "type": "number" } + "contains": {"type": "number"} } }, "definitions": { - "str": { "type": "string" } + "str": {"type": "string"} } }, "tests": [ diff --git a/spec/tests/issues/5_adding_dependency_after.json b/spec/tests/issues/5_adding_dependency_after.json index 4c59b491c9..9be83495ed 100644 --- a/spec/tests/issues/5_adding_dependency_after.json +++ b/spec/tests/issues/5_adding_dependency_after.json @@ -5,12 +5,12 @@ "tests": [ { "description": "valid object", - "data": { "first": "foo" }, + "data": {"first": "foo"}, "valid": true }, { "description": "valid object", - "data": { "first": 1 }, + "data": {"first": 1}, "valid": false } ] diff --git a/spec/tests/issues/5_recursive_references.json b/spec/tests/issues/5_recursive_references.json index df39ba9245..c9832fe979 100644 --- a/spec/tests/issues/5_recursive_references.json +++ b/spec/tests/issues/5_recursive_references.json @@ -5,27 +5,21 @@ "tests": [ { "description": "valid tree", - "data": { + "data": { "meta": "root", "nodes": [ { "value": 1, "subtree": { "meta": "child", - "nodes": [ - { "value": 1.1 }, - { "value": 1.2 } - ] + "nodes": [{"value": 1.1}, {"value": 1.2}] } }, { "value": 2, "subtree": { "meta": "child", - "nodes": [ - { "value": 2.1 }, - { "value": 2.2 } - ] + "nodes": [{"value": 2.1}, {"value": 2.2}] } } ] @@ -34,27 +28,21 @@ }, { "description": "invalid tree", - "data": { + "data": { "meta": "root", "nodes": [ { "value": 1, "subtree": { "meta": "child", - "nodes": [ - { "value": "string is invalid" }, - { "value": 1.2 } - ] + "nodes": [{"value": "string is invalid"}, {"value": 1.2}] } }, { "value": 2, "subtree": { "meta": "child", - "nodes": [ - { "value": 2.1 }, - { "value": 2.2 } - ] + "nodes": [{"value": 2.1}, {"value": 2.2}] } } ] diff --git a/spec/tests/issues/62_resolution_scope_change.json b/spec/tests/issues/62_resolution_scope_change.json index 0db6ec69c5..efe6a11118 100644 --- a/spec/tests/issues/62_resolution_scope_change.json +++ b/spec/tests/issues/62_resolution_scope_change.json @@ -2,20 +2,22 @@ { "description": "change resolution scope - change filename (#62)", "schema": { - "type" : "object", + "type": "object", "properties": { - "title": { "$ref": "http://localhost:1234/scope_foo.json#/definitions/bar" } + "title": { + "$ref": "http://localhost:1234/scope_foo.json#/definitions/bar" + } } }, "tests": [ { "description": "string is valid", - "data": { "title": "baz" }, + "data": {"title": "baz"}, "valid": true }, { "description": "number is invalid", - "data": { "title": 1 }, + "data": {"title": 1}, "valid": false } ] @@ -23,20 +25,22 @@ { "description": "resolution scope change - change folder (#62)", "schema": { - "type" : "object", + "type": "object", "properties": { - "list": { "$ref": "http://localhost:1234/scope_change.json#/definitions/baz" } + "list": { + "$ref": "http://localhost:1234/scope_change.json#/definitions/baz" + } } }, "tests": [ { "description": "number is valid", - "data": { "list": [1] }, + "data": {"list": [1]}, "valid": true }, { "description": "string is invalid", - "data": { "list": ["a"] }, + "data": {"list": ["a"]}, "valid": false } ] @@ -44,20 +48,22 @@ { "description": "resolution scope change - change folder in subschema (#62)", "schema": { - "type" : "object", + "type": "object", "properties": { - "list": { "$ref": "http://localhost:1234/scope_change.json#/definitions/baz/bar" } + "list": { + "$ref": "http://localhost:1234/scope_change.json#/definitions/baz/bar" + } } }, "tests": [ { "description": "number is valid", - "data": { "list": [1] }, + "data": {"list": [1]}, "valid": true }, { "description": "string is invalid", - "data": { "list": ["a"] }, + "data": {"list": ["a"]}, "valid": false } ] diff --git a/spec/tests/issues/63_id_property_not_in_schema.json b/spec/tests/issues/63_id_property_not_in_schema.json index ce425cc3fe..daf5c930b4 100644 --- a/spec/tests/issues/63_id_property_not_in_schema.json +++ b/spec/tests/issues/63_id_property_not_in_schema.json @@ -2,9 +2,11 @@ { "description": "id property in referenced schema in object that is not a schema (#63)", "schema": { - "type" : "object", + "type": "object", "properties": { - "title": { "$ref": "http://json-schema.org/draft-07/schema#/properties/title" } + "title": { + "$ref": "http://json-schema.org/draft-07/schema#/properties/title" + } } }, "tests": [ @@ -15,12 +17,12 @@ }, { "description": "string is valid", - "data": { "title": "foo" }, + "data": {"title": "foo"}, "valid": true }, { "description": "number is invalid", - "data": { "title": 1 }, + "data": {"title": 1}, "valid": false } ] diff --git a/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json b/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json index 651e3d019e..8e14e762c5 100644 --- a/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json +++ b/spec/tests/issues/70_1_recursive_hash_ref_in_remote_ref.json @@ -5,9 +5,9 @@ "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "tests": [ - { "data": 1, "valid": true, "description": "positive integer is valid" }, - { "data": 0, "valid": true, "description": "zero is valid" }, - { "data": -1, "valid": false, "description": "negative integer is invalid" } + {"data": 1, "valid": true, "description": "positive integer is valid"}, + {"data": 0, "valid": true, "description": "zero is valid"}, + {"data": -1, "valid": false, "description": "negative integer is invalid"} ] }, { @@ -17,41 +17,61 @@ "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" }, "tests": [ - { "data": 1, "valid": true, "description": "positive integer is valid" }, - { "data": 0, "valid": true, "description": "zero is valid" }, - { "data": -1, "valid": false, "description": "negative integer is invalid" } + {"data": 1, "valid": true, "description": "positive integer is valid"}, + {"data": 0, "valid": true, "description": "zero is valid"}, + {"data": -1, "valid": false, "description": "negative integer is invalid"} ] }, { "description": "local hash ref with remote hash ref without inner hash ref (#70, was passing)", "schema": { "definitions": { - "a": { "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" } + "a": { + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" + } }, "properties": { - "b": { "$ref": "#/definitions/a" } + "b": {"$ref": "#/definitions/a"} } }, "tests": [ - { "data": { "b": 1 }, "valid": true, "description": "positive integer is valid" }, - { "data": { "b": 0 }, "valid": true, "description": "zero is valid" }, - { "data": { "b": -1 }, "valid": false, "description": "negative integer is invalid" } + { + "data": {"b": 1}, + "valid": true, + "description": "positive integer is valid" + }, + {"data": {"b": 0}, "valid": true, "description": "zero is valid"}, + { + "data": {"b": -1}, + "valid": false, + "description": "negative integer is invalid" + } ] }, { "description": "local hash ref with remote hash ref that has inner hash ref (#70)", "schema": { "definitions": { - "a": { "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" } + "a": { + "$ref": "http://json-schema.org/draft-07/schema#/definitions/nonNegativeIntegerDefault0" + } }, "properties": { - "b": { "$ref": "#/definitions/a" } + "b": {"$ref": "#/definitions/a"} } }, "tests": [ - { "data": { "b": 1 }, "valid": true, "description": "positive integer is valid" }, - { "data": { "b": 0 }, "valid": true, "description": "zero is valid" }, - { "data": { "b": -1 }, "valid": false, "description": "negative integer is invalid" } + { + "data": {"b": 1}, + "valid": true, + "description": "positive integer is valid" + }, + {"data": {"b": 0}, "valid": true, "description": "zero is valid"}, + { + "data": {"b": -1}, + "valid": false, + "description": "negative integer is invalid" + } ] } ] diff --git a/spec/tests/issues/70_swagger_schema.json b/spec/tests/issues/70_swagger_schema.json index 3c9a2c80da..0b3da339a0 100644 --- a/spec/tests/issues/70_swagger_schema.json +++ b/spec/tests/issues/70_swagger_schema.json @@ -6,11 +6,7 @@ "$id": "http://swagger.io/v2/schema.json#", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "required": [ - "swagger", - "info", - "paths" - ], + "required": ["swagger", "info", "paths"], "additionalProperties": false, "patternProperties": { "^x-": { @@ -20,9 +16,7 @@ "properties": { "swagger": { "type": "string", - "enum": [ - "2.0" - ], + "enum": ["2.0"], "description": "The Swagger version of this document." }, "info": { @@ -82,10 +76,7 @@ "info": { "type": "object", "description": "General information about the API.", - "required": [ - "version", - "title" - ], + "required": ["version", "title"], "additionalProperties": false, "patternProperties": { "^x-": { @@ -145,9 +136,7 @@ }, "license": { "type": "object", - "required": [ - "name" - ], + "required": ["name"], "additionalProperties": false, "properties": { "name": { @@ -204,9 +193,7 @@ "type": "object", "additionalProperties": false, "description": "information about external documentation", - "required": [ - "url" - ], + "required": ["url"], "properties": { "description": { "type": "string" @@ -232,9 +219,7 @@ }, "operation": { "type": "object", - "required": [ - "responses" - ], + "required": ["responses"], "additionalProperties": false, "patternProperties": { "^x-": { @@ -363,9 +348,7 @@ }, "response": { "type": "object", - "required": [ - "description" - ], + "required": ["description"], "properties": { "description": { "type": "string" @@ -403,19 +386,11 @@ "header": { "type": "object", "additionalProperties": false, - "required": [ - "type" - ], + "required": ["type"], "properties": { "type": { "type": "string", - "enum": [ - "string", - "number", - "integer", - "boolean", - "array" - ] + "enum": ["string", "number", "integer", "boolean", "array"] }, "format": { "type": "string" @@ -482,11 +457,7 @@ }, "bodyParameter": { "type": "object", - "required": [ - "name", - "in", - "schema" - ], + "required": ["name", "in", "schema"], "patternProperties": { "^x-": { "$ref": "#/definitions/vendorExtension" @@ -504,9 +475,7 @@ "in": { "type": "string", "description": "Determines the location of the parameter.", - "enum": [ - "body" - ] + "enum": ["body"] }, "required": { "type": "boolean", @@ -535,9 +504,7 @@ "in": { "type": "string", "description": "Determines the location of the parameter.", - "enum": [ - "header" - ] + "enum": ["header"] }, "description": { "type": "string", @@ -549,13 +516,7 @@ }, "type": { "type": "string", - "enum": [ - "string", - "number", - "boolean", - "integer", - "array" - ] + "enum": ["string", "number", "boolean", "integer", "array"] }, "format": { "type": "string" @@ -623,9 +584,7 @@ "in": { "type": "string", "description": "Determines the location of the parameter.", - "enum": [ - "query" - ] + "enum": ["query"] }, "description": { "type": "string", @@ -642,13 +601,7 @@ }, "type": { "type": "string", - "enum": [ - "string", - "number", - "boolean", - "integer", - "array" - ] + "enum": ["string", "number", "boolean", "integer", "array"] }, "format": { "type": "string" @@ -716,9 +669,7 @@ "in": { "type": "string", "description": "Determines the location of the parameter.", - "enum": [ - "formData" - ] + "enum": ["formData"] }, "description": { "type": "string", @@ -801,23 +752,17 @@ "$ref": "#/definitions/vendorExtension" } }, - "required": [ - "required" - ], + "required": ["required"], "properties": { "required": { "type": "boolean", - "enum": [ - true - ], + "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" - ] + "enum": ["path"] }, "description": { "type": "string", @@ -829,13 +774,7 @@ }, "type": { "type": "string", - "enum": [ - "string", - "number", - "boolean", - "integer", - "array" - ] + "enum": ["string", "number", "boolean", "integer", "array"] }, "format": { "type": "string" @@ -889,11 +828,7 @@ }, "nonBodyParameter": { "type": "object", - "required": [ - "name", - "in", - "type" - ], + "required": ["name", "in", "type"], "oneOf": [ { "$ref": "#/definitions/headerParameterSubSchema" @@ -1056,9 +991,7 @@ "$ref": "#/definitions/vendorExtension" } }, - "required": [ - "type" - ], + "required": ["type"], "properties": { "format": { "type": "string" @@ -1077,9 +1010,7 @@ }, "type": { "type": "string", - "enum": [ - "file" - ] + "enum": ["file"] }, "readOnly": { "type": "boolean", @@ -1098,13 +1029,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "string", - "number", - "integer", - "boolean", - "array" - ] + "enum": ["string", "number", "integer", "boolean", "array"] }, "format": { "type": "string" @@ -1209,9 +1134,7 @@ "tag": { "type": "object", "additionalProperties": false, - "required": [ - "name" - ], + "required": ["name"], "properties": { "name": { "type": "string" @@ -1257,15 +1180,11 @@ "basicAuthenticationSecurity": { "type": "object", "additionalProperties": false, - "required": [ - "type" - ], + "required": ["type"], "properties": { "type": { "type": "string", - "enum": [ - "basic" - ] + "enum": ["basic"] }, "description": { "type": "string" @@ -1280,27 +1199,18 @@ "apiKeySecurity": { "type": "object", "additionalProperties": false, - "required": [ - "type", - "name", - "in" - ], + "required": ["type", "name", "in"], "properties": { "type": { "type": "string", - "enum": [ - "apiKey" - ] + "enum": ["apiKey"] }, "name": { "type": "string" }, "in": { "type": "string", - "enum": [ - "header", - "query" - ] + "enum": ["header", "query"] }, "description": { "type": "string" @@ -1315,23 +1225,15 @@ "oauth2ImplicitSecurity": { "type": "object", "additionalProperties": false, - "required": [ - "type", - "flow", - "authorizationUrl" - ], + "required": ["type", "flow", "authorizationUrl"], "properties": { "type": { "type": "string", - "enum": [ - "oauth2" - ] + "enum": ["oauth2"] }, "flow": { "type": "string", - "enum": [ - "implicit" - ] + "enum": ["implicit"] }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1353,23 +1255,15 @@ "oauth2PasswordSecurity": { "type": "object", "additionalProperties": false, - "required": [ - "type", - "flow", - "tokenUrl" - ], + "required": ["type", "flow", "tokenUrl"], "properties": { "type": { "type": "string", - "enum": [ - "oauth2" - ] + "enum": ["oauth2"] }, "flow": { "type": "string", - "enum": [ - "password" - ] + "enum": ["password"] }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1391,23 +1285,15 @@ "oauth2ApplicationSecurity": { "type": "object", "additionalProperties": false, - "required": [ - "type", - "flow", - "tokenUrl" - ], + "required": ["type", "flow", "tokenUrl"], "properties": { "type": { "type": "string", - "enum": [ - "oauth2" - ] + "enum": ["oauth2"] }, "flow": { "type": "string", - "enum": [ - "application" - ] + "enum": ["application"] }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1429,24 +1315,15 @@ "oauth2AccessCodeSecurity": { "type": "object", "additionalProperties": false, - "required": [ - "type", - "flow", - "authorizationUrl", - "tokenUrl" - ], + "required": ["type", "flow", "authorizationUrl", "tokenUrl"], "properties": { "type": { "type": "string", - "enum": [ - "oauth2" - ] + "enum": ["oauth2"] }, "flow": { "type": "string", - "enum": [ - "accessCode" - ] + "enum": ["accessCode"] }, "scopes": { "$ref": "#/definitions/oauth2Scopes" @@ -1503,34 +1380,18 @@ "description": "The transfer protocol of the API.", "items": { "type": "string", - "enum": [ - "http", - "https", - "ws", - "wss" - ] + "enum": ["http", "https", "ws", "wss"] }, "uniqueItems": true }, "collectionFormat": { "type": "string", - "enum": [ - "csv", - "ssv", - "tsv", - "pipes" - ], + "enum": ["csv", "ssv", "tsv", "pipes"], "default": "csv" }, "collectionFormatWithMulti": { "type": "string", - "enum": [ - "csv", - "ssv", - "tsv", - "pipes", - "multi" - ], + "enum": ["csv", "ssv", "tsv", "pipes", "multi"], "default": "csv" }, "title": { @@ -1580,9 +1441,7 @@ }, "jsonReference": { "type": "object", - "required": [ - "$ref" - ], + "required": ["$ref"], "additionalProperties": false, "properties": { "$ref": { diff --git a/spec/tests/issues/87_$_property.json b/spec/tests/issues/87_$_property.json index 378f92ed89..fb48531aad 100644 --- a/spec/tests/issues/87_$_property.json +++ b/spec/tests/issues/87_$_property.json @@ -3,13 +3,13 @@ "description": "$ in properties (#87)", "schema": { "properties": { - "$": { "type": "string" } + "$": {"type": "string"} } }, "tests": [ { "description": "valid", - "data": { "$": "foo" }, + "data": {"$": "foo"}, "valid": true } ] diff --git a/spec/tests/issues/94_dependencies_fail.json b/spec/tests/issues/94_dependencies_fail.json index 65ce4234fb..5aff18df29 100644 --- a/spec/tests/issues/94_dependencies_fail.json +++ b/spec/tests/issues/94_dependencies_fail.json @@ -3,24 +3,24 @@ "description": "second dependency is not checked (#94)", "schema": { "dependencies": { - "bar" : ["baz"], - "foo" : ["bar"] + "bar": ["baz"], + "foo": ["bar"] } }, "tests": [ { "description": "object with only foo is invalid (bar is missing)", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": false }, { "description": "object with foo and bar is invalid (baz is missing)", - "data": { "foo": 1, "bar": 2 }, + "data": {"foo": 1, "bar": 2}, "valid": false }, { "description": "object with foo, bar and baz is valid", - "data": { "foo": 1, "bar": 2, "baz": 3 }, + "data": {"foo": 1, "bar": 2, "baz": 3}, "valid": true } ] @@ -29,24 +29,24 @@ "description": "second dependency is checked when order is changed", "schema": { "dependencies": { - "foo" : ["bar"], - "bar" : ["baz"] + "foo": ["bar"], + "bar": ["baz"] } }, "tests": [ { "description": "object with only foo is invalid (bar is missing)", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": false }, { "description": "object with foo and bar is invalid (baz is missing)", - "data": { "foo": 1, "bar": 2 }, + "data": {"foo": 1, "bar": 2}, "valid": false }, { "description": "object with foo, bar and baz is valid", - "data": { "foo": 1, "bar": 2, "baz": 3 }, + "data": {"foo": 1, "bar": 2, "baz": 3}, "valid": true } ] diff --git a/spec/tests/rules/allOf.json b/spec/tests/rules/allOf.json index ca6c7a9b04..c2a49e74d4 100644 --- a/spec/tests/rules/allOf.json +++ b/spec/tests/rules/allOf.json @@ -2,9 +2,7 @@ { "description": "allOf with one empty schema", "schema": { - "allOf": [ - {} - ] + "allOf": [{}] }, "tests": [ { @@ -17,10 +15,7 @@ { "description": "allOf with two empty schemas", "schema": { - "allOf": [ - {}, - {} - ] + "allOf": [{}, {}] }, "tests": [ { @@ -33,10 +28,7 @@ { "description": "allOf with two schemas, the first is empty", "schema": { - "allOf": [ - {}, - { "type": "number" } - ] + "allOf": [{}, {"type": "number"}] }, "tests": [ { @@ -54,10 +46,7 @@ { "description": "allOf with two schemas, the second is empty", "schema": { - "allOf": [ - { "type": "number" }, - {} - ] + "allOf": [{"type": "number"}, {}] }, "tests": [ { diff --git a/spec/tests/rules/anyOf.json b/spec/tests/rules/anyOf.json index c5e3fa03c9..8c085edb34 100644 --- a/spec/tests/rules/anyOf.json +++ b/spec/tests/rules/anyOf.json @@ -2,10 +2,7 @@ { "description": "anyOf with one of schemas empty", "schema": { - "anyOf": [ - { "type": "number" }, - {} - ] + "anyOf": [{"type": "number"}, {}] }, "tests": [ { diff --git a/spec/tests/rules/dependencies.json b/spec/tests/rules/dependencies.json index 9a3f6166ee..7b24c53ddf 100644 --- a/spec/tests/rules/dependencies.json +++ b/spec/tests/rules/dependencies.json @@ -9,7 +9,7 @@ "tests": [ { "description": "object with property is valid", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": true }, { diff --git a/spec/tests/rules/if.json b/spec/tests/rules/if.json index 87e9e7bc21..82f9998ab7 100644 --- a/spec/tests/rules/if.json +++ b/spec/tests/rules/if.json @@ -2,8 +2,8 @@ { "description": "if/then keyword validation", "schema": { - "if": { "minimum": 10 }, - "then": { "multipleOf": 2 } + "if": {"minimum": 10}, + "then": {"multipleOf": 2} }, "tests": [ { @@ -26,9 +26,9 @@ { "description": "if/then/else keyword validation", "schema": { - "if": { "maximum": 10 }, - "then": { "multipleOf": 2 }, - "else": { "multipleOf": 5 } + "if": {"maximum": 10}, + "then": {"multipleOf": 2}, + "else": {"multipleOf": 5} }, "tests": [ { @@ -61,9 +61,9 @@ "$id": "http://example.com/if", "minimum": 10 }, - "then": { "$ref": "#/definitions/def" }, + "then": {"$ref": "#/definitions/def"}, "definitions": { - "def": { "multipleOf": 2 } + "def": {"multipleOf": 2} } }, "tests": [ @@ -87,8 +87,8 @@ { "description": "then/else without if should be ignored", "schema": { - "then": { "multipleOf": 2 }, - "else": { "multipleOf": 5 } + "then": {"multipleOf": 2}, + "else": {"multipleOf": 5} }, "tests": [ { @@ -116,7 +116,7 @@ { "description": "if without then/else should be ignored", "schema": { - "if": { "maximum": 10 } + "if": {"maximum": 10} }, "tests": [ { diff --git a/spec/tests/rules/items.json b/spec/tests/rules/items.json index cb80cc09ad..b1726fa6f5 100644 --- a/spec/tests/rules/items.json +++ b/spec/tests/rules/items.json @@ -3,7 +3,7 @@ "description": "items with empty schema", "schema": { "items": [{}], - "additionalItems": { "type": "string" } + "additionalItems": {"type": "string"} }, "tests": [ { @@ -26,13 +26,13 @@ "type": "array", "additionalItems": false, "items": [ - { "$ref": "#/definitions/sub-child" }, - { "$ref": "#/definitions/sub-child" } + {"$ref": "#/definitions/sub-child"}, + {"$ref": "#/definitions/sub-child"} ] }, "sub-child": { "type": "object", - "properties": { "foo": {} }, + "properties": {"foo": {}}, "required": ["foo"], "additionalProperties": false } @@ -40,37 +40,37 @@ "type": "array", "additionalItems": false, "items": [ - { "$ref": "#/definitions/child" }, - { "$ref": "#/definitions/child" }, - { "$ref": "#/definitions/child" } + {"$ref": "#/definitions/child"}, + {"$ref": "#/definitions/child"}, + {"$ref": "#/definitions/child"} ] }, "tests": [ { "description": "valid items", "data": [ - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}] ], "valid": true }, { "description": "too many children", "data": [ - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}] ], "valid": false }, { "description": "too many sub-children", "data": [ - [ {"foo": null}, {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] + [{"foo": null}, {"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}] ], "valid": false }, @@ -78,26 +78,23 @@ "description": "wrong child", "data": [ {"foo": null}, - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}] ], "valid": false }, { "description": "wrong sub-child", "data": [ - [ {"bar": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] + [{"bar": null}, {"foo": null}], + [{"foo": null}, {"foo": null}], + [{"foo": null}, {"foo": null}] ], "valid": false }, { "description": "fewer children is valid", - "data": [ - [ {"foo": null} ], - [ {"foo": null} ] - ], + "data": [[{"foo": null}], [{"foo": null}]], "valid": true } ] @@ -122,17 +119,20 @@ "tests": [ { "description": "valid nested array", - "data": [[[[1]], [[2],[3]]], [[[4], [5], [6]]]], + "data": [[[[1]], [[2], [3]]], [[[4], [5], [6]]]], "valid": true }, { "description": "nested array with invalid type", - "data": [[[["1"]], [[2],[3]]], [[[4], [5], [6]]]], + "data": [[[["1"]], [[2], [3]]], [[[4], [5], [6]]]], "valid": false }, { "description": "not deep enough", - "data": [[[1], [2],[3]], [[4], [5], [6]]], + "data": [ + [[1], [2], [3]], + [[4], [5], [6]] + ], "valid": false } ] diff --git a/spec/tests/rules/oneOf.json b/spec/tests/rules/oneOf.json index f40fac8063..e6433ac638 100644 --- a/spec/tests/rules/oneOf.json +++ b/spec/tests/rules/oneOf.json @@ -2,10 +2,7 @@ { "description": "oneOf with one of schemas empty", "schema": { - "oneOf": [ - { "type": "number" }, - {} - ] + "oneOf": [{"type": "number"}, {}] }, "tests": [ { @@ -24,10 +21,7 @@ "description": "oneOf with required", "schema": { "type": "object", - "oneOf": [ - { "required": ["foo", "bar"] }, - { "required": ["foo", "baz"] } - ] + "oneOf": [{"required": ["foo", "bar"]}, {"required": ["foo", "baz"]}] }, "tests": [ { @@ -42,7 +36,7 @@ }, { "description": "object with foo, bar and baz is invalid", - "data": {"foo": 1, "bar": 2, "baz" : 3}, + "data": {"foo": 1, "bar": 2, "baz": 3}, "valid": false } ] @@ -52,12 +46,35 @@ "schema": { "type": "object", "oneOf": [ - { "required": ["foo", "bar"] }, + {"required": ["foo", "bar"]}, { "required": [ - "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", - "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", - "u", "v", "w", "x", "y", "z" + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z" ] } ] @@ -71,19 +88,66 @@ { "description": "object with a, b, c, ... properties is valid", "data": { - "a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "f": 0, "g": 0, "h": 0, "i": 0, "j": 0, - "k": 0, "l": 0, "m": 0, "n": 0, "o": 0, "p": 0, "q": 0, "r": 0, "s": 0, "t": 0, - "u": 0, "v": 0, "w": 0, "x": 0, "y": 0, "z": 0 + "a": 0, + "b": 0, + "c": 0, + "d": 0, + "e": 0, + "f": 0, + "g": 0, + "h": 0, + "i": 0, + "j": 0, + "k": 0, + "l": 0, + "m": 0, + "n": 0, + "o": 0, + "p": 0, + "q": 0, + "r": 0, + "s": 0, + "t": 0, + "u": 0, + "v": 0, + "w": 0, + "x": 0, + "y": 0, + "z": 0 }, "valid": true }, { "description": "object with foo, bar and a, b, c ... is invalid", "data": { - "a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "f": 0, "g": 0, "h": 0, "i": 0, "j": 0, - "k": 0, "l": 0, "m": 0, "n": 0, "o": 0, "p": 0, "q": 0, "r": 0, "s": 0, "t": 0, - "u": 0, "v": 0, "w": 0, "x": 0, "y": 0, "z": 0, - "foo": 1, "bar": 2 + "a": 0, + "b": 0, + "c": 0, + "d": 0, + "e": 0, + "f": 0, + "g": 0, + "h": 0, + "i": 0, + "j": 0, + "k": 0, + "l": 0, + "m": 0, + "n": 0, + "o": 0, + "p": 0, + "q": 0, + "r": 0, + "s": 0, + "t": 0, + "u": 0, + "v": 0, + "w": 0, + "x": 0, + "y": 0, + "z": 0, + "foo": 1, + "bar": 2 }, "valid": false } diff --git a/spec/tests/rules/required.json b/spec/tests/rules/required.json index c84f4fd8ae..dc4d5be8e0 100644 --- a/spec/tests/rules/required.json +++ b/spec/tests/rules/required.json @@ -7,7 +7,7 @@ "tests": [ { "description": "object with property is valid", - "data": { "foo": 1 }, + "data": {"foo": 1}, "valid": true }, { diff --git a/spec/tests/rules/type.json b/spec/tests/rules/type.json index b73270dce7..f0f6be9299 100644 --- a/spec/tests/rules/type.json +++ b/spec/tests/rules/type.json @@ -25,12 +25,12 @@ "tests": [ { "description": "array is valid", - "data": [1,2,3], + "data": [1, 2, 3], "valid": true }, { "description": "object is valid", - "data": {"foo":123}, + "data": {"foo": 123}, "valid": true }, { @@ -58,12 +58,12 @@ "tests": [ { "description": "array is valid", - "data": [1,2,3], + "data": [1, 2, 3], "valid": true }, { "description": "object is valid", - "data": {"foo":123}, + "data": {"foo": 123}, "valid": true }, { diff --git a/spec/tests/rules/uniqueItems.json b/spec/tests/rules/uniqueItems.json index d283d20451..17339d49a0 100644 --- a/spec/tests/rules/uniqueItems.json +++ b/spec/tests/rules/uniqueItems.json @@ -2,7 +2,7 @@ { "description": "uniqueItems with algorithm using hash", "schema": { - "items": { "type": "string" }, + "items": {"type": "string"}, "uniqueItems": true }, "tests": [ @@ -31,7 +31,7 @@ { "description": "uniqueItems with multiple types when the list of types includes array", "schema": { - "items": { "type": ["array", "string"] }, + "items": {"type": ["array", "string"]}, "uniqueItems": true }, "tests": [ @@ -55,7 +55,7 @@ { "description": "uniqueItems with multiple types when the list of types includes object", "schema": { - "items": { "type": ["object", "string"] }, + "items": {"type": ["object", "string"]}, "uniqueItems": true }, "tests": [ @@ -79,7 +79,7 @@ { "description": "uniqueItems with multiple types when all types are scalar", "schema": { - "items": { "type": ["number", "string", "boolean", "null"] }, + "items": {"type": ["number", "string", "boolean", "null"]}, "uniqueItems": true }, "tests": [ diff --git a/spec/tests/schemas/advanced.json b/spec/tests/schemas/advanced.json index 7c6cea9ad4..7a8b115366 100644 --- a/spec/tests/schemas/advanced.json +++ b/spec/tests/schemas/advanced.json @@ -1,244 +1,244 @@ [ - { - "description": "advanced schema from z-schema benchmark (https://github.com/zaggino/z-schema)", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "/": { "$ref": "#/definitions/entry" } + { + "description": "advanced schema from z-schema benchmark (https://github.com/zaggino/z-schema)", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "/": {"$ref": "#/definitions/entry"} + }, + "patternProperties": { + "^(/[^/]+)+$": {"$ref": "#/definitions/entry"} + }, + "additionalProperties": false, + "required": ["/"], + "definitions": { + "entry": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "schema for an fstab entry", + "type": "object", + "required": ["storage"], + "properties": { + "storage": { + "type": "object", + "oneOf": [ + {"$ref": "#/definitions/entry/definitions/diskDevice"}, + {"$ref": "#/definitions/entry/definitions/diskUUID"}, + {"$ref": "#/definitions/entry/definitions/nfs"}, + {"$ref": "#/definitions/entry/definitions/tmpfs"} + ] }, - "patternProperties": { - "^(/[^/]+)+$": { "$ref": "#/definitions/entry" } + "fstype": { + "enum": ["ext3", "ext4", "btrfs"] }, - "additionalProperties": false, - "required": [ "/" ], - "definitions": { - "entry": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "schema for an fstab entry", - "type": "object", - "required": [ "storage" ], - "properties": { - "storage": { - "type": "object", - "oneOf": [ - { "$ref": "#/definitions/entry/definitions/diskDevice" }, - { "$ref": "#/definitions/entry/definitions/diskUUID" }, - { "$ref": "#/definitions/entry/definitions/nfs" }, - { "$ref": "#/definitions/entry/definitions/tmpfs" } - ] - }, - "fstype": { - "enum": [ "ext3", "ext4", "btrfs" ] - }, - "options": { - "type": "array", - "minItems": 1, - "items": { "type": "string" }, - "uniqueItems": true - }, - "readonly": { "type": "boolean" } - }, - "definitions": { - "diskDevice": { - "properties": { - "type": { "enum": [ "disk" ] }, - "device": { - "type": "string", - "pattern": "^/dev/[^/]+(/[^/]+)*$" - } - }, - "required": [ "type", "device" ], - "additionalProperties": false - }, - "diskUUID": { - "properties": { - "type": { "enum": [ "disk" ] }, - "label": { - "type": "string", - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" - } - }, - "required": [ "type", "label" ], - "additionalProperties": false - }, - "nfs": { - "properties": { - "type": { "enum": [ "nfs" ] }, - "remotePath": { - "type": "string", - "pattern": "^(/[^/]+)+$" - }, - "server": { - "type": "string", - "anyOf": [ - { "format": "hostname" }, - { "format": "ipv4" }, - { "format": "ipv6" } - ] - } - }, - "required": [ "type", "server", "remotePath" ], - "additionalProperties": false - }, - "tmpfs": { - "properties": { - "type": { "enum": [ "tmpfs" ] }, - "sizeInMB": { - "type": "integer", - "minimum": 16, - "maximum": 512 - } - }, - "required": [ "type", "sizeInMB" ], - "additionalProperties": false - } - } + "options": { + "type": "array", + "minItems": 1, + "items": {"type": "string"}, + "uniqueItems": true + }, + "readonly": {"type": "boolean"} + }, + "definitions": { + "diskDevice": { + "properties": { + "type": {"enum": ["disk"]}, + "device": { + "type": "string", + "pattern": "^/dev/[^/]+(/[^/]+)*$" } - } - }, - "tests": [ - { - "description": "valid object from z-schema benchmark", - "data": { - "/": { - "storage": { - "type": "disk", - "device": "/dev/sda1" - }, - "fstype": "btrfs", - "readonly": true - }, - "/var": { - "storage": { - "type": "disk", - "label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f" - }, - "fstype": "ext4", - "options": [ "nosuid" ] - }, - "/tmp": { - "storage": { - "type": "tmpfs", - "sizeInMB": 64 - } - }, - "/var/www": { - "storage": { - "type": "nfs", - "server": "my.nfs.server", - "remotePath": "/exports/mypath" - } - } - }, - "valid": true + }, + "required": ["type", "device"], + "additionalProperties": false }, - { - "description": "not object", - "data": 1, - "valid": false + "diskUUID": { + "properties": { + "type": {"enum": ["disk"]}, + "label": { + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + } + }, + "required": ["type", "label"], + "additionalProperties": false }, - { - "description": "root only is valid", - "data": { - "/": { - "storage": { - "type": "disk", - "device": "/dev/sda1" - }, - "fstype": "btrfs", - "readonly": true - } + "nfs": { + "properties": { + "type": {"enum": ["nfs"]}, + "remotePath": { + "type": "string", + "pattern": "^(/[^/]+)+$" }, - "valid": true + "server": { + "type": "string", + "anyOf": [ + {"format": "hostname"}, + {"format": "ipv4"}, + {"format": "ipv6"} + ] + } + }, + "required": ["type", "server", "remotePath"], + "additionalProperties": false }, - { - "description": "missing root entry", - "data": { - "no root/": { - "storage": { - "type": "disk", - "device": "/dev/sda1" - }, - "fstype": "btrfs", - "readonly": true - } - }, - "valid": false + "tmpfs": { + "properties": { + "type": {"enum": ["tmpfs"]}, + "sizeInMB": { + "type": "integer", + "minimum": 16, + "maximum": 512 + } + }, + "required": ["type", "sizeInMB"], + "additionalProperties": false + } + } + } + } + }, + "tests": [ + { + "description": "valid object from z-schema benchmark", + "data": { + "/": { + "storage": { + "type": "disk", + "device": "/dev/sda1" }, - { - "description": "invalid entry key", - "data": { - "/": { - "storage": { - "type": "disk", - "device": "/dev/sda1" - }, - "fstype": "btrfs", - "readonly": true - }, - "invalid/var": { - "storage": { - "type": "disk", - "label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f" - }, - "fstype": "ext4", - "options": [ "nosuid" ] - } - }, - "valid": false + "fstype": "btrfs", + "readonly": true + }, + "/var": { + "storage": { + "type": "disk", + "label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f" }, - { - "description": "missing storage in entry", - "data": { - "/": { - "fstype": "btrfs", - "readonly": true - } - }, - "valid": false + "fstype": "ext4", + "options": ["nosuid"] + }, + "/tmp": { + "storage": { + "type": "tmpfs", + "sizeInMB": 64 + } + }, + "/var/www": { + "storage": { + "type": "nfs", + "server": "my.nfs.server", + "remotePath": "/exports/mypath" + } + } + }, + "valid": true + }, + { + "description": "not object", + "data": 1, + "valid": false + }, + { + "description": "root only is valid", + "data": { + "/": { + "storage": { + "type": "disk", + "device": "/dev/sda1" }, - { - "description": "missing storage type", - "data": { - "/": { - "storage": { - "device": "/dev/sda1" - }, - "fstype": "btrfs", - "readonly": true - } - }, - "valid": false + "fstype": "btrfs", + "readonly": true + } + }, + "valid": true + }, + { + "description": "missing root entry", + "data": { + "no root/": { + "storage": { + "type": "disk", + "device": "/dev/sda1" }, - { - "description": "storage type should be a string", - "data": { - "/": { - "storage": { - "type": null, - "device": "/dev/sda1" - }, - "fstype": "btrfs", - "readonly": true - } - }, - "valid": false + "fstype": "btrfs", + "readonly": true + } + }, + "valid": false + }, + { + "description": "invalid entry key", + "data": { + "/": { + "storage": { + "type": "disk", + "device": "/dev/sda1" }, - { - "description": "storage device should match pattern", - "data": { - "/": { - "storage": { - "type": null, - "device": "invalid/dev/sda1" - }, - "fstype": "btrfs", - "readonly": true - } - }, - "valid": false - } - ] - } + "fstype": "btrfs", + "readonly": true + }, + "invalid/var": { + "storage": { + "type": "disk", + "label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f" + }, + "fstype": "ext4", + "options": ["nosuid"] + } + }, + "valid": false + }, + { + "description": "missing storage in entry", + "data": { + "/": { + "fstype": "btrfs", + "readonly": true + } + }, + "valid": false + }, + { + "description": "missing storage type", + "data": { + "/": { + "storage": { + "device": "/dev/sda1" + }, + "fstype": "btrfs", + "readonly": true + } + }, + "valid": false + }, + { + "description": "storage type should be a string", + "data": { + "/": { + "storage": { + "type": null, + "device": "/dev/sda1" + }, + "fstype": "btrfs", + "readonly": true + } + }, + "valid": false + }, + { + "description": "storage device should match pattern", + "data": { + "/": { + "storage": { + "type": null, + "device": "invalid/dev/sda1" + }, + "fstype": "btrfs", + "readonly": true + } + }, + "valid": false + } + ] + } ] diff --git a/spec/tests/schemas/basic.json b/spec/tests/schemas/basic.json index a650069084..45800a228b 100644 --- a/spec/tests/schemas/basic.json +++ b/spec/tests/schemas/basic.json @@ -1,135 +1,153 @@ [ - { - "description": "basic schema from z-schema benchmark (https://github.com/zaggino/z-schema)", - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Product set", + { + "description": "basic schema from z-schema benchmark (https://github.com/zaggino/z-schema)", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Product set", + "type": "array", + "items": { + "title": "Product", + "type": "object", + "properties": { + "id": { + "description": "The unique identifier for a product", + "type": "number" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number", + "exclusiveMinimum": 0 + }, + "tags": { "type": "array", "items": { - "title": "Product", - "type": "object", - "properties": { - "id": { - "description": "The unique identifier for a product", - "type": "number" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number", - "exclusiveMinimum": 0 - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - }, - "dimensions": { - "type": "object", - "properties": { - "length": {"type": "number"}, - "width": {"type": "number"}, - "height": {"type": "number"} - }, - "required": ["length", "width", "height"] - }, - "warehouseLocation": { - "description": "Coordinates of the warehouse with the product" - } - }, - "required": ["id", "name", "price"] - } - }, - "tests": [ - { - "description": "valid array from z-schema benchmark", - "data": [ - { - "id": 2, - "name": "An ice sculpture", - "price": 12.50, - "tags": ["cold", "ice"], - "dimensions": { - "length": 7.0, - "width": 12.0, - "height": 9.5 - }, - "warehouseLocation": { - "latitude": -78.75, - "longitude": 20.4 - } - }, - { - "id": 3, - "name": "A blue mouse", - "price": 25.50, - "dimensions": { - "length": 3.1, - "width": 1.0, - "height": 1.0 - }, - "warehouseLocation": { - "latitude": 54.4, - "longitude": -32.7 - } - } - ], - "valid": true - }, - { - "description": "not array", - "data": 1, - "valid": false - }, - { - "description": "array of not onjects", - "data": [1,2,3], - "valid": false - }, - { - "description": "missing required properties", - "data": [{}], - "valid": false + "type": "string" }, - { - "description": "required property of wrong type", - "data": [{"id": 1, "name": "product", "price": "not valid"}], - "valid": false + "minItems": 1, + "uniqueItems": true + }, + "dimensions": { + "type": "object", + "properties": { + "length": {"type": "number"}, + "width": {"type": "number"}, + "height": {"type": "number"} }, - { - "description": "smallest valid product", - "data": [{"id": 1, "name": "product", "price": 100}], - "valid": true - }, - { - "description": "tags should be array", - "data": [{"tags":{}, "id": 1, "name": "product", "price": 100}], - "valid": false - }, - { - "description": "dimensions should be object", - "data": [{"dimensions":[], "id": 1, "name": "product", "price": 100}], - "valid": false - }, - { - "description": "valid product with tag", - "data": [{"tags":["product"], "id": 1, "name": "product", "price": 100}], - "valid": true + "required": ["length", "width", "height"] + }, + "warehouseLocation": { + "description": "Coordinates of the warehouse with the product" + } + }, + "required": ["id", "name", "price"] + } + }, + "tests": [ + { + "description": "valid array from z-schema benchmark", + "data": [ + { + "id": 2, + "name": "An ice sculpture", + "price": 12.5, + "tags": ["cold", "ice"], + "dimensions": { + "length": 7.0, + "width": 12.0, + "height": 9.5 }, - { - "description": "dimensions miss required properties", - "data": [{"dimensions":{}, "tags":["product"], "id": 1, "name": "product", "price": 100}], - "valid": false + "warehouseLocation": { + "latitude": -78.75, + "longitude": 20.4 + } + }, + { + "id": 3, + "name": "A blue mouse", + "price": 25.5, + "dimensions": { + "length": 3.1, + "width": 1.0, + "height": 1.0 }, - { - "description": "valid product with tag and dimensions", - "data": [{"dimensions":{"length": 7,"width": 12,"height": 9.5}, "tags":["product"], "id": 1, "name": "product", "price": 100}], - "valid": true + "warehouseLocation": { + "latitude": 54.4, + "longitude": -32.7 } - ] - } + } + ], + "valid": true + }, + { + "description": "not array", + "data": 1, + "valid": false + }, + { + "description": "array of not onjects", + "data": [1, 2, 3], + "valid": false + }, + { + "description": "missing required properties", + "data": [{}], + "valid": false + }, + { + "description": "required property of wrong type", + "data": [{"id": 1, "name": "product", "price": "not valid"}], + "valid": false + }, + { + "description": "smallest valid product", + "data": [{"id": 1, "name": "product", "price": 100}], + "valid": true + }, + { + "description": "tags should be array", + "data": [{"tags": {}, "id": 1, "name": "product", "price": 100}], + "valid": false + }, + { + "description": "dimensions should be object", + "data": [{"dimensions": [], "id": 1, "name": "product", "price": 100}], + "valid": false + }, + { + "description": "valid product with tag", + "data": [ + {"tags": ["product"], "id": 1, "name": "product", "price": 100} + ], + "valid": true + }, + { + "description": "dimensions miss required properties", + "data": [ + { + "dimensions": {}, + "tags": ["product"], + "id": 1, + "name": "product", + "price": 100 + } + ], + "valid": false + }, + { + "description": "valid product with tag and dimensions", + "data": [ + { + "dimensions": {"length": 7, "width": 12, "height": 9.5}, + "tags": ["product"], + "id": 1, + "name": "product", + "price": 100 + } + ], + "valid": true + } + ] + } ] diff --git a/spec/tests/schemas/complex.json b/spec/tests/schemas/complex.json index 46d15446b3..306d37fbc1 100644 --- a/spec/tests/schemas/complex.json +++ b/spec/tests/schemas/complex.json @@ -3,7 +3,7 @@ "description": "complex schema from jsck benchmark (https://github.com/pandastrike/jsck)", "schema": { "type": "array", - "items": { "$ref": "#transaction" }, + "items": {"$ref": "#transaction"}, "minItems": 1, "definitions": { "base58": { @@ -19,7 +19,7 @@ "tx_id": { "$id": "#tx_id", "allOf": [ - { "$ref": "#hex" }, + {"$ref": "#hex"}, { "minLength": 64, "maxLength": 64 @@ -29,7 +29,7 @@ "address": { "$id": "#address", "allOf": [ - { "$ref": "#base58" }, + {"$ref": "#base58"}, { "minLength": 34, "maxLength": 34 @@ -39,7 +39,7 @@ "signature": { "$id": "#signature", "allOf": [ - { "$ref": "#hex" }, + {"$ref": "#hex"}, { "minLength": 128, "maxLength": 128 @@ -49,19 +49,11 @@ "transaction": { "$id": "#transaction", "additionalProperties": false, - "required": [ - "metadata", - "hash", - "inputs", - "outputs" - ], + "required": ["metadata", "hash", "inputs", "outputs"], "properties": { "metadata": { "type": "object", - "required": [ - "amount", - "fee" - ], + "required": ["amount", "fee"], "properties": { "amount": { "type": "integer" @@ -72,12 +64,7 @@ }, "status": { "type": "string", - "enum": [ - "unsigned", - "unconfirmed", - "confirmed", - "invalid" - ] + "enum": ["unsigned", "unconfirmed", "confirmed", "invalid"] }, "confirmations": { "type": "integer", @@ -94,15 +81,15 @@ "lock_time": { "type": "integer" }, - "hash": { "$ref": "#tx_id" }, + "hash": {"$ref": "#tx_id"}, "inputs": { "type": "array", - "items": { "$ref": "#input" }, + "items": {"$ref": "#input"}, "minItems": 1 }, "outputs": { "type": "array", - "items": { "$ref": "#output" }, + "items": {"$ref": "#output"}, "minItems": 1 } } @@ -111,25 +98,21 @@ "$id": "#input", "type": "object", "additionalProperties": false, - "required": [ - "index", - "output", - "script_sig" - ], + "required": ["index", "output", "script_sig"], "properties": { "index": { "type": "integer", "minimum": 0 }, - "output": { "$ref": "#output" }, - "sig_hash": { "$ref": "#hex" }, - "script_sig": { "$ref": "#hex" }, + "output": {"$ref": "#output"}, + "sig_hash": {"$ref": "#hex"}, + "script_sig": {"$ref": "#hex"}, "signatures": { "type": "object", "description": "A dictionary of signatures. Keys represent keypair names", "minProperties": 1, "maxProperties": 3, - "additionalProperties": { "$ref": "#signature" } + "additionalProperties": {"$ref": "#signature"} } } }, @@ -137,14 +120,9 @@ "$id": "#output", "type": "object", "additionalProperties": false, - "required": [ - "hash", - "index", - "value", - "script" - ], + "required": ["hash", "index", "value", "script"], "properties": { - "hash": { "$ref": "#tx_id" }, + "hash": {"$ref": "#tx_id"}, "index": { "type": "integer", "minimum": 0 @@ -157,23 +135,18 @@ "properties": { "type": { "type": "string", - "enum": [ - "standard", - "p2sh" - ] + "enum": ["standard", "p2sh"] }, "asm": { "type": "string" } } }, - "address": { "$ref": "#address" }, + "address": {"$ref": "#address"}, "metadata": { "type": "object", "dependencies": { - "wallet_path": [ - "public_seeds" - ] + "wallet_path": ["public_seeds"] }, "properties": { "wallet_path": { @@ -184,10 +157,7 @@ "minProperties": 1, "maxProperties": 3, "additionalProperties": { - "anyOf": [ - { "$ref": "#base58" }, - { "$ref": "#hex" } - ] + "anyOf": [{"$ref": "#base58"}, {"$ref": "#hex"}] } } } diff --git a/spec/tests/schemas/complex2.json b/spec/tests/schemas/complex2.json index d38a96c7d2..0dcf32ff36 100644 --- a/spec/tests/schemas/complex2.json +++ b/spec/tests/schemas/complex2.json @@ -3,7 +3,7 @@ "description": "complex schema from jsck benchmark without IDs in definitions", "schema": { "type": "array", - "items": { "$ref": "#/definitions/transaction" }, + "items": {"$ref": "#/definitions/transaction"}, "minItems": 1, "definitions": { "base58": { @@ -16,7 +16,7 @@ }, "tx_id": { "allOf": [ - { "$ref": "#/definitions/hex" }, + {"$ref": "#/definitions/hex"}, { "minLength": 64, "maxLength": 64 @@ -25,7 +25,7 @@ }, "address": { "allOf": [ - { "$ref": "#/definitions/base58" }, + {"$ref": "#/definitions/base58"}, { "minLength": 34, "maxLength": 34 @@ -34,7 +34,7 @@ }, "signature": { "allOf": [ - { "$ref": "#/definitions/hex" }, + {"$ref": "#/definitions/hex"}, { "minLength": 128, "maxLength": 128 @@ -43,19 +43,11 @@ }, "transaction": { "additionalProperties": false, - "required": [ - "metadata", - "hash", - "inputs", - "outputs" - ], + "required": ["metadata", "hash", "inputs", "outputs"], "properties": { "metadata": { "type": "object", - "required": [ - "amount", - "fee" - ], + "required": ["amount", "fee"], "properties": { "amount": { "type": "integer" @@ -66,12 +58,7 @@ }, "status": { "type": "string", - "enum": [ - "unsigned", - "unconfirmed", - "confirmed", - "invalid" - ] + "enum": ["unsigned", "unconfirmed", "confirmed", "invalid"] }, "confirmations": { "type": "integer", @@ -88,15 +75,15 @@ "lock_time": { "type": "integer" }, - "hash": { "$ref": "#/definitions/tx_id" }, + "hash": {"$ref": "#/definitions/tx_id"}, "inputs": { "type": "array", - "items": { "$ref": "#/definitions/input" }, + "items": {"$ref": "#/definitions/input"}, "minItems": 1 }, "outputs": { "type": "array", - "items": { "$ref": "#/definitions/output" }, + "items": {"$ref": "#/definitions/output"}, "minItems": 1 } } @@ -104,39 +91,30 @@ "input": { "type": "object", "additionalProperties": false, - "required": [ - "index", - "output", - "script_sig" - ], + "required": ["index", "output", "script_sig"], "properties": { "index": { "type": "integer", "minimum": 0 }, - "output": { "$ref": "#/definitions/output" }, - "sig_hash": { "$ref": "#/definitions/hex" }, - "script_sig": { "$ref": "#/definitions/hex" }, + "output": {"$ref": "#/definitions/output"}, + "sig_hash": {"$ref": "#/definitions/hex"}, + "script_sig": {"$ref": "#/definitions/hex"}, "signatures": { "type": "object", "description": "A dictionary of signatures. Keys represent keypair names", "minProperties": 1, "maxProperties": 3, - "additionalProperties": { "$ref": "#/definitions/signature" } + "additionalProperties": {"$ref": "#/definitions/signature"} } } }, "output": { "type": "object", "additionalProperties": false, - "required": [ - "hash", - "index", - "value", - "script" - ], + "required": ["hash", "index", "value", "script"], "properties": { - "hash": { "$ref": "#/definitions/tx_id" }, + "hash": {"$ref": "#/definitions/tx_id"}, "index": { "type": "integer", "minimum": 0 @@ -149,23 +127,18 @@ "properties": { "type": { "type": "string", - "enum": [ - "standard", - "p2sh" - ] + "enum": ["standard", "p2sh"] }, "asm": { "type": "string" } } }, - "address": { "$ref": "#/definitions/address" }, + "address": {"$ref": "#/definitions/address"}, "metadata": { "type": "object", "dependencies": { - "wallet_path": [ - "public_seeds" - ] + "wallet_path": ["public_seeds"] }, "properties": { "wallet_path": { @@ -177,8 +150,8 @@ "maxProperties": 3, "additionalProperties": { "anyOf": [ - { "$ref": "#/definitions/base58" }, - { "$ref": "#/definitions/hex" } + {"$ref": "#/definitions/base58"}, + {"$ref": "#/definitions/hex"} ] } } diff --git a/spec/tests/schemas/complex3.json b/spec/tests/schemas/complex3.json index 1591d7814e..0fd8a976bb 100644 --- a/spec/tests/schemas/complex3.json +++ b/spec/tests/schemas/complex3.json @@ -4,7 +4,7 @@ "schema": { "$id": "http://example.com/complex3.json", "type": "array", - "items": { "$ref": "#transaction" }, + "items": {"$ref": "#transaction"}, "minItems": 1, "definitions": { "base58": { @@ -20,7 +20,7 @@ "tx_id": { "$id": "#tx_id", "allOf": [ - { "$ref": "#hex" }, + {"$ref": "#hex"}, { "minLength": 64, "maxLength": 64 @@ -30,7 +30,7 @@ "address": { "$id": "#address", "allOf": [ - { "$ref": "#base58" }, + {"$ref": "#base58"}, { "minLength": 34, "maxLength": 34 @@ -40,7 +40,7 @@ "signature": { "$id": "#signature", "allOf": [ - { "$ref": "#hex" }, + {"$ref": "#hex"}, { "minLength": 128, "maxLength": 128 @@ -50,19 +50,11 @@ "transaction": { "$id": "#transaction", "additionalProperties": false, - "required": [ - "metadata", - "hash", - "inputs", - "outputs" - ], + "required": ["metadata", "hash", "inputs", "outputs"], "properties": { "metadata": { "type": "object", - "required": [ - "amount", - "fee" - ], + "required": ["amount", "fee"], "properties": { "amount": { "type": "integer" @@ -73,12 +65,7 @@ }, "status": { "type": "string", - "enum": [ - "unsigned", - "unconfirmed", - "confirmed", - "invalid" - ] + "enum": ["unsigned", "unconfirmed", "confirmed", "invalid"] }, "confirmations": { "type": "integer", @@ -95,15 +82,15 @@ "lock_time": { "type": "integer" }, - "hash": { "$ref": "#tx_id" }, + "hash": {"$ref": "#tx_id"}, "inputs": { "type": "array", - "items": { "$ref": "#input" }, + "items": {"$ref": "#input"}, "minItems": 1 }, "outputs": { "type": "array", - "items": { "$ref": "#output" }, + "items": {"$ref": "#output"}, "minItems": 1 } } @@ -112,25 +99,21 @@ "$id": "#input", "type": "object", "additionalProperties": false, - "required": [ - "index", - "output", - "script_sig" - ], + "required": ["index", "output", "script_sig"], "properties": { "index": { "type": "integer", "minimum": 0 }, - "output": { "$ref": "#output" }, - "sig_hash": { "$ref": "#hex" }, - "script_sig": { "$ref": "#hex" }, + "output": {"$ref": "#output"}, + "sig_hash": {"$ref": "#hex"}, + "script_sig": {"$ref": "#hex"}, "signatures": { "type": "object", "description": "A dictionary of signatures. Keys represent keypair names", "minProperties": 1, "maxProperties": 3, - "additionalProperties": { "$ref": "#signature" } + "additionalProperties": {"$ref": "#signature"} } } }, @@ -138,14 +121,9 @@ "$id": "#output", "type": "object", "additionalProperties": false, - "required": [ - "hash", - "index", - "value", - "script" - ], + "required": ["hash", "index", "value", "script"], "properties": { - "hash": { "$ref": "#tx_id" }, + "hash": {"$ref": "#tx_id"}, "index": { "type": "integer", "minimum": 0 @@ -158,23 +136,18 @@ "properties": { "type": { "type": "string", - "enum": [ - "standard", - "p2sh" - ] + "enum": ["standard", "p2sh"] }, "asm": { "type": "string" } } }, - "address": { "$ref": "#address" }, + "address": {"$ref": "#address"}, "metadata": { "type": "object", "dependencies": { - "wallet_path": [ - "public_seeds" - ] + "wallet_path": ["public_seeds"] }, "properties": { "wallet_path": { @@ -185,10 +158,7 @@ "minProperties": 1, "maxProperties": 3, "additionalProperties": { - "anyOf": [ - { "$ref": "#base58" }, - { "$ref": "#hex" } - ] + "anyOf": [{"$ref": "#base58"}, {"$ref": "#hex"}] } } } diff --git a/spec/tests/schemas/cosmicrealms.json b/spec/tests/schemas/cosmicrealms.json index 7ff641dc21..1b506608c7 100644 --- a/spec/tests/schemas/cosmicrealms.json +++ b/spec/tests/schemas/cosmicrealms.json @@ -6,39 +6,59 @@ "type": "object", "additionalProperties": false, "required": [ - "fullName", "age", "zip", "married", - "dozen", "dozenOrBakersDozen", - "favoriteEvenNumber", "topThreeFavoriteColors", - "favoriteSingleDigitWholeNumbers", "favoriteFiveLetterWord", - "emailAddresses", "ipAddresses" + "fullName", + "age", + "zip", + "married", + "dozen", + "dozenOrBakersDozen", + "favoriteEvenNumber", + "topThreeFavoriteColors", + "favoriteSingleDigitWholeNumbers", + "favoriteFiveLetterWord", + "emailAddresses", + "ipAddresses" ], "properties": { - "fullName": { "type": "string" }, - "age": { "type": "integer", "minimum": 0 }, - "optionalItem": { "type": "string" }, - "state": { "type": "string" }, - "city": { "type": "string" }, - "zip": { "type": "integer", "minimum": 0, "maximum": 99999 }, - "married": { "type": "boolean" }, - "dozen": { "type": "integer", "minimum": 12, "maximum": 12 }, - "dozenOrBakersDozen": { "type": "integer", "minimum": 12, "maximum": 13 }, - "favoriteEvenNumber": { "type": "integer", "multipleOf": 2 }, + "fullName": {"type": "string"}, + "age": {"type": "integer", "minimum": 0}, + "optionalItem": {"type": "string"}, + "state": {"type": "string"}, + "city": {"type": "string"}, + "zip": {"type": "integer", "minimum": 0, "maximum": 99999}, + "married": {"type": "boolean"}, + "dozen": {"type": "integer", "minimum": 12, "maximum": 12}, + "dozenOrBakersDozen": {"type": "integer", "minimum": 12, "maximum": 13}, + "favoriteEvenNumber": {"type": "integer", "multipleOf": 2}, "topThreeFavoriteColors": { - "type": "array", "minItems": 3, "maxItems": 3, "uniqueItems": true, - "items": { "type": "string" } + "type": "array", + "minItems": 3, + "maxItems": 3, + "uniqueItems": true, + "items": {"type": "string"} }, "favoriteSingleDigitWholeNumbers": { - "type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true, - "items": { "type": "integer", "minimum": 0, "maximum": 9 } + "type": "array", + "minItems": 1, + "maxItems": 10, + "uniqueItems": true, + "items": {"type": "integer", "minimum": 0, "maximum": 9} + }, + "favoriteFiveLetterWord": { + "type": "string", + "minLength": 5, + "maxLength": 5 }, - "favoriteFiveLetterWord": { "type": "string", "minLength": 5, "maxLength": 5 }, "emailAddresses": { - "type": "array", "minItems": 1, "uniqueItems": true, - "items": { "type": "string", "format": "email" } + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"type": "string", "format": "email"} }, "ipAddresses": { - "type": "array", "uniqueItems": true, - "items": { "type": "string", "format": "ipv4" } + "type": "array", + "uniqueItems": true, + "items": {"type": "string", "format": "ipv4"} } } }, @@ -62,17 +82,8 @@ "married": true, "age": 17, "zip": 65794, - "topThreeFavoriteColors": [ - "blue", - "black", - "yellow" - ], - "favoriteSingleDigitWholeNumbers": [ - 2, - 1, - 3, - 9 - ], + "topThreeFavoriteColors": ["blue", "black", "yellow"], + "favoriteSingleDigitWholeNumbers": [2, 1, 3, 9], "ipAddresses": [ "225.234.40.3", "96.216.243.54", @@ -87,7 +98,7 @@ "data": { "state": null, "city": 90912, - "zip": [ null ], + "zip": [null], "married": "married", "dozen": 90912, "dozenOrBakersDozen": null, diff --git a/spec/tests/schemas/medium.json b/spec/tests/schemas/medium.json index 6d285b88c6..bd0f51e274 100644 --- a/spec/tests/schemas/medium.json +++ b/spec/tests/schemas/medium.json @@ -5,22 +5,13 @@ "description": "A moderately complex schema with some nesting and value constraints", "type": "object", "additionalProperties": false, - "required": [ - "api_server", - "transport", - "storage", - "chain" - ], + "required": ["api_server", "transport", "storage", "chain"], "properties": { "api_server": { "description": "Settings for the HTTP API server", "type": "object", "additionalProperties": false, - "required": [ - "url", - "host", - "port" - ], + "required": ["url", "host", "port"], "properties": { "url": { "type": "string", @@ -38,9 +29,7 @@ "transport": { "description": "Settings for the Redis tranport", "additionalProperties": false, - "required": [ - "server" - ], + "required": ["server"], "properties": { "server": { "type": "string" @@ -60,11 +49,7 @@ }, "storage": { "description": "Settings for the PostgreSQL storage", - "required": [ - "server", - "database", - "user" - ], + "required": ["server", "database", "user"], "properties": { "server": { "type": "string" @@ -82,10 +67,7 @@ }, "chain": { "description": "Settings for the Chain.com client", - "required": [ - "api_key_id", - "api_key_secret" - ], + "required": ["api_key_id", "api_key_secret"], "properties": { "api_key_id": { "type": "string" diff --git a/spec/typescript/index.ts b/spec/typescript/index.ts index 0858180e99..9fac0b1f49 100644 --- a/spec/typescript/index.ts +++ b/spec/typescript/index.ts @@ -1,60 +1,60 @@ -import ajv = require("../.."); +import ajv = require("../..") // #region new() const options: ajv.Options = { - verbose: true, -}; + verbose: true, +} -let instance: ajv.Ajv; +let instance: ajv.Ajv -instance = ajv(); -instance = ajv(options); +instance = ajv() +instance = ajv(options) -instance = new ajv(); -instance = new ajv(options); +instance = new ajv() +instance = new ajv(options) // #endregion new() // #region validate() let data = { - foo: 42, + foo: 42, } -let result = instance.validate("", data); +let result = instance.validate("", data) if (typeof result === "boolean") { - // sync - console.log(result); + // sync + console.log(result) } else { - // async - result.then(value => { - data = value; - }); + // async + result.then((value) => { + data = value + }) } // #endregion validate() // #region compile() -const validator = instance.compile({}); -result = validator(data); +const validator = instance.compile({}) +result = validator(data) if (typeof result === "boolean") { - // sync - console.log(result); + // sync + console.log(result) } else { - // async - result.then(value => { - data = value; - }); + // async + result.then((value) => { + data = value + }) } // #endregion compile() // #region errors -const validationError: ajv.ValidationError = new ajv.ValidationError([]); -validationError instanceof ajv.ValidationError; -validationError.ajv === true; -validationError.validation === true; - -ajv.MissingRefError.message("", ""); -const missingRefError: ajv.MissingRefError = new ajv.MissingRefError("", "", ""); -missingRefError instanceof ajv.MissingRefError; -missingRefError.missingRef; +const validationError: ajv.ValidationError = new ajv.ValidationError([]) +validationError instanceof ajv.ValidationError +validationError.ajv === true +validationError.validation === true + +ajv.MissingRefError.message("", "") +const missingRefError: ajv.MissingRefError = new ajv.MissingRefError("", "", "") +missingRefError instanceof ajv.MissingRefError +missingRefError.missingRef // #endregion From 3025ab2c4ff7efb11bd5098b58d0b23ea6751e84 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 08:46:55 +0100 Subject: [PATCH 002/322] remove formats (moved to ajv-formats) --- .prettierignore | 1 - lib/ajv.js | 6 +- lib/compile/formats.js | 139 ---- package.json | 1 + spec/async_schemas.spec.js | 2 + spec/errors.spec.js | 20 +- spec/extras/$data/format.json | 76 -- spec/extras/propertyNames.json | 32 - spec/issues/617_full_format_leap_year.spec.js | 46 -- spec/json-schema.spec.js | 24 +- spec/options/options_validation.spec.js | 11 +- spec/options/ownProperties.spec.js | 6 +- spec/options/unknownFormats.spec.js | 4 + spec/resolve.spec.js | 2 +- spec/schema-tests.spec.js | 6 +- .../issues/1061_alternative_time_offsets.json | 74 -- spec/tests/rules/format.json | 730 ------------------ 17 files changed, 59 insertions(+), 1121 deletions(-) delete mode 100644 lib/compile/formats.js delete mode 100644 spec/extras/propertyNames.json delete mode 100644 spec/issues/617_full_format_leap_year.spec.js delete mode 100644 spec/tests/issues/1061_alternative_time_offsets.json diff --git a/.prettierignore b/.prettierignore index ca92f675b1..e2fcb36476 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,5 @@ spec/JSON-Schema-Test-Suite .browser coverage dist -node .nyc_output lib/dotjs diff --git a/lib/ajv.js b/lib/ajv.js index 6878427afe..228d83ba87 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -5,7 +5,6 @@ var compileSchema = require("./compile"), Cache = require("./cache"), SchemaObject = require("./compile/schema_obj"), stableStringify = require("fast-json-stable-stringify"), - formats = require("./compile/formats"), rules = require("./compile/rules"), $dataMetaSchema = require("./data"), util = require("./compile/util") @@ -60,7 +59,9 @@ function Ajv(opts) { this._schemas = {} this._refs = {} this._fragments = {} - this._formats = formats(opts.format) + this._formats = {} + const formatOpt = opts.format + opts.format = false this._cache = opts.cache || new Cache() this._loadingSchemas = {} @@ -80,6 +81,7 @@ function Ajv(opts) { if (opts.nullable) this.addKeyword("nullable", {metaSchema: {type: "boolean"}}) addInitialSchemas(this) + opts.format = formatOpt } /** diff --git a/lib/compile/formats.js b/lib/compile/formats.js deleted file mode 100644 index 4cbb53d888..0000000000 --- a/lib/compile/formats.js +++ /dev/null @@ -1,139 +0,0 @@ -"use strict" - -var util = require("./util") - -var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ -var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] -var TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i -var HOSTNAME = /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i -var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i -var URIREF = /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i -// uri-template: https://tools.ietf.org/html/rfc6570 -var URITEMPLATE = /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i -// For the source: https://gist.github.com/dperini/729294 -// For test cases: https://mathiasbynens.be/demo/url-regex -// @todo Delete current URL in favour of the commented out URL rule when this issue is fixed https://github.com/eslint/eslint/issues/7983. -// var URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu; -var URL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i -var UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i -var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/ -var JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i -var RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/ - -module.exports = formats - -function formats(mode) { - mode = mode == "full" ? "full" : "fast" - return util.copy(formats[mode]) -} - -formats.fast = { - // date: http://tools.ietf.org/html/rfc3339#section-5.6 - date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/, - // date-time: http://tools.ietf.org/html/rfc3339#section-5.6 - time: /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, - "date-time": /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, - // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js - uri: /^(?:[a-z][a-z0-9+-.]*:)(?:\/?\/)?[^\s]*$/i, - "uri-reference": /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, - "uri-template": URITEMPLATE, - url: URL, - // email (sources from jsen validator): - // http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363 - // http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'willful violation') - email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i, - hostname: HOSTNAME, - // optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html - ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, - // optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses - ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, - regex: regex, - // uuid: http://tools.ietf.org/html/rfc4122 - uuid: UUID, - // JSON-pointer: https://tools.ietf.org/html/rfc6901 - // uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A - "json-pointer": JSON_POINTER, - "json-pointer-uri-fragment": JSON_POINTER_URI_FRAGMENT, - // relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00 - "relative-json-pointer": RELATIVE_JSON_POINTER, -} - -formats.full = { - date: date, - time: time, - "date-time": date_time, - uri: uri, - "uri-reference": URIREF, - "uri-template": URITEMPLATE, - url: URL, - email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, - hostname: HOSTNAME, - ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, - ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, - regex: regex, - uuid: UUID, - "json-pointer": JSON_POINTER, - "json-pointer-uri-fragment": JSON_POINTER_URI_FRAGMENT, - "relative-json-pointer": RELATIVE_JSON_POINTER, -} - -function isLeapYear(year) { - // https://tools.ietf.org/html/rfc3339#appendix-C - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) -} - -function date(str) { - // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 - var matches = str.match(DATE) - if (!matches) return false - - var year = +matches[1] - var month = +matches[2] - var day = +matches[3] - - return ( - month >= 1 && - month <= 12 && - day >= 1 && - day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]) - ) -} - -function time(str, full) { - var matches = str.match(TIME) - if (!matches) return false - - var hour = matches[1] - var minute = matches[2] - var second = matches[3] - var timeZone = matches[5] - return ( - ((hour <= 23 && minute <= 59 && second <= 59) || - (hour == 23 && minute == 59 && second == 60)) && - (!full || timeZone) - ) -} - -var DATE_TIME_SEPARATOR = /t|\s/i -function date_time(str) { - // http://tools.ietf.org/html/rfc3339#section-5.6 - var dateTime = str.split(DATE_TIME_SEPARATOR) - return dateTime.length == 2 && date(dateTime[0]) && time(dateTime[1], true) -} - -var NOT_URI_FRAGMENT = /\/|:/ -function uri(str) { - // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." - return NOT_URI_FRAGMENT.test(str) && URI.test(str) -} - -var Z_ANCHOR = /[^\\]\\Z/ -function regex(str) { - if (Z_ANCHOR.test(str)) return false - try { - new RegExp(str) - return true - } catch (e) { - return false - } -} diff --git a/package.json b/package.json index ae9251003b..3986e2d3f5 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "devDependencies": { "@ajv-validator/config": "^0.1.0", "ajv-async": "^1.0.0", + "ajv-formats": "^0.1.0", "bluebird": "^3.5.3", "brfs": "^2.0.0", "browserify": "^16.2.0", diff --git a/spec/async_schemas.spec.js b/spec/async_schemas.spec.js index d4118a4ddc..aab643bb63 100644 --- a/spec/async_schemas.spec.js +++ b/spec/async_schemas.spec.js @@ -34,6 +34,8 @@ jsonSchemaTest(instances, { }) function addAsyncFormatsAndKeywords(ajv) { + ajv.addFormat("date", /^\d\d\d\d-[0-1]\d-[0-3]\d$/) + ajv.addFormat("english_word", { async: true, validate: checkWordOnServer, diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 82b9150e2e..b318dbe32c 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -765,17 +765,19 @@ describe("Validation errors", function () { it("should add propertyName to errors", function () { var schema = { type: "object", - propertyNames: {format: "email"}, + propertyNames: {pattern: "bar"}, } var data = { + bar: {}, "bar.baz@email.example.com": {}, } var invalidData = { - foo: {}, bar: {}, "bar.baz@email.example.com": {}, + foo: {}, + quux: {}, } test(ajv, 2) @@ -788,10 +790,10 @@ describe("Validation errors", function () { shouldBeInvalid(validate, invalidData, numErrors) shouldBeError( validate.errors[0], - "format", - "#/propertyNames/format", + "pattern", + "#/propertyNames/pattern", "", - 'should match format "email"' + 'should match pattern "bar"' ) shouldBeError( validate.errors[1], @@ -803,17 +805,17 @@ describe("Validation errors", function () { if (numErrors == 4) { shouldBeError( validate.errors[2], - "format", - "#/propertyNames/format", + "pattern", + "#/propertyNames/pattern", "", - 'should match format "email"' + 'should match pattern "bar"' ) shouldBeError( validate.errors[3], "propertyNames", "#/propertyNames", "", - "property name 'bar' is invalid" + "property name 'quux' is invalid" ) } } diff --git a/spec/extras/$data/format.json b/spec/extras/$data/format.json index b6b8dccb0e..48fb5f1009 100644 --- a/spec/extras/$data/format.json +++ b/spec/extras/$data/format.json @@ -12,46 +12,6 @@ } }, "tests": [ - { - "description": "a valid date-time string", - "data": { - "str": "1963-06-19T08:30:06.283185Z", - "strFormat": "date-time" - }, - "valid": true - }, - { - "description": "an invalid date-time string", - "data": { - "str": "06/19/1963 08:30:06 PST", - "strFormat": "date-time" - }, - "valid": false - }, - { - "description": "only RFC3339 not all of ISO 8601 are valid", - "data": { - "str": "2013-350T01:01:01", - "strFormat": "date-time" - }, - "valid": false - }, - { - "description": "a valid e-mail address", - "data": { - "str": "joe.bloggs@example.com", - "strFormat": "email" - }, - "valid": true - }, - { - "description": "an invalid e-mail address", - "data": { - "str": "2962", - "strFormat": "email" - }, - "valid": false - }, { "description": "allowed unknown format is valid", "data": { @@ -84,41 +44,5 @@ "valid": false } ] - }, - { - "description": "property name is the format for the property value", - "schema": { - "additionalProperties": { - "format": { - "$data": "0#" - } - } - }, - "tests": [ - { - "description": "valid formats", - "data": { - "date-time": "1963-06-19T08:30:06.283185Z", - "email": "joe.bloggs@example.com" - }, - "valid": true - }, - { - "description": "invalid date-time", - "data": { - "date-time": "06/19/1963 08:30:06 PST", - "email": "joe.bloggs@example.com" - }, - "valid": false - }, - { - "description": "invalid e-mail", - "data": { - "date-time": "1963-06-19T08:30:06.283185Z", - "email": "2962" - }, - "valid": false - } - ] } ] diff --git a/spec/extras/propertyNames.json b/spec/extras/propertyNames.json deleted file mode 100644 index 0be77ef07d..0000000000 --- a/spec/extras/propertyNames.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "description": "propertyNames validation", - "schema": { - "type": "object", - "propertyNames": {"format": "email"} - }, - "tests": [ - { - "description": "all property names valid", - "data": { - "foo@example.com": {}, - "bar.baz@email.example.com": {} - }, - "valid": true - }, - { - "description": "some property names invalid", - "data": { - "foo": {}, - "bar.baz@email.example.com": {} - }, - "valid": false - }, - { - "description": "object without properties is valid", - "data": {}, - "valid": true - } - ] - } -] diff --git a/spec/issues/617_full_format_leap_year.spec.js b/spec/issues/617_full_format_leap_year.spec.js deleted file mode 100644 index 71c375fdfb..0000000000 --- a/spec/issues/617_full_format_leap_year.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict" - -var Ajv = require("../ajv") -require("../chai").should() - -describe("PR #617, full date format validation should understand leap years", function () { - it("should handle non leap year affected dates with date-time", function () { - var ajv = new Ajv({format: "full"}) - - var schema = {format: "date-time"} - var validDateTime = "2016-01-31T00:00:00Z" - - ajv.validate(schema, validDateTime).should.equal(true) - }) - - it("should handle non leap year affected dates with date", function () { - var ajv = new Ajv({format: "full"}) - - var schema = {format: "date"} - var validDate = "2016-11-30" - - ajv.validate(schema, validDate).should.equal(true) - }) - - it("should handle year leaps as date-time", function () { - var ajv = new Ajv({format: "full"}) - - var schema = {format: "date-time"} - var validDateTime = "2016-02-29T00:00:00Z" - var invalidDateTime = "2017-02-29T00:00:00Z" - - ajv.validate(schema, validDateTime).should.equal(true) - ajv.validate(schema, invalidDateTime).should.equal(false) - }) - - it("should handle year leaps as date", function () { - var ajv = new Ajv({format: "full"}) - - var schema = {format: "date"} - var validDate = "2016-02-29" - var invalidDate = "2017-02-29" - - ajv.validate(schema, validDate).should.equal(true) - ajv.validate(schema, invalidDate).should.equal(false) - }) -}) diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 0cb64741a0..3505763ab4 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -14,13 +14,35 @@ var remoteRefs = { } var SKIP = { - 4: ["optional/zeroTerminatedFloats"], + 4: [ + "optional/zeroTerminatedFloats", + "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here + "optional/format", + ], + 6: [ + "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here + "optional/format", + ], 7: [ + "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here "optional/content", + "format/date", + "format/date-time", + "format/email", + "format/hostname", "format/idn-email", "format/idn-hostname", + "format/ipv4", + "format/ipv6", "format/iri", "format/iri-reference", + "format/json-pointer", + "format/regex", + "format/relative-json-pointer", + "format/time", + "format/uri", + "format/uri-reference", + "format/uri-template", ], } diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index 1abb6ba338..5ee2ff2df5 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -1,16 +1,17 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() +const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("validation options", function () { describe("format", function () { it("should not validate formats if option format == false", function () { - var ajv = new Ajv(), - ajvFF = new Ajv({format: false}) + var ajv = new Ajv({formats: {date: DATE_FORMAT}}), + ajvFF = new Ajv({formats: {date: DATE_FORMAT}, format: false}) - var schema = {format: "date-time"} - var invalideDateTime = "06/19/1963 08:30:06 PST" + var schema = {format: "date"} + var invalideDateTime = "06/19/1963" // expects hyphens ajv.validate(schema, invalideDateTime).should.equal(false) ajvFF.validate(schema, invalideDateTime).should.equal(true) diff --git a/spec/options/ownProperties.spec.js b/spec/options/ownProperties.spec.js index 708e6fc19c..d72cf863cb 100644 --- a/spec/options/ownProperties.spec.js +++ b/spec/options/ownProperties.spec.js @@ -144,12 +144,12 @@ describe("ownProperties option", function () { it("should only validate own properties with propertyNames", function () { var schema = { propertyNames: { - format: "email", + pattern: "foo", }, } - var obj = {"e@example.com": 2} - var proto = {"not email": 1} + var obj = {foo: 2} + var proto = {bar: 1} test(schema, obj, proto, 2) }) diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.js index af2b4d7059..302e892c50 100644 --- a/spec/options/unknownFormats.spec.js +++ b/spec/options/unknownFormats.spec.js @@ -2,6 +2,7 @@ var Ajv = require("../ajv") var should = require("../chai").should() +const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("unknownFormats option", function () { describe("= true (default)", function () { @@ -21,6 +22,7 @@ describe("unknownFormats option", function () { test(new Ajv({$data: true, unknownFormats: true})) function test(ajv) { + ajv.addFormat("date", DATE_FORMAT) var validate = ajv.compile({ properties: { foo: {format: {$data: "1/bar"}}, @@ -52,6 +54,7 @@ describe("unknownFormats option", function () { test(new Ajv({$data: true, unknownFormats: "ignore"})) function test(ajv) { + ajv.addFormat("date", DATE_FORMAT) var validate = ajv.compile({ properties: { foo: {format: {$data: "1/bar"}}, @@ -86,6 +89,7 @@ describe("unknownFormats option", function () { test(new Ajv({$data: true, unknownFormats: ["allowed"]})) function test(ajv) { + ajv.addFormat("date", DATE_FORMAT) var validate = ajv.compile({ properties: { foo: {format: {$data: "1/bar"}}, diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index e8aac93382..a4f03247b9 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -97,7 +97,7 @@ describe("resolve", function () { ip1: { $id: "urn:some:ip:prop", type: "string", - format: "ipv4", + pattern: "^(\\d+\\.){3}\\d+$", }, ip2: { $ref: "urn:some:ip:prop", diff --git a/spec/schema-tests.spec.js b/spec/schema-tests.spec.js index c1c26dd70c..3eabd831ca 100644 --- a/spec/schema-tests.spec.js +++ b/spec/schema-tests.spec.js @@ -1,6 +1,7 @@ "use strict" var jsonSchemaTest = require("json-schema-test"), + addFormats = require("ajv-formats"), getAjvInstances = require("./ajv_instances"), options = require("./ajv_options"), suite = require("./browser_test_suite"), @@ -25,7 +26,7 @@ var remoteRefsWithIds = [ require("./remotes/scope_change.json"), ] -instances.forEach(addRemoteRefs) +instances.forEach(addRemoteRefsAndFormats) jsonSchemaTest(instances, { description: @@ -46,7 +47,8 @@ jsonSchemaTest(instances, { timeout: 120000, }) -function addRemoteRefs(ajv) { +function addRemoteRefsAndFormats(ajv) { for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id) ajv.addSchema(remoteRefsWithIds) + addFormats(ajv) } diff --git a/spec/tests/issues/1061_alternative_time_offsets.json b/spec/tests/issues/1061_alternative_time_offsets.json deleted file mode 100644 index bf80f9581c..0000000000 --- a/spec/tests/issues/1061_alternative_time_offsets.json +++ /dev/null @@ -1,74 +0,0 @@ -[ - { - "description": "Support for alternative ISO-8601 timezone offset formats (#1061)", - "schema": {"format": "date-time"}, - "tests": [ - { - "description": "valid positiive two digit", - "data": "2016-01-31T02:31:17+01", - "valid": true - }, - { - "description": "valid negative two digit", - "data": "2016-01-31T02:31:17-01", - "valid": true - }, - { - "description": "valid positiive four digit no colon", - "data": "2016-01-31T02:31:17+0130", - "valid": true - }, - { - "description": "valid negative four digit no colon", - "data": "2016-01-31T02:31:17-0130", - "valid": true - }, - { - "description": "invalid positiive three digit no colon", - "data": "2016-01-31T02:31:17+013", - "valid": false - }, - { - "description": "invalid negative three digit no colon", - "data": "2016-01-31T02:31:17-013", - "valid": false - } - ] - }, - { - "description": "Support for alternative ISO-8601 timezone offset formats (#1061)", - "schema": {"format": "time"}, - "tests": [ - { - "description": "valid positiive two digit", - "data": "02:31:17+01", - "valid": true - }, - { - "description": "valid negative two digit", - "data": "02:31:17-01", - "valid": true - }, - { - "description": "valid positiive four digit no colon", - "data": "02:31:17+0130", - "valid": true - }, - { - "description": "valid negative four digit no colon", - "data": "02:31:17-0130", - "valid": true - }, - { - "description": "invalid positiive three digit no colon", - "data": "02:31:17+013", - "valid": false - }, - { - "description": "invalid negative three digit no colon", - "data": "02:31:17-013", - "valid": false - } - ] - } -] diff --git a/spec/tests/rules/format.json b/spec/tests/rules/format.json index 433a0aaf8f..d52f4e5db0 100644 --- a/spec/tests/rules/format.json +++ b/spec/tests/rules/format.json @@ -11,735 +11,5 @@ "valid": true } ] - }, - { - "description": "format: regex", - "schema": { - "format": "regex" - }, - "tests": [ - { - "description": "valid regex", - "data": "[0-9]", - "valid": true - }, - { - "description": "invalid regex", - "data": "[9-0]", - "valid": false - }, - { - "description": "not string is valid", - "data": 123, - "valid": true - } - ] - }, - { - "description": "format: uri", - "schema": { - "format": "uri" - }, - "tests": [ - { - "description": "valid uri", - "data": "urn:isbn:978-3-531-18621-4", - "valid": true - }, - { - "description": "invalid relative uri-reference", - "data": "/abc", - "valid": false - } - ] - }, - { - "description": "format: uri-template", - "schema": { - "format": "uri-template" - }, - "tests": [ - { - "description": "valid uri-template", - "data": "http://example.com/dictionary/{term:1}/{term}", - "valid": true - }, - { - "description": "invalid uri-template", - "data": "http://example.com/dictionary/{term:1}/{term", - "valid": false - } - ] - }, - { - "description": "format: hostname", - "schema": { - "format": "hostname" - }, - "tests": [ - { - "description": "valid hostname", - "data": "123.example.com", - "valid": true - }, - { - "description": "valid hostname - trailing dot", - "data": "123.example.com.", - "valid": true - }, - { - "description": "valid hostname - single label", - "data": "localhost", - "valid": true - }, - { - "description": "valid hostname - single label with trailing dot", - "data": "localhost.", - "valid": true - }, - { - "description": "valid hostname #312", - "data": "lead-routing-qa.lvuucj.0001.use1.cache.amazonaws.com", - "valid": true - }, - { - "description": "valid hostname - maximum length label (63 chars)", - "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.example.com", - "valid": true - }, - { - "description": "invalid hostname - label too long (64 chars)", - "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.example.com", - "valid": false - }, - { - "description": "valid hostname - maximum length hostname (255 octets)", - "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxy.example.com", - "valid": true - }, - { - "description": "valid hostname - maximum length hostname (255 octets) with trailing dot", - "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxy.example.com.", - "valid": true - }, - { - "description": "invalid hostname - hostname too long (256 octets)", - "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.example.com", - "valid": false - }, - { - "description": "invalid hostname - hostname too long (256 octets) with trailing dot", - "data": "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.example.com.", - "valid": false - } - ] - }, - { - "description": "validation of URL strings", - "schema": {"format": "url"}, - "tests": [ - { - "data": "http://foo.com/blah_blah", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.com/blah_blah/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.com/blah_blah_(wikipedia)", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.com/blah_blah_(wikipedia)_(again)", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://www.example.com/wpstyle/?p=364", - "description": "a valid URL string", - "valid": true - }, - { - "data": "https://www.example.com/foo/?bar=baz&inga=42&quux", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://✪df.ws/123", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid:password@example.com:8080", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid:password@example.com:8080/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid@example.com", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid@example.com/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid@example.com:8080", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid@example.com:8080/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid:password@example.com", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://userid:password@example.com/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://142.42.1.1/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://142.42.1.1:8080/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://➡.ws/䨹", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://⌘.ws", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://⌘.ws/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.com/blah_(wikipedia)#cite-1", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.com/blah_(wikipedia)_blah#cite-1", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.com/unicode_(✪)_in_parens", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.com/(something)?after=parens", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://☺.damowmow.com/", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://code.google.com/events/#&product=browser", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://j.mp", - "description": "a valid URL string", - "valid": true - }, - { - "data": "ftp://foo.bar/baz", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://foo.bar/?q=Test%20URL-encoded%20stuff", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://مثال.إختبار", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://例子.测试", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://उदाहरण.परीक्षा", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://1337.net", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://a.b-c.de", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://223.255.255.254", - "description": "a valid URL string", - "valid": true - }, - { - "data": "http://", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://.", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://..", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://../", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://?", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://??", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://??/", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://#", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://##", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://##/", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://foo.bar?q=Spaces should be encoded", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "//", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "//a", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "///a", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "///", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http:///a", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "foo.com", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "rdar://1234", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "h://test", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http:// shouldfail.com", - "description": "an invalid URL string", - "valid": false - }, - { - "data": ":// should fail", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://foo.bar/foo(bar)baz quux", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "ftps://foo.bar/", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://-error-.invalid/", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://a.b--c.de/", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://-a.b.co", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://a.b-.co", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://0.0.0.0", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://10.1.1.0", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://10.1.1.255", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://224.1.1.1", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://1.1.1.1.1", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://123.123.123", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://3628126748", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://.www.foo.bar/", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://www.foo.bar./", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://.www.foo.bar./", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://10.1.1.1", - "description": "an invalid URL string", - "valid": false - }, - { - "data": "http://10.1.1.254", - "description": "an invalid URL string", - "valid": false - } - ] - }, - { - "description": "validation of date strings", - "schema": {"format": "date"}, - "tests": [ - { - "description": "a valid date string", - "data": "1963-06-19", - "valid": true - }, - { - "description": "an invalid date string", - "data": "06/19/1963", - "valid": false - }, - { - "description": "only RFC3339 not all of ISO 8601 are valid", - "data": "2013-350", - "valid": false - } - ] - }, - { - "description": "validation of time strings", - "schema": {"format": "time"}, - "tests": [ - { - "description": "a valid time", - "data": "12:34:56", - "valid": true - }, - { - "description": "a valid time with milliseconds", - "data": "12:34:56.789", - "valid": true - }, - { - "description": "a valid time with timezone", - "data": "12:34:56+01:00", - "valid": true - }, - { - "description": "an invalid time format", - "data": "12.34.56", - "valid": false - }, - { - "description": "an invalid time", - "data": "12:34:67", - "valid": false - }, - { - "description": "a valid time (leap second)", - "data": "23:59:60", - "valid": true - } - ] - }, - { - "description": "validation of date-time strings", - "schema": {"format": "date-time"}, - "tests": [ - { - "description": "a valid date-time string", - "data": "1963-06-19T12:13:14Z", - "valid": true - }, - { - "description": "an invalid date-time string (no time)", - "data": "1963-06-19", - "valid": false - }, - { - "description": "an invalid date-time string (additional part)", - "data": "1963-06-19T12:13:14ZTinvalid", - "valid": false - }, - { - "description": "an invalid date-time string (invalid date)", - "data": "1963-20-19T12:13:14Z", - "valid": false - }, - { - "description": "an invalid date-time string (invalid time)", - "data": "1963-06-19T12:13:67Z", - "valid": false - }, - { - "description": "a valid date-time string (leap second)", - "data": "2016-12-31T23:59:60Z", - "valid": true - } - ] - }, - { - "description": "validation of uuid strings", - "schema": {"format": "uuid"}, - "tests": [ - { - "description": "a valid uuid", - "data": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6", - "valid": true - }, - { - "description": "a valid uuid with uri prefix", - "data": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6", - "valid": true - }, - { - "description": "not valid uuid", - "data": "f81d4fae7dec11d0a76500a0c91e6bf6", - "valid": false - } - ] - }, - { - "description": "validation of JSON-pointer strings", - "schema": {"format": "json-pointer"}, - "tests": [ - { - "description": "a valid JSON-pointer", - "data": "/foo/bar~0/baz~1/%a", - "valid": true - }, - { - "description": "empty string is valid", - "data": "", - "valid": true - }, - { - "description": "/ is valid", - "data": "/", - "valid": true - }, - { - "description": "not a valid JSON-pointer (~ not escaped)", - "data": "/foo/bar~", - "valid": false - }, - { - "description": "valid JSON-pointer with empty segment", - "data": "/foo//bar", - "valid": true - }, - { - "description": "valid JSON-pointer with the last empty segment", - "data": "/foo/bar/", - "valid": true - } - ] - }, - { - "description": "validation of JSON-pointer URI fragment strings", - "schema": {"format": "json-pointer-uri-fragment"}, - "tests": [ - { - "description": "a valid JSON-pointer as uri fragment", - "data": "#/foo/%25a", - "valid": true - }, - { - "description": "not a valid JSON-pointer as uri fragment (% not URL-encoded)", - "data": "#/foo/%a", - "valid": false - }, - { - "description": "valid JSON-pointer with empty segment as uri fragment", - "data": "#/foo//bar", - "valid": true - }, - { - "description": "valid JSON-pointer with the last empty segment as uri fragment", - "data": "#/foo/bar/", - "valid": true - } - ] - }, - { - "description": "validation of relative JSON-pointer strings", - "schema": {"format": "relative-json-pointer"}, - "tests": [ - { - "description": "a valid relative JSON-pointer", - "data": "1/foo/bar~0/baz~1/%a", - "valid": true - }, - { - "description": "a valid relative JSON-pointer with #", - "data": "2#", - "valid": true - }, - { - "description": "parent reference is valid", - "data": "1", - "valid": true - }, - { - "description": "empty string is invalid", - "data": "", - "valid": false - }, - { - "description": "not a valid relative JSON-pointer (~ not escaped)", - "data": "1/foo/bar~", - "valid": false - }, - { - "description": "not a valid relative JSON-pointer (leading 0)", - "data": "01/foo", - "valid": false - }, - { - "description": "not a valid relative JSON-pointer with # (leading 0)", - "data": "02#", - "valid": false - }, - { - "description": "valid relative JSON-pointer with empty segment", - "data": "1/foo//bar", - "valid": true - }, - { - "description": "valid relative JSON-pointer with the last empty segment", - "data": "1/foo/bar/", - "valid": true - } - ] } ] From ffbb010b3c05718af9df5e1a4d59f5fd2afb71b4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 09:07:42 +0100 Subject: [PATCH 003/322] style: eslint --- .eslintrc.yml | 7 ++++--- .jshintrc | 9 --------- lib/compile/async.js | 3 ++- lib/compile/index.js | 3 ++- lib/compile/resolve.js | 3 ++- lib/compile/util.js | 9 ++++++--- lib/keyword.js | 5 +++-- package.json | 3 +-- spec/async_validate.spec.js | 3 ++- spec/custom.spec.js | 14 +++++++++----- 10 files changed, 31 insertions(+), 28 deletions(-) delete mode 100644 .jshintrc diff --git a/.eslintrc.yml b/.eslintrc.yml index 9fe920d6ab..f8ed3b2c38 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,4 +1,6 @@ extends: eslint:recommended +parserOptions: + ecmaVersion: 6 env: node: true browser: true @@ -9,20 +11,19 @@ rules: curly: [2, multi-or-nest, consistent] dot-location: [2, property] dot-notation: 2 - indent-legacy: [2, 2, SwitchCase: 1] linebreak-style: [2, unix] new-cap: 2 no-console: [2, allow: [warn, error]] no-else-return: 2 no-eq-null: 2 + no-extra-semi: 0 no-fallthrough: 2 no-invalid-this: 2 no-return-assign: 2 no-shadow: 1 no-trailing-spaces: 2 no-use-before-define: [2, nofunc] - quotes: [2, single, avoid-escape] - semi: [2, always] + semi: 0 strict: [2, global] valid-jsdoc: [2, requireReturn: false] no-control-regex: 0 diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index fffe0aac2b..0000000000 --- a/.jshintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "laxcomma": true, - "laxbreak": true, - "curly": false, - "-W058": true, - "eqeqeq": false, - "node": true, - "validthis": true -} diff --git a/lib/compile/async.js b/lib/compile/async.js index 7709caddd5..bb758d767f 100644 --- a/lib/compile/async.js +++ b/lib/compile/async.js @@ -56,7 +56,7 @@ function compileAsync(schema, meta, callback) { function loadMissingSchema(e) { var ref = e.missingSchema - if (added(ref)) + if (added(ref)) { throw new Error( "Schema " + ref + @@ -64,6 +64,7 @@ function compileAsync(schema, meta, callback) { e.missingRef + " cannot be resolved" ) + } var schemaPromise = self._loadingSchemas[ref] if (!schemaPromise) { diff --git a/lib/compile/index.js b/lib/compile/index.js index 2dea7a530c..2eb4750d2a 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -264,10 +264,11 @@ function compile(schema, root, localRefs, baseId) { !deps.every(function (keyword) { return Object.prototype.hasOwnProperty.call(parentSchema, keyword) }) - ) + ) { throw new Error( "parent schema must have all required keywords: " + deps.join(",") ) + } var validateSchema = rule.definition.validateSchema if (validateSchema) { diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index 33ffb596af..bc47364820 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -255,10 +255,11 @@ function resolveIds(schema) { var id = self._getId(sch) var baseId = baseIds[parentJsonPtr] var fullPath = fullPaths[parentJsonPtr] + "/" + parentKeyword - if (keyIndex !== undefined) + if (keyIndex !== undefined) { fullPath += "/" + (typeof keyIndex == "number" ? keyIndex : util.escapeFragment(keyIndex)) + } if (typeof id == "string") { id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id) diff --git a/lib/compile/util.js b/lib/compile/util.js index 31d927d03e..e944274187 100644 --- a/lib/compile/util.js +++ b/lib/compile/util.js @@ -106,9 +106,10 @@ function checkDataTypes(dataTypes, data, strictNumbers) { delete types.object } if (types.number) delete types.integer - for (var t in types) + for (var t in types) { code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, true) + } return code } @@ -222,20 +223,22 @@ function getData($data, lvl, paths) { up = +matches[1] jsonPointer = matches[2] if (jsonPointer == "#") { - if (up >= lvl) + if (up >= lvl) { throw new Error( "Cannot access property/index " + up + " levels up, current level is " + lvl ) + } return paths[lvl - up] } - if (up > lvl) + if (up > lvl) { throw new Error( "Cannot access data " + up + " levels up, current level is " + lvl ) + } data = "data" + (lvl - up || "") if (!jsonPointer) return data } diff --git a/lib/keyword.js b/lib/keyword.js index 79cef51f2d..1f46ff4349 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -137,9 +137,10 @@ function validateKeyword(definition, throwError) { if (v(definition)) return true validateKeyword.errors = v.errors - if (throwError) + if (throwError) { throw new Error( "custom keyword definition is invalid: " + this.errorsText(v.errors) ) - else return false + } + return false } diff --git a/package.json b/package.json index 3986e2d3f5..f0a6191215 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ ], "scripts": { "eslint": "eslint lib/{compile/,}*.js spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", - "jshint": "jshint lib/{compile/,}*.js", - "lint": "npm run jshint && npm run eslint", + "lint": "npm run eslint", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", "test-spec": "mocha spec/{**/,}*.spec.js -R spec", diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index dd2525e660..0e9724a49b 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -361,10 +361,11 @@ describe("async schemas, formats and keywords", function () { return repeat(function () { return Promise.all( instances.map(function (_ajv) { - if (refSchema) + if (refSchema) { try { _ajv.addSchema(refSchema) } catch (e) {} + } var validate = _ajv.compile(schema) var data diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 00d410a84c..e36b29a64f 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -905,7 +905,7 @@ describe("Custom keywords", function () { shouldBeValid(validate, {foo: 3.99}) shouldBeInvalid(validate, {foo: 2}, numErrors) - if (customErrors) + if (customErrors) { shouldBeRangeError( validate.errors[0], ".foo", @@ -914,8 +914,9 @@ describe("Custom keywords", function () { 2, true ) + } shouldBeInvalid(validate, {foo: 4}, numErrors) - if (customErrors) + if (customErrors) { shouldBeRangeError( validate.errors[0], ".foo", @@ -924,6 +925,7 @@ describe("Custom keywords", function () { 4, true ) + } }) } @@ -982,18 +984,20 @@ describe("Custom keywords", function () { schema.length == 2 && typeof schema[0] == "number" && typeof schema[1] == "number" - if (!schemaValid) + if (!schemaValid) { throw new Error( "Invalid schema for range keyword, should be array of 2 numbers" ) + } var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined || typeof parentSchema.exclusiveRange == "boolean" - if (!exclusiveRangeSchemaValid) + if (!exclusiveRangeSchemaValid) { throw new Error( - "Invalid schema for exclusiveRange keyword, should be bolean" + "Invalid schema for exclusiveRange keyword, should be boolean" ) + } } function shouldBeValid(validate, data) { From 2a6d1a990fd0682391fe78607423667e521e8ebe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 09:31:50 +0100 Subject: [PATCH 004/322] docs: formats moved to ajv-formats --- KEYWORDS.md | 4 ++-- README.md | 33 +++++++++++++++++++++------------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/KEYWORDS.md b/KEYWORDS.md index fae3fb1b94..56175a1baa 100644 --- a/KEYWORDS.md +++ b/KEYWORDS.md @@ -160,7 +160,7 @@ _invalid_: `"def"`, `""` The value of the keyword should be a string. The data to be valid should match the format with this name. -Ajv defines these formats: date, date-time, uri, email, hostname, ipv4, ipv6, regex. +Ajv does not include any formats, they can be added with [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin. **Example** @@ -536,7 +536,7 @@ For data object to be valid each property name in this object should be valid ac **Example** -_schema_: +_schema_ (requires `email` format from [ajv-formats](https://github.com/ajv-validator/ajv-formats)): ```json { diff --git a/README.md b/README.md index c40b9c6c35..4c266ba595 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Performance of different validators by [json-schema-benchmark](https://github.co - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) - support of circular references between schemas - correct string lengths for strings with unicode pairs (can be turned off) - - [formats](#formats) defined by JSON Schema draft-07 standard and custom formats (can be turned off) + - [formats](#formats) defined by JSON Schema draft-07 standard (with [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin) and custom formats (can be turned off) - [validates schemas against meta-schema](#api-validateschema) - supports [browsers](#using-in-browser) and Node.js 0.10-14.x - [asynchronous loading](#asynchronous-schema-compilation) of referenced schemas during compilation @@ -287,11 +287,22 @@ JSON Schema specification defines several annotation keywords that describe sche ## Formats -Ajv implements formats defined by JSON Schema specification and several other formats. It is recommended NOT to use "format" keyword implementations with untrusted data, as they use potentially unsafe regular expressions - see [ReDoS attack](#redos-attack). +From version 7 Ajv does not include formats defined by JSON Schema specification - these and several others formats are provided by [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin. + +To add all formats from this plugin: + +```javascript +const ajv = new Ajv() +require("ajv-formats")(ajv) +``` + +See ajv-formats documentation for further details. + +It is recommended NOT to use "format" keyword implementations with untrusted data, as they use potentially unsafe regular expressions - see [ReDoS attack](#redos-attack). **Please note**: if you need to use "format" keyword to validate untrusted data, you MUST assess their suitability and safety for your validation scenarios. -The following formats are implemented for string validation with "format" keyword: +The following formats are defined in [ajv-formats](https://github.com/ajv-validator/ajv-formats) for string validation with "format" keyword: - _date_: full-date according to [RFC3339](http://tools.ietf.org/html/rfc3339#section-5.6). - _time_: time with optional time-zone. @@ -309,11 +320,9 @@ The following formats are implemented for string validation with "format" keywor - _json-pointer_: JSON-pointer according to [RFC6901](https://tools.ietf.org/html/rfc6901). - _relative-json-pointer_: relative JSON-pointer according to [this draft](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00). -**Please note**: JSON Schema draft-07 also defines formats `iri`, `iri-reference`, `idn-hostname` and `idn-email` for URLs, hostnames and emails with international characters. Ajv does not implement these formats. If you create Ajv plugin that implements them please make a PR to mention this plugin here. - -There are two modes of format validation: `fast` and `full`. This mode affects formats `date`, `time`, `date-time`, `uri`, `uri-reference`, and `email`. See [Options](#options) for details. +**Please note**: JSON Schema draft-07 also defines formats `iri`, `iri-reference`, `idn-hostname` and `idn-email` for URLs, hostnames and emails with international characters. These formats are available in [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) plugin. -You can add additional formats and replace any of the formats above using [addFormat](#api-addformat) method. +You can add (and replace) any formats using [addFormat](#api-addformat) method. The option `unknownFormats` allows changing the default behaviour when an unknown format is encountered. In this case Ajv can either fail schema compilation (default) or ignore it (default in versions before 5.0.0). You also can allow specific format(s) that will be ignored. See [Options](#options) for details. @@ -1107,7 +1116,7 @@ Defaults: uniqueItems: true, unicode: true, nullable: false, - format: false, + format: true, formats: {}, unknownFormats: true, schemas: {}, @@ -1159,8 +1168,7 @@ Defaults: - _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters. - _nullable_: support keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - _format_: formats validation mode. Option values: - - `"fast"` (default) - simplified and fast validation (see [Formats](#formats) for details of which formats are available and affected by this option). - - `"full"` - more restrictive and slow validation. E.g., 25:00:00 and 2015/14/33 will be invalid time and date in 'full' mode but it will be valid in 'fast' mode. + - `true` (default) - validate added formats (see [Formats](#formats)). - `false` - ignore all format keywords. - _formats_: an object with custom formats. Keys and values will be passed to `addFormat` method. - _keywords_: an object with custom keywords. Keys and values will be passed to `addKeyword` method. @@ -1335,16 +1343,17 @@ If you have published a useful plugin please submit a PR to add it to the next s ## Related packages -- [ajv-async](https://github.com/ajv-validator/ajv-async) - plugin to configure async validation mode +- [ajv-async](https://github.com/ajv-validator/ajv-async) - plugin to configure async validation mode (DEPRECATED) - [ajv-bsontype](https://github.com/BoLaMN/ajv-bsontype) - plugin to validate mongodb's bsonType formats - [ajv-cli](https://github.com/jessedc/ajv-cli) - command line interface +- [ajv-formats](https://github.com/ajv-validator/ajv-formats) - formats defined in JSON Schema specification. - [ajv-errors](https://github.com/ajv-validator/ajv-errors) - plugin for custom error messages - [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) - internationalised error messages - [ajv-istanbul](https://github.com/ajv-validator/ajv-istanbul) - plugin to instrument generated validation code to measure test coverage of your schemas - [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) - plugin with custom validation keywords (select, typeof, etc.) - [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) - plugin with keywords $merge and $patch - [ajv-pack](https://github.com/ajv-validator/ajv-pack) - produces a compact module exporting validation functions -- [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) - format validators for draft2019 that aren't already included in ajv (ie. `idn-hostname`, `idn-email`, `iri`, `iri-reference` and `duration`). +- [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) - format validators for draft2019 that aren't included in [ajv-formats](https://github.com/ajv-validator/ajv-formats) (ie. `idn-hostname`, `idn-email`, `iri`, `iri-reference` and `duration`). ## Some packages using Ajv From bac244e9b5344f1924991ff1e24264f06ba779fd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 10:46:41 +0100 Subject: [PATCH 005/322] fix: budle generation (const not supported atm) --- lib/ajv.js | 2 +- spec/options/options_validation.spec.js | 4 ++-- spec/options/unknownFormats.spec.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ajv.js b/lib/ajv.js index 228d83ba87..02541fc798 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -60,7 +60,7 @@ function Ajv(opts) { this._refs = {} this._fragments = {} this._formats = {} - const formatOpt = opts.format + var formatOpt = opts.format opts.format = false this._cache = opts.cache || new Cache() diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index 5ee2ff2df5..07d667a07d 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -1,8 +1,8 @@ "use strict" -const Ajv = require("../ajv") +var Ajv = require("../ajv") require("../chai").should() -const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ +var DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("validation options", function () { describe("format", function () { diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.js index 302e892c50..54815983f5 100644 --- a/spec/options/unknownFormats.spec.js +++ b/spec/options/unknownFormats.spec.js @@ -2,7 +2,7 @@ var Ajv = require("../ajv") var should = require("../chai").should() -const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ +var DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("unknownFormats option", function () { describe("= true (default)", function () { From 6fe3e5f4ef693e605d9bd14d111e59f47d46e1c2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 11:14:16 +0100 Subject: [PATCH 006/322] fix: browser tests --- package.json | 2 +- spec/json-schema.spec.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f0a6191215..cde5510b03 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", - "test-all": "npm run test-cov && if-node-version 10 npm run test-browser", + "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", "test": "npm run lint && npm run build && npm run test-all", "prepublish": "npm run build && npm run bundle", "watch": "watch \"npm run build\" ./lib/dot" diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 3505763ab4..a8ff0683f6 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -18,10 +18,12 @@ var SKIP = { "optional/zeroTerminatedFloats", "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here "optional/format", + "format", ], 6: [ "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here "optional/format", + "format", ], 7: [ "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here From 197a9e8c32506cb099884d794511d033363088dc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 16:42:02 +0100 Subject: [PATCH 007/322] feat: remove support for booleam exclusiveMaximum/Minimum (draft-04) --- KEYWORDS.md | 12 +-- lib/compile/rules.js | 6 +- lib/dot/_limit.jst | 115 +++------------------------- lib/dot/errors.def | 8 +- lib/dotjs/index.js | 2 + spec/errors.spec.js | 4 +- spec/extras/$data/maximum.json | 132 ++++++++++++++++----------------- spec/extras/$data/minimum.json | 122 ++++++++++++++---------------- spec/json-schema.spec.js | 22 ------ spec/options/schemaId.spec.js | 2 - 10 files changed, 145 insertions(+), 280 deletions(-) diff --git a/KEYWORDS.md b/KEYWORDS.md index 56175a1baa..73c3658350 100644 --- a/KEYWORDS.md +++ b/KEYWORDS.md @@ -76,11 +76,9 @@ Most other keywords apply only to a particular type of data. If the data is of d The value of keyword `maximum` (`minimum`) should be a number. This value is the maximum (minimum) allowed value for the data to be valid. -Draft-04: The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a boolean value. These keyword cannot be used without `maximum` (`minimum`). If this keyword value is equal to `true`, the data should not be equal to the value in `maximum` (`minimum`) keyword to be valid. +The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a number. This value is the exclusive maximum (minimum) allowed value for the data to be valid (the data equal to this keyword value is invalid). -Draft-06/07: The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a number. This value is the exclusive maximum (minimum) allowed value for the data to be valid (the data equal to this keyword value is invalid). - -Ajv supports both draft-04 and draft-06/07 syntaxes. +**Please note**: Boolean value for keywords `exclusiveMaximum` (`exclusiveMinimum`) is no longer supported. **Examples** @@ -90,15 +88,13 @@ Ajv supports both draft-04 and draft-06/07 syntaxes. _invalid_: `6`, `7` -2) _schema_: `{ "minimum": 5 }` +2. _schema_: `{ "minimum": 5 }` _valid_: `5`, `6`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) _invalid_: `4`, `4.5` -3. _schema_: - draft-04: `{ "minimum": 5, "exclusiveMinimum": true }` - draft-06/07: `{ "exclusiveMinimum": 5 }` +3. _schema_: `{ "exclusiveMinimum": 5 }` _valid_: `6`, `7`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) diff --git a/lib/compile/rules.js b/lib/compile/rules.js index a99691b439..9890a7fa23 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -8,8 +8,10 @@ module.exports = function rules() { { type: "number", rules: [ - {maximum: ["exclusiveMaximum"]}, - {minimum: ["exclusiveMinimum"]}, + "maximum", + "minimum", + "exclusiveMaximum", + "exclusiveMinimum", "multipleOf", "format", ], diff --git a/lib/dot/_limit.jst b/lib/dot/_limit.jst index f152189222..125f5fd6f9 100644 --- a/lib/dot/_limit.jst +++ b/lib/dot/_limit.jst @@ -3,111 +3,20 @@ {{# def.setupKeyword }} {{# def.$data }} -{{## def.setExclusiveLimit: - $exclusive = true; - $errorKeyword = $exclusiveKeyword; - $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; -#}} +{{# def.numberKeyword }} {{ - var $isMax = $keyword == 'maximum' - , $exclusiveKeyword = $isMax ? 'exclusiveMaximum' : 'exclusiveMinimum' - , $schemaExcl = it.schema[$exclusiveKeyword] - , $isDataExcl = it.opts.$data && $schemaExcl && $schemaExcl.$data - , $op = $isMax ? '<' : '>' - , $notOp = $isMax ? '>' : '<' - , $errorKeyword = undefined; - - if (!($isData || typeof $schema == 'number' || $schema === undefined)) { - throw new Error($keyword + ' must be number'); - } - if (!($isDataExcl || $schemaExcl === undefined - || typeof $schemaExcl == 'number' - || typeof $schemaExcl == 'boolean')) { - throw new Error($exclusiveKeyword + ' must be number or boolean'); + var $op /* used in errors */, $notOp; + switch ($keyword) { + case 'maximum': $op = '<='; $notOp = '>'; break; + case 'minimum': $op = '>='; $notOp = '<'; break; + case 'exclusiveMaximum': $op = '<'; $notOp = '>='; break; + case 'exclusiveMinimum': $op = '>'; $notOp = '<='; break; + default: throw Error('not _limit keyword ' + $keyword); } }} -{{? $isDataExcl }} - {{ - var $schemaValueExcl = it.util.getData($schemaExcl.$data, $dataLvl, it.dataPathArr) - , $exclusive = 'exclusive' + $lvl - , $exclType = 'exclType' + $lvl - , $exclIsNumber = 'exclIsNumber' + $lvl - , $opExpr = 'op' + $lvl - , $opStr = '\' + ' + $opExpr + ' + \''; - }} - var schemaExcl{{=$lvl}} = {{=$schemaValueExcl}}; - {{ $schemaValueExcl = 'schemaExcl' + $lvl; }} - - var {{=$exclusive}}; - var {{=$exclType}} = typeof {{=$schemaValueExcl}}; - if ({{=$exclType}} != 'boolean' && {{=$exclType}} != 'undefined' && {{=$exclType}} != 'number') { - {{ var $errorKeyword = $exclusiveKeyword; }} - {{# def.error:'_exclusiveLimit' }} - } else if ({{# def.$dataNotType:'number' }} - {{=$exclType}} == 'number' - ? ( - ({{=$exclusive}} = {{=$schemaValue}} === undefined || {{=$schemaValueExcl}} {{=$op}}= {{=$schemaValue}}) - ? {{=$data}} {{=$notOp}}= {{=$schemaValueExcl}} - : {{=$data}} {{=$notOp}} {{=$schemaValue}} - ) - : ( - ({{=$exclusive}} = {{=$schemaValueExcl}} === true) - ? {{=$data}} {{=$notOp}}= {{=$schemaValue}} - : {{=$data}} {{=$notOp}} {{=$schemaValue}} - ) - || {{=$data}} !== {{=$data}}) { - var op{{=$lvl}} = {{=$exclusive}} ? '{{=$op}}' : '{{=$op}}='; - {{ - if ($schema === undefined) { - $errorKeyword = $exclusiveKeyword; - $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; - $schemaValue = $schemaValueExcl; - $isData = $isDataExcl; - } - }} -{{??}} - {{ - var $exclIsNumber = typeof $schemaExcl == 'number' - , $opStr = $op; /*used in error*/ - }} - - {{? $exclIsNumber && $isData }} - {{ var $opExpr = '\'' + $opStr + '\''; /*used in error*/ }} - if ({{# def.$dataNotType:'number' }} - ( {{=$schemaValue}} === undefined - || {{=$schemaExcl}} {{=$op}}= {{=$schemaValue}} - ? {{=$data}} {{=$notOp}}= {{=$schemaExcl}} - : {{=$data}} {{=$notOp}} {{=$schemaValue}} ) - || {{=$data}} !== {{=$data}}) { - {{??}} - {{ - if ($exclIsNumber && $schema === undefined) { - {{# def.setExclusiveLimit }} - $schemaValue = $schemaExcl; - $notOp += '='; - } else { - if ($exclIsNumber) - $schemaValue = Math[$isMax ? 'min' : 'max']($schemaExcl, $schema); - - if ($schemaExcl === ($exclIsNumber ? $schemaValue : true)) { - {{# def.setExclusiveLimit }} - $notOp += '='; - } else { - $exclusive = false; - $opStr += '='; - } - } - - var $opExpr = '\'' + $opStr + '\''; /*used in error*/ - }} - - if ({{# def.$dataNotType:'number' }} - {{=$data}} {{=$notOp}} {{=$schemaValue}} - || {{=$data}} !== {{=$data}}) { - {{?}} -{{?}} - {{ $errorKeyword = $errorKeyword || $keyword; }} - {{# def.error:'_limit' }} - } {{? $breakOnError }} else { {{?}} +if ({{# def.$dataNotType:'number' }} {{=$data}} {{=$notOp}} {{=$schemaValue}} || {{=$data}} !== {{=$data}}) { + {{ var $errorKeyword = $keyword; }} + {{# def.error:'_limit' }} +} {{? $breakOnError }} else { {{?}} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 5c5752cb04..d5658834d5 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -102,8 +102,8 @@ 'enum': "'should be equal to one of the allowed values'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", - _limit: "'should be {{=$opStr}} {{#def.appendSchema}}", - _exclusiveLimit: "'{{=$exclusiveKeyword}} should be boolean'", + _limit: "'should be {{=$op}} {{#def.appendSchema}}", + _exclusiveLimit: "'should be {{=$op}} {{#def.appendSchema}}", _limitItems: "'should NOT have {{?$keyword=='maxItems'}}more{{??}}fewer{{?}} than {{#def.concatSchema}} items'", _limitLength: "'should NOT be {{?$keyword=='maxLength'}}longer{{??}}shorter{{?}} than {{#def.concatSchema}} characters'", _limitProperties:"'should NOT have {{?$keyword=='maxProperties'}}more{{??}}fewer{{?}} than {{#def.concatSchema}} properties'", @@ -139,7 +139,6 @@ format: "{{#def.schemaRefOrQS}}", 'if': "validate.schema{{=$schemaPath}}", _limit: "{{#def.schemaRefOrVal}}", - _exclusiveLimit: "validate.schema{{=$schemaPath}}", _limitItems: "{{#def.schemaRefOrVal}}", _limitLength: "{{#def.schemaRefOrVal}}", _limitProperties:"{{#def.schemaRefOrVal}}", @@ -173,8 +172,7 @@ 'enum': "{ allowedValues: schema{{=$lvl}} }", format: "{ format: {{#def.schemaValueQS}} }", 'if': "{ failingKeyword: {{=$ifClause}} }", - _limit: "{ comparison: {{=$opExpr}}, limit: {{=$schemaValue}}, exclusive: {{=$exclusive}} }", - _exclusiveLimit: "{}", + _limit: "{ comparison: '{{=$op}}', limit: {{=$schemaValue}} }", _limitItems: "{ limit: {{=$schemaValue}} }", _limitLength: "{ limit: {{=$schemaValue}} }", _limitProperties:"{ limit: {{=$schemaValue}} }", diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 8539441688..39eca6e7b0 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -10,6 +10,8 @@ module.exports = { contains: require("./contains"), dependencies: require("./dependencies"), enum: require("./enum"), + exclusiveMaximum: require("./_limit"), + exclusiveMinimum: require("./_limit"), format: require("./format"), if: require("./if"), items: require("./items"), diff --git a/spec/errors.spec.js b/spec/errors.spec.js index b318dbe32c..b408c58031 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -984,14 +984,12 @@ describe("Validation errors", function () { testError("exclusiveMinimum", "should be > 2", { comparison: ">", limit: 2, - exclusive: true, }) shouldBeInvalid(validate, 5) testError("exclusiveMaximum", "should be < 5", { comparison: "<", limit: 5, - exclusive: true, }) function testError(keyword, message, params) { @@ -1038,7 +1036,7 @@ describe("Validation errors", function () { "#/properties/smaller/exclusiveMaximum", _ajv._opts.jsonPointers ? "/smaller" : ".smaller", "should be < 4", - {comparison: "<", limit: 4, exclusive: true} + {comparison: "<", limit: 4} ) } }) diff --git a/spec/extras/$data/maximum.json b/spec/extras/$data/maximum.json index e9f9807c55..6469d19163 100644 --- a/spec/extras/$data/maximum.json +++ b/spec/extras/$data/maximum.json @@ -64,49 +64,57 @@ "schema": { "properties": { "number": { - "maximum": 3, - "exclusiveMaximum": {"$data": "1/maxIsExclusive"} + "maximum": 5, + "exclusiveMaximum": {"$data": "1/exclusiveMaximum"} }, - "maxIsExclusive": {} + "exclusiveMaximum": {} } }, "tests": [ { - "description": "below the maximum is valid when exclusiveMaximum is true", + "description": "exclusiveMaximum boolean no longer supported", "data": { - "number": 2, - "maxIsExclusive": true + "number": 4, + "exclusiveMaximum": true }, - "valid": true + "valid": false }, { - "description": "below the maximum is valid when exclusiveMaximum is false", + "description": "below the maximum is valid when exclusiveMaximum is strictly larger", "data": { - "number": 2, - "maxIsExclusive": false + "number": 4, + "exclusiveMaximum": 4.1 }, "valid": true }, + { + "description": "below the maximum is NOT valid when exclusiveMaximum is equal", + "data": { + "number": 4, + "exclusiveMaximum": 4 + }, + "valid": false + }, { "description": "below the maximum is valid when exclusiveMaximum is undefined", "data": { - "number": 2 + "number": 4 }, "valid": true }, { - "description": "boundary point is invalid when exclusiveMaximum is true", + "description": "boundary point is invalid when exclusiveMaximum is equal", "data": { "number": 3, - "maxIsExclusive": true + "exclusiveMaximum": 3 }, "valid": false }, { - "description": "boundary point is valid when exclusiveMaximum is false", + "description": "boundary point is valid when exclusiveMaximum is smaller", "data": { "number": 3, - "maxIsExclusive": false + "exclusiveMaximum": 3.1 }, "valid": true }, @@ -118,137 +126,121 @@ "valid": true }, { - "description": "above the maximum is invalid when exclusiveMaximum is true", + "description": "above the maximum is invalid", "data": { - "number": 4, - "maxIsExclusive": true + "number": 6 }, "valid": false }, { - "description": "above the maximum is invalid when exclusiveMaximum is false", + "description": "fails if value of exclusiveMaximum is not number", "data": { "number": 4, - "maxIsExclusive": false - }, - "valid": false - }, - { - "description": "above the maximum is invalid when exclusiveMaximum is undefined", - "data": { - "number": 4 - }, - "valid": false - }, - { - "description": "fails if value of exclusiveMaximum is not boolean", - "data": { - "number": 2, - "maxIsExclusive": "false" + "exclusiveMaximum": "5" }, "valid": false } ] }, { - "description": "one property is maximum for another and exclusiveMaximum is $data", + "description": "maximum and exclusiveMaximum are $data", "schema": { "properties": { "larger": {}, "smallerOrEqual": { "maximum": {"$data": "1/larger"}, - "exclusiveMaximum": {"$data": "1/maxIsExclusive"} + "exclusiveMaximum": {"$data": "1/exclusiveMaximum"} }, - "maxIsExclusive": {} + "exclusiveMaximum": {} } }, "tests": [ { - "description": "below the maximum is valid when exclusiveMaximum is true", + "description": "exclusiveMaximum boolean no longer supported", "data": { "larger": 3, "smallerOrEqual": 2, - "maxIsExclusive": true + "exclusiveMaximum": true }, - "valid": true + "valid": false }, { - "description": "below the maximum is valid when exclusiveMaximum is false", + "description": "below the maximum is valid when exclusiveMaximum is strictly larger", "data": { "larger": 3, "smallerOrEqual": 2, - "maxIsExclusive": false + "exclusiveMaximum": 2.1 }, "valid": true }, { - "description": "below the maximum is valid when exclusiveMaximum is undefined", + "description": "below the maximum is NOT valid when exclusiveMaximum is equal", "data": { "larger": 3, - "smallerOrEqual": 2 + "smallerOrEqual": 2, + "exclusiveMaximum": 2 }, - "valid": true + "valid": false }, { - "description": "boundary point is invalid when exclusiveMaximum is true", + "description": "below the maximum is valid when exclusiveMaximum is undefined", "data": { "larger": 3, - "smallerOrEqual": 3, - "maxIsExclusive": true + "smallerOrEqual": 2 }, - "valid": false + "valid": true }, { - "description": "boundary point is valid when exclusiveMaximum is false", + "description": "above the maximum is invalid", "data": { "larger": 3, - "smallerOrEqual": 3, - "maxIsExclusive": false + "smallerOrEqual": 4, + "exclusiveMaximum": 5 }, - "valid": true + "valid": false }, { - "description": "boundary point is valid when exclusiveMaximum is undefined", + "description": "above the maximum is invalid when exclusiveMaximum is undefined", "data": { "larger": 3, - "smallerOrEqual": 3 + "smallerOrEqual": 4 }, - "valid": true + "valid": false }, { - "description": "above the maximum is invalid when exclusiveMaximum is true", + "description": "fails if value of exclusiveMaximum is not number", "data": { "larger": 3, - "smallerOrEqual": 4, - "maxIsExclusive": true + "smallerOrEqual": 2, + "exclusiveMaximum": "5" }, "valid": false }, { - "description": "above the maximum is invalid when exclusiveMaximum is false", + "description": "boundary point is valid when exclusiveMaximum is strictly larger", "data": { "larger": 3, - "smallerOrEqual": 4, - "maxIsExclusive": false + "smallerOrEqual": 3, + "exclusiveMaximum": 3.1 }, - "valid": false + "valid": true }, { - "description": "above the maximum is invalid when exclusiveMaximum is undefined", + "description": "boundary point is invalid when exclusiveMaximum is equal", "data": { "larger": 3, - "smallerOrEqual": 4 + "smallerOrEqual": 3, + "exclusiveMaximum": 3 }, "valid": false }, { - "description": "fails if value of exclusiveMaximum is not boolean", + "description": "boundary point is valid when exclusiveMaximum is undefined", "data": { "larger": 3, - "smallerOrEqual": 2, - "maxIsExclusive": "false" + "smallerOrEqual": 3 }, - "valid": false + "valid": true } ] }, diff --git a/spec/extras/$data/minimum.json b/spec/extras/$data/minimum.json index 00ddc30a7c..861501ab18 100644 --- a/spec/extras/$data/minimum.json +++ b/spec/extras/$data/minimum.json @@ -58,28 +58,36 @@ "properties": { "number": { "minimum": 3, - "exclusiveMinimum": {"$data": "1/minIsExclusive"} + "exclusiveMinimum": {"$data": "1/exclusiveMinimum"} }, - "minIsExclusive": {} + "exclusiveMinimum": {} } }, "tests": [ { - "description": "above the minimum is valid when exclusiveMinimum is true", + "description": "exclusiveMinimum boolean no longer supported", "data": { "number": 4, - "minIsExclusive": true + "exclusiveMinimum": true }, - "valid": true + "valid": false }, { - "description": "above the minimum is valid when exclusiveMinimum is false", + "description": "above the minimum is valid when exclusiveMinimum is strictly smaller", "data": { "number": 4, - "minIsExclusive": false + "exclusiveMinimum": 3.9 }, "valid": true }, + { + "description": "above the minimum is NOT valid when exclusiveMinimum is equal", + "data": { + "number": 4, + "exclusiveMinimum": 4 + }, + "valid": false + }, { "description": "above the minimum is valid when exclusiveMinimum is undefined", "data": { @@ -88,18 +96,18 @@ "valid": true }, { - "description": "boundary point is invalid when exclusiveMinimum is true", + "description": "boundary point is invalid when exclusiveMinimum is equal", "data": { "number": 3, - "minIsExclusive": true + "exclusiveMinimum": 3 }, "valid": false }, { - "description": "boundary point is valid when exclusiveMinimum is false", + "description": "boundary point is valid when exclusiveMinimum is smaller", "data": { "number": 3, - "minIsExclusive": false + "exclusiveMinimum": 2.9 }, "valid": true }, @@ -111,137 +119,121 @@ "valid": true }, { - "description": "below the minimum is invalid when exclusiveMinimum is true", - "data": { - "number": 2, - "minIsExclusive": true - }, - "valid": false - }, - { - "description": "below the minimum is invalid when exclusiveMinimum is false", - "data": { - "number": 2, - "minIsExclusive": false - }, - "valid": false - }, - { - "description": "below the minimum is invalid when exclusiveMinimum is undefined", + "description": "below the minimum is invalid", "data": { "number": 2 }, "valid": false }, { - "description": "fails if value of exclusiveMinimum is not boolean", + "description": "fails if value of exclusiveMinimum is not number", "data": { "number": 4, - "minIsExclusive": "false" + "exclusiveMinimum": "3" }, "valid": false } ] }, { - "description": "one property is minimum for another and exclusiveMinimum is $data", + "description": "minimum and exclusiveMinimum are $data", "schema": { "properties": { "smaller": {}, "largerOrEqual": { "minimum": {"$data": "1/smaller"}, - "exclusiveMinimum": {"$data": "1/minIsExclusive"} + "exclusiveMinimum": {"$data": "1/exclusiveMinimum"} }, - "minIsExclusive": {} + "exclusiveMinimum": {} } }, "tests": [ { - "description": "above the minimum is valid when exclusiveMinimum is true", + "description": "exclusiveMinimum boolean no longer supported", "data": { "smaller": 3, "largerOrEqual": 4, - "minIsExclusive": true + "exclusiveMinimum": true }, - "valid": true + "valid": false }, { - "description": "above the minimum is valid when exclusiveMinimum is false", + "description": "above the minimum is valid when exclusiveMinimum is strictly smaller", "data": { "smaller": 3, "largerOrEqual": 4, - "minIsExclusive": false + "exclusiveMinimum": 3.9 }, "valid": true }, { - "description": "above the minimum is valid when exclusiveMinimum is undefined", + "description": "above the minimum is NOT valid when exclusiveMinimum is equal", "data": { "smaller": 3, - "largerOrEqual": 4 + "largerOrEqual": 4, + "exclusiveMinimum": 4 }, - "valid": true + "valid": false }, { - "description": "boundary point is invalid when exclusiveMinimum is true", + "description": "above the minimum is valid when exclusiveMinimum is undefined", "data": { "smaller": 3, - "largerOrEqual": 3, - "minIsExclusive": true + "largerOrEqual": 4 }, - "valid": false + "valid": true }, { - "description": "boundary point is valid when exclusiveMinimum is false", + "description": "below the minimum is invalid", "data": { "smaller": 3, - "largerOrEqual": 3, - "minIsExclusive": false + "largerOrEqual": 2, + "exclusiveMinimum": 1.5 }, - "valid": true + "valid": false }, { - "description": "boundary point is valid when exclusiveMinimum is undefined", + "description": "below the minimum is invalid when exclusiveMinimum is undefined", "data": { "smaller": 3, - "largerOrEqual": 3 + "largerOrEqual": 2 }, - "valid": true + "valid": false }, { - "description": "below the minimum is invalid when exclusiveMinimum is true", + "description": "fails if value of exclusiveMinimum is not number", "data": { "smaller": 3, - "largerOrEqual": 2, - "minIsExclusive": true + "largerOrEqual": 4, + "exclusiveMinimum": "3" }, "valid": false }, { - "description": "below the minimum is invalid when exclusiveMinimum is false", + "description": "boundary point is valid when exclusiveMinimum is strictly smaller", "data": { "smaller": 3, - "largerOrEqual": 2, - "minIsExclusive": false + "largerOrEqual": 3, + "exclusiveMinimum": 2.9 }, - "valid": false + "valid": true }, { - "description": "below the minimum is invalid when exclusiveMinimum is undefined", + "description": "boundary point is invalid when exclusiveMinimum is equal", "data": { "smaller": 3, - "largerOrEqual": 2 + "largerOrEqual": 3, + "exclusiveMinimum": 3 }, "valid": false }, { - "description": "fails if value of exclusiveMinimum is not boolean", + "description": "boundary point is valid when exclusiveMinimum is undefined", "data": { "smaller": 3, - "largerOrEqual": 4, - "minIsExclusive": "false" + "largerOrEqual": 3 }, - "valid": false + "valid": true } ] }, diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index a8ff0683f6..5d60c46ef7 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -14,12 +14,6 @@ var remoteRefs = { } var SKIP = { - 4: [ - "optional/zeroTerminatedFloats", - "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here - "optional/format", - "format", - ], 6: [ "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here "optional/format", @@ -48,18 +42,6 @@ var SKIP = { ], } -runTest( - getAjvInstances(options, {meta: false, schemaId: "id"}), - 4, - typeof window == "object" - ? suite( - require("./JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json", { - mode: "list", - }) - ) - : "./JSON-Schema-Test-Suite/tests/draft4/{**/,}*.json" -) - runTest( getAjvInstances(options, {meta: false}), 6, @@ -87,10 +69,6 @@ runTest( function runTest(instances, draft, tests) { instances.forEach(function (ajv) { switch (draft) { - case 4: - ajv.addMetaSchema(require("../lib/refs/json-schema-draft-04.json")) - ajv._opts.defaultMeta = "http://json-schema.org/draft-04/schema#" - break case 6: ajv.addMetaSchema(require("../lib/refs/json-schema-draft-06.json")) ajv._opts.defaultMeta = "http://json-schema.org/draft-06/schema#" diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index 7600f24fcd..bc40da2745 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -24,8 +24,6 @@ describe("schemaId option", function () { describe('= "id"', function () { it("should use id and ignore $id", function () { var ajv = new Ajv({schemaId: "id", meta: false}) - ajv.addMetaSchema(require("../../lib/refs/json-schema-draft-04.json")) - ajv._opts.defaultMeta = "http://json-schema.org/draft-04/schema#" ajv.addSchema({id: "mySchema1", type: "string"}) var validate = ajv.getSchema("mySchema1") From b1bb27e28026c023d8f786b421c01b18f99b6de2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 17:19:59 +0100 Subject: [PATCH 008/322] feat: remove schemaId option (and support for draft-04 "id" in schemas) --- README.md | 18 +---- lib/ajv.d.ts | 1 - lib/ajv.js | 40 ++--------- lib/compile/resolve.js | 12 ++-- lib/dot/validate.jst | 4 +- .../521_wrong_warning_id_property.spec.js | 2 +- spec/options/schemaId.spec.js | 67 +++++-------------- 7 files changed, 33 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 4c266ba595..4e4c604098 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Ajv: Another JSON Schema Validator -The fastest JSON Schema validator for Node.js and browser. Supports draft-04/06/07. +The fastest JSON Schema validator for Node.js and browser. Supports draft-06/07 (draft-04 is supported in v6). [![Build Status](https://travis-ci.org/ajv-validator/ajv.svg?branch=master)](https://travis-ci.org/ajv-validator/ajv) [![npm](https://img.shields.io/npm/v/ajv.svg)](https://www.npmjs.com/package/ajv) @@ -63,14 +63,7 @@ Thank you ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")) ``` -To use Ajv with draft-04 schemas in addition to explicitly adding meta-schema you also need to use option schemaId: - -```javascript -var ajv = new Ajv({schemaId: "id"}) -// If you want to use both draft-04 and draft-06/07 schemas: -// var ajv = new Ajv({schemaId: 'auto'}); -ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-04.json")) -``` +**Please note**: use Ajv v6 if you need draft-04 support - v7 does NOT support it. ## Contents @@ -128,7 +121,7 @@ Performance of different validators by [json-schema-benchmark](https://github.co ## Features -- Ajv implements full JSON Schema [draft-06/07](http://json-schema.org/) and draft-04 standards: +- Ajv implements full JSON Schema [draft-06/07](http://json-schema.org/) standards (draft-04 is supported in v6): - all validation keywords (see [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md)) - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) - support of circular references between schemas @@ -1122,7 +1115,6 @@ Defaults: schemas: {}, logger: undefined, // referenced schema options: - schemaId: '$id', missingRefs: true, extendRefs: 'ignore', // recommended 'fail' loadSchema: undefined, // function(uri: string): Promise {} @@ -1183,10 +1175,6 @@ Defaults: ##### Referenced schema options -- _schemaId_: this option defines which keywords are used as schema URI. Option value: - - `"$id"` (default) - only use `$id` keyword as schema URI (as specified in JSON Schema draft-06/07), ignore `id` keyword (if it is present a warning will be logged). - - `"id"` - only use `id` keyword as schema URI (as specified in JSON Schema draft-04), ignore `$id` keyword (if it is present a warning will be logged). - - `"auto"` - use both `$id` and `id` keywords as schema URI. If both are present (in the same schema object) and different the exception will be thrown during schema compilation. - _missingRefs_: handling of missing referenced schemas. Option values: - `true` (default) - if the reference cannot be resolved during compilation the exception is thrown. The thrown error has properties `missingRef` (with hash fragment) and `missingSchema` (without it). Both properties are resolved relative to the current base id (usually schema id, unless it was substituted). - `"ignore"` - to log error during compilation and always pass validation. diff --git a/lib/ajv.d.ts b/lib/ajv.d.ts index 57ac813893..14d47aebcb 100644 --- a/lib/ajv.d.ts +++ b/lib/ajv.d.ts @@ -184,7 +184,6 @@ declare namespace ajv { keywords?: object unknownFormats?: true | string[] | "ignore" schemas?: Array | object - schemaId?: "$id" | "id" | "auto" missingRefs?: true | "ignore" | "fail" extendRefs?: true | "ignore" | "fail" loadSchema?: ( diff --git a/lib/ajv.js b/lib/ajv.js index 02541fc798..bfdd44dcb7 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -67,7 +67,8 @@ function Ajv(opts) { this._loadingSchemas = {} this._compilations = [] this.RULES = rules() - this._getId = chooseGetId(opts) + if (opts.schemaId !== undefined && opts.schemaId !== "$id") + throw new Error("option schemaId is not supported from v7") opts.loopRequired = opts.loopRequired || Infinity if (opts.errorDataPath == "property") opts._errorDataPathProperty = true @@ -134,7 +135,7 @@ function addSchema(schema, key, _skipValidation, _meta) { this.addSchema(schema[i], undefined, _skipValidation, _meta) return this } - var id = this._getId(schema) + var id = schema.$id if (id !== undefined && typeof id != "string") throw new Error("schema id must be string") key = resolve.normalizeId(key || id) @@ -187,7 +188,7 @@ function defaultMeta(self) { var meta = self._opts.meta self._opts.defaultMeta = typeof meta == "object" - ? self._getId(meta) || meta + ? meta.$id || meta : self.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined @@ -267,7 +268,7 @@ function removeSchema(schemaKeyRef) { var serialize = this._opts.serialize var cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef this._cache.del(cacheKey) - var id = this._getId(schemaKeyRef) + var id = schemaKeyRef.$id if (id) { id = resolve.normalizeId(id) delete this._schemas[id] @@ -298,7 +299,7 @@ function _addSchema(schema, skipValidation, meta, shouldAddSchema) { shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false - var id = resolve.normalizeId(this._getId(schema)) + var id = resolve.normalizeId(schema.$id) if (id && shouldAddSchema) checkUnique(this, id) var willValidate = this._opts.validateSchema !== false && !skipValidation @@ -372,35 +373,6 @@ function _compile(schemaObj, root) { } } -function chooseGetId(opts) { - switch (opts.schemaId) { - case "auto": - return _get$IdOrId - case "id": - return _getId - default: - return _get$Id - } -} - -/* @this Ajv */ -function _getId(schema) { - if (schema.$id) this.logger.warn("schema $id ignored", schema.$id) - return schema.id -} - -/* @this Ajv */ -function _get$Id(schema) { - if (schema.id) this.logger.warn("schema id ignored", schema.id) - return schema.$id -} - -function _get$IdOrId(schema) { - if (schema.$id && schema.id && schema.$id != schema.id) - throw new Error("schema $id is different from id") - return schema.$id || schema.id -} - /** * Convert array of error message objects to string * @this Ajv diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index bc47364820..73891d270b 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -70,7 +70,7 @@ function resolveSchema(root, ref) { /* jshint validthis: true */ var p = URI.parse(ref), refPath = _getFullPath(p), - baseId = getFullPath(this._getId(root.schema)) + baseId = getFullPath(root.schema.$id) if (Object.keys(root.schema).length === 0 || refPath !== baseId) { var id = normalizeId(refPath) var refVal = this._refs[id] @@ -91,7 +91,7 @@ function resolveSchema(root, ref) { } } if (!root.schema) return - baseId = getFullPath(this._getId(root.schema)) + baseId = getFullPath(root.schema.$id) } return getJsonPointer.call(this, p, baseId, root.schema, root) } @@ -104,7 +104,7 @@ function resolveRecursive(root, ref, parsedRef) { var schema = res.schema var baseId = res.baseId root = res.root - var id = this._getId(schema) + var id = schema.$id if (id) baseId = resolveUrl(baseId, id) return getJsonPointer.call(this, parsedRef, baseId, schema, root) } @@ -132,7 +132,7 @@ function getJsonPointer(parsedRef, baseId, schema, root) { if (schema === undefined) break var id if (!PREVENT_SCOPE_CHANGE[part]) { - id = this._getId(schema) + id = schema.$id if (id) baseId = resolveUrl(baseId, id) if (schema.$ref) { var $ref = resolveUrl(baseId, schema.$ref) @@ -236,7 +236,7 @@ function resolveUrl(baseId, id) { /* @this Ajv */ function resolveIds(schema) { - var schemaId = normalizeId(this._getId(schema)) + var schemaId = normalizeId(schema.$id) var baseIds = {"": schemaId} var fullPaths = {"": getFullPath(schemaId, false)} var localRefs = {} @@ -252,7 +252,7 @@ function resolveIds(schema) { keyIndex ) { if (jsonPtr === "") return - var id = self._getId(sch) + var id = sch.$id var baseId = baseIds[parentJsonPtr] var fullPath = fullPaths[parentJsonPtr] + "/" + parentKeyword if (keyIndex !== undefined) { diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index fd833a535c..a671490682 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -17,7 +17,7 @@ {{ var $async = it.schema.$async === true , $refKeywords = it.util.schemaHasRulesExcept(it.schema, it.RULES.all, '$ref') - , $id = it.self._getId(it.schema); + , $id = it.schema.$id; }} {{ @@ -77,7 +77,7 @@ , $lvl = it.level = 0 , $dataLvl = it.dataLevel = 0 , $data = 'data'; - it.rootId = it.resolve.fullPath(it.self._getId(it.root.schema)); + it.rootId = it.resolve.fullPath(it.root.schema.$id); it.baseId = it.baseId || it.rootId; delete it.isTop; diff --git a/spec/issues/521_wrong_warning_id_property.spec.js b/spec/issues/521_wrong_warning_id_property.spec.js index 308ebe662b..57c5bbb7fb 100644 --- a/spec/issues/521_wrong_warning_id_property.spec.js +++ b/spec/issues/521_wrong_warning_id_property.spec.js @@ -5,7 +5,7 @@ require("../chai").should() describe('issue #521, incorrect warning with "id" property', function () { it("should not log warning", function () { - var ajv = new Ajv({schemaId: "$id"}) + var ajv = new Ajv() var consoleWarn = console.warn console.warn = function () { throw new Error("should not log warning") diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index bc40da2745..7473cfb4c4 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -3,67 +3,30 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("schemaId option", function () { - describe('= "$id" (default)', function () { - it("should use $id and ignore id", function () { - test(new Ajv()) - test(new Ajv({schemaId: "$id"})) - - function test(ajv) { - ajv.addSchema({$id: "mySchema1", type: "string"}) - var validate = ajv.getSchema("mySchema1") - validate("foo").should.equal(true) - validate(1).should.equal(false) - - validate = ajv.compile({id: "mySchema2", type: "string"}) - should.not.exist(ajv.getSchema("mySchema2")) - } +describe("removed schemaId option", function () { + it('should throw error if schemaId option is used and it is not equal to "$id"', function () { + new Ajv() + new Ajv({schemaId: "$id"}) + should.throw(function () { + new Ajv({schemaId: "id"}) }) - }) - - describe('= "id"', function () { - it("should use id and ignore $id", function () { - var ajv = new Ajv({schemaId: "id", meta: false}) - - ajv.addSchema({id: "mySchema1", type: "string"}) - var validate = ajv.getSchema("mySchema1") - validate("foo").should.equal(true) - validate(1).should.equal(false) - - validate = ajv.compile({$id: "mySchema2", type: "string"}) - should.not.exist(ajv.getSchema("mySchema2")) + should.throw(function () { + new Ajv({schemaId: "auto"}) }) }) - describe('= "auto"', function () { - it("should use both id and $id", function () { - var ajv = new Ajv({schemaId: "auto"}) + it("should use $id and ignore id", function () { + test(new Ajv()) + test(new Ajv({schemaId: "$id"})) + function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) var validate = ajv.getSchema("mySchema1") validate("foo").should.equal(true) validate(1).should.equal(false) - ajv.addSchema({id: "mySchema2", type: "string"}) - validate = ajv.getSchema("mySchema2") - validate("foo").should.equal(true) - validate(1).should.equal(false) - }) - - it("should throw if both id and $id are available and different", function () { - var ajv = new Ajv({schemaId: "auto"}) - - ajv.compile({ - id: "mySchema", - $id: "mySchema", - }) - - should.throw(function () { - ajv.compile({ - id: "mySchema1", - $id: "mySchema2", - }) - }) - }) + validate = ajv.compile({id: "mySchema2", type: "string"}) + should.not.exist(ajv.getSchema("mySchema2")) + } }) }) From 39055fb191f1773a9274948319f9808054000a40 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 22 Jul 2020 19:45:29 +0100 Subject: [PATCH 009/322] remove draft-04 meta-schema --- lib/refs/json-schema-draft-04.json | 145 ----------------------------- 1 file changed, 145 deletions(-) delete mode 100644 lib/refs/json-schema-draft-04.json diff --git a/lib/refs/json-schema-draft-04.json b/lib/refs/json-schema-draft-04.json deleted file mode 100644 index 04c4d3b9fd..0000000000 --- a/lib/refs/json-schema-draft-04.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "id": "http://json-schema.org/draft-04/schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": {"$ref": "#"} - }, - "positiveInteger": { - "type": "integer", - "minimum": 0 - }, - "positiveIntegerDefault0": { - "allOf": [{"$ref": "#/definitions/positiveInteger"}, {"default": 0}] - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": {"type": "string"}, - "minItems": 1, - "uniqueItems": true - } - }, - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "$schema": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": {}, - "multipleOf": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "maxLength": {"$ref": "#/definitions/positiveInteger"}, - "minLength": {"$ref": "#/definitions/positiveIntegerDefault0"}, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "anyOf": [{"type": "boolean"}, {"$ref": "#"}], - "default": {} - }, - "items": { - "anyOf": [{"$ref": "#"}, {"$ref": "#/definitions/schemaArray"}], - "default": {} - }, - "maxItems": {"$ref": "#/definitions/positiveInteger"}, - "minItems": {"$ref": "#/definitions/positiveIntegerDefault0"}, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxProperties": {"$ref": "#/definitions/positiveInteger"}, - "minProperties": {"$ref": "#/definitions/positiveIntegerDefault0"}, - "required": {"$ref": "#/definitions/stringArray"}, - "additionalProperties": { - "anyOf": [{"type": "boolean"}, {"$ref": "#"}], - "default": {} - }, - "definitions": { - "type": "object", - "additionalProperties": {"$ref": "#"}, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": {"$ref": "#"}, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": {"$ref": "#"}, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [{"$ref": "#"}, {"$ref": "#/definitions/stringArray"}] - } - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - {"$ref": "#/definitions/simpleTypes"}, - { - "type": "array", - "items": {"$ref": "#/definitions/simpleTypes"}, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "format": {"type": "string"}, - "allOf": {"$ref": "#/definitions/schemaArray"}, - "anyOf": {"$ref": "#/definitions/schemaArray"}, - "oneOf": {"$ref": "#/definitions/schemaArray"}, - "not": {"$ref": "#"} - }, - "dependencies": { - "exclusiveMaximum": ["maximum"], - "exclusiveMinimum": ["minimum"] - }, - "default": {} -} From 078ffa781c7798295d8ec9e0773d520d67a4b8c6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 26 Jul 2020 17:08:34 +0100 Subject: [PATCH 010/322] use dot v2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cde5510b03..579529ef54 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "chai": "^4.0.1", "coveralls": "^3.0.1", "del-cli": "^3.0.0", - "dot": "^1.0.3", + "dot": "^2.0.0-beta", "eslint": "^7.3.1", "gh-pages-generator": "^0.2.3", "glob": "^7.0.0", From 44e5d55b380c27406471a724ad49a121ddc744a2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 27 Jul 2020 21:11:44 +0100 Subject: [PATCH 011/322] Revert "use dot v2" This reverts commit 078ffa781c7798295d8ec9e0773d520d67a4b8c6. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 579529ef54..cde5510b03 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "chai": "^4.0.1", "coveralls": "^3.0.1", "del-cli": "^3.0.0", - "dot": "^2.0.0-beta", + "dot": "^1.0.3", "eslint": "^7.3.1", "gh-pages-generator": "^0.2.3", "glob": "^7.0.0", From 64ebe8880237808a2bc4f4461f769eef5b26dfa5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 9 Aug 2020 14:04:46 +0100 Subject: [PATCH 012/322] refactor: "const" keyword and number limit keywords moved to the new keyword definition --- lib/ajv.js | 14 ++++--- lib/compile/rules.js | 11 +---- lib/compile/util.js | 6 +++ lib/data.js | 1 + lib/definition_schema.js | 2 +- lib/dot/_limit.jst | 22 ---------- lib/dot/const.jst | 11 ----- lib/dot/inline.jst | 27 ++++++++++++ lib/dotjs/index.js | 5 --- lib/keyword.js | 89 ++++++++++++++++++++++++++++++++++++++-- lib/keywords/const.js | 23 +++++++++++ lib/keywords/index.js | 9 ++++ lib/keywords/limit.js | 48 ++++++++++++++++++++++ 13 files changed, 211 insertions(+), 57 deletions(-) delete mode 100644 lib/dot/_limit.jst delete mode 100644 lib/dot/const.jst create mode 100644 lib/dot/inline.jst create mode 100644 lib/keywords/const.js create mode 100644 lib/keywords/index.js create mode 100644 lib/keywords/limit.js diff --git a/lib/ajv.js b/lib/ajv.js index bfdd44dcb7..32147440d7 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -7,7 +7,8 @@ var compileSchema = require("./compile"), stableStringify = require("fast-json-stable-stringify"), rules = require("./compile/rules"), $dataMetaSchema = require("./data"), - util = require("./compile/util") + util = require("./compile/util"), + standardKeywords = require("./keywords") module.exports = Ajv @@ -76,7 +77,8 @@ function Ajv(opts) { this._metaOpts = getMetaSchemaOptions(this) if (opts.formats) addInitialFormats(this) - if (opts.keywords) addInitialKeywords(this) + addInitialKeywords(this, standardKeywords, true) + if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) if (opts.nullable) @@ -436,10 +438,10 @@ function addInitialFormats(self) { } } -function addInitialKeywords(self) { - for (var name in self._opts.keywords) { - var keyword = self._opts.keywords[name] - self.addKeyword(name, keyword) +function addInitialKeywords(self, keywords, skipValidation) { + for (var name in keywords) { + var keyword = keywords[name] + self.addKeyword(name, keyword, skipValidation) } } diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 9890a7fa23..b54dc2dcae 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -7,14 +7,7 @@ module.exports = function rules() { var RULES = [ { type: "number", - rules: [ - "maximum", - "minimum", - "exclusiveMaximum", - "exclusiveMinimum", - "multipleOf", - "format", - ], + rules: ["multipleOf", "format"], }, {type: "string", rules: ["maxLength", "minLength", "pattern", "format"]}, { @@ -32,7 +25,7 @@ module.exports = function rules() { {properties: ["additionalProperties", "patternProperties"]}, ], }, - {rules: ["$ref", "const", "enum", "not", "anyOf", "oneOf", "allOf", "if"]}, + {rules: ["$ref", "enum", "not", "anyOf", "oneOf", "allOf", "if"]}, ] var ALL = ["type", "$comment"] diff --git a/lib/compile/util.js b/lib/compile/util.js index e944274187..e4930cb6d7 100644 --- a/lib/compile/util.js +++ b/lib/compile/util.js @@ -23,6 +23,12 @@ module.exports = { unescapeJsonPointer: unescapeJsonPointer, escapeFragment: escapeFragment, escapeJsonPointer: escapeJsonPointer, + appendSchema: ($data, schemaCode) => + $data ? `" + ${schemaCode}` : `${schemaCode}"`, + dataNotType: ($data, schemaCode, schemaType) => + $data + ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` + : "", } function copy(o, to) { diff --git a/lib/data.js b/lib/data.js index 6409f58093..69b55b2b5d 100644 --- a/lib/data.js +++ b/lib/data.js @@ -1,5 +1,6 @@ "use strict" +// TODO use $data in keyword definitions var KEYWORDS = [ "multipleOf", "maximum", diff --git a/lib/definition_schema.js b/lib/definition_schema.js index c24c713148..e82c4bbbc0 100644 --- a/lib/definition_schema.js +++ b/lib/definition_schema.js @@ -11,9 +11,9 @@ module.exports = { type: "object", dependencies: { schema: ["validate"], - $data: ["validate"], statements: ["inline"], valid: {not: {required: ["macro"]}}, + $data: {anyOf: [{required: ["code"]}, {required: ["validate"]}]}, }, properties: { type: metaSchema.properties.type, diff --git a/lib/dot/_limit.jst b/lib/dot/_limit.jst deleted file mode 100644 index 125f5fd6f9..0000000000 --- a/lib/dot/_limit.jst +++ /dev/null @@ -1,22 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{# def.numberKeyword }} - -{{ - var $op /* used in errors */, $notOp; - switch ($keyword) { - case 'maximum': $op = '<='; $notOp = '>'; break; - case 'minimum': $op = '>='; $notOp = '<'; break; - case 'exclusiveMaximum': $op = '<'; $notOp = '>='; break; - case 'exclusiveMinimum': $op = '>'; $notOp = '<='; break; - default: throw Error('not _limit keyword ' + $keyword); - } -}} - -if ({{# def.$dataNotType:'number' }} {{=$data}} {{=$notOp}} {{=$schemaValue}} || {{=$data}} !== {{=$data}}) { - {{ var $errorKeyword = $keyword; }} - {{# def.error:'_limit' }} -} {{? $breakOnError }} else { {{?}} diff --git a/lib/dot/const.jst b/lib/dot/const.jst deleted file mode 100644 index 2aa22980d7..0000000000 --- a/lib/dot/const.jst +++ /dev/null @@ -1,11 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{? !$isData }} - var schema{{=$lvl}} = validate.schema{{=$schemaPath}}; -{{?}} -var {{=$valid}} = equal({{=$data}}, schema{{=$lvl}}); -{{# def.checkError:'const' }} -{{? $breakOnError }} else { {{?}} diff --git a/lib/dot/inline.jst b/lib/dot/inline.jst new file mode 100644 index 0000000000..94803a3502 --- /dev/null +++ b/lib/dot/inline.jst @@ -0,0 +1,27 @@ +{{# def.definitions }} +{{# def.errors }} +{{# def.setupKeyword }} +{{# def.$data }} + +{{ + var $rule = this + , $definition = 'definition' + $lvl + , $rDef = $rule.definition; + + var $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it); + if (!$ruleValidate) return; + $schemaValue = 'validate.schema' + $schemaPath; + var $validateCode = $ruleValidate.code; +}} + +{{ + var $ruleErrs = $validateCode + '.errors' + , $i = 'i' + $lvl + , $ruleErr = 'ruleErr' + $lvl; +}} + +var {{=$valid}}; + +{{= $ruleValidate.validate }} + +{{? $breakOnError }} if ({{=$valid}}) { {{?}} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 39eca6e7b0..93da591c06 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -6,17 +6,12 @@ module.exports = { allOf: require("./allOf"), anyOf: require("./anyOf"), $comment: require("./comment"), - const: require("./const"), contains: require("./contains"), dependencies: require("./dependencies"), enum: require("./enum"), - exclusiveMaximum: require("./_limit"), - exclusiveMinimum: require("./_limit"), format: require("./format"), if: require("./if"), items: require("./items"), - maximum: require("./_limit"), - minimum: require("./_limit"), maxItems: require("./_limitItems"), minItems: require("./_limitItems"), maxLength: require("./_limitLength"), diff --git a/lib/keyword.js b/lib/keyword.js index 1f46ff4349..97e2328a22 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -3,6 +3,7 @@ var IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i var customRuleCode = require("./dotjs/custom") var definitionSchema = require("./definition_schema") +var util = require("./compile/util") module.exports = { add: addKeyword, @@ -18,7 +19,7 @@ module.exports = { * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. * @return {Ajv} this for method chaining */ -function addKeyword(keyword, definition) { +function addKeyword(keyword, definition, _skipValidation) { /* jshint validthis: true */ /* eslint no-shadow: 0 */ var RULES = this.RULES @@ -29,7 +30,7 @@ function addKeyword(keyword, definition) { throw new Error("Keyword " + keyword + " is not a valid identifier") if (definition) { - this.validateKeyword(definition, true) + if (!_skipValidation) this.validateKeyword(definition, true) var dataType = definition.type if (Array.isArray(dataType)) { @@ -77,7 +78,7 @@ function addKeyword(keyword, definition) { keyword: keyword, definition: definition, custom: true, - code: customRuleCode, + code: definition.code ? ruleCode : customRuleCode, implements: definition.implements, } ruleGroup.rules.push(rule) @@ -87,6 +88,88 @@ function addKeyword(keyword, definition) { return this } +/** + * Get keyword + * @this rule object + * @param {String} keyword pre-defined or custom keyword. + * @return {String} compiled rule code. + */ +function ruleCode(it, keyword /*, ruleType */) { + const schema = it.schema[keyword] + const {schemaType, code, error, $data: $defData} = this.definition + let schemaCode + let out = "" + const $data = $defData && it.opts.$data && schema && schema.$data + if ($data) { + // TODO stop using it.level and maybe it.dataLevel + // schemaCode = it.getName("schema") + schemaCode = `schema${it.level}` + // TODO replace with const once it.level replaced with unique names + out += `var ${schemaCode} = ${util.getData( + $data, + it.dataLevel, + it.dataPathArr + )};` + } else { + if (schemaType && typeof schema !== schemaType) { + throw new Error(`${keyword} must be ${schemaType}`) + } + schemaCode = schemaRefOrVal() + } + const data = "data" + (it.dataLevel || "") + const cxt = {fail, keyword, data, $data, schemaCode, schemaType} + // TODO check that code called "fail" or another valid way to return code + code(cxt) + return out + + function fail(condition) { + out += `if (${condition}) { ${reportError()} }` + if (!it.opts.allErrors) out += `else {` + } + + function reportError() { + const errCode = errorObjectCode() + if (!it.compositeRule && !it.opts.allErrors) { + // TODO trim whitespace + return it.async + ? `throw new ValidationError([${errCode}]);` + : `validate.errors = [${errCode}]; + return false;` + } + return `const err = ${errCode}; + if (vErrors === null) vErrors = [err]; + else vErrors.push(err); + errors++;` + } + + function errorObjectCode() { + if (it.createErrors === false) return "{}" + // TODO trim whitespace + let out = `{ + keyword: "${keyword}", + dataPath: (dataPath || "") + ${it.errorPath}, + schemaPath: ${util.toQuotedString(it.errSchemaPath + "/" + keyword)}, + params: ${error.params(cxt)},` + if (it.opts.messages !== false) { + out += `message: ${error.message(cxt)},` + } + if (it.opts.verbose) { + // TODO trim whitespace + out += ` + schema: ${schemaRefOrVal()}, + parentSchema: validate.schema${it.schemaPath}, + data: ${data},` + } + return out + "}" + } + + function schemaRefOrVal() { + return schemaType === "number" && !$data + ? schema + : `validate.schema${it.schemaPath + util.getProperty(keyword)}` + } +} + /** * Get keyword * @this Ajv diff --git a/lib/keywords/const.js b/lib/keywords/const.js new file mode 100644 index 0000000000..a3a3e5a6e5 --- /dev/null +++ b/lib/keywords/const.js @@ -0,0 +1,23 @@ +"use strict" + +// {{# def.definitions }} +// {{# def.errors }} +// {{# def.setupKeyword }} +// {{# def.$data }} + +// {{? !$isData }} +// var schema{{=$lvl}} = validate.schema{{=$schemaPath}}; +// {{?}} +// var {{=$valid}} = equal({{=$data}}, schema{{=$lvl}}); +// {{# def.checkError:'const' }} +// {{? $breakOnError }} else { {{?}} + +module.exports = { + keywords: ["const"], + $data: true, + code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), + error: { + message: () => '"should be equal to constant"', + params: ({schemaCode}) => `{ allowedValue: ${schemaCode} }`, + }, +} diff --git a/lib/keywords/index.js b/lib/keywords/index.js new file mode 100644 index 0000000000..b6d5a71b9c --- /dev/null +++ b/lib/keywords/index.js @@ -0,0 +1,9 @@ +"use strict" + +module.exports = { + const: require("./const"), + exclusiveMaximum: require("./limit"), + exclusiveMinimum: require("./limit"), + maximum: require("./limit"), + minimum: require("./limit"), +} diff --git a/lib/keywords/limit.js b/lib/keywords/limit.js new file mode 100644 index 0000000000..5aaa1e68b3 --- /dev/null +++ b/lib/keywords/limit.js @@ -0,0 +1,48 @@ +// {{# def.definitions }} +// {{# def.errors }} +// {{# def.setupKeyword }} +// {{# def.$data }} + +// {{# def.numberKeyword }} + +// {{ +// var $op /* used in errors */, $notOp; +// switch ($keyword) { +// case 'maximum': $op = '<='; $notOp = '>'; break; +// case 'minimum': $op = '>='; $notOp = '<'; break; +// case 'exclusiveMaximum': $op = '<'; $notOp = '>='; break; +// case 'exclusiveMinimum': $op = '>'; $notOp = '<='; break; +// default: throw Error('not _limit keyword ' + $keyword); +// } +// }} + +// if ({{# def.$dataNotType:'number' }} {{=$data}} {{=$notOp}} {{=$schemaValue}} || {{=$data}} !== {{=$data}}) { +// {{ var $errorKeyword = $keyword; }} +// {{# def.error:'_limit' }} +// } {{? $breakOnError }} else { {{?}} + +const {appendSchema, dataNotType} = require("../compile/util") + +const OPS = { + maximum: {fail: ">", ok: "<="}, + minimum: {fail: "<", ok: ">="}, + exclusiveMaximum: {fail: ">=", ok: "<"}, + exclusiveMinimum: {fail: "<=", ok: ">"}, +} + +module.exports = { + keywords: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], + type: "number", + schemaType: "number", + $data: true, + code: ({keyword, fail, data, $data, schemaCode, schemaType}) => { + const dnt = dataNotType($data, schemaCode, schemaType) + fail(dnt + data + OPS[keyword].fail + schemaCode + ` || ${data}!==${data}`) + }, + error: { + message: ({keyword, $data, schemaCode}) => + `"should be ${OPS[keyword].ok} ${appendSchema($data, schemaCode)}`, + params: ({keyword, schemaCode}) => + `{ comparison: "${OPS[keyword].ok}", limit: ${schemaCode} }`, + }, +} From 99d7a5df6e0776e00f3d2c0258bbb2b52452da0d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 9 Aug 2020 18:36:49 +0100 Subject: [PATCH 013/322] test: disable browser tests (do not support ES6 yet) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cde5510b03..fc96b39993 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", - "test": "npm run lint && npm run build && npm run test-all", + "test": "npm run lint && npm run build && npm run test-cov", "prepublish": "npm run build && npm run bundle", "watch": "watch \"npm run build\" ./lib/dot" }, From 669e0c1d2c5223ef6837c5338654ab66c0231475 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 9 Aug 2020 18:47:20 +0100 Subject: [PATCH 014/322] remove unused code, fix eslint --- lib/dot/errors.def | 6 ------ lib/dot/inline.jst | 27 --------------------------- lib/keyword.js | 13 ++++++------- lib/keywords/const.js | 12 ------------ lib/keywords/limit.js | 23 +---------------------- 5 files changed, 7 insertions(+), 74 deletions(-) delete mode 100644 lib/dot/inline.jst diff --git a/lib/dot/errors.def b/lib/dot/errors.def index d5658834d5..99f93e2455 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -96,13 +96,11 @@ additionalItems: "'should NOT have more than {{=$schema.length}} items'", additionalProperties: "'{{? it.opts._errorDataPathProperty }}is an invalid additional property{{??}}should NOT have additional properties{{?}}'", anyOf: "'should match some schema in anyOf'", - const: "'should be equal to constant'", contains: "'should contain a valid item'", dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", 'enum': "'should be equal to one of the allowed values'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", - _limit: "'should be {{=$op}} {{#def.appendSchema}}", _exclusiveLimit: "'should be {{=$op}} {{#def.appendSchema}}", _limitItems: "'should NOT have {{?$keyword=='maxItems'}}more{{??}}fewer{{?}} than {{#def.concatSchema}} items'", _limitLength: "'should NOT be {{?$keyword=='maxLength'}}longer{{??}}shorter{{?}} than {{#def.concatSchema}} characters'", @@ -132,13 +130,11 @@ additionalItems: "false", additionalProperties: "false", anyOf: "validate.schema{{=$schemaPath}}", - const: "validate.schema{{=$schemaPath}}", contains: "validate.schema{{=$schemaPath}}", dependencies: "validate.schema{{=$schemaPath}}", 'enum': "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", 'if': "validate.schema{{=$schemaPath}}", - _limit: "{{#def.schemaRefOrVal}}", _limitItems: "{{#def.schemaRefOrVal}}", _limitLength: "{{#def.schemaRefOrVal}}", _limitProperties:"{{#def.schemaRefOrVal}}", @@ -166,13 +162,11 @@ additionalItems: "{ limit: {{=$schema.length}} }", additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", anyOf: "{}", - const: "{ allowedValue: schema{{=$lvl}} }", contains: "{}", dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{= it.util.escapeQuotes($deps.length==1 ? $deps[0] : $deps.join(\", \")) }}' }", 'enum': "{ allowedValues: schema{{=$lvl}} }", format: "{ format: {{#def.schemaValueQS}} }", 'if': "{ failingKeyword: {{=$ifClause}} }", - _limit: "{ comparison: '{{=$op}}', limit: {{=$schemaValue}} }", _limitItems: "{ limit: {{=$schemaValue}} }", _limitLength: "{ limit: {{=$schemaValue}} }", _limitProperties:"{ limit: {{=$schemaValue}} }", diff --git a/lib/dot/inline.jst b/lib/dot/inline.jst deleted file mode 100644 index 94803a3502..0000000000 --- a/lib/dot/inline.jst +++ /dev/null @@ -1,27 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{ - var $rule = this - , $definition = 'definition' + $lvl - , $rDef = $rule.definition; - - var $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it); - if (!$ruleValidate) return; - $schemaValue = 'validate.schema' + $schemaPath; - var $validateCode = $ruleValidate.code; -}} - -{{ - var $ruleErrs = $validateCode + '.errors' - , $i = 'i' + $lvl - , $ruleErr = 'ruleErr' + $lvl; -}} - -var {{=$valid}}; - -{{= $ruleValidate.validate }} - -{{? $breakOnError }} if ({{=$valid}}) { {{?}} diff --git a/lib/keyword.js b/lib/keyword.js index 97e2328a22..cbbf24f069 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -17,6 +17,7 @@ module.exports = { * @this Ajv * @param {String} keyword custom keyword, should be unique (including different from all standard, custom and macro keywords). * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. + * @param {Boolean} _skipValidation of keyword definition * @return {Ajv} this for method chaining */ function addKeyword(keyword, definition, _skipValidation) { @@ -89,8 +90,9 @@ function addKeyword(keyword, definition, _skipValidation) { } /** - * Get keyword - * @this rule object + * Generate keyword code + * @this rule + * @param {Object} it schema compilation context. * @param {String} keyword pre-defined or custom keyword. * @return {String} compiled rule code. */ @@ -111,9 +113,8 @@ function ruleCode(it, keyword /*, ruleType */) { it.dataPathArr )};` } else { - if (schemaType && typeof schema !== schemaType) { + if (schemaType && typeof schema !== schemaType) throw new Error(`${keyword} must be ${schemaType}`) - } schemaCode = schemaRefOrVal() } const data = "data" + (it.dataLevel || "") @@ -150,9 +151,7 @@ function ruleCode(it, keyword /*, ruleType */) { dataPath: (dataPath || "") + ${it.errorPath}, schemaPath: ${util.toQuotedString(it.errSchemaPath + "/" + keyword)}, params: ${error.params(cxt)},` - if (it.opts.messages !== false) { - out += `message: ${error.message(cxt)},` - } + if (it.opts.messages !== false) out += `message: ${error.message(cxt)},` if (it.opts.verbose) { // TODO trim whitespace out += ` diff --git a/lib/keywords/const.js b/lib/keywords/const.js index a3a3e5a6e5..55d8b7fb96 100644 --- a/lib/keywords/const.js +++ b/lib/keywords/const.js @@ -1,17 +1,5 @@ "use strict" -// {{# def.definitions }} -// {{# def.errors }} -// {{# def.setupKeyword }} -// {{# def.$data }} - -// {{? !$isData }} -// var schema{{=$lvl}} = validate.schema{{=$schemaPath}}; -// {{?}} -// var {{=$valid}} = equal({{=$data}}, schema{{=$lvl}}); -// {{# def.checkError:'const' }} -// {{? $breakOnError }} else { {{?}} - module.exports = { keywords: ["const"], $data: true, diff --git a/lib/keywords/limit.js b/lib/keywords/limit.js index 5aaa1e68b3..56a91988fa 100644 --- a/lib/keywords/limit.js +++ b/lib/keywords/limit.js @@ -1,25 +1,4 @@ -// {{# def.definitions }} -// {{# def.errors }} -// {{# def.setupKeyword }} -// {{# def.$data }} - -// {{# def.numberKeyword }} - -// {{ -// var $op /* used in errors */, $notOp; -// switch ($keyword) { -// case 'maximum': $op = '<='; $notOp = '>'; break; -// case 'minimum': $op = '>='; $notOp = '<'; break; -// case 'exclusiveMaximum': $op = '<'; $notOp = '>='; break; -// case 'exclusiveMinimum': $op = '>'; $notOp = '<='; break; -// default: throw Error('not _limit keyword ' + $keyword); -// } -// }} - -// if ({{# def.$dataNotType:'number' }} {{=$data}} {{=$notOp}} {{=$schemaValue}} || {{=$data}} !== {{=$data}}) { -// {{ var $errorKeyword = $keyword; }} -// {{# def.error:'_limit' }} -// } {{? $breakOnError }} else { {{?}} +"use strict" const {appendSchema, dataNotType} = require("../compile/util") From 971d9f9e9df63218b2ae75e67b084e832d6cdacc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 9 Aug 2020 19:51:46 +0100 Subject: [PATCH 015/322] add typescript, remove bundle from prepublish script --- lib/cache.js | 6 ++++-- lib/keyword.js | 2 -- lib/keywords/const.js | 2 -- lib/keywords/index.js | 2 -- lib/keywords/limit.js | 2 -- package.json | 12 +++++++----- spec/ajv.js | 2 +- tsconfig.json | 11 +++++++++++ 8 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 tsconfig.json diff --git a/lib/cache.js b/lib/cache.js index ae1a61f1d5..2539362d25 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,8 +1,10 @@ "use strict" -var Cache = (module.exports = function Cache() { +module.exports = Cache + +function Cache() { this._cache = {} -}) +} Cache.prototype.put = function Cache_put(key, value) { this._cache[key] = value diff --git a/lib/keyword.js b/lib/keyword.js index cbbf24f069..8ac16af1ae 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -1,5 +1,3 @@ -"use strict" - var IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i var customRuleCode = require("./dotjs/custom") var definitionSchema = require("./definition_schema") diff --git a/lib/keywords/const.js b/lib/keywords/const.js index 55d8b7fb96..6ac308eb1d 100644 --- a/lib/keywords/const.js +++ b/lib/keywords/const.js @@ -1,5 +1,3 @@ -"use strict" - module.exports = { keywords: ["const"], $data: true, diff --git a/lib/keywords/index.js b/lib/keywords/index.js index b6d5a71b9c..f756f97739 100644 --- a/lib/keywords/index.js +++ b/lib/keywords/index.js @@ -1,5 +1,3 @@ -"use strict" - module.exports = { const: require("./const"), exclusiveMaximum: require("./limit"), diff --git a/lib/keywords/limit.js b/lib/keywords/limit.js index 56a91988fa..41db52a61e 100644 --- a/lib/keywords/limit.js +++ b/lib/keywords/limit.js @@ -1,5 +1,3 @@ -"use strict" - const {appendSchema, dataNotType} = require("../compile/util") const OPS = { diff --git a/package.json b/package.json index fc96b39993..f75db55771 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "ajv", "version": "6.12.3", "description": "Another JSON Schema Validator", - "main": "lib/ajv.js", + "main": "dist/ajv.js", "typings": "lib/ajv.d.ts", "files": [ "lib/", @@ -23,12 +23,14 @@ "test-ts": "tsc --target ES5 --noImplicitAny --noEmit spec/typescript/index.ts", "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", "bundle-beautify": "node ./scripts/bundle.js js-beautify", - "build": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", + "dot": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", + "tsc": "tsc && cp -r lib/refs dist/refs", + "build": "npm run dot && npm run tsc", "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", "test": "npm run lint && npm run build && npm run test-cov", - "prepublish": "npm run build && npm run bundle", + "prepublish": "npm run build", "watch": "watch \"npm run build\" ./lib/dot" }, "nyc": { @@ -87,17 +89,17 @@ "js-beautify": "^1.7.3", "jshint": "^2.10.2", "json-schema-test": "^2.0.0", - "lint-staged": "^10.2.11", "karma": "^5.0.0", "karma-chrome-launcher": "^3.0.0", "karma-mocha": "^2.0.0", "karma-sauce-launcher": "^4.1.3", + "lint-staged": "^10.2.11", "mocha": "^8.0.1", "nyc": "^15.0.0", "pre-commit": "^1.1.1", "prettier": "^2.0.5", "require-globify": "^1.3.0", - "typescript": "^3.9.5", + "typescript": "^3.9.7", "uglify-js": "^3.6.9", "watch": "^1.0.0" }, diff --git a/spec/ajv.js b/spec/ajv.js index 2c3fcf374c..fbd6e578bb 100644 --- a/spec/ajv.js +++ b/spec/ajv.js @@ -1,4 +1,4 @@ "use strict" module.exports = - typeof window == "object" ? window.Ajv : require("" + "../lib/ajv") + typeof window == "object" ? window.Ajv : require("" + "../dist/ajv") diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..a151db8a52 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@ajv-validator/config", + "include": ["lib"], + "compilerOptions": { + "outDir": "dist", + "lib": ["ES2018", "DOM"], + "noImplicitAny": false, + "allowJs": true, + "declaration": false + } +} From 43d80f6395f97c07642c47d4c2de9fdd8ffceea2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 9 Aug 2020 21:43:27 +0100 Subject: [PATCH 016/322] refactor: move max*, min* keywords to the new definition format without doT templates --- .eslintrc.yml | 2 +- lib/ajv.js | 10 ++++------ lib/cache.js | 2 -- lib/compile/async.js | 2 -- lib/compile/equal.js | 2 -- lib/compile/error_classes.js | 2 -- lib/compile/index.js | 2 -- lib/compile/resolve.js | 2 -- lib/compile/rules.js | 8 ++------ lib/compile/schema_obj.js | 2 -- lib/compile/ucs2length.js | 2 -- lib/compile/util.js | 4 ++-- lib/data.js | 2 -- lib/definition_schema.js | 2 -- lib/dot/_limitItems.jst | 12 ------------ lib/dot/_limitLength.jst | 12 ------------ lib/dot/_limitProperties.jst | 12 ------------ lib/dot/errors.def | 10 ---------- lib/dotjs/index.js | 6 ------ lib/keyword.js | 17 +++++------------ lib/keywords/const.js | 2 +- lib/keywords/index.js | 6 ++++++ lib/keywords/limit.js | 10 ++++++---- lib/keywords/limitItems.js | 23 +++++++++++++++++++++++ lib/keywords/limitLength.js | 25 +++++++++++++++++++++++++ lib/keywords/limitProperties.js | 23 +++++++++++++++++++++++ package.json | 3 ++- scripts/.eslintrc.yml | 2 ++ spec/.eslintrc.yml | 2 ++ tsconfig.json | 2 ++ 30 files changed, 106 insertions(+), 105 deletions(-) delete mode 100644 lib/dot/_limitItems.jst delete mode 100644 lib/dot/_limitLength.jst delete mode 100644 lib/dot/_limitProperties.jst create mode 100644 lib/keywords/limitItems.js create mode 100644 lib/keywords/limitLength.js create mode 100644 lib/keywords/limitProperties.js diff --git a/.eslintrc.yml b/.eslintrc.yml index f8ed3b2c38..7fc6b7b447 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,6 +1,7 @@ extends: eslint:recommended parserOptions: ecmaVersion: 6 + sourceType: module env: node: true browser: true @@ -24,7 +25,6 @@ rules: no-trailing-spaces: 2 no-use-before-define: [2, nofunc] semi: 0 - strict: [2, global] valid-jsdoc: [2, requireReturn: false] no-control-regex: 0 no-useless-escape: 2 diff --git a/lib/ajv.js b/lib/ajv.js index 32147440d7..cf3264541d 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -1,5 +1,3 @@ -"use strict" - var compileSchema = require("./compile"), resolve = require("./compile/resolve"), Cache = require("./cache"), @@ -27,10 +25,10 @@ Ajv.prototype._compile = _compile Ajv.prototype.compileAsync = require("./compile/async") var customKeyword = require("./keyword") -Ajv.prototype.addKeyword = customKeyword.add -Ajv.prototype.getKeyword = customKeyword.get -Ajv.prototype.removeKeyword = customKeyword.remove -Ajv.prototype.validateKeyword = customKeyword.validate +Ajv.prototype.addKeyword = customKeyword.addKeyword +Ajv.prototype.getKeyword = customKeyword.getKeyword +Ajv.prototype.removeKeyword = customKeyword.removeKeyword +Ajv.prototype.validateKeyword = customKeyword.validateKeyword var errorClasses = require("./compile/error_classes") Ajv.ValidationError = errorClasses.Validation diff --git a/lib/cache.js b/lib/cache.js index 2539362d25..b970862efc 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,5 +1,3 @@ -"use strict" - module.exports = Cache function Cache() { diff --git a/lib/compile/async.js b/lib/compile/async.js index bb758d767f..270b9df070 100644 --- a/lib/compile/async.js +++ b/lib/compile/async.js @@ -1,5 +1,3 @@ -"use strict" - var MissingRefError = require("./error_classes").MissingRef module.exports = compileAsync diff --git a/lib/compile/equal.js b/lib/compile/equal.js index 4d060f8496..b67591643e 100644 --- a/lib/compile/equal.js +++ b/lib/compile/equal.js @@ -1,5 +1,3 @@ -"use strict" - // do NOT remove this file - it would break pre-compiled schemas // https://github.com/ajv-validator/ajv/issues/889 module.exports = require("fast-deep-equal") diff --git a/lib/compile/error_classes.js b/lib/compile/error_classes.js index cd6f59a5fe..32cdf81e95 100644 --- a/lib/compile/error_classes.js +++ b/lib/compile/error_classes.js @@ -1,5 +1,3 @@ -"use strict" - var resolve = require("./resolve") module.exports = { diff --git a/lib/compile/index.js b/lib/compile/index.js index 2eb4750d2a..3757e6d2c3 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,5 +1,3 @@ -"use strict" - var resolve = require("./resolve"), util = require("./util"), errorClasses = require("./error_classes"), diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index 73891d270b..78e270825c 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -1,5 +1,3 @@ -"use strict" - var URI = require("uri-js"), equal = require("fast-deep-equal"), util = require("./util"), diff --git a/lib/compile/rules.js b/lib/compile/rules.js index b54dc2dcae..12ffa6630d 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -1,5 +1,3 @@ -"use strict" - var ruleModules = require("../dotjs"), toHash = require("./util").toHash @@ -9,16 +7,14 @@ module.exports = function rules() { type: "number", rules: ["multipleOf", "format"], }, - {type: "string", rules: ["maxLength", "minLength", "pattern", "format"]}, + {type: "string", rules: ["pattern", "format"]}, { type: "array", - rules: ["maxItems", "minItems", "items", "contains", "uniqueItems"], + rules: ["items", "contains", "uniqueItems"], }, { type: "object", rules: [ - "maxProperties", - "minProperties", "required", "dependencies", "propertyNames", diff --git a/lib/compile/schema_obj.js b/lib/compile/schema_obj.js index 0d771e5458..41dfb69ef6 100644 --- a/lib/compile/schema_obj.js +++ b/lib/compile/schema_obj.js @@ -1,5 +1,3 @@ -"use strict" - var util = require("./util") module.exports = SchemaObject diff --git a/lib/compile/ucs2length.js b/lib/compile/ucs2length.js index 81cee8b8c2..935703e5ed 100644 --- a/lib/compile/ucs2length.js +++ b/lib/compile/ucs2length.js @@ -1,5 +1,3 @@ -"use strict" - // https://mathiasbynens.be/notes/javascript-encoding // https://github.com/bestiejs/punycode.js - punycode.ucs2.decode module.exports = function ucs2length(str) { diff --git a/lib/compile/util.js b/lib/compile/util.js index e4930cb6d7..e1fdc1851a 100644 --- a/lib/compile/util.js +++ b/lib/compile/util.js @@ -1,5 +1,3 @@ -"use strict" - module.exports = { copy: copy, checkDataType: checkDataType, @@ -25,6 +23,8 @@ module.exports = { escapeJsonPointer: escapeJsonPointer, appendSchema: ($data, schemaCode) => $data ? `" + ${schemaCode}` : `${schemaCode}"`, + concatSchema: ($data, schemaCode) => + $data ? `" + ${schemaCode} + "` : schemaCode, dataNotType: ($data, schemaCode, schemaType) => $data ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` diff --git a/lib/data.js b/lib/data.js index 69b55b2b5d..76b6b92e96 100644 --- a/lib/data.js +++ b/lib/data.js @@ -1,5 +1,3 @@ -"use strict" - // TODO use $data in keyword definitions var KEYWORDS = [ "multipleOf", diff --git a/lib/definition_schema.js b/lib/definition_schema.js index e82c4bbbc0..18b0f2552e 100644 --- a/lib/definition_schema.js +++ b/lib/definition_schema.js @@ -1,5 +1,3 @@ -"use strict" - var metaSchema = require("./refs/json-schema-draft-07.json") module.exports = { diff --git a/lib/dot/_limitItems.jst b/lib/dot/_limitItems.jst deleted file mode 100644 index 741329e776..0000000000 --- a/lib/dot/_limitItems.jst +++ /dev/null @@ -1,12 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{# def.numberKeyword }} - -{{ var $op = $keyword == 'maxItems' ? '>' : '<'; }} -if ({{# def.$dataNotType:'number' }} {{=$data}}.length {{=$op}} {{=$schemaValue}}) { - {{ var $errorKeyword = $keyword; }} - {{# def.error:'_limitItems' }} -} {{? $breakOnError }} else { {{?}} diff --git a/lib/dot/_limitLength.jst b/lib/dot/_limitLength.jst deleted file mode 100644 index 285c66bd2e..0000000000 --- a/lib/dot/_limitLength.jst +++ /dev/null @@ -1,12 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{# def.numberKeyword }} - -{{ var $op = $keyword == 'maxLength' ? '>' : '<'; }} -if ({{# def.$dataNotType:'number' }} {{# def.strLength }} {{=$op}} {{=$schemaValue}}) { - {{ var $errorKeyword = $keyword; }} - {{# def.error:'_limitLength' }} -} {{? $breakOnError }} else { {{?}} diff --git a/lib/dot/_limitProperties.jst b/lib/dot/_limitProperties.jst deleted file mode 100644 index c4c21551ab..0000000000 --- a/lib/dot/_limitProperties.jst +++ /dev/null @@ -1,12 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{# def.numberKeyword }} - -{{ var $op = $keyword == 'maxProperties' ? '>' : '<'; }} -if ({{# def.$dataNotType:'number' }} Object.keys({{=$data}}).length {{=$op}} {{=$schemaValue}}) { - {{ var $errorKeyword = $keyword; }} - {{# def.error:'_limitProperties' }} -} {{? $breakOnError }} else { {{?}} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 99f93e2455..3090a24eeb 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -101,10 +101,6 @@ 'enum': "'should be equal to one of the allowed values'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", - _exclusiveLimit: "'should be {{=$op}} {{#def.appendSchema}}", - _limitItems: "'should NOT have {{?$keyword=='maxItems'}}more{{??}}fewer{{?}} than {{#def.concatSchema}} items'", - _limitLength: "'should NOT be {{?$keyword=='maxLength'}}longer{{??}}shorter{{?}} than {{#def.concatSchema}} characters'", - _limitProperties:"'should NOT have {{?$keyword=='maxProperties'}}more{{??}}fewer{{?}} than {{#def.concatSchema}} properties'", multipleOf: "'should be multiple of {{#def.appendSchema}}", not: "'should NOT be valid'", oneOf: "'should match exactly one schema in oneOf'", @@ -135,9 +131,6 @@ 'enum': "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", 'if': "validate.schema{{=$schemaPath}}", - _limitItems: "{{#def.schemaRefOrVal}}", - _limitLength: "{{#def.schemaRefOrVal}}", - _limitProperties:"{{#def.schemaRefOrVal}}", multipleOf: "{{#def.schemaRefOrVal}}", not: "validate.schema{{=$schemaPath}}", oneOf: "validate.schema{{=$schemaPath}}", @@ -167,9 +160,6 @@ 'enum': "{ allowedValues: schema{{=$lvl}} }", format: "{ format: {{#def.schemaValueQS}} }", 'if': "{ failingKeyword: {{=$ifClause}} }", - _limitItems: "{ limit: {{=$schemaValue}} }", - _limitLength: "{ limit: {{=$schemaValue}} }", - _limitProperties:"{ limit: {{=$schemaValue}} }", multipleOf: "{ multipleOf: {{=$schemaValue}} }", not: "{}", oneOf: "{ passingSchemas: {{=$passingSchemas}} }", diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 93da591c06..42e0d193be 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -12,12 +12,6 @@ module.exports = { format: require("./format"), if: require("./if"), items: require("./items"), - maxItems: require("./_limitItems"), - minItems: require("./_limitItems"), - maxLength: require("./_limitLength"), - minLength: require("./_limitLength"), - maxProperties: require("./_limitProperties"), - minProperties: require("./_limitProperties"), multipleOf: require("./multipleOf"), not: require("./not"), oneOf: require("./oneOf"), diff --git a/lib/keyword.js b/lib/keyword.js index 8ac16af1ae..14a809e2df 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -3,13 +3,6 @@ var customRuleCode = require("./dotjs/custom") var definitionSchema = require("./definition_schema") var util = require("./compile/util") -module.exports = { - add: addKeyword, - get: getKeyword, - remove: removeKeyword, - validate: validateKeyword, -} - /** * Define custom keyword * @this Ajv @@ -18,7 +11,7 @@ module.exports = { * @param {Boolean} _skipValidation of keyword definition * @return {Ajv} this for method chaining */ -function addKeyword(keyword, definition, _skipValidation) { +export function addKeyword(keyword, definition, _skipValidation) { /* jshint validthis: true */ /* eslint no-shadow: 0 */ var RULES = this.RULES @@ -116,7 +109,7 @@ function ruleCode(it, keyword /*, ruleType */) { schemaCode = schemaRefOrVal() } const data = "data" + (it.dataLevel || "") - const cxt = {fail, keyword, data, $data, schemaCode, schemaType} + const cxt = {fail, keyword, data, $data, schemaCode, opts: it.opts} // TODO check that code called "fail" or another valid way to return code code(cxt) return out @@ -173,7 +166,7 @@ function ruleCode(it, keyword /*, ruleType */) { * @param {String} keyword pre-defined or custom keyword. * @return {Object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. */ -function getKeyword(keyword) { +export function getKeyword(keyword) { /* jshint validthis: true */ var rule = this.RULES.custom[keyword] return rule ? rule.definition : this.RULES.keywords[keyword] || false @@ -185,7 +178,7 @@ function getKeyword(keyword) { * @param {String} keyword pre-defined or custom keyword. * @return {Ajv} this for method chaining */ -function removeKeyword(keyword) { +export function removeKeyword(keyword) { /* jshint validthis: true */ var RULES = this.RULES delete RULES.keywords[keyword] @@ -210,7 +203,7 @@ function removeKeyword(keyword) { * @param {Boolean} throwError true to throw exception if definition is invalid * @return {boolean} validation result */ -function validateKeyword(definition, throwError) { +export function validateKeyword(definition, throwError) { validateKeyword.errors = null var v = (this._validateKeyword = this._validateKeyword || this.compile(definitionSchema, true)) diff --git a/lib/keywords/const.js b/lib/keywords/const.js index 6ac308eb1d..c5e2f09b57 100644 --- a/lib/keywords/const.js +++ b/lib/keywords/const.js @@ -4,6 +4,6 @@ module.exports = { code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), error: { message: () => '"should be equal to constant"', - params: ({schemaCode}) => `{ allowedValue: ${schemaCode} }`, + params: ({schemaCode}) => `{allowedValue: ${schemaCode}}`, }, } diff --git a/lib/keywords/index.js b/lib/keywords/index.js index f756f97739..efc0c0db12 100644 --- a/lib/keywords/index.js +++ b/lib/keywords/index.js @@ -4,4 +4,10 @@ module.exports = { exclusiveMinimum: require("./limit"), maximum: require("./limit"), minimum: require("./limit"), + maxItems: require("./limitItems"), + minItems: require("./limitItems"), + maxProperties: require("./limitProperties"), + minProperties: require("./limitProperties"), + maxLength: require("./limitLength"), + minLength: require("./limitLength"), } diff --git a/lib/keywords/limit.js b/lib/keywords/limit.js index 41db52a61e..09f03be6ea 100644 --- a/lib/keywords/limit.js +++ b/lib/keywords/limit.js @@ -7,19 +7,21 @@ const OPS = { exclusiveMinimum: {fail: "<=", ok: ">"}, } +const SCHEMA_TYPE = "number" + module.exports = { keywords: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], type: "number", - schemaType: "number", + schemaType: SCHEMA_TYPE, $data: true, - code: ({keyword, fail, data, $data, schemaCode, schemaType}) => { - const dnt = dataNotType($data, schemaCode, schemaType) + code({fail, keyword, data, $data, schemaCode}) { + const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) fail(dnt + data + OPS[keyword].fail + schemaCode + ` || ${data}!==${data}`) }, error: { message: ({keyword, $data, schemaCode}) => `"should be ${OPS[keyword].ok} ${appendSchema($data, schemaCode)}`, params: ({keyword, schemaCode}) => - `{ comparison: "${OPS[keyword].ok}", limit: ${schemaCode} }`, + `{comparison: "${OPS[keyword].ok}", limit: ${schemaCode}}`, }, } diff --git a/lib/keywords/limitItems.js b/lib/keywords/limitItems.js new file mode 100644 index 0000000000..a62a43725f --- /dev/null +++ b/lib/keywords/limitItems.js @@ -0,0 +1,23 @@ +const {concatSchema, dataNotType} = require("../compile/util") + +const SCHEMA_TYPE = "number" + +module.exports = { + keywords: ["maxItems", "minItems"], + type: "array", + schemaType: SCHEMA_TYPE, + $data: true, + code({fail, keyword, data, $data, schemaCode}) { + const op = keyword == "maxItems" ? ">" : "<" + const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) + fail(dnt + `${data}.length` + op + schemaCode) + }, + error: { + message({keyword, $data, schemaCode}) { + const comp = keyword == "maxItems" ? "more" : "fewer" + const sch = concatSchema($data, schemaCode) + return `"should NOT have ${comp} than ${sch} items"` + }, + params: ({schemaCode}) => `{limit: ${schemaCode}}`, + }, +} diff --git a/lib/keywords/limitLength.js b/lib/keywords/limitLength.js new file mode 100644 index 0000000000..1b865f7397 --- /dev/null +++ b/lib/keywords/limitLength.js @@ -0,0 +1,25 @@ +const {concatSchema, dataNotType} = require("../compile/util") + +const SCHEMA_TYPE = "number" + +module.exports = { + keywords: ["maxLength", "minLength"], + type: "string", + schemaType: SCHEMA_TYPE, + $data: true, + code({fail, keyword, data, $data, schemaCode, opts}) { + const op = keyword == "maxLength" ? ">" : "<" + const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) + const len = + opts.unicode === false ? `${data}.length` : `ucs2length(${data})` + fail(dnt + len + op + schemaCode) + }, + error: { + message({keyword, $data, schemaCode}) { + const comp = keyword == "maxLength" ? "more" : "fewer" + const sch = concatSchema($data, schemaCode) + return `"should NOT have ${comp} than ${sch} items"` + }, + params: ({schemaCode}) => `{limit: ${schemaCode}}`, + }, +} diff --git a/lib/keywords/limitProperties.js b/lib/keywords/limitProperties.js new file mode 100644 index 0000000000..86d42066ab --- /dev/null +++ b/lib/keywords/limitProperties.js @@ -0,0 +1,23 @@ +const {concatSchema, dataNotType} = require("../compile/util") + +const SCHEMA_TYPE = "number" + +module.exports = { + keywords: ["maxProperties", "minProperties"], + type: "object", + schemaType: SCHEMA_TYPE, + $data: true, + code({fail, keyword, data, $data, schemaCode}) { + const op = keyword == "maxProperties" ? ">" : "<" + const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) + fail(dnt + `Object.keys(${data}).length` + op + schemaCode) + }, + error: { + message({keyword, $data, schemaCode}) { + const comp = keyword == "maxProperties" ? "more" : "fewer" + const sch = concatSchema($data, schemaCode) + return `"should NOT have ${comp} than ${sch} items"` + }, + params: ({schemaCode}) => `{limit: ${schemaCode}}`, + }, +} diff --git a/package.json b/package.json index f75db55771..6b1141c5d5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", "bundle-beautify": "node ./scripts/bundle.js js-beautify", "dot": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", - "tsc": "tsc && cp -r lib/refs dist/refs", + "tsc": "tsc || true && cp -r lib/refs dist/refs", "build": "npm run dot && npm run tsc", "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", @@ -72,6 +72,7 @@ }, "devDependencies": { "@ajv-validator/config": "^0.1.0", + "@types/node": "^14.0.27", "ajv-async": "^1.0.0", "ajv-formats": "^0.1.0", "bluebird": "^3.5.3", diff --git a/scripts/.eslintrc.yml b/scripts/.eslintrc.yml index 493d7d312d..16d8b308a0 100644 --- a/scripts/.eslintrc.yml +++ b/scripts/.eslintrc.yml @@ -1,3 +1,5 @@ +parserOptions: + sourceType: script rules: no-console: 0 no-empty: [2, allowEmptyCatch: true] diff --git a/spec/.eslintrc.yml b/spec/.eslintrc.yml index f9c66d5380..6ba17543ac 100644 --- a/spec/.eslintrc.yml +++ b/spec/.eslintrc.yml @@ -1,3 +1,5 @@ +parserOptions: + sourceType: script rules: no-console: 0 no-empty: [2, allowEmptyCatch: true] diff --git a/tsconfig.json b/tsconfig.json index a151db8a52..24a0045bda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,9 @@ "compilerOptions": { "outDir": "dist", "lib": ["ES2018", "DOM"], + "types": ["node"], "noImplicitAny": false, + "noImplicitThis": false, "allowJs": true, "declaration": false } From ddca210a9f4196adaa86e4723642fd72a0e2b1d8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 9 Aug 2020 21:45:34 +0100 Subject: [PATCH 017/322] remove unused macro definition --- lib/dot/definitions.def | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index db4ea6f32c..e000696703 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -69,15 +69,6 @@ #}} -{{## def.strLength: - {{? it.opts.unicode === false }} - {{=$data}}.length - {{??}} - ucs2length({{=$data}}) - {{?}} -#}} - - {{## def.willOptimize: it.util.varOccurences($code, $nextData) < 2 #}} From e8cb96f2b17773b3bd3c5c20af6baa8e64fe6c46 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 9 Aug 2020 22:56:09 +0100 Subject: [PATCH 018/322] addVocabulary method, group keywords in validation vocabulary, keywords code to typescript --- lib/ajv.js | 5 +- lib/compile/util.js | 8 --- lib/keyword.js | 53 ++++++++++++++++++- lib/keywords/index.js | 13 ----- lib/vocabularies/util.ts | 23 ++++++++ .../validation/const.ts} | 0 lib/vocabularies/validation/index.ts | 7 +++ .../validation/limit.ts} | 10 ++-- .../validation/limitItems.ts} | 10 ++-- .../validation/limitLength.ts} | 10 ++-- .../validation/limitProperties.ts} | 10 ++-- 11 files changed, 104 insertions(+), 45 deletions(-) delete mode 100644 lib/keywords/index.js create mode 100644 lib/vocabularies/util.ts rename lib/{keywords/const.js => vocabularies/validation/const.ts} (100%) create mode 100644 lib/vocabularies/validation/index.ts rename lib/{keywords/limit.js => vocabularies/validation/limit.ts} (70%) rename lib/{keywords/limitItems.js => vocabularies/validation/limitItems.ts} (68%) rename lib/{keywords/limitLength.js => vocabularies/validation/limitLength.ts} (71%) rename lib/{keywords/limitProperties.js => vocabularies/validation/limitProperties.ts} (69%) diff --git a/lib/ajv.js b/lib/ajv.js index cf3264541d..348d2253a0 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -6,7 +6,7 @@ var compileSchema = require("./compile"), rules = require("./compile/rules"), $dataMetaSchema = require("./data"), util = require("./compile/util"), - standardKeywords = require("./keywords") + validationVocabulary = require("./vocabularies/validation") module.exports = Ajv @@ -25,6 +25,7 @@ Ajv.prototype._compile = _compile Ajv.prototype.compileAsync = require("./compile/async") var customKeyword = require("./keyword") +Ajv.prototype.addVocabulary = customKeyword.addVocabulary Ajv.prototype.addKeyword = customKeyword.addKeyword Ajv.prototype.getKeyword = customKeyword.getKeyword Ajv.prototype.removeKeyword = customKeyword.removeKeyword @@ -75,7 +76,7 @@ function Ajv(opts) { this._metaOpts = getMetaSchemaOptions(this) if (opts.formats) addInitialFormats(this) - addInitialKeywords(this, standardKeywords, true) + this.addVocabulary(validationVocabulary, true) if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) diff --git a/lib/compile/util.js b/lib/compile/util.js index e1fdc1851a..ab987413bf 100644 --- a/lib/compile/util.js +++ b/lib/compile/util.js @@ -21,14 +21,6 @@ module.exports = { unescapeJsonPointer: unescapeJsonPointer, escapeFragment: escapeFragment, escapeJsonPointer: escapeJsonPointer, - appendSchema: ($data, schemaCode) => - $data ? `" + ${schemaCode}` : `${schemaCode}"`, - concatSchema: ($data, schemaCode) => - $data ? `" + ${schemaCode} + "` : schemaCode, - dataNotType: ($data, schemaCode, schemaType) => - $data - ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` - : "", } function copy(o, to) { diff --git a/lib/keyword.js b/lib/keyword.js index 14a809e2df..3592b2e019 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -3,8 +3,58 @@ var customRuleCode = require("./dotjs/custom") var definitionSchema = require("./definition_schema") var util = require("./compile/util") +// export interface KeywordDefinition { +// type?: string | string[] +// async?: boolean +// $data?: boolean +// errors?: boolean | "full" +// metaSchema?: object +// // schema: false makes validate not to expect schema (ValidateFunction) +// schema?: boolean +// statements?: boolean +// dependencies?: string[] +// modifying?: boolean +// valid?: boolean +// // at least one of the following properties should be present +// validate?: SchemaValidateFunction | ValidateFunction +// compile?: ( +// schema: any, +// parentSchema: object, +// it: CompilationContext +// ) => ValidateFunction +// macro?: ( +// schema: any, +// parentSchema: object, +// it: CompilationContext +// ) => object | boolean +// inline?: ( +// it: CompilationContext, +// keyword: string, +// schema: any, +// parentSchema: object +// ) => string +// code?: { +// } +// } + +/** + * Define vocabulary + * @this Ajv + * @param {Array} definitions array of keyword definitions + * @param {Boolean} _skipValidation skip definition validation + * @return {Ajv} this for method chaining + */ +export function addVocabulary(definitions, _skipValidation) { + for (const def of definitions) { + for (const keyword of def.keywords) + this.addKeyword(keyword, def, _skipValidation) + } + return this +} + +// TODO use overloading when switched to typescript to allow not passing keyword /** - * Define custom keyword + * Define keyword * @this Ajv * @param {String} keyword custom keyword, should be unique (including different from all standard, custom and macro keywords). * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. @@ -12,7 +62,6 @@ var util = require("./compile/util") * @return {Ajv} this for method chaining */ export function addKeyword(keyword, definition, _skipValidation) { - /* jshint validthis: true */ /* eslint no-shadow: 0 */ var RULES = this.RULES if (RULES.keywords[keyword]) diff --git a/lib/keywords/index.js b/lib/keywords/index.js deleted file mode 100644 index efc0c0db12..0000000000 --- a/lib/keywords/index.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - const: require("./const"), - exclusiveMaximum: require("./limit"), - exclusiveMinimum: require("./limit"), - maximum: require("./limit"), - minimum: require("./limit"), - maxItems: require("./limitItems"), - minItems: require("./limitItems"), - maxProperties: require("./limitProperties"), - minProperties: require("./limitProperties"), - maxLength: require("./limitLength"), - minLength: require("./limitLength"), -} diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts new file mode 100644 index 0000000000..a0b531d51f --- /dev/null +++ b/lib/vocabularies/util.ts @@ -0,0 +1,23 @@ +export function appendSchema( + schemaCode: string, + $data?: string | false +): string { + return $data ? `" + ${schemaCode}` : `${schemaCode}"` +} + +export function concatSchema( + schemaCode: string, + $data?: string | false +): string { + return $data ? `" + ${schemaCode} + "` : schemaCode +} + +export function dataNotType( + schemaCode: string, + schemaType?: string, + $data?: string | false +): string { + return $data + ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` + : "" +} diff --git a/lib/keywords/const.js b/lib/vocabularies/validation/const.ts similarity index 100% rename from lib/keywords/const.js rename to lib/vocabularies/validation/const.ts diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts new file mode 100644 index 0000000000..e556e9910e --- /dev/null +++ b/lib/vocabularies/validation/index.ts @@ -0,0 +1,7 @@ +module.exports = [ + require("./const"), + require("./limit"), + require("./limitItems"), + require("./limitProperties"), + require("./limitLength"), +] diff --git a/lib/keywords/limit.js b/lib/vocabularies/validation/limit.ts similarity index 70% rename from lib/keywords/limit.js rename to lib/vocabularies/validation/limit.ts index 09f03be6ea..0a49feea7e 100644 --- a/lib/keywords/limit.js +++ b/lib/vocabularies/validation/limit.ts @@ -1,4 +1,4 @@ -const {appendSchema, dataNotType} = require("../compile/util") +import {appendSchema, dataNotType} from "../util" const OPS = { maximum: {fail: ">", ok: "<="}, @@ -7,20 +7,20 @@ const OPS = { exclusiveMinimum: {fail: "<=", ok: ">"}, } -const SCHEMA_TYPE = "number" +const SCH_TYPE = "number" module.exports = { keywords: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], type: "number", - schemaType: SCHEMA_TYPE, + schemaType: SCH_TYPE, $data: true, code({fail, keyword, data, $data, schemaCode}) { - const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) + const dnt = dataNotType(schemaCode, SCH_TYPE, $data) fail(dnt + data + OPS[keyword].fail + schemaCode + ` || ${data}!==${data}`) }, error: { message: ({keyword, $data, schemaCode}) => - `"should be ${OPS[keyword].ok} ${appendSchema($data, schemaCode)}`, + `"should be ${OPS[keyword].ok} ${appendSchema(schemaCode, $data)}`, params: ({keyword, schemaCode}) => `{comparison: "${OPS[keyword].ok}", limit: ${schemaCode}}`, }, diff --git a/lib/keywords/limitItems.js b/lib/vocabularies/validation/limitItems.ts similarity index 68% rename from lib/keywords/limitItems.js rename to lib/vocabularies/validation/limitItems.ts index a62a43725f..133bc713e4 100644 --- a/lib/keywords/limitItems.js +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,21 +1,21 @@ -const {concatSchema, dataNotType} = require("../compile/util") +import {concatSchema, dataNotType} from "../util" -const SCHEMA_TYPE = "number" +const SCH_TYPE = "number" module.exports = { keywords: ["maxItems", "minItems"], type: "array", - schemaType: SCHEMA_TYPE, + schemaType: SCH_TYPE, $data: true, code({fail, keyword, data, $data, schemaCode}) { const op = keyword == "maxItems" ? ">" : "<" - const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) + const dnt = dataNotType(schemaCode, SCH_TYPE, $data) fail(dnt + `${data}.length` + op + schemaCode) }, error: { message({keyword, $data, schemaCode}) { const comp = keyword == "maxItems" ? "more" : "fewer" - const sch = concatSchema($data, schemaCode) + const sch = concatSchema(schemaCode, $data) return `"should NOT have ${comp} than ${sch} items"` }, params: ({schemaCode}) => `{limit: ${schemaCode}}`, diff --git a/lib/keywords/limitLength.js b/lib/vocabularies/validation/limitLength.ts similarity index 71% rename from lib/keywords/limitLength.js rename to lib/vocabularies/validation/limitLength.ts index 1b865f7397..d19a43a3f7 100644 --- a/lib/keywords/limitLength.js +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,15 +1,15 @@ -const {concatSchema, dataNotType} = require("../compile/util") +import {concatSchema, dataNotType} from "../util" -const SCHEMA_TYPE = "number" +const SCH_TYPE = "number" module.exports = { keywords: ["maxLength", "minLength"], type: "string", - schemaType: SCHEMA_TYPE, + schemaType: SCH_TYPE, $data: true, code({fail, keyword, data, $data, schemaCode, opts}) { const op = keyword == "maxLength" ? ">" : "<" - const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) + const dnt = dataNotType(schemaCode, SCH_TYPE, $data) const len = opts.unicode === false ? `${data}.length` : `ucs2length(${data})` fail(dnt + len + op + schemaCode) @@ -17,7 +17,7 @@ module.exports = { error: { message({keyword, $data, schemaCode}) { const comp = keyword == "maxLength" ? "more" : "fewer" - const sch = concatSchema($data, schemaCode) + const sch = concatSchema(schemaCode, $data) return `"should NOT have ${comp} than ${sch} items"` }, params: ({schemaCode}) => `{limit: ${schemaCode}}`, diff --git a/lib/keywords/limitProperties.js b/lib/vocabularies/validation/limitProperties.ts similarity index 69% rename from lib/keywords/limitProperties.js rename to lib/vocabularies/validation/limitProperties.ts index 86d42066ab..26e9239768 100644 --- a/lib/keywords/limitProperties.js +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,21 +1,21 @@ -const {concatSchema, dataNotType} = require("../compile/util") +import {concatSchema, dataNotType} from "../util" -const SCHEMA_TYPE = "number" +const SCH_TYPE = "number" module.exports = { keywords: ["maxProperties", "minProperties"], type: "object", - schemaType: SCHEMA_TYPE, + schemaType: SCH_TYPE, $data: true, code({fail, keyword, data, $data, schemaCode}) { const op = keyword == "maxProperties" ? ">" : "<" - const dnt = dataNotType($data, schemaCode, SCHEMA_TYPE) + const dnt = dataNotType(schemaCode, SCH_TYPE, $data) fail(dnt + `Object.keys(${data}).length` + op + schemaCode) }, error: { message({keyword, $data, schemaCode}) { const comp = keyword == "maxProperties" ? "more" : "fewer" - const sch = concatSchema($data, schemaCode) + const sch = concatSchema(schemaCode, $data) return `"should NOT have ${comp} than ${sch} items"` }, params: ({schemaCode}) => `{limit: ${schemaCode}}`, From 42319bf11483a6f62a9ae2e40be479cf5b68c0e4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 11:02:10 +0100 Subject: [PATCH 019/322] style: update eslint rules --- .eslintrc.yml | 66 ++++++++++++++++++++++++-------------- lib/compile/async.js | 1 - package.json | 1 + spec/async.spec.js | 1 - spec/custom_rules/index.js | 7 ++-- 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 7fc6b7b447..f9edbf0efe 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,30 +1,48 @@ -extends: eslint:recommended -parserOptions: - ecmaVersion: 6 - sourceType: module env: + es6: true node: true browser: true +extends: + - "eslint:recommended" + - prettier +parserOptions: + ecmaVersion: 2018 + sourceType: module rules: - block-scoped-var: 2 - callback-return: 2 - complexity: [2, 16] - curly: [2, multi-or-nest, consistent] - dot-location: [2, property] - dot-notation: 2 - linebreak-style: [2, unix] - new-cap: 2 - no-console: [2, allow: [warn, error]] - no-else-return: 2 - no-eq-null: 2 - no-extra-semi: 0 - no-fallthrough: 2 - no-invalid-this: 2 - no-return-assign: 2 - no-shadow: 1 - no-trailing-spaces: 2 - no-use-before-define: [2, nofunc] + block-scoped-var: error + callback-return: error + complexity: [error, 16] + curly: [error, multi-or-nest, consistent] + dot-location: [error, property] + dot-notation: error + id-match: error + linebreak-style: [error, unix] + new-cap: error + no-console: [error, allow: [warn, error]] + no-debugger: error + no-duplicate-imports: error + no-else-return: error + no-eq-null: error + no-eval: error + no-fallthrough: error + no-invalid-this: error + no-new-wrappers: error + no-path-concat: error + no-redeclare: error + no-return-assign: error + no-sequences: error + no-shadow: warn + no-template-curly-in-string: error + no-trailing-spaces: error + no-undef-init: error + no-use-before-define: [error, nofunc] + prefer-const: error + radix: error semi: 0 - valid-jsdoc: [2, requireReturn: false] + valid-jsdoc: [error, requireReturn: false] no-control-regex: 0 - no-useless-escape: 2 + no-useless-escape: error + no-void: error + # eqeqeq: [error, smart] + # no-var: error + # prefer-arrow-callback: error diff --git a/lib/compile/async.js b/lib/compile/async.js index 270b9df070..c223825ec2 100644 --- a/lib/compile/async.js +++ b/lib/compile/async.js @@ -13,7 +13,6 @@ module.exports = compileAsync */ function compileAsync(schema, meta, callback) { /* eslint no-shadow: 0 */ - /* global Promise */ /* jshint validthis: true */ var self = this if (typeof this._opts.loadSchema != "function") diff --git a/package.json b/package.json index 6b1141c5d5..53a4d05b80 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "del-cli": "^3.0.0", "dot": "^1.0.3", "eslint": "^7.3.1", + "eslint-config-prettier": "^6.11.0", "gh-pages-generator": "^0.2.3", "glob": "^7.0.0", "husky": "^4.2.5", diff --git a/spec/async.spec.js b/spec/async.spec.js index 691de0b202..f5d7f7bab1 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -1,5 +1,4 @@ "use strict" -/* global Promise */ var Ajv = require("./ajv"), should = require("./chai").should() diff --git a/spec/custom_rules/index.js b/spec/custom_rules/index.js index 534fc1495f..9dedfbdffe 100644 --- a/spec/custom_rules/index.js +++ b/spec/custom_rules/index.js @@ -1,11 +1,14 @@ "use strict" var fs = require("fs"), + path = require("path"), doT = require("dot") module.exports = { - range: doT.compile(fs.readFileSync(__dirname + "/range.jst", "utf8")), + range: doT.compile( + fs.readFileSync(path.join(__dirname, "range.jst"), "utf8") + ), rangeWithErrors: doT.compile( - fs.readFileSync(__dirname + "/range_with_errors.jst", "utf8") + fs.readFileSync(path.join(__dirname, "range_with_errors.jst"), "utf8") ), } From 0c4f81900ba173eec9c32f440dd51b8fbf237e56 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 11:27:50 +0100 Subject: [PATCH 020/322] style: typescript eslint rules --- .eslintrc.yml | 15 +++++++++++++++ lib/ajv.d.ts | 16 ++++++++-------- lib/compile/{schema_obj.js => schema_obj.ts} | 4 +--- package.json | 4 +++- 4 files changed, 27 insertions(+), 12 deletions(-) rename lib/compile/{schema_obj.js => schema_obj.ts} (53%) diff --git a/.eslintrc.yml b/.eslintrc.yml index f9edbf0efe..0e76b5efd9 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -8,6 +8,21 @@ extends: parserOptions: ecmaVersion: 2018 sourceType: module +overrides: + - files: ["*.ts"] + extends: + - "eslint:recommended" + - "plugin:@typescript-eslint/recommended" + - "plugin:@typescript-eslint/recommended-requiring-type-checking" + - "prettier/@typescript-eslint" + parser: "@typescript-eslint/parser" + parserOptions: + project: ["./tsconfig.json"] + plugins: ["@typescript-eslint"] + rules: + "@typescript-eslint/ban-types": off + "@typescript-eslint/no-empty-interface": off + "@typescript-eslint/no-explicit-any": off rules: block-scoped-var: error callback-return: error diff --git a/lib/ajv.d.ts b/lib/ajv.d.ts index 14d47aebcb..cb24845d69 100644 --- a/lib/ajv.d.ts +++ b/lib/ajv.d.ts @@ -1,11 +1,3 @@ -declare var ajv: { - (options?: ajv.Options): ajv.Ajv - new (options?: ajv.Options): ajv.Ajv - ValidationError: typeof AjvErrors.ValidationError - MissingRefError: typeof AjvErrors.MissingRefError - $dataMetaSchema: object -} - declare namespace AjvErrors { class ValidationError extends Error { constructor(errors: Array) @@ -26,6 +18,14 @@ declare namespace AjvErrors { } } +declare const ajv: { + (options?: ajv.Options): ajv.Ajv + new (options?: ajv.Options): ajv.Ajv + ValidationError: typeof AjvErrors.ValidationError + MissingRefError: typeof AjvErrors.MissingRefError + $dataMetaSchema: object +} + declare namespace ajv { type ValidationError = AjvErrors.ValidationError diff --git a/lib/compile/schema_obj.js b/lib/compile/schema_obj.ts similarity index 53% rename from lib/compile/schema_obj.js rename to lib/compile/schema_obj.ts index 41dfb69ef6..f727f26734 100644 --- a/lib/compile/schema_obj.js +++ b/lib/compile/schema_obj.ts @@ -1,7 +1,5 @@ -var util = require("./util") - module.exports = SchemaObject function SchemaObject(obj) { - util.copy(obj, this) + Object.assign(this, obj) } diff --git a/package.json b/package.json index 53a4d05b80..9a41d4ebde 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint lib/{compile/,}*.js spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint lib/{compile/,}*.{js,ts} spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "lint": "npm run eslint", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", @@ -73,6 +73,8 @@ "devDependencies": { "@ajv-validator/config": "^0.1.0", "@types/node": "^14.0.27", + "@typescript-eslint/eslint-plugin": "^3.8.0", + "@typescript-eslint/parser": "^3.8.0", "ajv-async": "^1.0.0", "ajv-formats": "^0.1.0", "bluebird": "^3.5.3", From 49b13dd43fbb56e6531ce766dc996cca09027d75 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 14:20:22 +0100 Subject: [PATCH 021/322] refactor: keyword and util to typescript --- .eslintrc.yml | 7 + lib/ajv.js | 5 +- lib/compile/{util.js => util.ts} | 135 +++++------- lib/dot/definitions.def | 2 +- lib/{keyword.js => keyword.ts} | 113 +++++----- lib/types.ts | 202 ++++++++++++++++++ lib/vocabularies/util.ts | 8 +- lib/vocabularies/validation/const.ts | 6 +- lib/vocabularies/validation/index.ts | 6 +- lib/vocabularies/validation/limit.ts | 5 +- lib/vocabularies/validation/limitItems.ts | 5 +- lib/vocabularies/validation/limitLength.ts | 5 +- .../validation/limitProperties.ts | 5 +- spec/ajv_async_instances.js | 3 +- spec/ajv_instances.js | 10 +- tsconfig.json | 1 + 16 files changed, 361 insertions(+), 157 deletions(-) rename lib/compile/{util.js => util.ts} (68%) rename lib/{keyword.js => keyword.ts} (76%) create mode 100644 lib/types.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 0e76b5efd9..6ef74d2af0 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -20,9 +20,16 @@ overrides: project: ["./tsconfig.json"] plugins: ["@typescript-eslint"] rules: + no-var: 0 "@typescript-eslint/ban-types": off "@typescript-eslint/no-empty-interface": off "@typescript-eslint/no-explicit-any": off + "@typescript-eslint/no-unsafe-call": off + "@typescript-eslint/no-unsafe-member-access": off + "@typescript-eslint/no-unsafe-assignment": off + "@typescript-eslint/restrict-plus-operands": off + "@typescript-eslint/no-unsafe-return": off + "@typescript-eslint/no-var-requires": off rules: block-scoped-var: error callback-return: error diff --git a/lib/ajv.js b/lib/ajv.js index 348d2253a0..1e4a89697d 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -5,7 +5,6 @@ var compileSchema = require("./compile"), stableStringify = require("fast-json-stable-stringify"), rules = require("./compile/rules"), $dataMetaSchema = require("./data"), - util = require("./compile/util"), validationVocabulary = require("./vocabularies/validation") module.exports = Ajv @@ -54,7 +53,7 @@ var META_SUPPORT_DATA = ["/properties"] */ function Ajv(opts) { if (!(this instanceof Ajv)) return new Ajv(opts) - opts = this._opts = util.copy(opts) || {} + opts = this._opts = {...(opts || {})} setLogger(this) this._schemas = {} this._refs = {} @@ -450,7 +449,7 @@ function checkUnique(self, id) { } function getMetaSchemaOptions(self) { - var metaOpts = util.copy(self._opts) + var metaOpts = {...self._opts} for (var i = 0; i < META_IGNORE_OPTIONS.length; i++) delete metaOpts[META_IGNORE_OPTIONS[i]] return metaOpts diff --git a/lib/compile/util.js b/lib/compile/util.ts similarity index 68% rename from lib/compile/util.js rename to lib/compile/util.ts index ab987413bf..cbdffde363 100644 --- a/lib/compile/util.js +++ b/lib/compile/util.ts @@ -1,91 +1,61 @@ +// TODO switch to exports + module.exports = { - copy: copy, checkDataType: checkDataType, checkDataTypes: checkDataTypes, - coerceToTypes: coerceToTypes, - toHash: toHash, - getProperty: getProperty, + coerceToTypes, + toHash, escapeQuotes: escapeQuotes, equal: require("fast-deep-equal"), ucs2length: require("./ucs2length"), varOccurences: varOccurences, varReplace: varReplace, - schemaHasRules: schemaHasRules, + schemaHasRules, schemaHasRulesExcept: schemaHasRulesExcept, schemaUnknownRules: schemaUnknownRules, - toQuotedString: toQuotedString, + toQuotedString, getPathExpr: getPathExpr, getPath: getPath, getData: getData, + getProperty, unescapeFragment: unescapeFragment, unescapeJsonPointer: unescapeJsonPointer, escapeFragment: escapeFragment, escapeJsonPointer: escapeJsonPointer, } -function copy(o, to) { - to = to || {} - for (var key in o) to[key] = o[key] - return to -} - -function checkDataType(dataType, data, strictNumbers, negate) { - var EQUAL = negate ? " !== " : " === ", - AND = negate ? " || " : " && ", - OK = negate ? "!" : "", - NOT = negate ? "" : "!" +function checkDataType( + dataType: string, + data: string, + strictNumbers: boolean, + negate: boolean +): string { + var EQ = negate ? " !== " : " === ", + OK = negate ? "!" : "" switch (dataType) { case "null": - return data + EQUAL + "null" + return data + EQ + "null" case "array": - return OK + "Array.isArray(" + data + ")" + return OK + `Array.isArray(${data})` case "object": return ( - "(" + OK + - data + - AND + - "typeof " + - data + - EQUAL + - '"object"' + - AND + - NOT + - "Array.isArray(" + - data + - "))" + `(${data} && typeof ${data} === "object" && !Array.isArray(${data}))` ) case "integer": return ( - "(typeof " + - data + - EQUAL + - '"number"' + - AND + - NOT + - "(" + - data + - " % 1)" + - AND + - data + - EQUAL + - data + - (strictNumbers ? AND + OK + "isFinite(" + data + ")" : "") + - ")" + OK + + `(typeof ${data} === "number" && !(${data} % 1) && !isNaN(${data})` + + (strictNumbers ? ` && isFinite(${data}))` : ")") ) case "number": return ( - "(typeof " + - data + - EQUAL + - '"' + - dataType + - '"' + - (strictNumbers ? AND + OK + "isFinite(" + data + ")" : "") + - ")" + OK + + `(typeof ${data} === "number"` + + (strictNumbers ? `&& isFinite(${data}))` : ")") ) default: - return "typeof " + data + EQUAL + '"' + dataType + '"' + return `typeof ${data} ${EQ} "${dataType}"` } } @@ -113,41 +83,41 @@ function checkDataTypes(dataTypes, data, strictNumbers) { } } -var COERCE_TO_TYPES = toHash(["string", "number", "integer", "boolean", "null"]) -function coerceToTypes(optionCoerceTypes, dataTypes) { +const COERCE_TYPES = toHash(["string", "number", "integer", "boolean", "null"]) +function coerceToTypes( + optionCoerceTypes: undefined | boolean | "array", + dataTypes: string[] +): string[] | void { if (Array.isArray(dataTypes)) { - var types = [] - for (var i = 0; i < dataTypes.length; i++) { - var t = dataTypes[i] - if (COERCE_TO_TYPES[t]) types[types.length] = t - else if (optionCoerceTypes === "array" && t === "array") - types[types.length] = t + const types: string[] = [] + for (const t of dataTypes) { + if (COERCE_TYPES[t] || (optionCoerceTypes === "array" && t === "array")) + types.push(t) } if (types.length) return types - } else if (COERCE_TO_TYPES[dataTypes]) { - return [dataTypes] - } else if (optionCoerceTypes === "array" && dataTypes === "array") { - return ["array"] + return } + if (COERCE_TYPES[dataTypes]) return [dataTypes] + if (optionCoerceTypes === "array" && dataTypes === "array") return ["array"] } -function toHash(arr) { +function toHash(arr: string[]): {[key: string]: true} { var hash = {} for (var i = 0; i < arr.length; i++) hash[arr[i]] = true return hash } -var IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i -var SINGLE_QUOTE = /'|\\/g -function getProperty(key) { - return typeof key == "number" - ? "[" + key + "]" +const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i +const SINGLE_QUOTE = /'|\\/g +export function getProperty(key: string | number): string { + return typeof key === "number" + ? `[${key}]` : IDENTIFIER.test(key) - ? "." + key - : "['" + escapeQuotes(key) + "']" + ? `.${key}` + : `['${escapeQuotes(key)}']` } -function escapeQuotes(str) { +function escapeQuotes(str: string): string { return str .replace(SINGLE_QUOTE, "\\$&") .replace(/\n/g, "\\n") @@ -168,7 +138,10 @@ function varReplace(str, dataVar, expr) { return str.replace(new RegExp(dataVar, "g"), expr + "$1") } -function schemaHasRules(schema, rules) { +function schemaHasRules( + schema: object | boolean, + rules: object +): boolean | undefined { if (typeof schema == "boolean") return !schema for (var key in schema) if (rules[key]) return true } @@ -183,8 +156,8 @@ function schemaUnknownRules(schema, rules) { for (var key in schema) if (!rules[key]) return key } -function toQuotedString(str) { - return "'" + escapeQuotes(str) + "'" +export function toQuotedString(str: string): string { + return `'${escapeQuotes(str)}'` } function getPathExpr(currentPath, expr, jsonPointers, isNumber) { @@ -207,7 +180,7 @@ function getPath(currentPath, prop, jsonPointers) { var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ -function getData($data, lvl, paths) { +export function getData($data: string, lvl: number, paths: string[]): string { var up, jsonPointer, data, matches if ($data === "") return "rootData" if ($data[0] == "/") { @@ -216,7 +189,7 @@ function getData($data, lvl, paths) { jsonPointer = $data data = "rootData" } else { - matches = $data.match(RELATIVE_JSON_POINTER) + matches = RELATIVE_JSON_POINTER.exec($data) if (!matches) throw new Error("Invalid JSON-pointer: " + $data) up = +matches[1] jsonPointer = matches[2] diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index e000696703..9fd84bac47 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -30,7 +30,7 @@ {{## def.setupNextLevel: {{ - var $it = it.util.copy(it); + var $it = {...it}; var $closingBraces = ''; $it.level++; var $nextValid = 'valid' + $it.level; diff --git a/lib/keyword.js b/lib/keyword.ts similarity index 76% rename from lib/keyword.js rename to lib/keyword.ts index 3592b2e019..2f612e7224 100644 --- a/lib/keyword.js +++ b/lib/keyword.ts @@ -1,41 +1,16 @@ -var IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i -var customRuleCode = require("./dotjs/custom") -var definitionSchema = require("./definition_schema") -var util = require("./compile/util") +import { + KeywordDefinition, + Vocabulary, + ErrorObject, + ValidateFunction, + CompilationContext, +} from "./types" -// export interface KeywordDefinition { -// type?: string | string[] -// async?: boolean -// $data?: boolean -// errors?: boolean | "full" -// metaSchema?: object -// // schema: false makes validate not to expect schema (ValidateFunction) -// schema?: boolean -// statements?: boolean -// dependencies?: string[] -// modifying?: boolean -// valid?: boolean -// // at least one of the following properties should be present -// validate?: SchemaValidateFunction | ValidateFunction -// compile?: ( -// schema: any, -// parentSchema: object, -// it: CompilationContext -// ) => ValidateFunction -// macro?: ( -// schema: any, -// parentSchema: object, -// it: CompilationContext -// ) => object | boolean -// inline?: ( -// it: CompilationContext, -// keyword: string, -// schema: any, -// parentSchema: object -// ) => string -// code?: { -// } -// } +import {getData, getProperty, toQuotedString} from "./compile/util" + +const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i +const customRuleCode = require("./dotjs/custom") +const definitionSchema = require("./definition_schema") /** * Define vocabulary @@ -44,8 +19,17 @@ var util = require("./compile/util") * @param {Boolean} _skipValidation skip definition validation * @return {Ajv} this for method chaining */ -export function addVocabulary(definitions, _skipValidation) { +export function addVocabulary( + definitions: Vocabulary, + _skipValidation?: boolean +): object { + // TODO return Ajv for (const def of definitions) { + if (!def.keywords) { + throw new Error( + 'Vocabulary keywords must have "keywords" property in definition' + ) + } for (const keyword of def.keywords) this.addKeyword(keyword, def, _skipValidation) } @@ -61,7 +45,12 @@ export function addVocabulary(definitions, _skipValidation) { * @param {Boolean} _skipValidation of keyword definition * @return {Ajv} this for method chaining */ -export function addKeyword(keyword, definition, _skipValidation) { +export function addKeyword( + keyword: string, + definition: KeywordDefinition, + _skipValidation?: boolean +): object { + // TODO return type Ajv /* eslint no-shadow: 0 */ var RULES = this.RULES if (RULES.keywords[keyword]) @@ -136,10 +125,19 @@ export function addKeyword(keyword, definition, _skipValidation) { * @param {String} keyword pre-defined or custom keyword. * @return {String} compiled rule code. */ -function ruleCode(it, keyword /*, ruleType */) { +function ruleCode( + it: CompilationContext, + keyword: string /*, ruleType */ +): string { const schema = it.schema[keyword] - const {schemaType, code, error, $data: $defData} = this.definition - let schemaCode + const { + schemaType, + code, + error, + $data: $defData, + }: KeywordDefinition = this.definition + if (!code) throw new Error('"code" must be defined') + let schemaCode: string | number let out = "" const $data = $defData && it.opts.$data && schema && schema.$data if ($data) { @@ -147,7 +145,7 @@ function ruleCode(it, keyword /*, ruleType */) { // schemaCode = it.getName("schema") schemaCode = `schema${it.level}` // TODO replace with const once it.level replaced with unique names - out += `var ${schemaCode} = ${util.getData( + out += `var ${schemaCode} = ${getData( $data, it.dataLevel, it.dataPathArr @@ -163,12 +161,12 @@ function ruleCode(it, keyword /*, ruleType */) { code(cxt) return out - function fail(condition) { + function fail(condition: string) { out += `if (${condition}) { ${reportError()} }` if (!it.opts.allErrors) out += `else {` } - function reportError() { + function reportError(): string { const errCode = errorObjectCode() if (!it.compositeRule && !it.opts.allErrors) { // TODO trim whitespace @@ -185,11 +183,13 @@ function ruleCode(it, keyword /*, ruleType */) { function errorObjectCode() { if (it.createErrors === false) return "{}" + if (!(error && error.message && error.params)) + throw new Error('"error" must have "message" and "params" functions') // TODO trim whitespace let out = `{ keyword: "${keyword}", dataPath: (dataPath || "") + ${it.errorPath}, - schemaPath: ${util.toQuotedString(it.errSchemaPath + "/" + keyword)}, + schemaPath: ${toQuotedString(it.errSchemaPath + "/" + keyword)}, params: ${error.params(cxt)},` if (it.opts.messages !== false) out += `message: ${error.message(cxt)},` if (it.opts.verbose) { @@ -202,10 +202,10 @@ function ruleCode(it, keyword /*, ruleType */) { return out + "}" } - function schemaRefOrVal() { + function schemaRefOrVal(): string | number { return schemaType === "number" && !$data ? schema - : `validate.schema${it.schemaPath + util.getProperty(keyword)}` + : `validate.schema${it.schemaPath + getProperty(keyword)}` } } @@ -215,7 +215,7 @@ function ruleCode(it, keyword /*, ruleType */) { * @param {String} keyword pre-defined or custom keyword. * @return {Object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. */ -export function getKeyword(keyword) { +export function getKeyword(keyword: string): KeywordDefinition | boolean { /* jshint validthis: true */ var rule = this.RULES.custom[keyword] return rule ? rule.definition : this.RULES.keywords[keyword] || false @@ -227,7 +227,8 @@ export function getKeyword(keyword) { * @param {String} keyword pre-defined or custom keyword. * @return {Ajv} this for method chaining */ -export function removeKeyword(keyword) { +export function removeKeyword(keyword: string): object { + // TODO return type should be Ajv /* jshint validthis: true */ var RULES = this.RULES delete RULES.keywords[keyword] @@ -245,6 +246,11 @@ export function removeKeyword(keyword) { return this } +export interface KeywordValidator { + (definition: KeywordDefinition, throwError: boolean): boolean + errors?: ErrorObject[] | null +} + /** * Validate keyword definition * @this Ajv @@ -252,9 +258,12 @@ export function removeKeyword(keyword) { * @param {Boolean} throwError true to throw exception if definition is invalid * @return {boolean} validation result */ -export function validateKeyword(definition, throwError) { +export const validateKeyword: KeywordValidator = function ( + definition, + throwError +) { validateKeyword.errors = null - var v = (this._validateKeyword = + var v: ValidateFunction = (this._validateKeyword = this._validateKeyword || this.compile(definitionSchema, true)) if (v(definition)) return true diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000000..f942b8b81f --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,202 @@ +interface Options { + $data?: boolean + allErrors?: boolean + verbose?: boolean + jsonPointers?: boolean + uniqueItems?: boolean + unicode?: boolean + format?: false | string + formats?: object + keywords?: object + unknownFormats?: true | string[] | "ignore" + schemas?: object[] | object + missingRefs?: true | "ignore" | "fail" + extendRefs?: true | "ignore" | "fail" + loadSchema?: ( + uri: string, + cb?: (err: Error, schema: object) => void + ) => PromiseLike + removeAdditional?: boolean | "all" | "failing" + useDefaults?: boolean | "empty" | "shared" + coerceTypes?: boolean | "array" + strictDefaults?: boolean | "log" + strictKeywords?: boolean | "log" + strictNumbers?: boolean + async?: boolean | string + transpile?: string | ((code: string) => string) + meta?: boolean | object + validateSchema?: boolean | "log" + addUsedSchema?: boolean + inlineRefs?: boolean | number + passContext?: boolean + loopRequired?: number + ownProperties?: boolean + multipleOfPrecision?: boolean | number + errorDataPath?: string + messages?: boolean + sourceCode?: boolean + processCode?: (code: string, schema: object) => string + cache?: object + logger?: Logger | false + nullable?: boolean + serialize?: ((schema: object | boolean) => any) | false +} + +interface Logger { + log(...args: any[]): any + warn(...args: any[]): any + error(...args: any[]): any +} + +export interface ValidateFunction { + ( + data: any, + dataPath?: string, + parentData?: object | any[], + parentDataProperty?: string | number, + rootData?: object | any[] + ): boolean | PromiseLike + schema?: object | boolean + errors?: null | ErrorObject[] + refs?: object + refVal?: any[] + root?: ValidateFunction | object + $async?: true + source?: object +} + +export interface SchemaValidateFunction { + ( + schema: any, + data: any, + parentSchema?: object, + dataPath?: string, + parentData?: object | any[], + parentDataProperty?: string | number, + rootData?: object | any[] + ): boolean | PromiseLike + errors?: ErrorObject[] +} + +export interface ErrorObject { + keyword: string + dataPath: string + schemaPath: string + params: object // TODO add interface + // Added to validation errors of propertyNames keyword schema + propertyName?: string + // Excluded if messages set to false. + message?: string + // These are added with the `verbose` option. + schema?: any + parentSchema?: object + data?: any +} + +export interface CompilationContext { + level: number + dataLevel: number + dataPathArr: string[] + schema: any + schemaPath: string + errorPath: string + errSchemaPath: string + createErrors?: boolean // TODO maybe remove later + baseId: string + async: boolean + opts: Options + formats: { + [index: string]: Format | undefined + } + keywords: { + [index: string]: KeywordDefinition | undefined + } + compositeRule: boolean + validate: (schema: object) => boolean + util: object // TODO + self: object // TODO +} + +export interface KeywordDefinition { + keywords?: string[] + type?: string | string[] + schemaType?: string + async?: boolean + $data?: boolean + errors?: boolean | "full" + metaSchema?: object + // schema: false makes validate not to expect schema (ValidateFunction) + schema?: boolean + statements?: boolean + dependencies?: string[] + modifying?: boolean + valid?: boolean + // at least one of the following properties should be present + validate?: SchemaValidateFunction | ValidateFunction + compile?: ( + schema: any, + parentSchema: object, + it: CompilationContext + ) => ValidateFunction + macro?: ( + schema: any, + parentSchema: object, + it: CompilationContext + ) => object | boolean + inline?: ( + it: CompilationContext, + keyword: string, + schema: any, + parentSchema: object + ) => string + code?: (cxt: KeywordContext) => string | void + error?: { + message: (cxt: KeywordContext) => string + params: (cxt: KeywordContext) => string + } + validateSchema?: ValidateFunction +} + +export type Vocabulary = KeywordDefinition[] + +export interface KeywordContext { + fail: (condition: string) => void + keyword: string + data: string + $data?: string | false + schemaCode: string | number + opts: Options +} + +export type FormatMode = "fast" | "full" + +type SN = string | number + +export type FormatValidator = (data: T) => boolean + +export type FormatCompare = (data1: T, data2: T) => boolean + +export type AsyncFormatValidator = ( + data: T +) => PromiseLike + +export interface FormatDefinition { + type: T extends string ? "string" : "number" + validate: FormatValidator | (T extends string ? string | RegExp : never) + async?: false | undefined + compare?: FormatCompare +} + +export interface AsyncFormatDefinition { + type: T extends string ? "string" : "number" + validate: AsyncFormatValidator + async: true + compare?: FormatCompare +} + +export type Format = + | string + | RegExp + | FormatValidator + | FormatDefinition + | AsyncFormatDefinition diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index a0b531d51f..974b21656e 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,19 +1,19 @@ export function appendSchema( - schemaCode: string, + schemaCode: string | number, $data?: string | false ): string { return $data ? `" + ${schemaCode}` : `${schemaCode}"` } export function concatSchema( - schemaCode: string, + schemaCode: string | number, $data?: string | false -): string { +): string | number { return $data ? `" + ${schemaCode} + "` : schemaCode } export function dataNotType( - schemaCode: string, + schemaCode: string | number, schemaType?: string, $data?: string | false ): string { diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index c5e2f09b57..d1728b749d 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,4 +1,6 @@ -module.exports = { +import {KeywordDefinition} from "../../types" + +const def: KeywordDefinition = { keywords: ["const"], $data: true, code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), @@ -7,3 +9,5 @@ module.exports = { params: ({schemaCode}) => `{allowedValue: ${schemaCode}}`, }, } + +module.exports = def diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index e556e9910e..be5e85d7b8 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -1,7 +1,11 @@ -module.exports = [ +import {Vocabulary} from "../../types" + +const defs: Vocabulary = [ require("./const"), require("./limit"), require("./limitItems"), require("./limitProperties"), require("./limitLength"), ] + +module.exports = defs diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 0a49feea7e..e995403dd1 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,3 +1,4 @@ +import {KeywordDefinition} from "../../types" import {appendSchema, dataNotType} from "../util" const OPS = { @@ -9,7 +10,7 @@ const OPS = { const SCH_TYPE = "number" -module.exports = { +const def: KeywordDefinition = { keywords: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], type: "number", schemaType: SCH_TYPE, @@ -25,3 +26,5 @@ module.exports = { `{comparison: "${OPS[keyword].ok}", limit: ${schemaCode}}`, }, } + +module.exports = def diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 133bc713e4..b398076f33 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,8 +1,9 @@ +import {KeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" const SCH_TYPE = "number" -module.exports = { +const def: KeywordDefinition = { keywords: ["maxItems", "minItems"], type: "array", schemaType: SCH_TYPE, @@ -21,3 +22,5 @@ module.exports = { params: ({schemaCode}) => `{limit: ${schemaCode}}`, }, } + +module.exports = def diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index d19a43a3f7..c8d769beeb 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,8 +1,9 @@ +import {KeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" const SCH_TYPE = "number" -module.exports = { +const def: KeywordDefinition = { keywords: ["maxLength", "minLength"], type: "string", schemaType: SCH_TYPE, @@ -23,3 +24,5 @@ module.exports = { params: ({schemaCode}) => `{limit: ${schemaCode}}`, }, } + +module.exports = def diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index 26e9239768..aedbfe5a75 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,8 +1,9 @@ +import {KeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" const SCH_TYPE = "number" -module.exports = { +const def: KeywordDefinition = { keywords: ["maxProperties", "minProperties"], type: "object", schemaType: SCH_TYPE, @@ -21,3 +22,5 @@ module.exports = { params: ({schemaCode}) => `{limit: ${schemaCode}}`, }, } + +module.exports = def diff --git a/spec/ajv_async_instances.js b/spec/ajv_async_instances.js index 8a41945a45..acc0a66c92 100644 --- a/spec/ajv_async_instances.js +++ b/spec/ajv_async_instances.js @@ -1,7 +1,6 @@ "use strict" var Ajv = require("./ajv"), - util = require("../lib/compile/util"), setupAsync = require("./ajv-async") module.exports = getAjvInstances @@ -19,7 +18,7 @@ function getAjvInstances(opts) { ] options.forEach(function (_opts) { - util.copy(opts, _opts) + Object.assign(_opts, opts) var ajv = getAjv(_opts) if (ajv) instances.push(ajv) }) diff --git a/spec/ajv_instances.js b/spec/ajv_instances.js index 4ca9fa5385..d8711c5153 100644 --- a/spec/ajv_instances.js +++ b/spec/ajv_instances.js @@ -11,8 +11,8 @@ function getAjvInstances(options, extraOpts) { function _getAjvInstances(opts, useOpts) { var optNames = Object.keys(opts) if (optNames.length) { - opts = copy(opts) - var useOpts1 = copy(useOpts), + opts = {...opts} + var useOpts1 = {...useOpts}, optName = optNames[0] useOpts1[optName] = opts[optName] delete opts[optName] @@ -22,9 +22,3 @@ function _getAjvInstances(opts, useOpts) { } return [new Ajv(useOpts)] } - -function copy(o, to) { - to = to || {} - for (var key in o) to[key] = o[key] - return to -} diff --git a/tsconfig.json b/tsconfig.json index 24a0045bda..1021121369 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "types": ["node"], "noImplicitAny": false, "noImplicitThis": false, + "noImplicitReturns": false, "allowJs": true, "declaration": false } From 883ecafcdaa45211576465f1a73913366fa3643a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 15:10:13 +0100 Subject: [PATCH 022/322] multipleOf to typescript (change: multipleOf $data undefined -> invalid) --- lib/compile/rules.js | 2 +- lib/dot/errors.def | 5 --- lib/dot/multipleOf.jst | 22 ---------- lib/dotjs/index.js | 1 - lib/keyword.ts | 17 +++++++- lib/types.ts | 3 ++ lib/vocabularies/validation/index.ts | 16 ++++--- lib/vocabularies/validation/multipleOf.ts | 52 +++++++++++++++++++++++ spec/extras/$data/multipleOf.json | 4 +- 9 files changed, 84 insertions(+), 38 deletions(-) delete mode 100644 lib/dot/multipleOf.jst create mode 100644 lib/vocabularies/validation/multipleOf.ts diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 12ffa6630d..21f862bcf5 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -5,7 +5,7 @@ module.exports = function rules() { var RULES = [ { type: "number", - rules: ["multipleOf", "format"], + rules: ["format"], }, {type: "string", rules: ["pattern", "format"]}, { diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 3090a24eeb..4dd64bf5f5 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -86,8 +86,6 @@ #}} -{{## def.concatSchema:{{?$isData}}' + {{=$schemaValue}} + '{{??}}{{=$schema}}{{?}}#}} -{{## def.appendSchema:{{?$isData}}' + {{=$schemaValue}}{{??}}{{=$schemaValue}}'{{?}}#}} {{## def.concatSchemaEQ:{{?$isData}}' + {{=$schemaValue}} + '{{??}}{{=it.util.escapeQuotes($schema)}}{{?}}#}} {{## def._errorMessages = { @@ -101,7 +99,6 @@ 'enum': "'should be equal to one of the allowed values'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", - multipleOf: "'should be multiple of {{#def.appendSchema}}", not: "'should NOT be valid'", oneOf: "'should match exactly one schema in oneOf'", pattern: "'should match pattern \"{{#def.concatSchemaEQ}}\"'", @@ -131,7 +128,6 @@ 'enum': "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", 'if': "validate.schema{{=$schemaPath}}", - multipleOf: "{{#def.schemaRefOrVal}}", not: "validate.schema{{=$schemaPath}}", oneOf: "validate.schema{{=$schemaPath}}", pattern: "{{#def.schemaRefOrQS}}", @@ -160,7 +156,6 @@ 'enum': "{ allowedValues: schema{{=$lvl}} }", format: "{ format: {{#def.schemaValueQS}} }", 'if': "{ failingKeyword: {{=$ifClause}} }", - multipleOf: "{ multipleOf: {{=$schemaValue}} }", not: "{}", oneOf: "{ passingSchemas: {{=$passingSchemas}} }", pattern: "{ pattern: {{#def.schemaValueQS}} }", diff --git a/lib/dot/multipleOf.jst b/lib/dot/multipleOf.jst deleted file mode 100644 index 6d88a456f3..0000000000 --- a/lib/dot/multipleOf.jst +++ /dev/null @@ -1,22 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{# def.numberKeyword }} - -var division{{=$lvl}}; -if ({{?$isData}} - {{=$schemaValue}} !== undefined && ( - typeof {{=$schemaValue}} != 'number' || - {{?}} - (division{{=$lvl}} = {{=$data}} / {{=$schemaValue}}, - {{? it.opts.multipleOfPrecision }} - Math.abs(Math.round(division{{=$lvl}}) - division{{=$lvl}}) > 1e-{{=it.opts.multipleOfPrecision}} - {{??}} - division{{=$lvl}} !== parseInt(division{{=$lvl}}) - {{?}} - ) - {{?$isData}} ) {{?}} ) { - {{# def.error:'multipleOf' }} -} {{? $breakOnError }} else { {{?}} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 42e0d193be..6a416bea05 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -12,7 +12,6 @@ module.exports = { format: require("./format"), if: require("./if"), items: require("./items"), - multipleOf: require("./multipleOf"), not: require("./not"), oneOf: require("./oneOf"), pattern: require("./pattern"), diff --git a/lib/keyword.ts b/lib/keyword.ts index 2f612e7224..923955ed32 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -156,12 +156,25 @@ function ruleCode( schemaCode = schemaRefOrVal() } const data = "data" + (it.dataLevel || "") - const cxt = {fail, keyword, data, $data, schemaCode, opts: it.opts} + const cxt = { + fail, + write, + keyword, + data, + $data, + schemaCode, + level: it.level, + opts: it.opts, + } // TODO check that code called "fail" or another valid way to return code code(cxt) return out - function fail(condition: string) { + function write(str: string): void { + out += str + } + + function fail(condition: string): void { out += `if (${condition}) { ${reportError()} }` if (!it.opts.allErrors) out += `else {` } diff --git a/lib/types.ts b/lib/types.ts index f942b8b81f..00e67bd5b6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -161,10 +161,13 @@ export type Vocabulary = KeywordDefinition[] export interface KeywordContext { fail: (condition: string) => void + write: (str: string) => void keyword: string data: string $data?: string | false schemaCode: string | number + // TODO replace level with namespace + level: number opts: Options } diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index be5e85d7b8..5033edce6a 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -1,11 +1,17 @@ import {Vocabulary} from "../../types" -const defs: Vocabulary = [ - require("./const"), +const validation: Vocabulary = [ + // number require("./limit"), - require("./limitItems"), - require("./limitProperties"), + require("./multipleOf"), + // string require("./limitLength"), + // object + require("./limitProperties"), + // array + require("./limitItems"), + // any + require("./const"), ] -module.exports = defs +module.exports = validation diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts new file mode 100644 index 0000000000..3878d46cc7 --- /dev/null +++ b/lib/vocabularies/validation/multipleOf.ts @@ -0,0 +1,52 @@ +// {{# def.definitions }} +// {{# def.errors }} +// {{# def.setupKeyword }} +// {{# def.$data }} + +// {{# def.numberKeyword }} + +// var division{{=$lvl}}; +// if ({{?$isData}} +// {{=$schemaValue}} !== undefined && ( +// typeof {{=$schemaValue}} != 'number' || +// {{?}} +// (division{{=$lvl}} = {{=$data}} / {{=$schemaValue}}, +// {{? it.opts.multipleOfPrecision }} +// Math.abs(Math.round(division{{=$lvl}}) - division{{=$lvl}}) > 1e-{{=it.opts.multipleOfPrecision}} +// {{??}} +// division{{=$lvl}} !== parseInt(division{{=$lvl}}) +// {{?}} +// ) +// {{?$isData}} ) {{?}} ) { +// {{# def.error:'multipleOf' }} +// } {{? $breakOnError }} else { {{?}} + +import {KeywordDefinition} from "../../types" +import {appendSchema, dataNotType} from "../util" + +const SCH_TYPE = "number" + +const def: KeywordDefinition = { + keywords: ["multipleOf"], + type: "number", + schemaType: SCH_TYPE, + $data: true, + code({write, fail, data, $data, schemaCode, level, opts}) { + const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const res = `division${level}` + const prec = opts.multipleOfPrecision + const invalid = prec + ? `Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` + : `${res} !== parseInt(${res})` + // TODO replace with let + write(`var ${res};`) + fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) + }, + error: { + message: ({$data, schemaCode}) => + `"should be multiple of ${appendSchema(schemaCode, $data)}`, + params: ({schemaCode}) => `{multipleOf: ${schemaCode}}`, + }, +} + +module.exports = def diff --git a/spec/extras/$data/multipleOf.json b/spec/extras/$data/multipleOf.json index 525e331c20..939a04a7b6 100644 --- a/spec/extras/$data/multipleOf.json +++ b/spec/extras/$data/multipleOf.json @@ -57,11 +57,11 @@ "valid": false }, { - "description": "valid if value of multipleOf is undefined", + "description": "invalid if value of multipleOf is undefined", "data": { "multiple": 10 }, - "valid": true + "valid": false } ] }, From df6bcddd3c7a7eb91a72067aecfae888d1d00d3e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 15:34:01 +0100 Subject: [PATCH 023/322] refactor: Cache, SchemaObject to typescript --- lib/ajv.js | 5 +++-- lib/cache.js | 21 --------------------- lib/cache.ts | 25 +++++++++++++++++++++++++ lib/compile/resolve.js | 3 ++- lib/compile/schema_obj.ts | 8 ++++---- lib/types.ts | 4 +++- 6 files changed, 37 insertions(+), 29 deletions(-) delete mode 100644 lib/cache.js create mode 100644 lib/cache.ts diff --git a/lib/ajv.js b/lib/ajv.js index 1e4a89697d..c5b071d3b7 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -1,7 +1,8 @@ +import SchemaObject from "./compile/schema_obj" +import Cache from "./cache" + var compileSchema = require("./compile"), resolve = require("./compile/resolve"), - Cache = require("./cache"), - SchemaObject = require("./compile/schema_obj"), stableStringify = require("fast-json-stable-stringify"), rules = require("./compile/rules"), $dataMetaSchema = require("./data"), diff --git a/lib/cache.js b/lib/cache.js deleted file mode 100644 index b970862efc..0000000000 --- a/lib/cache.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = Cache - -function Cache() { - this._cache = {} -} - -Cache.prototype.put = function Cache_put(key, value) { - this._cache[key] = value -} - -Cache.prototype.get = function Cache_get(key) { - return this._cache[key] -} - -Cache.prototype.del = function Cache_del(key) { - delete this._cache[key] -} - -Cache.prototype.clear = function Cache_clear() { - this._cache = {} -} diff --git a/lib/cache.ts b/lib/cache.ts new file mode 100644 index 0000000000..1aebbb5404 --- /dev/null +++ b/lib/cache.ts @@ -0,0 +1,25 @@ +import SchemaObject from "./compile/schema_obj" + +export default class Cache { + _cache: {[key: string]: SchemaObject} + + constructor() { + this._cache = {} + } + + put(key: string, value: SchemaObject): void { + this._cache[key] = value + } + + get(key: string): SchemaObject { + return this._cache[key] + } + + del(key: string): void { + delete this._cache[key] + } + + clear(): void { + this._cache = {} + } +} diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index 78e270825c..3ac493db6f 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -1,7 +1,8 @@ +import SchemaObject from "./schema_obj" + var URI = require("uri-js"), equal = require("fast-deep-equal"), util = require("./util"), - SchemaObject = require("./schema_obj"), traverse = require("json-schema-traverse") module.exports = resolve diff --git a/lib/compile/schema_obj.ts b/lib/compile/schema_obj.ts index f727f26734..8bfe31f219 100644 --- a/lib/compile/schema_obj.ts +++ b/lib/compile/schema_obj.ts @@ -1,5 +1,5 @@ -module.exports = SchemaObject - -function SchemaObject(obj) { - Object.assign(this, obj) +export default class SchemaObject { + constructor(obj: object) { + Object.assign(this, obj) + } } diff --git a/lib/types.ts b/lib/types.ts index 00e67bd5b6..d1809a8b89 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,3 +1,5 @@ +import Cache from "./cache" + interface Options { $data?: boolean allErrors?: boolean @@ -36,7 +38,7 @@ interface Options { messages?: boolean sourceCode?: boolean processCode?: (code: string, schema: object) => string - cache?: object + cache?: Cache logger?: Logger | false nullable?: boolean serialize?: ((schema: object | boolean) => any) | false From 5283a8684a09f82d42164b7684d868a1b47bb9b3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 16:02:07 +0100 Subject: [PATCH 024/322] keyword definition "keyword" property --- lib/keyword.ts | 14 +++++++++----- lib/types.ts | 2 +- lib/vocabularies/validation/const.ts | 2 +- lib/vocabularies/validation/limit.ts | 2 +- lib/vocabularies/validation/limitItems.ts | 2 +- lib/vocabularies/validation/limitLength.ts | 2 +- lib/vocabularies/validation/limitProperties.ts | 2 +- lib/vocabularies/validation/multipleOf.ts | 2 +- 8 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/keyword.ts b/lib/keyword.ts index 923955ed32..c88eff3329 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -23,15 +23,19 @@ export function addVocabulary( definitions: Vocabulary, _skipValidation?: boolean ): object { - // TODO return Ajv + // TODO return type Ajv for (const def of definitions) { - if (!def.keywords) { + if (!def.keyword) { throw new Error( - 'Vocabulary keywords must have "keywords" property in definition' + 'Vocabulary keywords must have "keyword" property in definition' ) } - for (const keyword of def.keywords) - this.addKeyword(keyword, def, _skipValidation) + if (Array.isArray(def.keyword)) { + for (const keyword of def.keyword) + this.addKeyword(keyword, def, _skipValidation) + } else { + this.addKeyword(def.keyword, def, _skipValidation) + } } return this } diff --git a/lib/types.ts b/lib/types.ts index d1809a8b89..73cb7c3785 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -120,7 +120,7 @@ export interface CompilationContext { } export interface KeywordDefinition { - keywords?: string[] + keyword?: string | string[] type?: string | string[] schemaType?: string async?: boolean diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index d1728b749d..829acdd7cf 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,7 +1,7 @@ import {KeywordDefinition} from "../../types" const def: KeywordDefinition = { - keywords: ["const"], + keyword: "const", $data: true, code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), error: { diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index e995403dd1..83ac742e56 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -11,7 +11,7 @@ const OPS = { const SCH_TYPE = "number" const def: KeywordDefinition = { - keywords: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], + keyword: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], type: "number", schemaType: SCH_TYPE, $data: true, diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index b398076f33..51c4d2d62b 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -4,7 +4,7 @@ import {concatSchema, dataNotType} from "../util" const SCH_TYPE = "number" const def: KeywordDefinition = { - keywords: ["maxItems", "minItems"], + keyword: ["maxItems", "minItems"], type: "array", schemaType: SCH_TYPE, $data: true, diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index c8d769beeb..ff70460a51 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -4,7 +4,7 @@ import {concatSchema, dataNotType} from "../util" const SCH_TYPE = "number" const def: KeywordDefinition = { - keywords: ["maxLength", "minLength"], + keyword: ["maxLength", "minLength"], type: "string", schemaType: SCH_TYPE, $data: true, diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index aedbfe5a75..fa875ac40e 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -4,7 +4,7 @@ import {concatSchema, dataNotType} from "../util" const SCH_TYPE = "number" const def: KeywordDefinition = { - keywords: ["maxProperties", "minProperties"], + keyword: ["maxProperties", "minProperties"], type: "object", schemaType: SCH_TYPE, $data: true, diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 3878d46cc7..a6f4c0f2c4 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -27,7 +27,7 @@ import {appendSchema, dataNotType} from "../util" const SCH_TYPE = "number" const def: KeywordDefinition = { - keywords: ["multipleOf"], + keyword: "multipleOf", type: "number", schemaType: SCH_TYPE, $data: true, From ed5972d76a1cb66a224200c385cb4e257ab00388 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 18:19:53 +0100 Subject: [PATCH 025/322] refactor: util types --- lib/compile/index.js | 15 ++-- lib/compile/resolve.js | 11 ++- lib/compile/rules.js | 5 +- lib/compile/ucs2length.js | 4 +- lib/compile/util.ts | 177 ++++++++++++++++++++------------------ lib/dot/definitions.def | 2 +- lib/keyword.ts | 3 +- 7 files changed, 113 insertions(+), 104 deletions(-) diff --git a/lib/compile/index.js b/lib/compile/index.js index 3757e6d2c3..2b5af9b406 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,3 +1,7 @@ +import ucs2length from "./ucs2length" +import {toQuotedString} from "./util" +const equal = require("fast-deep-equal") + var resolve = require("./resolve"), util = require("./util"), errorClasses = require("./error_classes"), @@ -9,9 +13,6 @@ var validateGenerator = require("../dotjs/validate") * Functions below are used inside compiled validations function */ -var ucs2length = util.ucs2length -var equal = require("fast-deep-equal") - // this error is thrown by async schemas to return validation errors via exception var ValidationError = errorClasses.Validation @@ -241,7 +242,7 @@ function compile(schema, root, localRefs, baseId) { case "number": return "" + value case "string": - return util.toQuotedString(value) + return toQuotedString(value) case "object": if (value === null) return "null" var valueStr = stableStringify(value) @@ -364,11 +365,7 @@ function compIndex(schema, root, baseId) { function patternCode(i, patterns) { return ( - "var pattern" + - i + - " = new RegExp(" + - util.toQuotedString(patterns[i]) + - ");" + "var pattern" + i + " = new RegExp(" + toQuotedString(patterns[i]) + ");" ) } diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index 3ac493db6f..b5c40801d1 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -1,8 +1,7 @@ import SchemaObject from "./schema_obj" - +import {toHash, escapeFragment, unescapeFragment} from "./util" var URI = require("uri-js"), equal = require("fast-deep-equal"), - util = require("./util"), traverse = require("json-schema-traverse") module.exports = resolve @@ -109,7 +108,7 @@ function resolveRecursive(root, ref, parsedRef) { } } -var PREVENT_SCOPE_CHANGE = util.toHash([ +var PREVENT_SCOPE_CHANGE = toHash([ "properties", "patternProperties", "enum", @@ -126,7 +125,7 @@ function getJsonPointer(parsedRef, baseId, schema, root) { for (var i = 1; i < parts.length; i++) { var part = parts[i] if (part) { - part = util.unescapeFragment(part) + part = unescapeFragment(part) schema = schema[part] if (schema === undefined) break var id @@ -149,7 +148,7 @@ function getJsonPointer(parsedRef, baseId, schema, root) { return {schema: schema, root: root, baseId: baseId} } -var SIMPLE_INLINED = util.toHash([ +var SIMPLE_INLINED = toHash([ "type", "format", "pattern", @@ -257,7 +256,7 @@ function resolveIds(schema) { if (keyIndex !== undefined) { fullPath += "/" + - (typeof keyIndex == "number" ? keyIndex : util.escapeFragment(keyIndex)) + (typeof keyIndex == "number" ? keyIndex : escapeFragment(keyIndex)) } if (typeof id == "string") { diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 21f862bcf5..4288ecca62 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -1,5 +1,6 @@ -var ruleModules = require("../dotjs"), - toHash = require("./util").toHash +import {toHash} from "./util" + +var ruleModules = require("../dotjs") module.exports = function rules() { var RULES = [ diff --git a/lib/compile/ucs2length.js b/lib/compile/ucs2length.js index 935703e5ed..ca71f1a14a 100644 --- a/lib/compile/ucs2length.js +++ b/lib/compile/ucs2length.js @@ -1,6 +1,8 @@ // https://mathiasbynens.be/notes/javascript-encoding // https://github.com/bestiejs/punycode.js - punycode.ucs2.decode -module.exports = function ucs2length(str) { +module.exports = ucs2length + +export default function ucs2length(str) { var length = 0, len = str.length, pos = 0, diff --git a/lib/compile/util.ts b/lib/compile/util.ts index cbdffde363..f7b0158029 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,37 +1,32 @@ -// TODO switch to exports - +// TODO switch to exports - below are used in dot templates module.exports = { - checkDataType: checkDataType, - checkDataTypes: checkDataTypes, + checkDataType, + checkDataTypes, coerceToTypes, toHash, - escapeQuotes: escapeQuotes, - equal: require("fast-deep-equal"), - ucs2length: require("./ucs2length"), - varOccurences: varOccurences, - varReplace: varReplace, + escapeQuotes, + varOccurrences, + varReplace, schemaHasRules, - schemaHasRulesExcept: schemaHasRulesExcept, - schemaUnknownRules: schemaUnknownRules, + schemaHasRulesExcept, + schemaUnknownRules, toQuotedString, - getPathExpr: getPathExpr, - getPath: getPath, - getData: getData, + getPathExpr, + getPath, + getData, getProperty, - unescapeFragment: unescapeFragment, - unescapeJsonPointer: unescapeJsonPointer, - escapeFragment: escapeFragment, - escapeJsonPointer: escapeJsonPointer, + unescapeFragment, + escapeFragment, } -function checkDataType( +export function checkDataType( dataType: string, data: string, strictNumbers: boolean, negate: boolean ): string { - var EQ = negate ? " !== " : " === ", - OK = negate ? "!" : "" + const EQ = negate ? " !== " : " === " + const OK = negate ? "!" : "" switch (dataType) { case "null": return data + EQ + "null" @@ -59,32 +54,30 @@ function checkDataType( } } -function checkDataTypes(dataTypes, data, strictNumbers) { - switch (dataTypes.length) { - case 1: - return checkDataType(dataTypes[0], data, strictNumbers, true) - default: - var code = "" - var types = toHash(dataTypes) - if (types.array && types.object) { - code = types.null ? "(" : "(!" + data + " || " - code += "typeof " + data + ' !== "object")' - delete types.null - delete types.array - delete types.object - } - if (types.number) delete types.integer - for (var t in types) { - code += - (code ? " && " : "") + checkDataType(t, data, strictNumbers, true) - } - - return code +export function checkDataTypes( + dataTypes: string[], + data: string, + strictNumbers: boolean +): string { + if (dataTypes.length === 1) + return checkDataType(dataTypes[0], data, strictNumbers, true) + let code = "" + const types = toHash(dataTypes) + if (types.array && types.object) { + code = types.null ? "(" : `(!${data} || ` + code += `typeof ${data} !== "object")` + delete types.null + delete types.array + delete types.object } + if (types.number) delete types.integer + for (const t in types) + code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, true) + return code } const COERCE_TYPES = toHash(["string", "number", "integer", "boolean", "null"]) -function coerceToTypes( +export function coerceToTypes( optionCoerceTypes: undefined | boolean | "array", dataTypes: string[] ): string[] | void { @@ -101,9 +94,9 @@ function coerceToTypes( if (optionCoerceTypes === "array" && dataTypes === "array") return ["array"] } -function toHash(arr: string[]): {[key: string]: true} { - var hash = {} - for (var i = 0; i < arr.length; i++) hash[arr[i]] = true +export function toHash(arr: string[]): {[key: string]: true} { + const hash = {} + for (const item of arr) hash[item] = true return hash } @@ -117,7 +110,7 @@ export function getProperty(key: string | number): string { : `['${escapeQuotes(key)}']` } -function escapeQuotes(str: string): string { +export function escapeQuotes(str: string): string { return str .replace(SINGLE_QUOTE, "\\$&") .replace(/\n/g, "\\n") @@ -126,72 +119,91 @@ function escapeQuotes(str: string): string { .replace(/\t/g, "\\t") } -function varOccurences(str, dataVar) { +export function varOccurrences(str: string, dataVar: string): number { dataVar += "[^0-9]" - var matches = str.match(new RegExp(dataVar, "g")) + /* eslint-disable @typescript-eslint/prefer-regexp-exec */ + const matches = str.match(new RegExp(dataVar, "g")) return matches ? matches.length : 0 } -function varReplace(str, dataVar, expr) { +export function varReplace(str: string, dataVar: string, expr: string): string { dataVar += "([^0-9])" expr = expr.replace(/\$/g, "$$$$") return str.replace(new RegExp(dataVar, "g"), expr + "$1") } -function schemaHasRules( +// TODO rules, schema? +export function schemaHasRules( schema: object | boolean, rules: object ): boolean | undefined { if (typeof schema == "boolean") return !schema - for (var key in schema) if (rules[key]) return true + for (const key in schema) if (rules[key]) return true } -function schemaHasRulesExcept(schema, rules, exceptKeyword) { - if (typeof schema == "boolean") return !schema && exceptKeyword != "not" - for (var key in schema) if (key != exceptKeyword && rules[key]) return true +// TODO rules, schema? +export function schemaHasRulesExcept( + schema: object, + rules: object, + exceptKeyword: string +): boolean | undefined { + if (typeof schema == "boolean") return !schema && exceptKeyword !== "not" + for (const key in schema) if (key != exceptKeyword && rules[key]) return true } -function schemaUnknownRules(schema, rules) { - if (typeof schema == "boolean") return - for (var key in schema) if (!rules[key]) return key +// TODO rules, schema? +export function schemaUnknownRules( + schema: object, + rules: object +): string | undefined { + if (typeof schema === "boolean") return + for (const key in schema) if (!rules[key]) return key } export function toQuotedString(str: string): string { return `'${escapeQuotes(str)}'` } -function getPathExpr(currentPath, expr, jsonPointers, isNumber) { - var path = jsonPointers // false by default - ? "'/' + " + - expr + +export function getPathExpr( + currentPath: string, + expr: string, + jsonPointers?: boolean, + isNumber?: boolean +): string { + const path = jsonPointers // false by default + ? `'/' + ${expr}` + (isNumber ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") : isNumber - ? "'[' + " + expr + " + ']'" - : "'[\\'' + " + expr + " + '\\']'" + ? `'[' + ${expr} + ']'` + : `'[\\'' + ${expr} + '\\']'` return joinPaths(currentPath, path) } -function getPath(currentPath, prop, jsonPointers) { - var path = jsonPointers // false by default +export function getPath( + currentPath: string, + prop: string, + jsonPointers?: boolean +): string { + const path = jsonPointers // false by default ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop)) return joinPaths(currentPath, path) } -var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ -var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ +const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ +const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ export function getData($data: string, lvl: number, paths: string[]): string { - var up, jsonPointer, data, matches + let jsonPointer, data if ($data === "") return "rootData" - if ($data[0] == "/") { + if ($data[0] === "/") { if (!JSON_POINTER.test($data)) throw new Error("Invalid JSON-pointer: " + $data) jsonPointer = $data data = "rootData" } else { - matches = RELATIVE_JSON_POINTER.exec($data) + const matches = RELATIVE_JSON_POINTER.exec($data) if (!matches) throw new Error("Invalid JSON-pointer: " + $data) - up = +matches[1] + const up: number = +matches[1] jsonPointer = matches[2] if (jsonPointer == "#") { if (up >= lvl) { @@ -214,10 +226,9 @@ export function getData($data: string, lvl: number, paths: string[]): string { if (!jsonPointer) return data } - var expr = data - var segments = jsonPointer.split("/") - for (var i = 0; i < segments.length; i++) { - var segment = segments[i] + let expr = data + const segments = jsonPointer.split("/") + for (const segment of segments) { if (segment) { data += getProperty(unescapeJsonPointer(segment)) expr += " && " + data @@ -226,23 +237,23 @@ export function getData($data: string, lvl: number, paths: string[]): string { return expr } -function joinPaths(a, b) { - if (a == '""') return b - return (a + " + " + b).replace(/([^\\])' \+ '/g, "$1") +export function joinPaths(a: string, b: string): string { + if (a === '""' || a === "''") return b + return `${a} + ${b}`.replace(/([^\\])' \+ '/g, "$1") } -function unescapeFragment(str) { +export function unescapeFragment(str: string): string { return unescapeJsonPointer(decodeURIComponent(str)) } -function escapeFragment(str) { +export function escapeFragment(str: string): string { return encodeURIComponent(escapeJsonPointer(str)) } -function escapeJsonPointer(str) { +export function escapeJsonPointer(str: string): string { return str.replace(/~/g, "~0").replace(/\//g, "~1") } -function unescapeJsonPointer(str) { +export function unescapeJsonPointer(str: string): string { return str.replace(/~1/g, "/").replace(/~0/g, "~") } diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index 9fd84bac47..745382f1d1 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -70,7 +70,7 @@ {{## def.willOptimize: - it.util.varOccurences($code, $nextData) < 2 + it.util.varOccurrences($code, $nextData) < 2 #}} diff --git a/lib/keyword.ts b/lib/keyword.ts index c88eff3329..1a5f9d7558 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -200,8 +200,7 @@ function ruleCode( function errorObjectCode() { if (it.createErrors === false) return "{}" - if (!(error && error.message && error.params)) - throw new Error('"error" must have "message" and "params" functions') + if (!error) throw new Error('keyword definition must have "error" property') // TODO trim whitespace let out = `{ keyword: "${keyword}", From 9436ab2c554e4e1048ba4de3d082a97b7b8ebf8e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 20:34:45 +0100 Subject: [PATCH 026/322] refactor: pattern keyword to typescript --- .eslintrc.yml | 2 ++ lib/compile/rules.js | 7 ++----- lib/dot/errors.def | 3 --- lib/dot/pattern.jst | 14 -------------- lib/dotjs/index.js | 1 - lib/keyword.ts | 15 ++++++++++----- lib/types.ts | 5 ++++- lib/vocabularies/util.ts | 14 ++++++++++---- lib/vocabularies/validation/index.ts | 1 + lib/vocabularies/validation/pattern.ts | 25 +++++++++++++++++++++++++ 10 files changed, 54 insertions(+), 33 deletions(-) delete mode 100644 lib/dot/pattern.jst create mode 100644 lib/vocabularies/validation/pattern.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 6ef74d2af0..db9cba8912 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -21,6 +21,8 @@ overrides: plugins: ["@typescript-eslint"] rules: no-var: 0 + "@typescript-eslint/restrict-template-expressions": + [error, allowBoolean: true] "@typescript-eslint/ban-types": off "@typescript-eslint/no-empty-interface": off "@typescript-eslint/no-explicit-any": off diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 4288ecca62..4c43c2935d 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -4,11 +4,8 @@ var ruleModules = require("../dotjs") module.exports = function rules() { var RULES = [ - { - type: "number", - rules: ["format"], - }, - {type: "string", rules: ["pattern", "format"]}, + {type: "number", rules: ["format"]}, + {type: "string", rules: ["format"]}, { type: "array", rules: ["items", "contains", "uniqueItems"], diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 4dd64bf5f5..614e3c53d7 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -101,7 +101,6 @@ 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", not: "'should NOT be valid'", oneOf: "'should match exactly one schema in oneOf'", - pattern: "'should match pattern \"{{#def.concatSchemaEQ}}\"'", propertyNames: "'property name \\'{{=$invalidName}}\\' is invalid'", required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", type: "'should be {{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}'", @@ -130,7 +129,6 @@ 'if': "validate.schema{{=$schemaPath}}", not: "validate.schema{{=$schemaPath}}", oneOf: "validate.schema{{=$schemaPath}}", - pattern: "{{#def.schemaRefOrQS}}", propertyNames: "validate.schema{{=$schemaPath}}", required: "validate.schema{{=$schemaPath}}", type: "validate.schema{{=$schemaPath}}", @@ -158,7 +156,6 @@ 'if': "{ failingKeyword: {{=$ifClause}} }", not: "{}", oneOf: "{ passingSchemas: {{=$passingSchemas}} }", - pattern: "{ pattern: {{#def.schemaValueQS}} }", propertyNames: "{ propertyName: '{{=$invalidName}}' }", required: "{ missingProperty: '{{=$missingProperty}}' }", type: "{ type: '{{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }", diff --git a/lib/dot/pattern.jst b/lib/dot/pattern.jst deleted file mode 100644 index 3a37ef6cb8..0000000000 --- a/lib/dot/pattern.jst +++ /dev/null @@ -1,14 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{ - var $regexp = $isData - ? '(new RegExp(' + $schemaValue + '))' - : it.usePattern($schema); -}} - -if ({{# def.$dataNotType:'string' }} !{{=$regexp}}.test({{=$data}}) ) { - {{# def.error:'pattern' }} -} {{? $breakOnError }} else { {{?}} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 6a416bea05..6604e908ab 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -14,7 +14,6 @@ module.exports = { items: require("./items"), not: require("./not"), oneOf: require("./oneOf"), - pattern: require("./pattern"), properties: require("./properties"), propertyNames: require("./propertyNames"), required: require("./required"), diff --git a/lib/keyword.ts b/lib/keyword.ts index 1a5f9d7558..f8be3e50b8 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -7,6 +7,7 @@ import { } from "./types" import {getData, getProperty, toQuotedString} from "./compile/util" +import {quotedString} from "./vocabularies/util" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i const customRuleCode = require("./dotjs/custom") @@ -141,7 +142,7 @@ function ruleCode( $data: $defData, }: KeywordDefinition = this.definition if (!code) throw new Error('"code" must be defined') - let schemaCode: string | number + let schemaCode: string | number | boolean let out = "" const $data = $defData && it.opts.$data && schema && schema.$data if ($data) { @@ -166,7 +167,9 @@ function ruleCode( keyword, data, $data, + schema, schemaCode, + usePattern: it.usePattern, level: it.level, opts: it.opts, } @@ -218,10 +221,12 @@ function ruleCode( return out + "}" } - function schemaRefOrVal(): string | number { - return schemaType === "number" && !$data - ? schema - : `validate.schema${it.schemaPath + getProperty(keyword)}` + function schemaRefOrVal(): string | number | boolean { + if (!$data) { + if (schemaType === "number" || schemaType === "boolean") return schema + if (schemaType === "string") return quotedString(schema) + } + return `validate.schema${it.schemaPath + getProperty(keyword)}` } } diff --git a/lib/types.ts b/lib/types.ts index 73cb7c3785..7d5d3e6efe 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -115,6 +115,7 @@ export interface CompilationContext { } compositeRule: boolean validate: (schema: object) => boolean + usePattern: (str: string) => string util: object // TODO self: object // TODO } @@ -164,10 +165,12 @@ export type Vocabulary = KeywordDefinition[] export interface KeywordContext { fail: (condition: string) => void write: (str: string) => void + usePattern: (str: string) => string keyword: string data: string $data?: string | false - schemaCode: string | number + schema: any + schemaCode: string | number | boolean // TODO replace level with namespace level: number opts: Options diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 974b21656e..c3c53ead43 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,19 +1,25 @@ export function appendSchema( - schemaCode: string | number, + schemaCode: string | number | boolean, $data?: string | false ): string { return $data ? `" + ${schemaCode}` : `${schemaCode}"` } export function concatSchema( - schemaCode: string | number, + schemaCode: string | number | boolean, $data?: string | false -): string | number { +): string | number | boolean { return $data ? `" + ${schemaCode} + "` : schemaCode } +export function quotedString(str: string): string { + return JSON.stringify(str) + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029") +} + export function dataNotType( - schemaCode: string | number, + schemaCode: string | number | boolean, schemaType?: string, $data?: string | false ): string { diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 5033edce6a..95ccda3265 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -6,6 +6,7 @@ const validation: Vocabulary = [ require("./multipleOf"), // string require("./limitLength"), + require("./pattern"), // object require("./limitProperties"), // array diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts new file mode 100644 index 0000000000..08f7237c11 --- /dev/null +++ b/lib/vocabularies/validation/pattern.ts @@ -0,0 +1,25 @@ +import {KeywordDefinition} from "../../types" +import {dataNotType} from "../util" + +const SCH_TYPE = "string" + +const def: KeywordDefinition = { + keyword: "pattern", + type: "string", + schemaType: SCH_TYPE, + $data: true, + code({fail, usePattern, data, $data, schema, schemaCode}) { + const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const regExp = $data ? `(new RegExp(${schemaCode}))` : usePattern(schema) + fail(dnt + `!${regExp}.test(${data})`) + }, + error: { + message: ({$data, schemaCode}) => + $data + ? `'should match pattern "' + ${schemaCode} + '"'` + : `"should match pattern \\"${(schemaCode).slice(1, -1)}\\""`, + params: ({schemaCode}) => `{pattern: ${schemaCode}}`, + }, +} + +module.exports = def From 8d77a1dcf288acd60fb359e842f9478ad0e0c82f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 22:01:50 +0100 Subject: [PATCH 027/322] refactor: Scope class to generate function scope with unique names --- lib/compile/index.js | 2 ++ lib/compile/scope.ts | 13 ++++++++++ lib/keyword.ts | 9 +++---- lib/types.ts | 5 ++-- lib/vocabularies/validation/multipleOf.ts | 30 +++-------------------- 5 files changed, 24 insertions(+), 35 deletions(-) create mode 100644 lib/compile/scope.ts diff --git a/lib/compile/index.js b/lib/compile/index.js index 2b5af9b406..0d253094e2 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,3 +1,4 @@ +import Scope from "./scope" import ucs2length from "./ucs2length" import {toQuotedString} from "./util" const equal = require("fast-deep-equal") @@ -92,6 +93,7 @@ function compile(schema, root, localRefs, baseId) { schemaPath: "", errSchemaPath: "#", errorPath: '""', + scope: new Scope(), MissingRefError: errorClasses.MissingRef, RULES: RULES, validate: validateGenerator, diff --git a/lib/compile/scope.ts b/lib/compile/scope.ts new file mode 100644 index 0000000000..ff4e5bb5f4 --- /dev/null +++ b/lib/compile/scope.ts @@ -0,0 +1,13 @@ +export default class Scope { + _names: {[key: string]: number} + + constructor() { + this._names = {} + } + + getName(prefix: string): string { + if (!this._names[prefix]) this._names[prefix] = 0 + const num = this._names[prefix]++ + return `${prefix}_${num}` + } +} diff --git a/lib/keyword.ts b/lib/keyword.ts index f8be3e50b8..3f25795733 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -146,11 +146,8 @@ function ruleCode( let out = "" const $data = $defData && it.opts.$data && schema && schema.$data if ($data) { - // TODO stop using it.level and maybe it.dataLevel - // schemaCode = it.getName("schema") - schemaCode = `schema${it.level}` - // TODO replace with const once it.level replaced with unique names - out += `var ${schemaCode} = ${getData( + schemaCode = it.scope.getName("schema") + out += `const ${schemaCode} = ${getData( $data, it.dataLevel, it.dataPathArr @@ -169,8 +166,8 @@ function ruleCode( $data, schema, schemaCode, + scope: it.scope, usePattern: it.usePattern, - level: it.level, opts: it.opts, } // TODO check that code called "fail" or another valid way to return code diff --git a/lib/types.ts b/lib/types.ts index 7d5d3e6efe..c5a7e63a7d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,5 @@ import Cache from "./cache" +import Scope from "./compile/scope" interface Options { $data?: boolean @@ -103,6 +104,7 @@ export interface CompilationContext { schemaPath: string errorPath: string errSchemaPath: string + scope: Scope createErrors?: boolean // TODO maybe remove later baseId: string async: boolean @@ -166,13 +168,12 @@ export interface KeywordContext { fail: (condition: string) => void write: (str: string) => void usePattern: (str: string) => string + scope: Scope keyword: string data: string $data?: string | false schema: any schemaCode: string | number | boolean - // TODO replace level with namespace - level: number opts: Options } diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index a6f4c0f2c4..5f2be0f30f 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,26 +1,3 @@ -// {{# def.definitions }} -// {{# def.errors }} -// {{# def.setupKeyword }} -// {{# def.$data }} - -// {{# def.numberKeyword }} - -// var division{{=$lvl}}; -// if ({{?$isData}} -// {{=$schemaValue}} !== undefined && ( -// typeof {{=$schemaValue}} != 'number' || -// {{?}} -// (division{{=$lvl}} = {{=$data}} / {{=$schemaValue}}, -// {{? it.opts.multipleOfPrecision }} -// Math.abs(Math.round(division{{=$lvl}}) - division{{=$lvl}}) > 1e-{{=it.opts.multipleOfPrecision}} -// {{??}} -// division{{=$lvl}} !== parseInt(division{{=$lvl}}) -// {{?}} -// ) -// {{?$isData}} ) {{?}} ) { -// {{# def.error:'multipleOf' }} -// } {{? $breakOnError }} else { {{?}} - import {KeywordDefinition} from "../../types" import {appendSchema, dataNotType} from "../util" @@ -31,15 +8,14 @@ const def: KeywordDefinition = { type: "number", schemaType: SCH_TYPE, $data: true, - code({write, fail, data, $data, schemaCode, level, opts}) { + code({write, fail, scope, data, $data, schemaCode, opts}) { const dnt = dataNotType(schemaCode, SCH_TYPE, $data) - const res = `division${level}` + const res = scope.getName("res") const prec = opts.multipleOfPrecision const invalid = prec ? `Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : `${res} !== parseInt(${res})` - // TODO replace with let - write(`var ${res};`) + write(`let ${res};`) fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) }, error: { From 0a1fa0f4b08485fdef71a9f987c6c6c80826e78f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 22:26:19 +0100 Subject: [PATCH 028/322] test: only use files in dist --- spec/custom.spec.js | 2 +- spec/json-schema.spec.js | 2 +- spec/security.spec.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index e36b29a64f..a6d2b449ee 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -2,7 +2,7 @@ var getAjvInstances = require("./ajv_instances"), should = require("./chai").should(), - equal = require("../lib/compile/equal"), + equal = require("../dist/compile/equal"), customRules = require("./custom_rules") describe("Custom keywords", function () { diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 5d60c46ef7..5e70535e8d 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -70,7 +70,7 @@ function runTest(instances, draft, tests) { instances.forEach(function (ajv) { switch (draft) { case 6: - ajv.addMetaSchema(require("../lib/refs/json-schema-draft-06.json")) + ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) ajv._opts.defaultMeta = "http://json-schema.org/draft-06/schema#" break } diff --git a/spec/security.spec.js b/spec/security.spec.js index 0b3436073e..73a09e5118 100644 --- a/spec/security.spec.js +++ b/spec/security.spec.js @@ -7,7 +7,7 @@ var jsonSchemaTest = require("json-schema-test"), after = require("./after_test") var instances = getAjvInstances(options, { - schemas: [require("../lib/refs/json-schema-secure.json")], + schemas: [require("../dist/refs/json-schema-secure.json")], }) jsonSchemaTest(instances, { From 7aea117962a152e5eae00df5b2302fed62c0c0e2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 22:38:28 +0100 Subject: [PATCH 029/322] fix: ucs2length --- lib/compile/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compile/index.js b/lib/compile/index.js index 0d253094e2..d3df7b4eb3 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,7 +1,7 @@ import Scope from "./scope" -import ucs2length from "./ucs2length" import {toQuotedString} from "./util" const equal = require("fast-deep-equal") +const ucs2length = require("./ucs2length") var resolve = require("./resolve"), util = require("./util"), From acfd737c3130837af32be10d569493f7ba403428 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 22:43:19 +0100 Subject: [PATCH 030/322] refactor: ucs2length to typescript --- lib/compile/{ucs2length.js => ucs2length.ts} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename lib/compile/{ucs2length.js => ucs2length.ts} (67%) diff --git a/lib/compile/ucs2length.js b/lib/compile/ucs2length.ts similarity index 67% rename from lib/compile/ucs2length.js rename to lib/compile/ucs2length.ts index ca71f1a14a..c877648569 100644 --- a/lib/compile/ucs2length.js +++ b/lib/compile/ucs2length.ts @@ -2,18 +2,18 @@ // https://github.com/bestiejs/punycode.js - punycode.ucs2.decode module.exports = ucs2length -export default function ucs2length(str) { - var length = 0, - len = str.length, - pos = 0, - value +export default function ucs2length(str: string): number { + let length = 0 + let len = str.length + let pos = 0 + let value: number while (pos < len) { length++ value = str.charCodeAt(pos++) if (value >= 0xd800 && value <= 0xdbff && pos < len) { // high surrogate, and there is a next character value = str.charCodeAt(pos) - if ((value & 0xfc00) == 0xdc00) pos++ // low surrogate + if ((value & 0xfc00) === 0xdc00) pos++ // low surrogate } } return length From de3d04728744102da62cb1e4dbc963c14010bfad Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 10 Aug 2020 23:11:33 +0100 Subject: [PATCH 031/322] refactor: error classes to typescript --- lib/ajv.js | 6 +++--- lib/compile/async.js | 2 +- lib/compile/error_classes.js | 28 ---------------------------- lib/compile/error_classes.ts | 35 +++++++++++++++++++++++++++++++++++ lib/compile/index.js | 6 +++--- lib/compile/resolve.js | 11 ++++++----- lib/compile/ucs2length.ts | 2 +- 7 files changed, 49 insertions(+), 41 deletions(-) delete mode 100644 lib/compile/error_classes.js create mode 100644 lib/compile/error_classes.ts diff --git a/lib/ajv.js b/lib/ajv.js index c5b071d3b7..bf81f3ca0e 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -1,5 +1,6 @@ import SchemaObject from "./compile/schema_obj" import Cache from "./cache" +import {ValidationError, MissingRefError} from "./compile/error_classes" var compileSchema = require("./compile"), resolve = require("./compile/resolve"), @@ -31,9 +32,8 @@ Ajv.prototype.getKeyword = customKeyword.getKeyword Ajv.prototype.removeKeyword = customKeyword.removeKeyword Ajv.prototype.validateKeyword = customKeyword.validateKeyword -var errorClasses = require("./compile/error_classes") -Ajv.ValidationError = errorClasses.Validation -Ajv.MissingRefError = errorClasses.MissingRef +Ajv.ValidationError = ValidationError +Ajv.MissingRefError = MissingRefError Ajv.$dataMetaSchema = $dataMetaSchema var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" diff --git a/lib/compile/async.js b/lib/compile/async.js index c223825ec2..7a79cc0368 100644 --- a/lib/compile/async.js +++ b/lib/compile/async.js @@ -1,4 +1,4 @@ -var MissingRefError = require("./error_classes").MissingRef +import {MissingRefError} from "./error_classes" module.exports = compileAsync diff --git a/lib/compile/error_classes.js b/lib/compile/error_classes.js deleted file mode 100644 index 32cdf81e95..0000000000 --- a/lib/compile/error_classes.js +++ /dev/null @@ -1,28 +0,0 @@ -var resolve = require("./resolve") - -module.exports = { - Validation: errorSubclass(ValidationError), - MissingRef: errorSubclass(MissingRefError), -} - -function ValidationError(errors) { - this.message = "validation failed" - this.errors = errors - this.ajv = this.validation = true -} - -MissingRefError.message = function (baseId, ref) { - return "can't resolve reference " + ref + " from id " + baseId -} - -function MissingRefError(baseId, ref, message) { - this.message = message || MissingRefError.message(baseId, ref) - this.missingRef = resolve.url(baseId, ref) - this.missingSchema = resolve.normalizeId(resolve.fullPath(this.missingRef)) -} - -function errorSubclass(Subclass) { - Subclass.prototype = Object.create(Error.prototype) - Subclass.prototype.constructor = Subclass - return Subclass -} diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts new file mode 100644 index 0000000000..7bda2f7e16 --- /dev/null +++ b/lib/compile/error_classes.ts @@ -0,0 +1,35 @@ +import {ErrorObject} from "../types" + +const resolve = require("./resolve") + +export class ValidationError extends Error { + errors: ErrorObject[] + ajv: true + validation: true + + constructor(errors: ErrorObject[]) { + super("validation failed") + this.errors = errors + this.ajv = this.validation = true + } +} + +export class MissingRefError extends Error { + missingRef: string + missingSchema: string + + static message(baseId: string, ref: string): string { + return `can't resolve reference ${ref} from id ${baseId}` + } + + constructor(baseId: string, ref: string, message?: string) { + super(message || MissingRefError.message(baseId, ref)) + this.missingRef = resolve.url(baseId, ref) + this.missingSchema = resolve.normalizeId(resolve.fullPath(this.missingRef)) + } +} + +module.exports = { + ValidationError, + MissingRefError, +} diff --git a/lib/compile/index.js b/lib/compile/index.js index d3df7b4eb3..d4bf8f4fa1 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,11 +1,11 @@ import Scope from "./scope" import {toQuotedString} from "./util" +import {MissingRefError} from "./error_classes" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") var resolve = require("./resolve"), util = require("./util"), - errorClasses = require("./error_classes"), stableStringify = require("fast-json-stable-stringify") var validateGenerator = require("../dotjs/validate") @@ -15,7 +15,7 @@ var validateGenerator = require("../dotjs/validate") */ // this error is thrown by async schemas to return validation errors via exception -var ValidationError = errorClasses.Validation +const ValidationError = require("./error_classes").ValidationError module.exports = compile @@ -94,7 +94,7 @@ function compile(schema, root, localRefs, baseId) { errSchemaPath: "#", errorPath: '""', scope: new Scope(), - MissingRefError: errorClasses.MissingRef, + MissingRefError, RULES: RULES, validate: validateGenerator, util: util, diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index b5c40801d1..e884f801ee 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -1,11 +1,10 @@ import SchemaObject from "./schema_obj" import {toHash, escapeFragment, unescapeFragment} from "./util" + var URI = require("uri-js"), equal = require("fast-deep-equal"), traverse = require("json-schema-traverse") -module.exports = resolve - resolve.normalizeId = normalizeId resolve.fullPath = getFullPath resolve.url = resolveUrl @@ -212,7 +211,7 @@ function countKeys(schema) { return count } -function getFullPath(id, normalize) { +export function getFullPath(id, normalize) { if (normalize !== false) id = normalizeId(id) var p = URI.parse(id) return _getFullPath(p) @@ -223,11 +222,11 @@ function _getFullPath(p) { } var TRAILING_SLASH_HASH = /#\/?$/ -function normalizeId(id) { +export function normalizeId(id) { return id ? id.replace(TRAILING_SLASH_HASH, "") : "" } -function resolveUrl(baseId, id) { +export function resolveUrl(baseId, id) { id = normalizeId(id) return URI.resolve(baseId, id) } @@ -283,3 +282,5 @@ function resolveIds(schema) { return localRefs } + +module.exports = resolve diff --git a/lib/compile/ucs2length.ts b/lib/compile/ucs2length.ts index c877648569..7a5e356e68 100644 --- a/lib/compile/ucs2length.ts +++ b/lib/compile/ucs2length.ts @@ -3,8 +3,8 @@ module.exports = ucs2length export default function ucs2length(str: string): number { + const len = str.length let length = 0 - let len = str.length let pos = 0 let value: number while (pos < len) { From 08d1224470496abd8f2d1e9668ed7d813da0b7e1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 09:53:43 +0100 Subject: [PATCH 032/322] refactor: schemaType --- lib/vocabularies/validation/limit.ts | 6 ++---- lib/vocabularies/validation/limitItems.ts | 6 ++---- lib/vocabularies/validation/limitLength.ts | 6 ++---- lib/vocabularies/validation/limitProperties.ts | 6 ++---- lib/vocabularies/validation/multipleOf.ts | 6 ++---- lib/vocabularies/validation/pattern.ts | 6 ++---- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 83ac742e56..ee11d57800 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -8,15 +8,13 @@ const OPS = { exclusiveMinimum: {fail: "<=", ok: ">"}, } -const SCH_TYPE = "number" - const def: KeywordDefinition = { keyword: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], type: "number", - schemaType: SCH_TYPE, + schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode}) { - const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + data + OPS[keyword].fail + schemaCode + ` || ${data}!==${data}`) }, error: { diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 51c4d2d62b..2e2ae3901f 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,16 +1,14 @@ import {KeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" -const SCH_TYPE = "number" - const def: KeywordDefinition = { keyword: ["maxItems", "minItems"], type: "array", - schemaType: SCH_TYPE, + schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode}) { const op = keyword == "maxItems" ? ">" : "<" - const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + `${data}.length` + op + schemaCode) }, error: { diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index ff70460a51..2914c5f589 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,16 +1,14 @@ import {KeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" -const SCH_TYPE = "number" - const def: KeywordDefinition = { keyword: ["maxLength", "minLength"], type: "string", - schemaType: SCH_TYPE, + schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode, opts}) { const op = keyword == "maxLength" ? ">" : "<" - const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) const len = opts.unicode === false ? `${data}.length` : `ucs2length(${data})` fail(dnt + len + op + schemaCode) diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index fa875ac40e..d992da81bd 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,16 +1,14 @@ import {KeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" -const SCH_TYPE = "number" - const def: KeywordDefinition = { keyword: ["maxProperties", "minProperties"], type: "object", - schemaType: SCH_TYPE, + schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode}) { const op = keyword == "maxProperties" ? ">" : "<" - const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + `Object.keys(${data}).length` + op + schemaCode) }, error: { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 5f2be0f30f..9522994868 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,15 +1,13 @@ import {KeywordDefinition} from "../../types" import {appendSchema, dataNotType} from "../util" -const SCH_TYPE = "number" - const def: KeywordDefinition = { keyword: "multipleOf", type: "number", - schemaType: SCH_TYPE, + schemaType: "number", $data: true, code({write, fail, scope, data, $data, schemaCode, opts}) { - const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) const res = scope.getName("res") const prec = opts.multipleOfPrecision const invalid = prec diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 08f7237c11..eea214c4c0 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,15 +1,13 @@ import {KeywordDefinition} from "../../types" import {dataNotType} from "../util" -const SCH_TYPE = "string" - const def: KeywordDefinition = { keyword: "pattern", type: "string", - schemaType: SCH_TYPE, + schemaType: "string", $data: true, code({fail, usePattern, data, $data, schema, schemaCode}) { - const dnt = dataNotType(schemaCode, SCH_TYPE, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) const regExp = $data ? `(new RegExp(${schemaCode}))` : usePattern(schema) fail(dnt + `!${regExp}.test(${data})`) }, From 36f61773bc343681da32557fd1e0181e4f3b4574 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 11:41:31 +0100 Subject: [PATCH 033/322] feat: optimize enum keyword, option loopEnum, to typescript --- lib/ajv.js | 1 + lib/compile/rules.js | 2 +- lib/dot/enum.jst | 30 ------------- lib/dot/errors.def | 3 -- lib/dotjs/index.js | 1 - lib/keyword.ts | 7 ++- lib/types.ts | 1 + lib/vocabularies/validation/enum.ts | 66 ++++++++++++++++++++++++++++ lib/vocabularies/validation/index.ts | 1 + 9 files changed, 76 insertions(+), 36 deletions(-) delete mode 100644 lib/dot/enum.jst create mode 100644 lib/vocabularies/validation/enum.ts diff --git a/lib/ajv.js b/lib/ajv.js index bf81f3ca0e..a52501c64e 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -71,6 +71,7 @@ function Ajv(opts) { throw new Error("option schemaId is not supported from v7") opts.loopRequired = opts.loopRequired || Infinity + opts.loopEnum = opts.loopEnum || Infinity if (opts.errorDataPath == "property") opts._errorDataPathProperty = true if (opts.serialize === undefined) opts.serialize = stableStringify this._metaOpts = getMetaSchemaOptions(this) diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 4c43c2935d..9af452fe3e 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -19,7 +19,7 @@ module.exports = function rules() { {properties: ["additionalProperties", "patternProperties"]}, ], }, - {rules: ["$ref", "enum", "not", "anyOf", "oneOf", "allOf", "if"]}, + {rules: ["$ref", "not", "anyOf", "oneOf", "allOf", "if"]}, ] var ALL = ["type", "$comment"] diff --git a/lib/dot/enum.jst b/lib/dot/enum.jst deleted file mode 100644 index 357c2e8c08..0000000000 --- a/lib/dot/enum.jst +++ /dev/null @@ -1,30 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{ - var $i = 'i' + $lvl - , $vSchema = 'schema' + $lvl; -}} - -{{? !$isData }} - var {{=$vSchema}} = validate.schema{{=$schemaPath}}; -{{?}} -var {{=$valid}}; - -{{?$isData}}{{# def.check$dataIsArray }}{{?}} - -{{=$valid}} = false; - -for (var {{=$i}}=0; {{=$i}}<{{=$vSchema}}.length; {{=$i}}++) - if (equal({{=$data}}, {{=$vSchema}}[{{=$i}}])) { - {{=$valid}} = true; - break; - } - -{{? $isData }} } {{?}} - -{{# def.checkError:'enum' }} - -{{? $breakOnError }} else { {{?}} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 614e3c53d7..7f018caaaa 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -96,7 +96,6 @@ anyOf: "'should match some schema in anyOf'", contains: "'should contain a valid item'", dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", - 'enum': "'should be equal to one of the allowed values'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", not: "'should NOT be valid'", @@ -124,7 +123,6 @@ anyOf: "validate.schema{{=$schemaPath}}", contains: "validate.schema{{=$schemaPath}}", dependencies: "validate.schema{{=$schemaPath}}", - 'enum': "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", 'if': "validate.schema{{=$schemaPath}}", not: "validate.schema{{=$schemaPath}}", @@ -151,7 +149,6 @@ anyOf: "{}", contains: "{}", dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{= it.util.escapeQuotes($deps.length==1 ? $deps[0] : $deps.join(\", \")) }}' }", - 'enum': "{ allowedValues: schema{{=$lvl}} }", format: "{ format: {{#def.schemaValueQS}} }", 'if': "{ failingKeyword: {{=$ifClause}} }", not: "{}", diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 6604e908ab..a1fff68f09 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -8,7 +8,6 @@ module.exports = { $comment: require("./comment"), contains: require("./contains"), dependencies: require("./dependencies"), - enum: require("./enum"), format: require("./format"), if: require("./if"), items: require("./items"), diff --git a/lib/keyword.ts b/lib/keyword.ts index 3f25795733..296263a8b2 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -153,7 +153,12 @@ function ruleCode( it.dataPathArr )};` } else { - if (schemaType && typeof schema !== schemaType) + if ( + schemaType && + !(schemaType === "array" + ? Array.isArray(schema) + : typeof schema === schemaType) + ) throw new Error(`${keyword} must be ${schemaType}`) schemaCode = schemaRefOrVal() } diff --git a/lib/types.ts b/lib/types.ts index c5a7e63a7d..76c50de23e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -33,6 +33,7 @@ interface Options { inlineRefs?: boolean | number passContext?: boolean loopRequired?: number + loopEnum?: number ownProperties?: boolean multipleOfPrecision?: boolean | number errorDataPath?: string diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts new file mode 100644 index 0000000000..a8743db783 --- /dev/null +++ b/lib/vocabularies/validation/enum.ts @@ -0,0 +1,66 @@ +import {KeywordDefinition} from "../../types" +import {quotedString} from "../util" + +const def: KeywordDefinition = { + keyword: "enum", + schemaType: "array", + $data: true, + code({write, fail, scope, data, $data, schema, schemaCode, opts}) { + const valid = scope.getName("valid") + if ($data) { + write(`let ${valid};`) + // TODO trim whitespace + write( + `if (${schemaCode} === undefined) ${valid} = true; + else { + ${valid} = false; + if (Array.isArray(${schemaCode})) {` + ) + loopEnum(schemaCode) + write("}}") + fail(`!${valid}`) + } else { + if (schema.length === 0) throw new Error("enum must have non-empty array") + const vSchema = scope.getName("schema") + write(`const ${vSchema} = ${schemaCode};`) + if (schema.length > (opts.loopEnum as number)) { + write(`let ${valid} = false;`) + loopEnum(vSchema) + fail(`!${valid}`) + } else { + let cond: string = schema.reduce( + (c, _, i) => (c += (c && "||") + equalCode(vSchema, i)), + "" + ) + fail(`!(${cond})`) + } + } + + function loopEnum(sch: string): void { + // TODO trim whitespace + write( + `for (const v of ${sch}) { + if (equal(${data}, v)) { + ${valid} = true; + break; + } + }` + ) + } + + function equalCode(vSchema: string, i: number): string { + let sch = schema[i] + if (sch && typeof sch === "object") { + return `equal(${data}, ${vSchema}[${i}])` + } + if (typeof sch === "string") sch = quotedString(sch) + return `${data} === ${sch}` + } + }, + error: { + message: () => '"should be equal to one of the allowed values"', + params: ({schemaCode}) => `{allowedValues: ${schemaCode}}`, + }, +} + +module.exports = def diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 95ccda3265..1f2ba75d4a 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -13,6 +13,7 @@ const validation: Vocabulary = [ require("./limitItems"), // any require("./const"), + require("./enum"), ] module.exports = validation From d36783cbde6abacb3adba59f4f9eb1be8c36ded2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 12:05:26 +0100 Subject: [PATCH 034/322] loopEnum: test, docs --- README.md | 2 ++ package.json | 2 +- spec/options/options_code.spec.js | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e4c604098..a10f079aba 100644 --- a/README.md +++ b/README.md @@ -1135,6 +1135,7 @@ Defaults: inlineRefs: true, passContext: false, loopRequired: Infinity, + loopEnum: Infinity, ownProperties: false, multipleOfPrecision: false, errorDataPath: 'object', // deprecated @@ -1237,6 +1238,7 @@ Defaults: - integer number - to limit the maximum number of keywords of the schema that will be inlined. - _passContext_: pass validation context to custom keyword functions. If this option is `true` and you pass some context to the compiled validation function with `validate.call(context, data)`, the `context` will be available as `this` in your custom keywords. By default `this` is Ajv instance. - _loopRequired_: by default `required` keyword is compiled into a single expression (or a sequence of statements in `allErrors` mode). In case of a very large number of properties in this keyword it may result in a very big validation function. Pass integer to set the number of properties above which `required` keyword will be validated in a loop - smaller validation function size but also worse performance. +- _loopEnum_: by default `enum` keyword is compiled into a single expression. In case of a very large number of allowed values it may result in a large validation function. Pass integer to set the number of values above which `enum` keyword will be validated in a loop. - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). - _errorDataPath_ (deprecated): set `dataPath` to point to 'object' (default) or to 'property' when validating keywords `required`, `additionalProperties` and `dependencies`. diff --git a/package.json b/package.json index 9a41d4ebde..7f5a2fe4b1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", "bundle-beautify": "node ./scripts/bundle.js js-beautify", "dot": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", - "tsc": "tsc || true && cp -r lib/refs dist/refs", + "tsc": "tsc && cp -r lib/refs dist/refs", "build": "npm run dot && npm run tsc", "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js index 742b0d1b2b..14ae029f82 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.js @@ -106,4 +106,21 @@ describe("code generation options", function () { return storeContext } }) + + describe("loopEnum option", () => { + it("should use loop if more values than specified", () => { + const ajv1 = new Ajv() + const ajv2 = new Ajv({loopEnum: 2}) + test(ajv1, {enum: ["foo", "bar"]}) + test(ajv2, {enum: ["foo", "bar"]}) + test(ajv1, {enum: ["foo", "bar", "baz"]}) + test(ajv2, {enum: ["foo", "bar", "baz"]}) + + function test(ajv, schema) { + ajv.validate(schema, "foo").should.equal(true) + ajv.validate(schema, "boo").should.equal(false) + ajv.validate(schema, 1).should.equal(false) + } + }) + }) }) From 84f76cddf313eb04838baa2f1547b46dd4370f71 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 14:53:33 +0100 Subject: [PATCH 035/322] refactor(enum): only use "valid" name if it is needed --- lib/vocabularies/validation/enum.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index a8743db783..ee0c743b53 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -6,8 +6,8 @@ const def: KeywordDefinition = { schemaType: "array", $data: true, code({write, fail, scope, data, $data, schema, schemaCode, opts}) { - const valid = scope.getName("valid") if ($data) { + const valid = scope.getName("valid") write(`let ${valid};`) // TODO trim whitespace write( @@ -16,7 +16,7 @@ const def: KeywordDefinition = { ${valid} = false; if (Array.isArray(${schemaCode})) {` ) - loopEnum(schemaCode) + loopEnum(schemaCode, valid) write("}}") fail(`!${valid}`) } else { @@ -24,8 +24,9 @@ const def: KeywordDefinition = { const vSchema = scope.getName("schema") write(`const ${vSchema} = ${schemaCode};`) if (schema.length > (opts.loopEnum as number)) { + const valid = scope.getName("valid") write(`let ${valid} = false;`) - loopEnum(vSchema) + loopEnum(vSchema, valid) fail(`!${valid}`) } else { let cond: string = schema.reduce( @@ -36,7 +37,7 @@ const def: KeywordDefinition = { } } - function loopEnum(sch: string): void { + function loopEnum(sch: string, valid: string): void { // TODO trim whitespace write( `for (const v of ${sch}) { From 24ffbec98302f5aa8ca055f3ef38fe5e7dcd2db1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 15:10:25 +0100 Subject: [PATCH 036/322] style: curly --- .eslintrc.yml | 2 +- lib/ajv.js | 33 +++++++++++++++++++++----------- lib/compile/async.js | 3 ++- lib/compile/index.js | 6 ++++-- lib/compile/resolve.js | 12 ++++++++---- lib/compile/util.ts | 12 ++++++++---- lib/keyword.ts | 15 ++++++++++----- scripts/bundle.js | 3 ++- spec/after_test.js | 3 ++- spec/ajv_options.js | 3 ++- spec/browser_test_suite.js | 3 ++- spec/coercion.spec.js | 3 ++- spec/custom.spec.js | 18 +++++++++++------ spec/options/useDefaults.spec.js | 9 ++++++--- 14 files changed, 83 insertions(+), 42 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index db9cba8912..21456dde72 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -36,7 +36,7 @@ rules: block-scoped-var: error callback-return: error complexity: [error, 16] - curly: [error, multi-or-nest, consistent] + curly: [error, multi-line, consistent] dot-location: [error, property] dot-notation: error id-match: error diff --git a/lib/ajv.js b/lib/ajv.js index a52501c64e..6aae75ef41 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -67,8 +67,9 @@ function Ajv(opts) { this._loadingSchemas = {} this._compilations = [] this.RULES = rules() - if (opts.schemaId !== undefined && opts.schemaId !== "$id") + if (opts.schemaId !== undefined && opts.schemaId !== "$id") { throw new Error("option schemaId is not supported from v7") + } opts.loopRequired = opts.loopRequired || Infinity opts.loopEnum = opts.loopEnum || Infinity @@ -81,8 +82,9 @@ function Ajv(opts) { if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) - if (opts.nullable) + if (opts.nullable) { this.addKeyword("nullable", {metaSchema: {type: "boolean"}}) + } addInitialSchemas(this) opts.format = formatOpt } @@ -133,13 +135,15 @@ function compile(schema, _meta) { */ function addSchema(schema, key, _skipValidation, _meta) { if (Array.isArray(schema)) { - for (var i = 0; i < schema.length; i++) + for (var i = 0; i < schema.length; i++) { this.addSchema(schema[i], undefined, _skipValidation, _meta) + } return this } var id = schema.$id - if (id !== undefined && typeof id != "string") + if (id !== undefined && typeof id != "string") { throw new Error("schema id must be string") + } key = resolve.normalizeId(key || id) checkUnique(this, key) this._schemas[key] = this._addSchema(schema, _skipValidation, _meta, true) @@ -169,8 +173,9 @@ function addMetaSchema(schema, key, skipValidation) { */ function validateSchema(schema, throwOrLogError) { var $schema = schema.$schema - if ($schema !== undefined && typeof $schema != "string") + if ($schema !== undefined && typeof $schema != "string") { throw new Error("$schema must be a string") + } $schema = $schema || this._opts.defaultMeta || defaultMeta(this) if (!$schema) { this.logger.warn("meta-schema not available") @@ -292,8 +297,9 @@ function _removeAllSchemas(self, schemas, regex) { /* @this Ajv */ function _addSchema(schema, skipValidation, meta, shouldAddSchema) { - if (typeof schema != "object" && typeof schema != "boolean") + if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema should be object or boolean") + } var serialize = this._opts.serialize var cacheKey = serialize ? serialize(schema) : schema var cached = this._cache.get(cacheKey) @@ -309,8 +315,9 @@ function _addSchema(schema, skipValidation, meta, shouldAddSchema) { if ( willValidate && !(recursiveMeta = id && id == resolve.normalizeId(schema.$schema)) - ) + ) { this.validateSchema(schema, true) + } var localRefs = resolve.ids.call(this, schema) @@ -418,8 +425,9 @@ function addDefaultMetaSchema(self) { } if (self._opts.meta === false) return var metaSchema = require("./refs/json-schema-draft-07.json") - if (self._opts.$data) + if (self._opts.$data) { metaSchema = $dataMetaSchema(metaSchema, META_SUPPORT_DATA) + } self.addMetaSchema(metaSchema, META_SCHEMA_ID, true) self._refs["http://json-schema.org/schema"] = META_SCHEMA_ID } @@ -446,14 +454,16 @@ function addInitialKeywords(self, keywords, skipValidation) { } function checkUnique(self, id) { - if (self._schemas[id] || self._refs[id]) + if (self._schemas[id] || self._refs[id]) { throw new Error('schema with key or id "' + id + '" already exists') + } } function getMetaSchemaOptions(self) { var metaOpts = {...self._opts} - for (var i = 0; i < META_IGNORE_OPTIONS.length; i++) + for (var i = 0; i < META_IGNORE_OPTIONS.length; i++) { delete metaOpts[META_IGNORE_OPTIONS[i]] + } return metaOpts } @@ -465,8 +475,9 @@ function setLogger(self) { if (logger === undefined) logger = console if ( !(typeof logger == "object" && logger.log && logger.warn && logger.error) - ) + ) { throw new Error("logger must implement log, warn and error methods") + } self.logger = logger } } diff --git a/lib/compile/async.js b/lib/compile/async.js index 7a79cc0368..d248f2dbbf 100644 --- a/lib/compile/async.js +++ b/lib/compile/async.js @@ -15,8 +15,9 @@ function compileAsync(schema, meta, callback) { /* eslint no-shadow: 0 */ /* jshint validthis: true */ var self = this - if (typeof this._opts.loadSchema != "function") + if (typeof this._opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") + } if (typeof meta == "function") { callback = meta diff --git a/lib/compile/index.js b/lib/compile/index.js index d4bf8f4fa1..8d330f9173 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -79,8 +79,9 @@ function compile(schema, root, localRefs, baseId) { function localCompile(_schema, _root, localRefs, baseId) { var isRoot = !_root || (_root && _root.schema == _schema) - if (_root.schema != root.schema) + if (_root.schema != root.schema) { return compile.call(self, _schema, _root, localRefs, baseId) + } var $async = _schema.$async === true @@ -301,8 +302,9 @@ function compile(schema, root, localRefs, baseId) { if (!validate) return } - if (validate === undefined) + if (validate === undefined) { throw new Error('custom keyword "' + rule.keyword + '"failed to compile') + } var index = customRules.length customRules[index] = validate diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index e884f801ee..f620540093 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -80,8 +80,9 @@ function resolveSchema(root, ref) { refVal = this._schemas[id] if (refVal instanceof SchemaObject) { if (!refVal.validate) this._compile(refVal) - if (id == normalizeId(ref)) + if (id == normalizeId(ref)) { return {schema: refVal, root: root, baseId: baseId} + } root = refVal } else { return @@ -143,8 +144,9 @@ function getJsonPointer(parsedRef, baseId, schema, root) { } } } - if (schema !== undefined && schema !== root.schema) + if (schema !== undefined && schema !== root.schema) { return {schema: schema, root: root, baseId: baseId} + } } var SIMPLE_INLINED = toHash([ @@ -264,12 +266,14 @@ function resolveIds(schema) { var refVal = self._refs[id] if (typeof refVal == "string") refVal = self._refs[refVal] if (refVal && refVal.schema) { - if (!equal(sch, refVal.schema)) + if (!equal(sch, refVal.schema)) { throw new Error('id "' + id + '" resolves to more than one schema') + } } else if (id != normalizeId(fullPath)) { if (id[0] == "#") { - if (localRefs[id] && !equal(sch, localRefs[id])) + if (localRefs[id] && !equal(sch, localRefs[id])) { throw new Error('id "' + id + '" resolves to more than one schema') + } localRefs[id] = sch } else { self._refs[id] = fullPath diff --git a/lib/compile/util.ts b/lib/compile/util.ts index f7b0158029..15d83e4bde 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -59,8 +59,9 @@ export function checkDataTypes( data: string, strictNumbers: boolean ): string { - if (dataTypes.length === 1) + if (dataTypes.length === 1) { return checkDataType(dataTypes[0], data, strictNumbers, true) + } let code = "" const types = toHash(dataTypes) if (types.array && types.object) { @@ -71,8 +72,9 @@ export function checkDataTypes( delete types.object } if (types.number) delete types.integer - for (const t in types) + for (const t in types) { code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, true) + } return code } @@ -84,8 +86,9 @@ export function coerceToTypes( if (Array.isArray(dataTypes)) { const types: string[] = [] for (const t of dataTypes) { - if (COERCE_TYPES[t] || (optionCoerceTypes === "array" && t === "array")) + if (COERCE_TYPES[t] || (optionCoerceTypes === "array" && t === "array")) { types.push(t) + } } if (types.length) return types return @@ -196,8 +199,9 @@ export function getData($data: string, lvl: number, paths: string[]): string { let jsonPointer, data if ($data === "") return "rootData" if ($data[0] === "/") { - if (!JSON_POINTER.test($data)) + if (!JSON_POINTER.test($data)) { throw new Error("Invalid JSON-pointer: " + $data) + } jsonPointer = $data data = "rootData" } else { diff --git a/lib/keyword.ts b/lib/keyword.ts index 296263a8b2..027fb617c0 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -32,8 +32,9 @@ export function addVocabulary( ) } if (Array.isArray(def.keyword)) { - for (const keyword of def.keyword) + for (const keyword of def.keyword) { this.addKeyword(keyword, def, _skipValidation) + } } else { this.addKeyword(def.keyword, def, _skipValidation) } @@ -58,19 +59,22 @@ export function addKeyword( // TODO return type Ajv /* eslint no-shadow: 0 */ var RULES = this.RULES - if (RULES.keywords[keyword]) + if (RULES.keywords[keyword]) { throw new Error("Keyword " + keyword + " is already defined") + } - if (!IDENTIFIER.test(keyword)) + if (!IDENTIFIER.test(keyword)) { throw new Error("Keyword " + keyword + " is not a valid identifier") + } if (definition) { if (!_skipValidation) this.validateKeyword(definition, true) var dataType = definition.type if (Array.isArray(dataType)) { - for (var i = 0; i < dataType.length; i++) + for (var i = 0; i < dataType.length; i++) { _addRule(keyword, dataType[i], definition) + } } else { _addRule(keyword, dataType, definition) } @@ -158,8 +162,9 @@ function ruleCode( !(schemaType === "array" ? Array.isArray(schema) : typeof schema === schemaType) - ) + ) { throw new Error(`${keyword} must be ${schemaType}`) + } schemaCode = schemaRefOrVal() } const data = "data" + (it.dataLevel || "") diff --git a/scripts/bundle.js b/scripts/bundle.js index 24b2ceefa5..93f941617a 100644 --- a/scripts/bundle.js +++ b/scripts/bundle.js @@ -62,7 +62,8 @@ browserify(bOpts) if (result.map) fs.writeFileSync(outputFile + ".min.js.map", result.map) if (standalone) fs.writeFileSync(outputFile + ".bundle.js", buf) if (result.warnings) { - for (var j = 0, jl = result.warnings.length; j < jl; ++j) + for (var j = 0, jl = result.warnings.length; j < jl; ++j) { console.warn("UglifyJS warning:", result.warnings[j]) + } } }) diff --git a/spec/after_test.js b/spec/after_test.js index 12c552067a..7a12f0e99b 100644 --- a/spec/after_test.js +++ b/spec/after_test.js @@ -13,7 +13,8 @@ exports.each = function (res) { should.equal(res.errors, null) } else { res.errors.should.be.an("array") - for (var i = 0; i < res.errors.length; i++) + for (var i = 0; i < res.errors.length; i++) { res.errors[i].should.be.an("object") + } } } diff --git a/spec/ajv_options.js b/spec/ajv_options.js index 711fa9678d..0131ca6a15 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -14,7 +14,8 @@ var options = fullTest } : {allErrors: true} -if (fullTest && !isBrowser) +if (fullTest && !isBrowser) { options.processCode = require("js-beautify").js_beautify +} module.exports = options diff --git a/spec/browser_test_suite.js b/spec/browser_test_suite.js index f7016eb0e2..b2fa375789 100644 --- a/spec/browser_test_suite.js +++ b/spec/browser_test_suite.js @@ -2,8 +2,9 @@ module.exports = function (suite) { suite.forEach(function (file) { - if (file.name.indexOf("optional/format") == 0) + if (file.name.indexOf("optional/format") == 0) { file.name = file.name.replace("optional/", "") + } file.test = file.module }) return suite diff --git a/spec/coercion.spec.js b/spec/coercion.spec.js index 64406f7ee4..36d12a63cb 100644 --- a/spec/coercion.spec.js +++ b/spec/coercion.spec.js @@ -196,8 +196,9 @@ describe("Type coercion", function () { ) { instances.forEach(function (_ajv) { var valid = _ajv.validate(schema, test.from) - if (valid !== canCoerce) + if (valid !== canCoerce) { console.log(toType, ".", fromType, test, schema, ajv.errors) + } valid.should.equal(canCoerce) }) }) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index a6d2b449ee..6bf04de505 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -23,8 +23,9 @@ describe("Custom keywords", function () { testEvenKeyword({type: "number", validate: validateEven}) function validateEven(schema, data) { - if (typeof schema != "boolean") + if (typeof schema != "boolean") { throw new Error('The value of "even" keyword must be boolean') + } return data % 2 ? !schema : schema } }) @@ -122,8 +123,9 @@ describe("Custom keywords", function () { shouldBeInvalidSchema({"x-even": "not_boolean"}) function compileEven(schema) { - if (typeof schema != "boolean") + if (typeof schema != "boolean") { throw new Error('The value of "even" keyword must be boolean') + } return schema ? isEven : isOdd } @@ -357,8 +359,9 @@ describe("Custom keywords", function () { ) function macroDeepProperties(_schema) { - if (typeof _schema != "object") + if (typeof _schema != "object") { throw new Error("schema of deepProperty should be an object") + } var expanded = [] @@ -628,8 +631,9 @@ describe("Custom keywords", function () { function compileEven(schema) { compileCalled = true - if (typeof schema != "boolean") + if (typeof schema != "boolean") { throw new Error('The value of "even" keyword must be boolean') + } return schema ? isEven : isOdd } @@ -884,11 +888,13 @@ describe("Custom keywords", function () { shouldBeValid(validate, "abc") shouldBeInvalid(validate, 1.99, numErrors) - if (customErrors) + if (customErrors) { shouldBeRangeError(validate.errors[0], "", "#/x-range", ">=", 2) + } shouldBeInvalid(validate, 4.01, numErrors) - if (customErrors) + if (customErrors) { shouldBeRangeError(validate.errors[0], "", "#/x-range", "<=", 4) + } schema = { properties: { diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index b660a0c5ba..b73676701a 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -155,10 +155,13 @@ describe("useDefaults options", function () { var data2 = {} validate(data2).should.equal(true) - if (useDefaultsMode == "reference") + if (useDefaultsMode == "reference") { data2.items.should.eql(["a-default", "another-value"]) - else if (useDefaultsMode == "value") data2.items.should.eql(["a-default"]) - else throw new Error("unknown useDefaults mode") + } else if (useDefaultsMode == "value") { + data2.items.should.eql(["a-default"]) + } else { + throw new Error("unknown useDefaults mode") + } } }) From 99c2b286a7fcf82ed0484b45f15371f505e94ff7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 16:23:01 +0100 Subject: [PATCH 037/322] style: prefer-arrow-callback --- .eslintrc.yml | 2 +- lib/compile/async.js | 14 +- lib/compile/index.js | 6 +- lib/compile/resolve.js | 76 +++--- lib/compile/rules.js | 6 +- scripts/bundle.js | 2 +- scripts/compile-dots.js | 4 +- spec/ajv.spec.js | 164 ++++++------ spec/ajv_async_instances.js | 2 +- spec/async.spec.js | 94 ++++--- spec/async_validate.spec.js | 84 +++--- spec/boolean.spec.js | 124 ++++----- spec/browser_test_suite.js | 2 +- spec/coercion.spec.js | 79 +++--- spec/custom.spec.js | 240 +++++++++--------- spec/errors.spec.js | 130 +++++----- ...1_addKeyword_and_schema_without_id.spec.js | 4 +- ...1_allErrors_custom_keyword_skipped.spec.js | 6 +- spec/issues/182_nan_validation.spec.js | 8 +- .../204_options_schemas_data_together.spec.js | 4 +- spec/issues/210_mutual_recur_frags.spec.js | 6 +- .../240_mutual_recur_frags_common_ref.spec.js | 6 +- .../259_validate_meta_against_itself.spec.js | 4 +- .../273_error_schemaPath_refd_schema.spec.js | 4 +- .../342_uniqueItems_non-json_objects.spec.js | 10 +- .../485_type_validation_priority.spec.js | 8 +- spec/issues/50_refs_with_definitions.spec.js | 4 +- .../521_wrong_warning_id_property.spec.js | 6 +- .../533_missing_ref_error_when_ignore.spec.js | 8 +- ...3_removeAdditional_to_remove_proto.spec.js | 4 +- .../768_passContext_recursive_ref.spec.js | 36 +-- spec/issues/8_shared_refs.spec.js | 4 +- ...5_removeAdditional_custom_keywords.spec.js | 4 +- spec/json-schema.spec.js | 2 +- spec/options/comment.spec.js | 18 +- spec/options/meta_validateSchema.spec.js | 30 +-- spec/options/nullable.spec.js | 16 +- spec/options/options_add_schemas.spec.js | 44 ++-- spec/options/options_code.spec.js | 36 ++- spec/options/options_refs.spec.js | 34 +-- spec/options/options_reporting.spec.js | 36 +-- spec/options/options_validation.spec.js | 26 +- spec/options/ownProperties.spec.js | 24 +- spec/options/removeAdditional.spec.js | 10 +- spec/options/schemaId.spec.js | 10 +- spec/options/strictDefaults.spec.js | 42 +-- spec/options/strictKeywords.spec.js | 26 +- spec/options/strictNumbers.spec.js | 16 +- spec/options/unknownFormats.spec.js | 24 +- spec/options/useDefaults.spec.js | 26 +- spec/resolve.spec.js | 52 ++-- 51 files changed, 797 insertions(+), 830 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 21456dde72..8229515a87 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -60,6 +60,7 @@ rules: no-trailing-spaces: error no-undef-init: error no-use-before-define: [error, nofunc] + prefer-arrow-callback: error prefer-const: error radix: error semi: 0 @@ -69,4 +70,3 @@ rules: no-void: error # eqeqeq: [error, smart] # no-var: error - # prefer-arrow-callback: error diff --git a/lib/compile/async.js b/lib/compile/async.js index d248f2dbbf..697eb2abbe 100644 --- a/lib/compile/async.js +++ b/lib/compile/async.js @@ -24,15 +24,13 @@ function compileAsync(schema, meta, callback) { meta = undefined } - var p = loadMetaSchemaOf(schema).then(function () { + var p = loadMetaSchemaOf(schema).then(() => { var schemaObj = self._addSchema(schema, undefined, meta) return schemaObj.validate || _compileAsync(schemaObj) }) if (callback) { - p.then(function (v) { - callback(null, v) - }, callback) + p.then((v) => callback(null, v), callback) } return p @@ -71,16 +69,14 @@ function compileAsync(schema, meta, callback) { } return schemaPromise - .then(function (sch) { + .then((sch) => { if (!added(ref)) { - return loadMetaSchemaOf(sch).then(function () { + return loadMetaSchemaOf(sch).then(() => { if (!added(ref)) self.addSchema(sch, ref, undefined, meta) }) } }) - .then(function () { - return _compileAsync(schemaObj) - }) + .then(() => _compileAsync(schemaObj)) function removePromise() { delete self._loadingSchemas[ref] diff --git a/lib/compile/index.js b/lib/compile/index.js index 8d330f9173..8f6ad6c14a 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -263,9 +263,9 @@ function compile(schema, root, localRefs, baseId) { var deps = rule.definition.dependencies if ( deps && - !deps.every(function (keyword) { - return Object.prototype.hasOwnProperty.call(parentSchema, keyword) - }) + !deps.every((keyword) => + Object.prototype.hasOwnProperty.call(parentSchema, keyword) + ) ) { throw new Error( "parent schema must have all required keywords: " + deps.join(",") diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index f620540093..0db6153a89 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -241,48 +241,54 @@ function resolveIds(schema) { var localRefs = {} var self = this - traverse(schema, {allKeys: true}, function ( - sch, - jsonPtr, - rootSchema, - parentJsonPtr, - parentKeyword, - parentSchema, - keyIndex - ) { - if (jsonPtr === "") return - var id = sch.$id - var baseId = baseIds[parentJsonPtr] - var fullPath = fullPaths[parentJsonPtr] + "/" + parentKeyword - if (keyIndex !== undefined) { - fullPath += - "/" + - (typeof keyIndex == "number" ? keyIndex : escapeFragment(keyIndex)) - } + traverse( + schema, + {allKeys: true}, + ( + sch, + jsonPtr, + rootSchema, + parentJsonPtr, + parentKeyword, + parentSchema, + keyIndex + ) => { + if (jsonPtr === "") return + var id = sch.$id + var baseId = baseIds[parentJsonPtr] + var fullPath = fullPaths[parentJsonPtr] + "/" + parentKeyword + if (keyIndex !== undefined) { + fullPath += + "/" + + (typeof keyIndex == "number" ? keyIndex : escapeFragment(keyIndex)) + } - if (typeof id == "string") { - id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id) + if (typeof id == "string") { + id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id) - var refVal = self._refs[id] - if (typeof refVal == "string") refVal = self._refs[refVal] - if (refVal && refVal.schema) { - if (!equal(sch, refVal.schema)) { - throw new Error('id "' + id + '" resolves to more than one schema') - } - } else if (id != normalizeId(fullPath)) { - if (id[0] == "#") { - if (localRefs[id] && !equal(sch, localRefs[id])) { + var refVal = self._refs[id] + if (typeof refVal == "string") refVal = self._refs[refVal] + if (refVal && refVal.schema) { + if (!equal(sch, refVal.schema)) { throw new Error('id "' + id + '" resolves to more than one schema') } - localRefs[id] = sch - } else { - self._refs[id] = fullPath + } else if (id != normalizeId(fullPath)) { + if (id[0] == "#") { + if (localRefs[id] && !equal(sch, localRefs[id])) { + throw new Error( + 'id "' + id + '" resolves to more than one schema' + ) + } + localRefs[id] = sch + } else { + self._refs[id] = fullPath + } } } + baseIds[jsonPtr] = baseId + fullPaths[jsonPtr] = fullPath } - baseIds[jsonPtr] = baseId - fullPaths[jsonPtr] = fullPath - }) + ) return localRefs } diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 9af452fe3e..574ff37a9a 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -54,14 +54,14 @@ module.exports = function rules() { RULES.all = toHash(ALL) RULES.types = toHash(TYPES) - RULES.forEach(function (group) { - group.rules = group.rules.map(function (keyword) { + RULES.forEach((group) => { + group.rules = group.rules.map((keyword) => { var implKeywords if (typeof keyword == "object") { var key = Object.keys(keyword)[0] implKeywords = keyword[key] keyword = key - implKeywords.forEach(function (k) { + implKeywords.forEach((k) => { ALL.push(k) RULES.all[k] = true }) diff --git a/scripts/bundle.js b/scripts/bundle.js index 93f941617a..8325ec848d 100644 --- a/scripts/bundle.js +++ b/scripts/bundle.js @@ -22,7 +22,7 @@ if (standalone) bOpts.standalone = standalone browserify(bOpts) .require(path.join(packageDir, json.main), {expose: json.name}) - .bundle(function (err, buf) { + .bundle((err, buf) => { if (err) { console.error("browserify error:", err) process.exit(1) diff --git a/scripts/compile-dots.js b/scripts/compile-dots.js index e1e8babb3e..ccb92a39ce 100644 --- a/scripts/compile-dots.js +++ b/scripts/compile-dots.js @@ -11,7 +11,7 @@ var defsRootPath = process.argv[2] || path.join(__dirname, "../lib") var defs = {} var defFiles = glob.sync("./dot/**/*.def", {cwd: defsRootPath}) -defFiles.forEach(function (f) { +defFiles.forEach((f) => { var name = path.basename(f, ".def") defs[name] = fs.readFileSync(path.join(defsRootPath, f)) }) @@ -43,7 +43,7 @@ var VARS = [ "$validate", ] -files.forEach(function (f) { +files.forEach((f) => { var keyword = path.basename(f, ".jst") var targetPath = path.join(dotjsPath, keyword + ".js") var template = fs.readFileSync(path.join(filesRootPath, f)) diff --git a/spec/ajv.spec.js b/spec/ajv.spec.js index c5842f6471..07ca56c74f 100644 --- a/spec/ajv.spec.js +++ b/spec/ajv.spec.js @@ -4,19 +4,19 @@ var Ajv = require("./ajv"), should = require("./chai").should(), stableStringify = require("fast-json-stable-stringify") -describe("Ajv", function () { +describe("Ajv", () => { var ajv - beforeEach(function () { + beforeEach(() => { ajv = new Ajv() }) - it("should create instance", function () { + it("should create instance", () => { ajv.should.be.instanceof(Ajv) }) - describe("compile method", function () { - it("should compile schema and return validating function", function () { + describe("compile method", () => { + it("should compile schema and return validating function", () => { var validate = ajv.compile({type: "integer"}) validate.should.be.a("function") validate(1).should.equal(true) @@ -24,7 +24,7 @@ describe("Ajv", function () { validate("1").should.equal(false) }) - it("should cache compiled functions for the same schema", function () { + it("should cache compiled functions for the same schema", () => { var v1 = ajv.compile({ $id: "//e.com/int.json", type: "integer", @@ -38,20 +38,20 @@ describe("Ajv", function () { v1.should.equal(v2) }) - it("should throw if different schema has the same id", function () { + it("should throw if different schema has the same id", () => { ajv.compile({$id: "//e.com/int.json", type: "integer"}) - should.throw(function () { + should.throw(() => { ajv.compile({$id: "//e.com/int.json", type: "integer", minimum: 1}) }) }) - it("should throw if invalid schema is compiled", function () { - should.throw(function () { + it("should throw if invalid schema is compiled", () => { + should.throw(() => { ajv.compile({type: null}) }) }) - it("should throw if compiled schema has an invalid JavaScript code", function () { + it("should throw if compiled schema has an invalid JavaScript code", () => { ajv.addKeyword("even", {inline: badEvenCode}) var schema = {even: true} var validate = ajv.compile(schema) @@ -59,7 +59,7 @@ describe("Ajv", function () { validate(3).should.equal(false) schema = {even: false} - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) @@ -70,15 +70,15 @@ describe("Ajv", function () { }) }) - describe("validate method", function () { - it("should compile schema and validate data against it", function () { + describe("validate method", () => { + it("should compile schema and validate data against it", () => { ajv.validate({type: "integer"}, 1).should.equal(true) ajv.validate({type: "integer"}, "1").should.equal(false) ajv.validate({type: "string"}, "a").should.equal(true) ajv.validate({type: "string"}, 1).should.equal(false) }) - it("should validate against previously compiled schema by id (also see addSchema)", function () { + it("should validate against previously compiled schema by id (also see addSchema)", () => { ajv .validate({$id: "//e.com/int.json", type: "integer"}, 1) .should.equal(true) @@ -92,15 +92,15 @@ describe("Ajv", function () { ajv.validate("//e.com/str.json", 1).should.equal(false) }) - it("should throw exception if no schema with ref", function () { + it("should throw exception if no schema with ref", () => { ajv.validate({$id: "integer", type: "integer"}, 1).should.equal(true) ajv.validate("integer", 1).should.equal(true) - should.throw(function () { + should.throw(() => { ajv.validate("string", "foo") }) }) - it("should validate schema fragment by ref", function () { + it("should validate schema fragment by ref", () => { ajv.addSchema({ $id: "http://e.com/types.json", definitions: { @@ -117,7 +117,7 @@ describe("Ajv", function () { .should.equal(false) }) - it("should return schema fragment by id", function () { + it("should return schema fragment by id", () => { ajv.addSchema({ $id: "http://e.com/types.json", definitions: { @@ -131,8 +131,8 @@ describe("Ajv", function () { }) }) - describe("addSchema method", function () { - it("should add and compile schema with key", function () { + describe("addSchema method", () => { + it("should add and compile schema with key", () => { ajv.addSchema({type: "integer"}, "int") var validate = ajv.getSchema("int") validate.should.be.a("function") @@ -144,19 +144,19 @@ describe("Ajv", function () { ajv.validate("int", "1").should.equal(false) }) - it("should add and compile schema without key", function () { + it("should add and compile schema without key", () => { ajv.addSchema({type: "integer"}) ajv.validate("", 1).should.equal(true) ajv.validate("", "1").should.equal(false) }) - it("should add and compile schema with id", function () { + it("should add and compile schema with id", () => { ajv.addSchema({$id: "//e.com/int.json", type: "integer"}) ajv.validate("//e.com/int.json", 1).should.equal(true) ajv.validate("//e.com/int.json", "1").should.equal(false) }) - it("should normalize schema keys and ids", function () { + it("should normalize schema keys and ids", () => { ajv.addSchema({$id: "//e.com/int.json#", type: "integer"}, "int#") ajv.validate("int", 1).should.equal(true) ajv.validate("int", "1").should.equal(false) @@ -168,7 +168,7 @@ describe("Ajv", function () { ajv.validate("//e.com/int.json#/", "1").should.equal(false) }) - it("should add and compile array of schemas with ids", function () { + it("should add and compile array of schemas with ids", () => { ajv.addSchema([ {$id: "//e.com/int.json", type: "integer"}, {$id: "//e.com/str.json", type: "string"}, @@ -188,43 +188,43 @@ describe("Ajv", function () { ajv.validate("//e.com/str.json", 1).should.equal(false) }) - it("should throw on duplicate key", function () { + it("should throw on duplicate key", () => { ajv.addSchema({type: "integer"}, "int") - should.throw(function () { + should.throw(() => { ajv.addSchema({type: "integer", minimum: 1}, "int") }) }) - it("should throw on duplicate normalized key", function () { + it("should throw on duplicate normalized key", () => { ajv.addSchema({type: "number"}, "num") - should.throw(function () { + should.throw(() => { ajv.addSchema({type: "integer"}, "num#") }) - should.throw(function () { + should.throw(() => { ajv.addSchema({type: "integer"}, "num#/") }) }) - it("should allow only one schema without key and id", function () { + it("should allow only one schema without key and id", () => { ajv.addSchema({type: "number"}) - should.throw(function () { + should.throw(() => { ajv.addSchema({type: "integer"}) }) - should.throw(function () { + should.throw(() => { ajv.addSchema({type: "integer"}, "") }) - should.throw(function () { + should.throw(() => { ajv.addSchema({type: "integer"}, "#") }) }) - it("should throw if schema is not an object", function () { - should.throw(function () { + it("should throw if schema is not an object", () => { + should.throw(() => { ajv.addSchema("foo") }) }) - it("should throw if schema id is not a string", function () { + it("should throw if schema id is not a string", () => { try { ajv.addSchema({$id: 1, type: "integer"}) throw new Error("should have throw exception") @@ -233,28 +233,28 @@ describe("Ajv", function () { } }) - it("should return instance of itself", function () { + it("should return instance of itself", () => { var res = ajv.addSchema({type: "integer"}, "int") res.should.equal(ajv) }) }) - describe("getSchema method", function () { - it("should return compiled schema by key", function () { + describe("getSchema method", () => { + it("should return compiled schema by key", () => { ajv.addSchema({type: "integer"}, "int") var validate = ajv.getSchema("int") validate(1).should.equal(true) validate("1").should.equal(false) }) - it("should return compiled schema by id or ref", function () { + it("should return compiled schema by id or ref", () => { ajv.addSchema({$id: "//e.com/int.json", type: "integer"}) var validate = ajv.getSchema("//e.com/int.json") validate(1).should.equal(true) validate("1").should.equal(false) }) - it("should return compiled schema without key or with empty key", function () { + it("should return compiled schema without key or with empty key", () => { ajv.addSchema({type: "integer"}) var validate = ajv.getSchema("") validate(1).should.equal(true) @@ -265,7 +265,7 @@ describe("Ajv", function () { v("1").should.equal(false) }) - it("should return schema fragment by ref", function () { + it("should return schema fragment by ref", () => { ajv.addSchema({ $id: "http://e.com/types.json", definitions: { @@ -279,7 +279,7 @@ describe("Ajv", function () { vInt("1").should.equal(false) }) - it("should return schema fragment by ref with protocol-relative URIs", function () { + it("should return schema fragment by ref with protocol-relative URIs", () => { ajv.addSchema({ $id: "//e.com/types.json", definitions: { @@ -293,7 +293,7 @@ describe("Ajv", function () { vInt("1").should.equal(false) }) - it("should return schema fragment by id", function () { + it("should return schema fragment by id", () => { ajv.addSchema({ $id: "http://e.com/types.json", definitions: { @@ -308,8 +308,8 @@ describe("Ajv", function () { }) }) - describe("removeSchema method", function () { - it("should remove schema by key", function () { + describe("removeSchema method", () => { + it("should remove schema by key", () => { var schema = {type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema, "int") @@ -323,7 +323,7 @@ describe("Ajv", function () { should.not.exist(ajv._cache.get(str)) }) - it("should remove schema by id", function () { + it("should remove schema by id", () => { var schema = {$id: "//e.com/int.json", type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema) @@ -337,7 +337,7 @@ describe("Ajv", function () { should.not.exist(ajv._cache.get(str)) }) - it("should remove schema by schema object", function () { + it("should remove schema by schema object", () => { var schema = {type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema) @@ -346,7 +346,7 @@ describe("Ajv", function () { should.not.exist(ajv._cache.get(str)) }) - it("should remove schema with id by schema object", function () { + it("should remove schema with id by schema object", () => { var schema = {$id: "//e.com/int.json", type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema) @@ -356,14 +356,14 @@ describe("Ajv", function () { should.not.exist(ajv._cache.get(str)) }) - it("should not throw if there is no schema with passed id", function () { + it("should not throw if there is no schema with passed id", () => { should.not.exist(ajv.getSchema("//e.com/int.json")) - should.not.throw(function () { + should.not.throw(() => { ajv.removeSchema("//e.com/int.json") }) }) - it("should remove all schemas but meta-schemas if called without an arguments", function () { + it("should remove all schemas but meta-schemas if called without an arguments", () => { var schema1 = {$id: "//e.com/int.json", type: "integer"}, str1 = stableStringify(schema1) ajv.addSchema(schema1) @@ -379,7 +379,7 @@ describe("Ajv", function () { should.not.exist(ajv._cache.get(str2)) }) - it("should remove all schemas but meta-schemas with key/id matching pattern", function () { + it("should remove all schemas but meta-schemas with key/id matching pattern", () => { var schema1 = {$id: "//e.com/int.json", type: "integer"}, str1 = stableStringify(schema1) ajv.addSchema(schema1) @@ -401,40 +401,36 @@ describe("Ajv", function () { ajv._cache.get(str3).should.be.an("object") }) - it("should return instance of itself", function () { + it("should return instance of itself", () => { var res = ajv.addSchema({type: "integer"}, "int").removeSchema("int") res.should.equal(ajv) }) }) - describe("addFormat method", function () { - it("should add format as regular expression", function () { + describe("addFormat method", () => { + it("should add format as regular expression", () => { ajv.addFormat("identifier", /^[a-z_$][a-z0-9_$]*$/i) testFormat() }) - it("should add format as string", function () { + it("should add format as string", () => { ajv.addFormat("identifier", "^[A-Za-z_$][A-Za-z0-9_$]*$") testFormat() }) - it("should add format as function", function () { - ajv.addFormat("identifier", function (str) { - return /^[a-z_$][a-z0-9_$]*$/i.test(str) - }) + it("should add format as function", () => { + ajv.addFormat("identifier", (str) => /^[a-z_$][a-z0-9_$]*$/i.test(str)) testFormat() }) - it("should add format as object", function () { + it("should add format as object", () => { ajv.addFormat("identifier", { - validate: function (str) { - return /^[a-z_$][a-z0-9_$]*$/i.test(str) - }, + validate: (str) => /^[a-z_$][a-z0-9_$]*$/i.test(str), }) testFormat() }) - it("should return instance of itself", function () { + it("should return instance of itself", () => { var res = ajv.addFormat("identifier", /^[a-z_$][a-z0-9_$]*$/i) res.should.equal(ajv) }) @@ -446,8 +442,8 @@ describe("Ajv", function () { validate(123).should.equal(true) } - describe("formats for number", function () { - it("should validate only numbers", function () { + describe("formats for number", () => { + it("should validate only numbers", () => { ajv.addFormat("positive", { type: "number", validate: function (x) { @@ -464,7 +460,7 @@ describe("Ajv", function () { validate("abc").should.equal(true) }) - it("should validate numbers with format via $data", function () { + it("should validate numbers with format via $data", () => { ajv = new Ajv({$data: true}) ajv.addFormat("positive", { type: "number", @@ -487,8 +483,8 @@ describe("Ajv", function () { }) }) - describe("validateSchema method", function () { - it("should validate schema against meta-schema", function () { + describe("validateSchema method", () => { + it("should validate schema against meta-schema", () => { var valid = ajv.validateSchema({ $schema: "http://json-schema.org/draft-07/schema#", type: "number", @@ -509,8 +505,8 @@ describe("Ajv", function () { ajv.errors[2].keyword.should.equal("anyOf") }) - it("should throw exception if meta-schema is unknown", function () { - should.throw(function () { + it("should throw exception if meta-schema is unknown", () => { + should.throw(() => { ajv.validateSchema({ $schema: "http://example.com/unknown/schema#", type: "number", @@ -518,8 +514,8 @@ describe("Ajv", function () { }) }) - it("should throw exception if $schema is not a string", function () { - should.throw(function () { + it("should throw exception if $schema is not a string", () => { + should.throw(() => { ajv.validateSchema({ $schema: {}, type: "number", @@ -527,43 +523,43 @@ describe("Ajv", function () { }) }) - describe("sub-schema validation outside of definitions during compilation", function () { - it("maximum", function () { + describe("sub-schema validation outside of definitions during compilation", () => { + it("maximum", () => { passValidationThrowCompile({ $ref: "#/foo", foo: {maximum: "bar"}, }) }) - it("exclusiveMaximum", function () { + it("exclusiveMaximum", () => { passValidationThrowCompile({ $ref: "#/foo", foo: {exclusiveMaximum: "bar"}, }) }) - it("maxItems", function () { + it("maxItems", () => { passValidationThrowCompile({ $ref: "#/foo", foo: {maxItems: "bar"}, }) }) - it("maxLength", function () { + it("maxLength", () => { passValidationThrowCompile({ $ref: "#/foo", foo: {maxLength: "bar"}, }) }) - it("maxProperties", function () { + it("maxProperties", () => { passValidationThrowCompile({ $ref: "#/foo", foo: {maxProperties: "bar"}, }) }) - it("multipleOf", function () { + it("multipleOf", () => { passValidationThrowCompile({ $ref: "#/foo", foo: {maxProperties: "bar"}, @@ -572,7 +568,7 @@ describe("Ajv", function () { function passValidationThrowCompile(schema) { ajv.validateSchema(schema).should.equal(true) - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) } diff --git a/spec/ajv_async_instances.js b/spec/ajv_async_instances.js index acc0a66c92..a35f50d8db 100644 --- a/spec/ajv_async_instances.js +++ b/spec/ajv_async_instances.js @@ -17,7 +17,7 @@ function getAjvInstances(opts) { {transpile: true, allErrors: true}, ] - options.forEach(function (_opts) { + options.forEach((_opts) => { Object.assign(_opts, opts) var ajv = getAjv(_opts) if (ajv) instances.push(ajv) diff --git a/spec/async.spec.js b/spec/async.spec.js index f5d7f7bab1..3c31d31f06 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -3,7 +3,7 @@ var Ajv = require("./ajv"), should = require("./chai").should() -describe("compileAsync method", function () { +describe("compileAsync method", () => { var ajv, loadCallCount var SCHEMAS = { @@ -79,19 +79,19 @@ describe("compileAsync method", function () { }, } - beforeEach(function () { + beforeEach(() => { loadCallCount = 0 ajv = new Ajv({loadSchema: loadSchema}) }) - it("should compile schemas loading missing schemas with options.loadSchema function", function () { + it("should compile schemas loading missing schemas with options.loadSchema function", () => { var schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, }, } - return ajv.compileAsync(schema).then(function (validate) { + return ajv.compileAsync(schema).then((validate) => { should.equal(loadCallCount, 2) validate.should.be.a("function") validate({a: {b: 2}}).should.equal(true) @@ -99,14 +99,14 @@ describe("compileAsync method", function () { }) }) - it("should compile schemas loading missing schemas and return function via callback", function (done) { + it("should compile schemas loading missing schemas and return function via callback", (done) => { var schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, }, } - ajv.compileAsync(schema, function (err, validate) { + ajv.compileAsync(schema, (err, validate) => { should.equal(loadCallCount, 2) should.not.exist(err) validate.should.be.a("function") @@ -116,14 +116,14 @@ describe("compileAsync method", function () { }) }) - it("should correctly load schemas when missing reference has JSON path", function () { + it("should correctly load schemas when missing reference has JSON path", () => { var schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json#/properties/b"}, }, } - return ajv.compileAsync(schema).then(function (validate) { + return ajv.compileAsync(schema).then((validate) => { should.equal(loadCallCount, 2) validate.should.be.a("function") validate({a: 2}).should.equal(true) @@ -131,14 +131,14 @@ describe("compileAsync method", function () { }) }) - it("should correctly compile with remote schemas that have mutual references", function () { + it("should correctly compile with remote schemas that have mutual references", () => { var schema = { $id: "http://example.com/root.json", properties: { tree: {$ref: "tree.json"}, }, } - return ajv.compileAsync(schema).then(function (validate) { + return ajv.compileAsync(schema).then((validate) => { validate.should.be.a("function") var validData = { tree: [{name: "a", subtree: [{name: "a.a"}]}, {name: "b"}], @@ -149,14 +149,14 @@ describe("compileAsync method", function () { }) }) - it("should correctly compile with remote schemas that reference the compiled schema", function () { + it("should correctly compile with remote schemas that reference the compiled schema", () => { var schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "recursive.json"}, }, } - return ajv.compileAsync(schema).then(function (validate) { + return ajv.compileAsync(schema).then((validate) => { should.equal(loadCallCount, 1) validate.should.be.a("function") var validData = {a: {b: {a: {b: {}}}}} @@ -166,7 +166,7 @@ describe("compileAsync method", function () { }) }) - it('should resolve reference containing "properties" segment with the same property (issue #220)', function () { + it('should resolve reference containing "properties" segment with the same property (issue #220)', () => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -175,7 +175,7 @@ describe("compileAsync method", function () { }, }, } - return ajv.compileAsync(schema).then(function (validate) { + return ajv.compileAsync(schema).then((validate) => { should.equal(loadCallCount, 2) validate.should.be.a("function") validate({a: "foo"}).should.equal(true) @@ -183,12 +183,12 @@ describe("compileAsync method", function () { }) }) - describe("loading metaschemas (#334)", function () { - it("should load metaschema if not available", function () { + describe("loading metaschemas (#334)", () => { + it("should load metaschema if not available", () => { return test(SCHEMAS["http://example.com/foobar.json"], 1) }) - it("should load metaschema of referenced schema if not available", function () { + it("should load metaschema of referenced schema if not available", () => { return test({$ref: "http://example.com/foobar.json"}, 2) }) @@ -200,7 +200,7 @@ describe("compileAsync method", function () { }, }) - return ajv.compileAsync(schema).then(function (validate) { + return ajv.compileAsync(schema).then((validate) => { should.equal(loadCallCount, expectedLoadCallCount) validate.should.be.a("function") validate("foo").should.equal(true) @@ -209,18 +209,18 @@ describe("compileAsync method", function () { } }) - it("should return compiled schema on the next tick if there are no references (#51)", function () { + it("should return compiled schema on the next tick if there are no references (#51)", () => { var schema = { $id: "http://example.com/int2plus.json", type: "integer", minimum: 2, } var beforeCallback1 - var p1 = ajv.compileAsync(schema).then(function (validate) { + var p1 = ajv.compileAsync(schema).then((validate) => { beforeCallback1.should.equal(true) spec(validate) var beforeCallback2 - var p2 = ajv.compileAsync(schema).then(function (_validate) { + var p2 = ajv.compileAsync(schema).then((_validate) => { beforeCallback2.should.equal(true) spec(_validate) }) @@ -240,7 +240,7 @@ describe("compileAsync method", function () { } }) - it("should queue calls so only one compileAsync executes at a time (#52)", function () { + it("should queue calls so only one compileAsync executes at a time (#52)", () => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -262,26 +262,26 @@ describe("compileAsync method", function () { } }) - it("should throw exception if loadSchema is not passed", function (done) { + it("should throw exception if loadSchema is not passed", (done) => { var schema = { $id: "http://example.com/int2plus.json", type: "integer", minimum: 2, } ajv = new Ajv() - should.throw(function () { - ajv.compileAsync(schema, function () { + should.throw(() => { + ajv.compileAsync(schema, () => { done(new Error("it should have thrown exception")) }) }) - setTimeout(function () { + setTimeout(() => { // function is needed for the test to pass in Firefox 4 done() }) }) - describe("should return error via callback", function () { - it("if passed schema is invalid", function (done) { + describe("should return error via callback", () => { + it("if passed schema is invalid", (done) => { var invalidSchema = { $id: "http://example.com/int2plus.json", type: "integer", @@ -290,7 +290,7 @@ describe("compileAsync method", function () { ajv.compileAsync(invalidSchema, shouldFail(done)) }) - it("if loaded schema is invalid", function (done) { + it("if loaded schema is invalid", (done) => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -300,7 +300,7 @@ describe("compileAsync method", function () { ajv.compileAsync(schema, shouldFail(done)) }) - it("if required schema is loaded but the reference cannot be resolved", function (done) { + it("if required schema is loaded but the reference cannot be resolved", (done) => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -310,7 +310,7 @@ describe("compileAsync method", function () { ajv.compileAsync(schema, shouldFail(done)) }) - it("if loadSchema returned error", function (done) { + it("if loadSchema returned error", (done) => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -325,7 +325,7 @@ describe("compileAsync method", function () { } }) - it("if schema compilation throws some other exception", function (done) { + it("if schema compilation throws some other exception", (done) => { ajv.addKeyword("badkeyword", {compile: badCompile}) var schema = {badkeyword: true} ajv.compileAsync(schema, shouldFail(done)) @@ -336,7 +336,7 @@ describe("compileAsync method", function () { }) function shouldFail(done) { - return function (err, validate) { + return (err, validate) => { should.exist(err) should.not.exist(validate) done() @@ -344,8 +344,8 @@ describe("compileAsync method", function () { } }) - describe("should return error via promise", function () { - it("if passed schema is invalid", function () { + describe("should return error via promise", () => { + it("if passed schema is invalid", () => { var invalidSchema = { $id: "http://example.com/int2plus.json", type: "integer", @@ -354,7 +354,7 @@ describe("compileAsync method", function () { return shouldReject(ajv.compileAsync(invalidSchema)) }) - it("if loaded schema is invalid", function () { + it("if loaded schema is invalid", () => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -364,7 +364,7 @@ describe("compileAsync method", function () { return shouldReject(ajv.compileAsync(schema)) }) - it("if required schema is loaded but the reference cannot be resolved", function () { + it("if required schema is loaded but the reference cannot be resolved", () => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -374,7 +374,7 @@ describe("compileAsync method", function () { return shouldReject(ajv.compileAsync(schema)) }) - it("if loadSchema returned error", function () { + it("if loadSchema returned error", () => { var schema = { $id: "http://example.com/parent.json", properties: { @@ -389,7 +389,7 @@ describe("compileAsync method", function () { } }) - it("if schema compilation throws some other exception", function () { + it("if schema compilation throws some other exception", () => { ajv.addKeyword("badkeyword", {compile: badCompile}) var schema = {badkeyword: true} return shouldReject(ajv.compileAsync(schema)) @@ -401,19 +401,17 @@ describe("compileAsync method", function () { function shouldReject(p) { return p.then( - function (validate) { + (validate) => { should.not.exist(validate) throw new Error("Promise has resolved; it should have rejected") }, - function (err) { - should.exist(err) - } + (err) => should.exist(err) ) } }) - describe("schema with multiple remote properties, the first is recursive schema (#801)", function () { - it("should validate data", function () { + describe("schema with multiple remote properties, the first is recursive schema (#801)", () => { + it("should validate data", () => { var schema = { $id: "http://example.com/list.json", type: "object", @@ -422,7 +420,7 @@ describe("compileAsync method", function () { }, } - return ajv.compileAsync(schema).then(function (validate) { + return ajv.compileAsync(schema).then((validate) => { validate({foo: {}}).should.equal(true) }) }) @@ -430,8 +428,8 @@ describe("compileAsync method", function () { function loadSchema(uri) { loadCallCount++ - return new Promise(function (resolve, reject) { - setTimeout(function () { + return new Promise((resolve, reject) => { + setTimeout(() => { if (SCHEMAS[uri]) resolve(SCHEMAS[uri]) else reject(new Error("404")) }, 10) diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index 0e9724a49b..7d53f86f62 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -9,20 +9,20 @@ describe("async schemas, formats and keywords", function () { this.timeout(30000) var ajv, instances - beforeEach(function () { + beforeEach(() => { instances = getAjvInstances() ajv = instances[0] }) - describe("async schemas without async elements", function () { - it("should return result as promise", function () { + describe("async schemas without async elements", () => { + it("should return result as promise", () => { var schema = { $async: true, type: "string", maxLength: 3, } - return repeat(function () { + return repeat(() => { return Promise.all(instances.map(test)) }) @@ -37,7 +37,7 @@ describe("async schemas, formats and keywords", function () { } }) - it("should fail compilation if async schema is inside sync schema", function () { + it("should fail compilation if async schema is inside sync schema", () => { var schema = { properties: { foo: { @@ -48,7 +48,7 @@ describe("async schemas, formats and keywords", function () { }, } - shouldThrowFunc("async schema in sync schema", function () { + shouldThrowFunc("async schema in sync schema", () => { ajv.compile(schema) }) @@ -58,17 +58,17 @@ describe("async schemas, formats and keywords", function () { }) }) - describe("async formats", function () { + describe("async formats", () => { beforeEach(addFormatEnglishWord) - it("should fail compilation if async format is inside sync schema", function () { - instances.forEach(function (_ajv) { + it("should fail compilation if async format is inside sync schema", () => { + instances.forEach((_ajv) => { var schema = { type: "string", format: "english_word", } - shouldThrowFunc("async format in sync schema", function () { + shouldThrowFunc("async format in sync schema", () => { _ajv.compile(schema) }) schema.$async = true @@ -77,9 +77,9 @@ describe("async schemas, formats and keywords", function () { }) }) - describe("async custom keywords", function () { - beforeEach(function () { - instances.forEach(function (_ajv) { + describe("async custom keywords", () => { + beforeEach(() => { + instances.forEach((_ajv) => { _ajv.addKeyword("idExists", { async: true, type: "number", @@ -96,8 +96,8 @@ describe("async schemas, formats and keywords", function () { }) }) - it("should fail compilation if async keyword is inside sync schema", function () { - instances.forEach(function (_ajv) { + it("should fail compilation if async keyword is inside sync schema", () => { + instances.forEach((_ajv) => { var schema = { type: "object", properties: { @@ -108,7 +108,7 @@ describe("async schemas, formats and keywords", function () { }, } - shouldThrowFunc("async keyword in sync schema", function () { + shouldThrowFunc("async keyword in sync schema", () => { _ajv.compile(schema) }) @@ -117,9 +117,9 @@ describe("async schemas, formats and keywords", function () { }) }) - it("should return custom error", function () { + it("should return custom error", () => { return Promise.all( - instances.map(function (_ajv) { + instances.map((_ajv) => { var schema = { $async: true, type: "object", @@ -186,13 +186,13 @@ describe("async schemas, formats and keywords", function () { } }) - describe("async referenced schemas", function () { - beforeEach(function () { + describe("async referenced schemas", () => { + beforeEach(() => { instances = getAjvInstances({inlineRefs: false, extendRefs: "ignore"}) addFormatEnglishWord() }) - it("should validate referenced async schema", function () { + it("should validate referenced async schema", () => { var schema = { $async: true, definitions: { @@ -207,9 +207,9 @@ describe("async schemas, formats and keywords", function () { }, } - return repeat(function () { + return repeat(() => { return Promise.all( - instances.map(function (_ajv) { + instances.map((_ajv) => { var validate = _ajv.compile(schema) var validData = {word: "tomorrow"} @@ -224,7 +224,7 @@ describe("async schemas, formats and keywords", function () { }) }) - it("should validate recursive async schema", function () { + it("should validate recursive async schema", () => { var schema = { $async: true, definitions: { @@ -245,7 +245,7 @@ describe("async schemas, formats and keywords", function () { return recursiveTest(schema) }) - it("should validate recursive ref to async sub-schema, issue #612", function () { + it("should validate recursive ref to async sub-schema, issue #612", () => { var schema = { $async: true, type: "object", @@ -271,7 +271,7 @@ describe("async schemas, formats and keywords", function () { return recursiveTest(schema) }) - it("should validate ref from referenced async schema to root schema", function () { + it("should validate ref from referenced async schema to root schema", () => { var schema = { $async: true, definitions: { @@ -295,7 +295,7 @@ describe("async schemas, formats and keywords", function () { return recursiveTest(schema) }) - it("should validate refs between two async schemas", function () { + it("should validate refs between two async schemas", () => { var schemaObj = { $id: "http://e.com/obj.json#", $async: true, @@ -320,7 +320,7 @@ describe("async schemas, formats and keywords", function () { return recursiveTest(schemaObj, schemaWord) }) - it("should fail compilation if sync schema references async schema", function () { + it("should fail compilation if sync schema references async schema", () => { var schema = { $id: "http://e.com/obj.json#", type: "object", @@ -347,7 +347,7 @@ describe("async schemas, formats and keywords", function () { validate: checkWordOnServer, }) - shouldThrowFunc("async schema referenced by sync schema", function () { + shouldThrowFunc("async schema referenced by sync schema", () => { ajv.compile(schema) }) @@ -358,9 +358,9 @@ describe("async schemas, formats and keywords", function () { }) function recursiveTest(schema, refSchema) { - return repeat(function () { + return repeat(() => { return Promise.all( - instances.map(function (_ajv) { + instances.map((_ajv) => { if (refSchema) { try { _ajv.addSchema(refSchema) @@ -396,7 +396,7 @@ describe("async schemas, formats and keywords", function () { }) function addFormatEnglishWord() { - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { _ajv.addFormat("english_word", { async: true, validate: checkWordOnServer, @@ -415,7 +415,7 @@ function checkWordOnServer(str) { function shouldThrowFunc(message, func) { var err - should.throw(function () { + should.throw(() => { try { func() } catch (e) { @@ -428,38 +428,32 @@ function shouldThrowFunc(message, func) { } function shouldBeValid(p, data) { - return p.then(function (valid) { - valid.should.equal(data) - }) + return p.then((valid) => valid.should.equal(data)) } var SHOULD_BE_INVALID = "test: should be invalid" function shouldBeInvalid(p, expectedMessages) { - return checkNotValid(p).then(function (err) { + return checkNotValid(p).then((err) => { err.should.be.instanceof(Ajv.ValidationError) err.errors.should.be.an("array") err.validation.should.equal(true) if (expectedMessages) { - var messages = err.errors.map(function (e) { - return e.message - }) + var messages = err.errors.map((e) => e.message) messages.should.eql(expectedMessages) } }) } function shouldThrow(p, exception) { - return checkNotValid(p).then(function (err) { - err.message.should.equal(exception) - }) + return checkNotValid(p).then((err) => err.message.should.equal(exception)) } function checkNotValid(p) { return p - .then(function (/* valid */) { + .then((/* valid */) => { throw new Error(SHOULD_BE_INVALID) }) - .catch(function (err) { + .catch((err) => { err.should.be.instanceof(Error) if (err.message == SHOULD_BE_INVALID) throw err return err diff --git a/spec/boolean.spec.js b/spec/boolean.spec.js index 5da22820f4..4e63cb5bc2 100644 --- a/spec/boolean.spec.js +++ b/spec/boolean.spec.js @@ -3,22 +3,22 @@ var Ajv = require("./ajv") require("./chai").should() -describe("boolean schemas", function () { +describe("boolean schemas", () => { var ajvs - before(function () { + before(() => { ajvs = [new Ajv(), new Ajv({allErrors: true}), new Ajv({inlineRefs: false})] }) - describe("top level schema", function () { - describe("schema = true", function () { - it("should validate any data as valid", function () { + describe("top level schema", () => { + describe("schema = true", () => { + it("should validate any data as valid", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should validate any data as invalid", function () { + describe("schema = false", () => { + it("should validate any data as invalid", () => { ajvs.forEach(test(false, false)) }) }) @@ -31,15 +31,15 @@ describe("boolean schemas", function () { } }) - describe("in properties / sub-properties", function () { - describe("schema = true", function () { - it("should be valid with any property value", function () { + describe("in properties / sub-properties", () => { + describe("schema = true", () => { + it("should be valid with any property value", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any property value", function () { + describe("schema = false", () => { + it("should be invalid with any property value", () => { ajvs.forEach(test(false, false)) }) }) @@ -73,15 +73,15 @@ describe("boolean schemas", function () { } }) - describe("in items / sub-items", function () { - describe("schema = true", function () { - it("should be valid with any item value", function () { + describe("in items / sub-items", () => { + describe("schema = true", () => { + it("should be valid with any item value", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any item value", function () { + describe("schema = false", () => { + it("should be invalid with any item value", () => { ajvs.forEach(test(false, false)) }) }) @@ -130,15 +130,15 @@ describe("boolean schemas", function () { } }) - describe("in dependencies and sub-dependencies", function () { - describe("schema = true", function () { - it("should be valid with any property value", function () { + describe("in dependencies and sub-dependencies", () => { + describe("schema = true", () => { + it("should be valid with any property value", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any property value", function () { + describe("schema = false", () => { + it("should be invalid with any property value", () => { ajvs.forEach(test(false, false)) }) }) @@ -172,15 +172,15 @@ describe("boolean schemas", function () { } }) - describe("in patternProperties", function () { - describe("schema = true", function () { - it("should be valid with any property matching pattern", function () { + describe("in patternProperties", () => { + describe("schema = true", () => { + it("should be valid with any property matching pattern", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any property matching pattern", function () { + describe("schema = false", () => { + it("should be invalid with any property matching pattern", () => { ajvs.forEach(test(false, false)) }) }) @@ -214,15 +214,15 @@ describe("boolean schemas", function () { } }) - describe("in propertyNames", function () { - describe("schema = true", function () { - it("should be valid with any property", function () { + describe("in propertyNames", () => { + describe("schema = true", () => { + it("should be valid with any property", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any property", function () { + describe("schema = false", () => { + it("should be invalid with any property", () => { ajvs.forEach(test(false, false)) }) }) @@ -243,15 +243,15 @@ describe("boolean schemas", function () { } }) - describe("in contains", function () { - describe("schema = true", function () { - it("should be valid with any items", function () { + describe("in contains", () => { + describe("schema = true", () => { + it("should be valid with any items", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any items", function () { + describe("schema = false", () => { + it("should be invalid with any items", () => { ajvs.forEach(test(false, false)) }) }) @@ -277,15 +277,15 @@ describe("boolean schemas", function () { } }) - describe("in not", function () { - describe("schema = true", function () { - it("should be invalid with any data", function () { + describe("in not", () => { + describe("schema = true", () => { + it("should be invalid with any data", () => { ajvs.forEach(test(true, false)) }) }) - describe("schema = false", function () { - it("should be valid with any data", function () { + describe("schema = false", () => { + it("should be valid with any data", () => { ajvs.forEach(test(false, true)) }) }) @@ -302,15 +302,15 @@ describe("boolean schemas", function () { } }) - describe("in allOf", function () { - describe("schema = true", function () { - it("should be valid with any data", function () { + describe("in allOf", () => { + describe("schema = true", () => { + it("should be valid with any data", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any data", function () { + describe("schema = false", () => { + it("should be invalid with any data", () => { ajvs.forEach(test(false, false)) }) }) @@ -334,15 +334,15 @@ describe("boolean schemas", function () { } }) - describe("in anyOf", function () { - describe("schema = true", function () { - it("should be valid with any data", function () { + describe("in anyOf", () => { + describe("schema = true", () => { + it("should be valid with any data", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any data", function () { + describe("schema = false", () => { + it("should be invalid with any data", () => { ajvs.forEach(test(false, false)) }) }) @@ -366,15 +366,15 @@ describe("boolean schemas", function () { } }) - describe("in oneOf", function () { - describe("schema = true", function () { - it("should be valid with any data", function () { + describe("in oneOf", () => { + describe("schema = true", () => { + it("should be valid with any data", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any data", function () { + describe("schema = false", () => { + it("should be invalid with any data", () => { ajvs.forEach(test(false, false)) }) }) @@ -398,15 +398,15 @@ describe("boolean schemas", function () { } }) - describe("in $ref", function () { - describe("schema = true", function () { - it("should be valid with any data", function () { + describe("in $ref", () => { + describe("schema = true", () => { + it("should be valid with any data", () => { ajvs.forEach(test(true, true)) }) }) - describe("schema = false", function () { - it("should be invalid with any data", function () { + describe("schema = false", () => { + it("should be invalid with any data", () => { ajvs.forEach(test(false, false)) }) }) diff --git a/spec/browser_test_suite.js b/spec/browser_test_suite.js index b2fa375789..d09b49b5e2 100644 --- a/spec/browser_test_suite.js +++ b/spec/browser_test_suite.js @@ -1,7 +1,7 @@ "use strict" module.exports = function (suite) { - suite.forEach(function (file) { + suite.forEach((file) => { if (file.name.indexOf("optional/format") == 0) { file.name = file.name.replace("optional/", "") } diff --git a/spec/coercion.spec.js b/spec/coercion.spec.js index 36d12a63cb..988cb8fc18 100644 --- a/spec/coercion.spec.js +++ b/spec/coercion.spec.js @@ -159,22 +159,22 @@ coercionArrayRules.array = { object: [{from: {}, to: undefined}], } -describe("Type coercion", function () { +describe("Type coercion", () => { var ajv, fullAjv, instances - beforeEach(function () { + beforeEach(() => { ajv = new Ajv({coerceTypes: true, verbose: true}) fullAjv = new Ajv({coerceTypes: true, verbose: true, allErrors: true}) instances = [ajv, fullAjv] }) - it("should coerce scalar values", function () { - testRules(coercionRules, function ( + it("should coerce scalar values", () => { + testRules(coercionRules, ( test, schema, canCoerce /*, toType, fromType*/ - ) { - instances.forEach(function (_ajv) { + ) => { + instances.forEach((_ajv) => { var valid = _ajv.validate(schema, test.from) //if (valid !== canCoerce) console.log('true', toType, fromType, test, ajv.errors); valid.should.equal(canCoerce) @@ -182,34 +182,31 @@ describe("Type coercion", function () { }) }) - it("should coerce scalar values (coerceTypes = array)", function () { + it("should coerce scalar values (coerceTypes = array)", () => { ajv = new Ajv({coerceTypes: "array", verbose: true}) fullAjv = new Ajv({coerceTypes: "array", verbose: true, allErrors: true}) instances = [ajv, fullAjv] - testRules(coercionArrayRules, function ( - test, - schema, - canCoerce, - toType, - fromType - ) { - instances.forEach(function (_ajv) { - var valid = _ajv.validate(schema, test.from) - if (valid !== canCoerce) { - console.log(toType, ".", fromType, test, schema, ajv.errors) - } - valid.should.equal(canCoerce) - }) - }) + testRules( + coercionArrayRules, + (test, schema, canCoerce, toType, fromType) => { + instances.forEach((_ajv) => { + var valid = _ajv.validate(schema, test.from) + if (valid !== canCoerce) { + console.log(toType, ".", fromType, test, schema, ajv.errors) + } + valid.should.equal(canCoerce) + }) + } + ) }) - it("should coerce values in objects/arrays and update properties/items", function () { - testRules(coercionRules, function ( + it("should coerce values in objects/arrays and update properties/items", () => { + testRules(coercionRules, ( test, schema, canCoerce /*, toType, fromType*/ - ) { + ) => { var schemaObject = { type: "object", properties: { @@ -227,7 +224,7 @@ describe("Type coercion", function () { items: schemaObject, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { testCoercion(_ajv, schemaArray, [test.from], [test.to]) testCoercion(_ajv, schemaObject, {foo: test.from}, {foo: test.to}) testCoercion(_ajv, schemaArrObj, [{foo: test.from}], [{foo: test.to}]) @@ -242,7 +239,7 @@ describe("Type coercion", function () { }) }) - it("should coerce to multiple types in order with number type", function () { + it("should coerce to multiple types in order with number type", () => { var schema = { type: "object", properties: { @@ -252,7 +249,7 @@ describe("Type coercion", function () { }, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { var data _ajv.validate(schema, (data = {foo: "1"})).should.equal(true) @@ -284,7 +281,7 @@ describe("Type coercion", function () { }) }) - it("should coerce to multiple types in order with integer type", function () { + it("should coerce to multiple types in order with integer type", () => { var schema = { type: "object", properties: { @@ -294,7 +291,7 @@ describe("Type coercion", function () { }, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { var data _ajv.validate(schema, (data = {foo: "1"})).should.equal(true) @@ -323,7 +320,7 @@ describe("Type coercion", function () { }) }) - it("should fail to coerce non-number if multiple properties/items are coerced (issue #152)", function () { + it("should fail to coerce non-number if multiple properties/items are coerced (issue #152)", () => { var schema = { type: "object", properties: { @@ -337,7 +334,7 @@ describe("Type coercion", function () { items: {type: "number"}, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { var data = {foo: "123", bar: "bar"} _ajv.validate(schema, data).should.equal(false) data.should.eql({foo: 123, bar: "bar"}) @@ -348,7 +345,7 @@ describe("Type coercion", function () { }) }) - it("should update data if the schema is in ref that is not inlined", function () { + it("should update data if the schema is in ref that is not inlined", () => { instances.push(new Ajv({coerceTypes: true, inlineRefs: false})) var schema = { @@ -399,7 +396,7 @@ describe("Type coercion", function () { }, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { testCoercion(schema, {foo: "1"}, {foo: 1}) testCoercion(schema2, {foo: "1"}, {foo: 1}) testCoercion(schemaRecursive, {foo: {foo: "1"}}, {foo: {foo: 1}}) @@ -418,13 +415,13 @@ describe("Type coercion", function () { }) }) - it("should generate one error for type with coerceTypes option (issue #469)", function () { + it("should generate one error for type with coerceTypes option (issue #469)", () => { var schema = { type: "number", minimum: 10, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { var validate = _ajv.compile(schema) validate(9).should.equal(false) validate.errors.length.should.equal(1) @@ -436,13 +433,13 @@ describe("Type coercion", function () { }) }) - it('should check "uniqueItems" after coercion', function () { + it('should check "uniqueItems" after coercion', () => { var schema = { items: {type: "number"}, uniqueItems: true, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { var validate = _ajv.compile(schema) validate([1, "2", 3]).should.equal(true) @@ -452,13 +449,13 @@ describe("Type coercion", function () { }) }) - it('should check "contains" after coercion', function () { + it('should check "contains" after coercion', () => { var schema = { items: {type: "number"}, contains: {const: 2}, } - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { var validate = _ajv.compile(schema) validate([1, "2", 3]).should.equal(true) @@ -471,7 +468,7 @@ describe("Type coercion", function () { for (var toType in rules) { for (var fromType in rules[toType]) { var tests = rules[toType][fromType] - tests.forEach(function (test) { + tests.forEach((test) => { var canCoerce = test.to !== undefined var schema = canCoerce ? Array.isArray(test.to) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 6bf04de505..d0f944b84d 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -5,10 +5,10 @@ var getAjvInstances = require("./ajv_instances"), equal = require("../dist/compile/equal"), customRules = require("./custom_rules") -describe("Custom keywords", function () { +describe("Custom keywords", () => { var ajv, instances - beforeEach(function () { + beforeEach(() => { instances = getAjvInstances({ allErrors: true, verbose: true, @@ -17,9 +17,9 @@ describe("Custom keywords", function () { ajv = instances[0] }) - describe("custom rules", function () { - describe('rule with "interpreted" keyword validation', function () { - it("should add and validate rule", function () { + describe("custom rules", () => { + describe('rule with "interpreted" keyword validation', () => { + it("should add and validate rule", () => { testEvenKeyword({type: "number", validate: validateEven}) function validateEven(schema, data) { @@ -30,7 +30,7 @@ describe("Custom keywords", function () { } }) - it("should add, validate keyword schema and validate rule", function () { + it("should add, validate keyword schema and validate rule", () => { testEvenKeyword({ type: "number", validate: validateEven, @@ -44,7 +44,7 @@ describe("Custom keywords", function () { } }) - it('should pass parent schema to "interpreted" keyword validation', function () { + it('should pass parent schema to "interpreted" keyword validation', () => { testRangeKeyword({ type: "number", validate: validateRange, @@ -59,7 +59,7 @@ describe("Custom keywords", function () { } }) - it('should validate meta schema and pass parent schema to "interpreted" keyword validation', function () { + it('should validate meta schema and pass parent schema to "interpreted" keyword validation', () => { testRangeKeyword({ type: "number", validate: validateRange, @@ -80,7 +80,7 @@ describe("Custom keywords", function () { } }) - it('should allow defining custom errors for "interpreted" keyword', function () { + it('should allow defining custom errors for "interpreted" keyword', () => { testRangeKeyword({type: "number", validate: validateRange}, true) function validateRange(schema, data, parentSchema) { @@ -117,8 +117,8 @@ describe("Custom keywords", function () { }) }) - describe('rule with "compiled" keyword validation', function () { - it("should add and validate rule", function () { + describe('rule with "compiled" keyword validation', () => { + it("should add and validate rule", () => { testEvenKeyword({type: "number", compile: compileEven}) shouldBeInvalidSchema({"x-even": "not_boolean"}) @@ -137,7 +137,7 @@ describe("Custom keywords", function () { } }) - it("should add, validate keyword schema and validate rule", function () { + it("should add, validate keyword schema and validate rule", () => { testEvenKeyword({ type: "number", compile: compileEven, @@ -157,19 +157,19 @@ describe("Custom keywords", function () { } }) - it("should compile keyword validating function only once per schema", function () { + it("should compile keyword validating function only once per schema", () => { testConstantKeyword({compile: compileConstant}) }) - it("should allow multiple schemas for the same keyword", function () { + it("should allow multiple schemas for the same keyword", () => { testMultipleConstantKeyword({compile: compileConstant}) }) - it('should pass parent schema to "compiled" keyword validation', function () { + it('should pass parent schema to "compiled" keyword validation', () => { testRangeKeyword({type: "number", compile: compileRange}) }) - it("should allow multiple parent schemas for the same keyword", function () { + it("should allow multiple parent schemas for the same keyword", () => { testMultipleRangeKeyword({type: "number", compile: compileRange}) }) }) @@ -194,38 +194,34 @@ describe("Custom keywords", function () { var max = schema[1] return parentSchema.exclusiveRange === true - ? function (data) { - return data > min && data < max - } - : function (data) { - return data >= min && data <= max - } + ? (data) => data > min && data < max + : (data) => data >= min && data <= max } }) - describe("macro rules", function () { - it('should add and validate rule with "macro" keyword', function () { + describe("macro rules", () => { + it('should add and validate rule with "macro" keyword', () => { testEvenKeyword({type: "number", macro: macroEven}, 2) }) - it("should add and expand macro rule", function () { + it("should add and expand macro rule", () => { testConstantKeyword({macro: macroConstant}, 2) }) - it("should allow multiple schemas for the same macro keyword", function () { + it("should allow multiple schemas for the same macro keyword", () => { testMultipleConstantKeyword({macro: macroConstant}, 2) }) - it('should pass parent schema to "macro" keyword', function () { + it('should pass parent schema to "macro" keyword', () => { testRangeKeyword({type: "number", macro: macroRange}, undefined, 2) }) - it("should allow multiple parent schemas for the same macro keyword", function () { + it("should allow multiple parent schemas for the same macro keyword", () => { testMultipleRangeKeyword({type: "number", macro: macroRange}, 2) }) - it("should support resolving $ref without id or $id", function () { - instances.forEach(function (_ajv) { + it("should support resolving $ref without id or $id", () => { + instances.forEach((_ajv) => { _ajv.addKeyword("macroRef", { macro: function (schema, parentSchema, it) { it.baseId.should.equal("#") @@ -264,8 +260,8 @@ describe("Custom keywords", function () { }) }) - it("should recursively expand macro keywords", function () { - instances.forEach(function (_ajv) { + it("should recursively expand macro keywords", () => { + instances.forEach((_ajv) => { _ajv.addKeyword("deepProperties", { type: "object", macro: macroDeepProperties, @@ -383,8 +379,8 @@ describe("Custom keywords", function () { }) }) - it("should correctly expand multiple macros on the same level", function () { - instances.forEach(function (_ajv) { + it("should correctly expand multiple macros on the same level", () => { + instances.forEach((_ajv) => { _ajv.addKeyword("range", {type: "number", macro: macroRange}) _ajv.addKeyword("even", {type: "number", macro: macroEven}) @@ -406,8 +402,8 @@ describe("Custom keywords", function () { }) }) - it("should validate macro keyword when it resolves to the same keyword as exists", function () { - instances.forEach(function (_ajv) { + it("should validate macro keyword when it resolves to the same keyword as exists", () => { + instances.forEach((_ajv) => { _ajv.addKeyword("range", {type: "number", macro: macroRange}) var schema = { @@ -422,8 +418,8 @@ describe("Custom keywords", function () { }) }) - it("should correctly expand macros in subschemas", function () { - instances.forEach(function (_ajv) { + it("should correctly expand macros in subschemas", () => { + instances.forEach((_ajv) => { _ajv.addKeyword("range", {type: "number", macro: macroRange}) var schema = { @@ -442,8 +438,8 @@ describe("Custom keywords", function () { }) }) - it("should correctly expand macros in macro expansions", function () { - instances.forEach(function (_ajv) { + it("should correctly expand macros in macro expansions", () => { + instances.forEach((_ajv) => { _ajv.addKeyword("range", {type: "number", macro: macroRange}) _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) _ajv.addKeyword("myContains", {type: "array", macro: macroContains}) @@ -471,11 +467,11 @@ describe("Custom keywords", function () { }) }) - it("should throw exception if macro expansion is an invalid schema", function () { + it("should throw exception if macro expansion is an invalid schema", () => { ajv.addKeyword("invalid", {macro: macroInvalid}) var schema = {invalid: true} - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) @@ -504,16 +500,16 @@ describe("Custom keywords", function () { } }) - describe("inline rules", function () { - it('should add and validate rule with "inline" code keyword', function () { + describe("inline rules", () => { + it('should add and validate rule with "inline" code keyword', () => { testEvenKeyword({type: "number", inline: inlineEven}) }) - it('should pass parent schema to "inline" keyword', function () { + it('should pass parent schema to "inline" keyword', () => { testRangeKeyword({type: "number", inline: inlineRange, statements: true}) }) - it('should define "inline" keyword as template', function () { + it('should define "inline" keyword as template', () => { var inlineRangeTemplate = customRules.range testRangeKeyword({ @@ -523,7 +519,7 @@ describe("Custom keywords", function () { }) }) - it('should define "inline" keyword without errors', function () { + it('should define "inline" keyword without errors', () => { var inlineRangeTemplate = customRules.range testRangeKeyword({ @@ -534,7 +530,7 @@ describe("Custom keywords", function () { }) }) - it("should allow defining optional errors", function () { + it("should allow defining optional errors", () => { var inlineRangeTemplate = customRules.rangeWithErrors testRangeKeyword( @@ -547,7 +543,7 @@ describe("Custom keywords", function () { ) }) - it("should allow defining required errors", function () { + it("should allow defining required errors", () => { var inlineRangeTemplate = customRules.rangeWithErrors testRangeKeyword( @@ -588,8 +584,8 @@ describe("Custom keywords", function () { } }) - describe("$data reference support with custom keywords (with $data option)", function () { - beforeEach(function () { + describe("$data reference support with custom keywords (with $data option)", () => { + beforeEach(() => { instances = getAjvInstances( { allErrors: true, @@ -601,7 +597,7 @@ describe("Custom keywords", function () { ajv = instances[0] }) - it('should validate "interpreted" rule', function () { + it('should validate "interpreted" rule', () => { testEvenKeyword$data({ type: "number", $data: true, @@ -614,7 +610,7 @@ describe("Custom keywords", function () { } }) - it('should validate rule with "compile" and "validate" funcs', function () { + it('should validate rule with "compile" and "validate" funcs', () => { var compileCalled testEvenKeyword$data({ type: "number", @@ -645,7 +641,7 @@ describe("Custom keywords", function () { } }) - it('should validate with "compile" and "validate" funcs with meta-schema', function () { + it('should validate with "compile" and "validate" funcs with meta-schema', () => { var compileCalled testEvenKeyword$data({ type: "number", @@ -674,7 +670,7 @@ describe("Custom keywords", function () { } }) - it('should validate rule with "macro" and "validate" funcs', function () { + it('should validate rule with "macro" and "validate" funcs', () => { var macroCalled testEvenKeyword$data( { @@ -700,7 +696,7 @@ describe("Custom keywords", function () { } }) - it('should validate with "macro" and "validate" funcs with meta-schema', function () { + it('should validate with "macro" and "validate" funcs with meta-schema', () => { var macroCalled testEvenKeyword$data( { @@ -726,7 +722,7 @@ describe("Custom keywords", function () { } }) - it('should validate rule with "inline" and "validate" funcs', function () { + it('should validate rule with "inline" and "validate" funcs', () => { var inlineCalled testEvenKeyword$data({ type: "number", @@ -748,7 +744,7 @@ describe("Custom keywords", function () { } }) - it('should validate with "inline" and "validate" funcs with meta-schema', function () { + it('should validate with "inline" and "validate" funcs with meta-schema', () => { var inlineCalled testEvenKeyword$data({ type: "number", @@ -771,12 +767,12 @@ describe("Custom keywords", function () { } }) - it('should fail if keyword definition has "$data" but no "validate"', function () { - should.throw(function () { + it('should fail if keyword definition has "$data" but no "validate"', () => { + should.throw(() => { ajv.addKeyword("even", { type: "number", $data: true, - macro: function () { + macro: () => { return {} }, }) @@ -785,7 +781,7 @@ describe("Custom keywords", function () { }) function testEvenKeyword(definition, numErrors) { - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { _ajv.addKeyword("x-even", definition) var schema = {"x-even": true} var validate = _ajv.compile(schema) @@ -798,7 +794,7 @@ describe("Custom keywords", function () { } function testEvenKeyword$data(definition, numErrors) { - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { _ajv.addKeyword("x-even-$data", definition) var schema = {"x-even-$data": true} @@ -835,7 +831,7 @@ describe("Custom keywords", function () { } function testConstantKeyword(definition, numErrors) { - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { _ajv.addKeyword("myConstant", definition) var schema = {myConstant: "abc"} @@ -848,7 +844,7 @@ describe("Custom keywords", function () { } function testMultipleConstantKeyword(definition, numErrors) { - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { _ajv.addKeyword("x-constant", definition) var schema = { @@ -875,7 +871,7 @@ describe("Custom keywords", function () { } function testRangeKeyword(definition, customErrors, numErrors) { - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { _ajv.addKeyword("x-range", definition) _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) @@ -936,7 +932,7 @@ describe("Custom keywords", function () { } function testMultipleRangeKeyword(definition, numErrors) { - instances.forEach(function (_ajv) { + instances.forEach((_ajv) => { _ajv.addKeyword("x-range", definition) _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) @@ -1017,14 +1013,14 @@ describe("Custom keywords", function () { } function shouldBeInvalidSchema(schema) { - instances.forEach(function (_ajv) { - should.throw(function () { + instances.forEach((_ajv) => { + should.throw(() => { _ajv.compile(schema) }) }) } - describe("addKeyword method", function () { + describe("addKeyword method", () => { var TEST_TYPES = [ undefined, "number", @@ -1033,13 +1029,13 @@ describe("Custom keywords", function () { ["number", "string"], ] - it("should throw if defined keyword is passed", function () { + it("should throw if defined keyword is passed", () => { testThrow(["minimum", "maximum", "multipleOf", "minLength", "maxLength"]) testThrowDuplicate("custom") function testThrow(keywords) { - TEST_TYPES.forEach(function (dataType, index) { - should.throw(function () { + TEST_TYPES.forEach((dataType, index) => { + should.throw(() => { addKeyword(keywords[index], dataType) }) }) @@ -1047,11 +1043,11 @@ describe("Custom keywords", function () { function testThrowDuplicate(keywordPrefix) { var index = 0 - TEST_TYPES.forEach(function (dataType1) { - TEST_TYPES.forEach(function (dataType2) { + TEST_TYPES.forEach((dataType1) => { + TEST_TYPES.forEach((dataType2) => { var keyword = keywordPrefix + index++ addKeyword(keyword, dataType1) - should.throw(function () { + should.throw(() => { addKeyword(keyword, dataType2) }) }) @@ -1059,67 +1055,67 @@ describe("Custom keywords", function () { } }) - it("should throw if keyword is not a valid name", function () { - should.not.throw(function () { + it("should throw if keyword is not a valid name", () => { + should.not.throw(() => { ajv.addKeyword("mykeyword", { - validate: function () { + validate: () => { return true }, }) }) - should.not.throw(function () { + should.not.throw(() => { ajv.addKeyword("hyphens-are-valid", { - validate: function () { + validate: () => { return true }, }) }) - should.throw(function () { + should.throw(() => { ajv.addKeyword("3-start-with-number-not-valid`", { - validate: function () { + validate: () => { return true }, }) }) - should.throw(function () { + should.throw(() => { ajv.addKeyword("-start-with-hyphen-not-valid`", { - validate: function () { + validate: () => { return true }, }) }) - should.throw(function () { + should.throw(() => { ajv.addKeyword("spaces not valid`", { - validate: function () { + validate: () => { return true }, }) }) }) - it("should return instance of itself", function () { + it("should return instance of itself", () => { var res = ajv.addKeyword("any", { - validate: function () { + validate: () => { return true }, }) res.should.equal(ajv) }) - it("should throw if unknown type is passed", function () { - should.throw(function () { + it("should throw if unknown type is passed", () => { + should.throw(() => { addKeyword("custom1", "wrongtype") }) - should.throw(function () { + should.throw(() => { addKeyword("custom2", ["number", "wrongtype"]) }) - should.throw(function () { + should.throw(() => { addKeyword("custom3", ["number", undefined]) }) }) @@ -1127,22 +1123,22 @@ describe("Custom keywords", function () { function addKeyword(keyword, dataType) { ajv.addKeyword(keyword, { type: dataType, - validate: function () {}, + validate: () => {}, }) } }) - describe("getKeyword", function () { - it("should return boolean for pre-defined and unknown keywords", function () { + describe("getKeyword", () => { + it("should return boolean for pre-defined and unknown keywords", () => { ajv.getKeyword("type").should.equal(true) ajv.getKeyword("properties").should.equal(true) ajv.getKeyword("additionalProperties").should.equal(true) ajv.getKeyword("unknown").should.equal(false) }) - it("should return keyword definition for custom keywords", function () { + it("should return keyword definition for custom keywords", () => { var definition = { - validate: function () { + validate: () => { return true }, } @@ -1152,8 +1148,8 @@ describe("Custom keywords", function () { }) }) - describe("removeKeyword", function () { - it("should remove and allow redefining custom keyword", function () { + describe("removeKeyword", () => { + it("should remove and allow redefining custom keyword", () => { ajv.addKeyword("positive", { type: "number", validate: function (schema, data) { @@ -1167,7 +1163,7 @@ describe("Custom keywords", function () { validate(0).should.equal(false) validate(1).should.equal(true) - should.throw(function () { + should.throw(() => { ajv.addKeyword("positive", { type: "number", validate: function (sch, data) { @@ -1191,7 +1187,7 @@ describe("Custom keywords", function () { validate(1).should.equal(true) }) - it("should remove and allow redefining standard keyword", function () { + it("should remove and allow redefining standard keyword", () => { var schema = {minimum: 1} var validate = ajv.compile(schema) validate(0).should.equal(false) @@ -1221,10 +1217,10 @@ describe("Custom keywords", function () { validate(2).should.equal(true) }) - it("should return instance of itself", function () { + it("should return instance of itself", () => { var res = ajv .addKeyword("any", { - validate: function () { + validate: () => { return true }, }) @@ -1233,14 +1229,14 @@ describe("Custom keywords", function () { }) }) - describe("custom keywords mutating data", function () { - it("should NOT update data without option modifying", function () { - should.throw(function () { + describe("custom keywords mutating data", () => { + it("should NOT update data without option modifying", () => { + should.throw(() => { testModifying(false) }) }) - it("should update data with option modifying", function () { + it("should update data with option modifying", () => { testModifying(true) }) @@ -1286,17 +1282,17 @@ describe("Custom keywords", function () { } }) - describe("custom keywords with predefined validation result", function () { - it("should ignore result from validation function", function () { + describe("custom keywords with predefined validation result", () => { + it("should ignore result from validation function", () => { ajv.addKeyword("pass", { - validate: function () { + validate: () => { return false }, valid: true, }) ajv.addKeyword("fail", { - validate: function () { + validate: () => { return true }, valid: false, @@ -1306,19 +1302,19 @@ describe("Custom keywords", function () { ajv.validate({fail: ""}, 1).should.equal(false) }) - it("should throw exception if used with macro keyword", function () { - should.throw(function () { + it("should throw exception if used with macro keyword", () => { + should.throw(() => { ajv.addKeyword("pass", { - macro: function () { + macro: () => { return {} }, valid: true, }) }) - should.throw(function () { + should.throw(() => { ajv.addKeyword("fail", { - macro: function () { + macro: () => { return {not: {}} }, valid: false, @@ -1327,8 +1323,8 @@ describe("Custom keywords", function () { }) }) - describe('"dependencies" in keyword definition', function () { - it("should require properties in the parent schema", function () { + describe('"dependencies" in keyword definition', () => { + it("should require properties in the parent schema", () => { ajv.addKeyword("allRequired", { macro: function (schema, parentSchema) { return schema @@ -1343,7 +1339,7 @@ describe("Custom keywords", function () { allRequired: true, } - should.throw(function () { + should.throw(() => { ajv.compile(invalidSchema) }) @@ -1359,13 +1355,13 @@ describe("Custom keywords", function () { v({}).should.equal(false) }) - it("'dependencies'should be array of valid strings", function () { + it("'dependencies'should be array of valid strings", () => { ajv.addKeyword("newKeyword1", { metaSchema: {type: "boolean"}, dependencies: ["dep1"], }) - should.throw(function () { + should.throw(() => { ajv.addKeyword("newKeyword2", { metaSchema: {type: "boolean"}, dependencies: [1], diff --git a/spec/errors.spec.js b/spec/errors.spec.js index b408c58031..97db2a6081 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -3,10 +3,10 @@ var Ajv = require("./ajv"), should = require("./chai").should() -describe("Validation errors", function () { +describe("Validation errors", () => { var ajv, ajvJP, fullAjv - beforeEach(function () { + beforeEach(() => { createInstances() }) @@ -26,7 +26,7 @@ describe("Validation errors", function () { }) } - it("error should include dataPath", function () { + it("error should include dataPath", () => { var schema = { properties: { foo: {type: "number"}, @@ -36,7 +36,7 @@ describe("Validation errors", function () { testSchema1(schema) }) - it('"refs" error should include dataPath', function () { + it('"refs" error should include dataPath', () => { var schema = { definitions: { num: {type: "number"}, @@ -49,13 +49,13 @@ describe("Validation errors", function () { testSchema1(schema, "#/definitions/num") }) - describe('"additionalProperties" errors', function () { - it('should include property in dataPath with option errorDataPath="property"', function () { + describe('"additionalProperties" errors', () => { + it('should include property in dataPath with option errorDataPath="property"', () => { createInstances("property") testAdditional("property") }) - it("should NOT include property in dataPath WITHOUT option errorDataPath", function () { + it("should NOT include property in dataPath WITHOUT option errorDataPath", () => { testAdditional() }) @@ -120,30 +120,26 @@ describe("Validation errors", function () { if (errorDataPath == "property") { fullValidate.errors - .filter(function (err) { - return err.keyword == "additionalProperties" - }) - .map(function (err) { - return fullAjv._opts.jsonPointers + .filter((err) => err.keyword == "additionalProperties") + .map((err) => + fullAjv._opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2, -2) - }) - .forEach(function (p) { - delete invalidData[p] - }) + ) + .forEach((p) => delete invalidData[p]) invalidData.should.eql({foo: 1, bar: 2}) } } }) - describe('errors when "additionalProperties" is schema', function () { - it('should include property in dataPath with option errorDataPath="property"', function () { + describe('errors when "additionalProperties" is schema', () => { + it('should include property in dataPath with option errorDataPath="property"', () => { createInstances("property") testAdditionalIsSchema("property") }) - it("should NOT include property in dataPath WITHOUT option errorDataPath", function () { + it("should NOT include property in dataPath WITHOUT option errorDataPath", () => { testAdditionalIsSchema() }) @@ -212,13 +208,13 @@ describe("Validation errors", function () { } }) - describe('"required" errors', function () { - it('should include missing property in dataPath with option errorDataPath="property"', function () { + describe('"required" errors', () => { + it('should include missing property in dataPath with option errorDataPath="property"', () => { createInstances("property") testRequired("property") }) - it("should NOT include missing property in dataPath WITHOUT option errorDataPath", function () { + it("should NOT include missing property in dataPath WITHOUT option errorDataPath", () => { testRequired() }) @@ -230,12 +226,12 @@ describe("Validation errors", function () { _testRequired(errorDataPath, schema, "#", ".") } - it('large data/schemas with option errorDataPath="property"', function () { + it('large data/schemas with option errorDataPath="property"', () => { createInstances("property") testRequiredLargeSchema("property") }) - it("large data/schemas WITHOUT option errorDataPath", function () { + it("large data/schemas WITHOUT option errorDataPath", () => { testRequiredLargeSchema() }) @@ -337,12 +333,12 @@ describe("Validation errors", function () { } } - it('with "properties" with option errorDataPath="property"', function () { + it('with "properties" with option errorDataPath="property"', () => { createInstances("property") testRequiredAndProperties("property") }) - it('with "properties" WITHOUT option errorDataPath', function () { + it('with "properties" WITHOUT option errorDataPath', () => { testRequiredAndProperties() }) @@ -359,12 +355,12 @@ describe("Validation errors", function () { _testRequired(errorDataPath, schema) } - it('in "anyOf" with option errorDataPath="property"', function () { + it('in "anyOf" with option errorDataPath="property"', () => { createInstances("property") testRequiredInAnyOf("property") }) - it('in "anyOf" WITHOUT option errorDataPath', function () { + it('in "anyOf" WITHOUT option errorDataPath', () => { testRequiredInAnyOf() }) @@ -376,7 +372,7 @@ describe("Validation errors", function () { _testRequired(errorDataPath, schema, "#/anyOf/0", ".", 1) } - it("should not validate required twice in large schemas with loopRequired option", function () { + it("should not validate required twice in large schemas with loopRequired option", () => { ajv = new Ajv({loopRequired: 1, allErrors: true}) var schema = { @@ -393,7 +389,7 @@ describe("Validation errors", function () { validate.errors.should.have.length(2) }) - it("should not validate required twice with $data ref", function () { + it("should not validate required twice with $data ref", () => { ajv = new Ajv({$data: true, allErrors: true}) var schema = { @@ -411,13 +407,13 @@ describe("Validation errors", function () { }) }) - describe('"dependencies" errors', function () { - it('should include missing property in dataPath with option errorDataPath="property"', function () { + describe('"dependencies" errors', () => { + it('should include missing property in dataPath with option errorDataPath="property"', () => { createInstances("property") testDependencies("property") }) - it("should NOT include missing property in dataPath WITHOUT option errorDataPath", function () { + it("should NOT include missing property in dataPath WITHOUT option errorDataPath", () => { testDependencies() }) @@ -629,7 +625,7 @@ describe("Validation errors", function () { : "should NOT have additional properties" } - it('"items" errors should include item index without quotes in dataPath (#48)', function () { + it('"items" errors should include item index without quotes in dataPath (#48)', () => { var schema1 = { $id: "schema1", type: "array", @@ -733,7 +729,7 @@ describe("Validation errors", function () { ) }) - it("should have correct schema path for additionalItems", function () { + it("should have correct schema path for additionalItems", () => { var schema = { type: "array", items: [{type: "integer"}, {type: "integer"}], @@ -761,8 +757,8 @@ describe("Validation errors", function () { } }) - describe('"propertyNames" errors', function () { - it("should add propertyName to errors", function () { + describe('"propertyNames" errors', () => { + it("should add propertyName to errors", () => { var schema = { type: "object", propertyNames: {pattern: "bar"}, @@ -822,8 +818,8 @@ describe("Validation errors", function () { }) }) - describe("oneOf errors", function () { - it("should have errors from inner schemas", function () { + describe("oneOf errors", () => { + it("should have errors from inner schemas", () => { var schema = { oneOf: [{type: "number"}, {type: "integer"}], } @@ -841,7 +837,7 @@ describe("Validation errors", function () { } }) - it("should return passing schemas in error params", function () { + it("should return passing schemas in error params", () => { var schema = { oneOf: [{type: "number"}, {type: "integer"}, {const: 1.5}], } @@ -871,8 +867,8 @@ describe("Validation errors", function () { }) }) - describe("anyOf errors", function () { - it("should have errors from inner schemas", function () { + describe("anyOf errors", () => { + it("should have errors from inner schemas", () => { var schema = { anyOf: [{type: "number"}, {type: "integer"}], } @@ -890,13 +886,13 @@ describe("Validation errors", function () { }) }) - describe("type errors", function () { - describe("integer", function () { - it("should have only one error in {allErrors: false} mode", function () { + describe("type errors", () => { + describe("integer", () => { + it("should have only one error in {allErrors: false} mode", () => { test(ajv) }) - it("should return all errors in {allErrors: true} mode", function () { + it("should return all errors in {allErrors: true} mode", () => { test(fullAjv, 2) }) @@ -915,12 +911,12 @@ describe("Validation errors", function () { } }) - describe("keyword for another type", function () { - it("should have only one error in {allErrors: false} mode", function () { + describe("keyword for another type", () => { + it("should have only one error in {allErrors: false} mode", () => { test(ajv) }) - it("should return all errors in {allErrors: true} mode", function () { + it("should return all errors in {allErrors: true} mode", () => { test(fullAjv, 2) }) @@ -939,12 +935,12 @@ describe("Validation errors", function () { } }) - describe("array of types", function () { - it("should have only one error in {allErrors: false} mode", function () { + describe("array of types", () => { + it("should have only one error in {allErrors: false} mode", () => { test(ajv) }) - it("should return all errors in {allErrors: true} mode", function () { + it("should return all errors in {allErrors: true} mode", () => { test(fullAjv, 2) }) @@ -967,15 +963,15 @@ describe("Validation errors", function () { }) }) - describe("exclusiveMaximum/Minimum errors", function () { - it("should include limits in error message", function () { + describe("exclusiveMaximum/Minimum errors", () => { + it("should include limits in error message", () => { var schema = { type: "integer", exclusiveMinimum: 2, exclusiveMaximum: 5, } - ;[ajv, fullAjv].forEach(function (_ajv) { + ;[ajv, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) shouldBeValid(validate, 3) shouldBeValid(validate, 4) @@ -999,7 +995,7 @@ describe("Validation errors", function () { }) }) - it("should include limits in error message with $data", function () { + it("should include limits in error message with $data", () => { var schema = { properties: { smaller: { @@ -1017,7 +1013,7 @@ describe("Validation errors", function () { verbose: true, jsonPointers: true, }) - ;[ajv, fullAjv].forEach(function (_ajv) { + ;[ajv, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) shouldBeValid(validate, {smaller: 2, larger: 4}) shouldBeValid(validate, {smaller: 3, larger: 4}) @@ -1043,17 +1039,17 @@ describe("Validation errors", function () { }) }) - describe("if/then/else errors", function () { + describe("if/then/else errors", () => { var validate, numErrors - it("if/then/else should include failing keyword in message and params", function () { + it("if/then/else should include failing keyword in message and params", () => { var schema = { if: {maximum: 10}, then: {multipleOf: 2}, else: {multipleOf: 5}, } - ;[ajv, fullAjv].forEach(function (_ajv) { + ;[ajv, fullAjv].forEach((_ajv) => { prepareTest(_ajv, schema) shouldBeValid(validate, 8) shouldBeValid(validate, 15) @@ -1066,13 +1062,13 @@ describe("Validation errors", function () { }) }) - it("if/then should include failing keyword in message and params", function () { + it("if/then should include failing keyword in message and params", () => { var schema = { if: {maximum: 10}, then: {multipleOf: 2}, } - ;[ajv, fullAjv].forEach(function (_ajv) { + ;[ajv, fullAjv].forEach((_ajv) => { prepareTest(_ajv, schema) shouldBeValid(validate, 8) shouldBeValid(validate, 11) @@ -1083,13 +1079,13 @@ describe("Validation errors", function () { }) }) - it("if/else should include failing keyword in message and params", function () { + it("if/else should include failing keyword in message and params", () => { var schema = { if: {maximum: 10}, else: {multipleOf: 5}, } - ;[ajv, fullAjv].forEach(function (_ajv) { + ;[ajv, fullAjv].forEach((_ajv) => { prepareTest(_ajv, schema) shouldBeValid(validate, 7) shouldBeValid(validate, 8) @@ -1130,14 +1126,14 @@ describe("Validation errors", function () { } }) - describe("uniqueItems errors", function () { - it("should not return uniqueItems error when non-unique items are of a different type than required", function () { + describe("uniqueItems errors", () => { + it("should not return uniqueItems error when non-unique items are of a different type than required", () => { var schema = { items: {type: "number"}, uniqueItems: true, } - ;[ajvJP, fullAjv].forEach(function (_ajv) { + ;[ajvJP, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) shouldBeValid(validate, [1, 2, 3]) diff --git a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js index 3c00e17f04..097fa41325 100644 --- a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js +++ b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #1001: addKeyword breaks schema without ID", function () { - it("should allow using schemas without ID with addKeyword", function () { +describe("issue #1001: addKeyword breaks schema without ID", () => { + it("should allow using schemas without ID with addKeyword", () => { var schema = { definitions: { foo: {}, diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js index e79fa641b0..64399c047e 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #181, custom keyword is not validated in allErrors mode if there were previous error", function () { - it("should validate custom keyword that doesn't create errors", function () { +describe("issue #181, custom keyword is not validated in allErrors mode if there were previous error", () => { + it("should validate custom keyword that doesn't create errors", () => { testCustomKeywordErrors({ type: "object", errors: true, @@ -14,7 +14,7 @@ describe("issue #181, custom keyword is not validated in allErrors mode if there }) }) - it("should validate custom keyword that creates errors", function () { + it("should validate custom keyword that creates errors", () => { testCustomKeywordErrors({ type: "object", errors: true, diff --git a/spec/issues/182_nan_validation.spec.js b/spec/issues/182_nan_validation.spec.js index cdb756265f..9c487549e9 100644 --- a/spec/issues/182_nan_validation.spec.js +++ b/spec/issues/182_nan_validation.spec.js @@ -3,17 +3,17 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #182, NaN validation", function () { - it("should not pass minimum/maximum validation", function () { +describe("issue #182, NaN validation", () => { + it("should not pass minimum/maximum validation", () => { testNaN({minimum: 1}, false) testNaN({maximum: 1}, false) }) - it("should pass type: number validation", function () { + it("should pass type: number validation", () => { testNaN({type: "number"}, true) }) - it("should not pass type: integer validation", function () { + it("should not pass type: integer validation", () => { testNaN({type: "integer"}, false) }) diff --git a/spec/issues/204_options_schemas_data_together.spec.js b/spec/issues/204_options_schemas_data_together.spec.js index 1863993343..54eea554bc 100644 --- a/spec/issues/204_options_schemas_data_together.spec.js +++ b/spec/issues/204_options_schemas_data_together.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #204, options schemas and $data used together", function () { - it("should use v5 metaschemas by default", function () { +describe("issue #204, options schemas and $data used together", () => { + it("should use v5 metaschemas by default", () => { var ajv = new Ajv({ schemas: [{$id: "str", type: "string"}], $data: true, diff --git a/spec/issues/210_mutual_recur_frags.spec.js b/spec/issues/210_mutual_recur_frags.spec.js index 4da6c4cfe7..a10ac80c8f 100644 --- a/spec/issues/210_mutual_recur_frags.spec.js +++ b/spec/issues/210_mutual_recur_frags.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #210, mutual recursive $refs that are schema fragments", function () { - it("should compile and validate schema when one ref is fragment", function () { +describe("issue #210, mutual recursive $refs that are schema fragments", () => { + it("should compile and validate schema when one ref is fragment", () => { var ajv = new Ajv() ajv.addSchema({ @@ -35,7 +35,7 @@ describe("issue #210, mutual recursive $refs that are schema fragments", functio validate({baz: {quux: {baz: "foo"}}}).should.equal(false) }) - it("should compile and validate schema when both refs are fragments", function () { + it("should compile and validate schema when both refs are fragments", () => { var ajv = new Ajv() ajv.addSchema({ diff --git a/spec/issues/240_mutual_recur_frags_common_ref.spec.js b/spec/issues/240_mutual_recur_frags_common_ref.spec.js index 9bcbc076d4..f496925f76 100644 --- a/spec/issues/240_mutual_recur_frags_common_ref.spec.js +++ b/spec/issues/240_mutual_recur_frags_common_ref.spec.js @@ -3,7 +3,7 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #240, mutually recursive fragment refs reference a common schema", function () { +describe("issue #240, mutually recursive fragment refs reference a common schema", () => { var apiSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://api.schema#", @@ -35,7 +35,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema }, } - it("should compile and validate schema when one ref is fragment", function () { + it("should compile and validate schema when one ref is fragment", () => { var ajv = new Ajv() var librarySchema = { @@ -128,7 +128,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema testSchema(validate) }) - it("should compile and validate schema when both refs are fragments", function () { + it("should compile and validate schema when both refs are fragments", () => { var ajv = new Ajv() var librarySchema = { diff --git a/spec/issues/259_validate_meta_against_itself.spec.js b/spec/issues/259_validate_meta_against_itself.spec.js index ff9cffd6a6..80595e1339 100644 --- a/spec/issues/259_validate_meta_against_itself.spec.js +++ b/spec/issues/259_validate_meta_against_itself.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #259, support validating [meta-]schemas against themselves", function () { - it('should add schema before validation if "id" is the same as "$schema"', function () { +describe("issue #259, support validating [meta-]schemas against themselves", () => { + it('should add schema before validation if "id" is the same as "$schema"', () => { var ajv = new Ajv() var hyperSchema = require("../remotes/hyper-schema.json") ajv.addMetaSchema(hyperSchema) diff --git a/spec/issues/273_error_schemaPath_refd_schema.spec.js b/spec/issues/273_error_schemaPath_refd_schema.spec.js index f8242643c2..d8f4825dc1 100644 --- a/spec/issues/273_error_schemaPath_refd_schema.spec.js +++ b/spec/issues/273_error_schemaPath_refd_schema.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe.skip("issue #273, schemaPath in error in referenced schema", function () { - it("should have canonic reference with hash after file name", function () { +describe.skip("issue #273, schemaPath in error in referenced schema", () => { + it("should have canonic reference with hash after file name", () => { test(new Ajv()) test(new Ajv({inlineRefs: false})) diff --git a/spec/issues/342_uniqueItems_non-json_objects.spec.js b/spec/issues/342_uniqueItems_non-json_objects.spec.js index 81ca66bad5..005e43de8f 100644 --- a/spec/issues/342_uniqueItems_non-json_objects.spec.js +++ b/spec/issues/342_uniqueItems_non-json_objects.spec.js @@ -3,21 +3,21 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #342, support uniqueItems with some non-JSON objects", function () { +describe("issue #342, support uniqueItems with some non-JSON objects", () => { var validate - before(function () { + before(() => { var ajv = new Ajv() validate = ajv.compile({uniqueItems: true}) }) - it("should allow different RegExps", function () { + it("should allow different RegExps", () => { validate([/foo/, /bar/]).should.equal(true) validate([/foo/gi, /foo/gi]).should.equal(false) validate([/foo/, {}]).should.equal(true) }) - it("should allow different Dates", function () { + it("should allow different Dates", () => { validate([new Date("2016-11-11"), new Date("2016-11-12")]).should.equal( true ) @@ -27,7 +27,7 @@ describe("issue #342, support uniqueItems with some non-JSON objects", function validate([new Date("2016-11-11"), {}]).should.equal(true) }) - it("should allow undefined properties", function () { + it("should allow undefined properties", () => { validate([{}, {foo: undefined}]).should.equal(true) validate([{foo: undefined}, {}]).should.equal(true) validate([{foo: undefined}, {bar: undefined}]).should.equal(true) diff --git a/spec/issues/485_type_validation_priority.spec.js b/spec/issues/485_type_validation_priority.spec.js index a05b28c151..331dc140c7 100644 --- a/spec/issues/485_type_validation_priority.spec.js +++ b/spec/issues/485_type_validation_priority.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #485, order of type validation", function () { - it("should validate types before keywords", function () { +describe("issue #485, order of type validation", () => { + it("should validate types before keywords", () => { var ajv = new Ajv({allErrors: true}) var validate = ajv.compile({ type: ["integer", "string"], @@ -23,9 +23,9 @@ describe("issue #485, order of type validation", function () { function checkErrors(expectedErrs) { validate.errors.should.have.length(expectedErrs.length) - expectedErrs.forEach(function (keyword, i) { + expectedErrs.forEach((keyword, i) => validate.errors[i].keyword.should.equal(keyword) - }) + ) } }) }) diff --git a/spec/issues/50_refs_with_definitions.spec.js b/spec/issues/50_refs_with_definitions.spec.js index 4750d9101b..323af8f396 100644 --- a/spec/issues/50_refs_with_definitions.spec.js +++ b/spec/issues/50_refs_with_definitions.spec.js @@ -3,13 +3,13 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe('issue #50: references with "definitions"', function () { +describe('issue #50: references with "definitions"', () => { it("should be supported by addSchema", spec("addSchema")) it("should be supported by compile", spec("addSchema")) function spec(method) { - return function () { + return () => { var result var ajv = new Ajv() diff --git a/spec/issues/521_wrong_warning_id_property.spec.js b/spec/issues/521_wrong_warning_id_property.spec.js index 57c5bbb7fb..3ebde21835 100644 --- a/spec/issues/521_wrong_warning_id_property.spec.js +++ b/spec/issues/521_wrong_warning_id_property.spec.js @@ -3,11 +3,11 @@ var Ajv = require("../ajv") require("../chai").should() -describe('issue #521, incorrect warning with "id" property', function () { - it("should not log warning", function () { +describe('issue #521, incorrect warning with "id" property', () => { + it("should not log warning", () => { var ajv = new Ajv() var consoleWarn = console.warn - console.warn = function () { + console.warn = () => { throw new Error("should not log warning") } diff --git a/spec/issues/533_missing_ref_error_when_ignore.spec.js b/spec/issues/533_missing_ref_error_when_ignore.spec.js index 933544f574..b28277c1e0 100644 --- a/spec/issues/533_missing_ref_error_when_ignore.spec.js +++ b/spec/issues/533_missing_ref_error_when_ignore.spec.js @@ -3,7 +3,7 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', function () { +describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', () => { var schema = { type: "object", properties: { @@ -12,16 +12,16 @@ describe('issue #533, throwing missing ref exception with option missingRefs: "i }, } - it("should pass validation without throwing exception", function () { + it("should pass validation without throwing exception", () => { var ajv = new Ajv({missingRefs: "ignore"}) var validate = ajv.compile(schema) validate({foo: "anything"}).should.equal(true) validate({foo: "anything", bar: "whatever"}).should.equal(true) }) - it("should throw exception during schema compilation with option missingRefs: true", function () { + it("should throw exception during schema compilation with option missingRefs: true", () => { var ajv = new Ajv() - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) }) diff --git a/spec/issues/743_removeAdditional_to_remove_proto.spec.js b/spec/issues/743_removeAdditional_to_remove_proto.spec.js index 6f8b5c0385..9de9372303 100644 --- a/spec/issues/743_removeAdditional_to_remove_proto.spec.js +++ b/spec/issues/743_removeAdditional_to_remove_proto.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #743, property __proto__ should be removed with removeAdditional option", function () { - it("should remove additional properties", function () { +describe("issue #743, property __proto__ should be removed with removeAdditional option", () => { + it("should remove additional properties", () => { var ajv = new Ajv({removeAdditional: true}) var schema = { diff --git a/spec/issues/768_passContext_recursive_ref.spec.js b/spec/issues/768_passContext_recursive_ref.spec.js index c820f34581..22b47ed20e 100644 --- a/spec/issues/768_passContext_recursive_ref.spec.js +++ b/spec/issues/768_passContext_recursive_ref.spec.js @@ -3,56 +3,48 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #768, fix passContext in recursive $ref", function () { +describe("issue #768, fix passContext in recursive $ref", () => { var ajv, contexts - beforeEach(function () { + beforeEach(() => { contexts = [] }) - describe("passContext = true", function () { - it("should pass this value as context to custom keyword validation function", function () { + describe("passContext = true", () => { + it("should pass this value as context to custom keyword validation function", () => { var validate = getValidate(true) var self = {} validate.call(self, {bar: "a", baz: {bar: "b"}}) contexts.should.have.length(2) - contexts.forEach(function (ctx) { - ctx.should.equal(self) - }) + contexts.forEach((ctx) => ctx.should.equal(self)) }) }) - describe("passContext = false", function () { - it("should pass ajv instance as context to custom keyword validation function", function () { + describe("passContext = false", () => { + it("should pass ajv instance as context to custom keyword validation function", () => { var validate = getValidate(false) validate({bar: "a", baz: {bar: "b"}}) contexts.should.have.length(2) - contexts.forEach(function (ctx) { - ctx.should.equal(ajv) - }) + contexts.forEach((ctx) => ctx.should.equal(ajv)) }) }) - describe("ref is fragment and passContext = true", function () { - it("should pass this value as context to custom keyword validation function", function () { + describe("ref is fragment and passContext = true", () => { + it("should pass this value as context to custom keyword validation function", () => { var validate = getValidateFragments(true) var self = {} validate.call(self, {baz: {corge: "a", quux: {baz: {corge: "b"}}}}) contexts.should.have.length(2) - contexts.forEach(function (ctx) { - ctx.should.equal(self) - }) + contexts.forEach((ctx) => ctx.should.equal(self)) }) }) - describe("ref is fragment and passContext = false", function () { - it("should pass ajv instance as context to custom keyword validation function", function () { + describe("ref is fragment and passContext = false", () => { + it("should pass ajv instance as context to custom keyword validation function", () => { var validate = getValidateFragments(false) validate({baz: {corge: "a", quux: {baz: {corge: "b"}}}}) contexts.should.have.length(2) - contexts.forEach(function (ctx) { - ctx.should.equal(ajv) - }) + contexts.forEach((ctx) => ctx.should.equal(ajv)) }) }) diff --git a/spec/issues/8_shared_refs.spec.js b/spec/issues/8_shared_refs.spec.js index 9b13c31057..fe24eaa97a 100644 --- a/spec/issues/8_shared_refs.spec.js +++ b/spec/issues/8_shared_refs.spec.js @@ -3,13 +3,13 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #8: schema with shared references", function () { +describe("issue #8: schema with shared references", () => { it("should be supported by addSchema", spec("addSchema")) it("should be supported by compile", spec("compile")) function spec(method) { - return function () { + return () => { var ajv = new Ajv() var propertySchema = { diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js index 7735a590f7..949918a7d9 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #955: option removeAdditional breaks custom keywords", function () { - it("should support custom keywords with option removeAdditional", function () { +describe("issue #955: option removeAdditional breaks custom keywords", () => { + it("should support custom keywords with option removeAdditional", () => { var ajv = new Ajv({removeAdditional: "all"}) ajv.addKeyword("minTrimmedLength", { diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 5e70535e8d..7aad65885d 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -67,7 +67,7 @@ runTest( ) function runTest(instances, draft, tests) { - instances.forEach(function (ajv) { + instances.forEach((ajv) => { switch (draft) { case 6: ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) diff --git a/spec/options/comment.spec.js b/spec/options/comment.spec.js index 154626c64e..98cfc5cc7a 100644 --- a/spec/options/comment.spec.js +++ b/spec/options/comment.spec.js @@ -3,16 +3,16 @@ var Ajv = require("../ajv") require("../chai").should() -describe("$comment option", function () { - describe("= true", function () { +describe("$comment option", () => { + describe("= true", () => { var logCalls, consoleLog - beforeEach(function () { + beforeEach(() => { consoleLog = console.log console.log = log }) - afterEach(function () { + afterEach(() => { console.log = consoleLog }) @@ -20,7 +20,7 @@ describe("$comment option", function () { logCalls.push(Array.prototype.slice.call(arguments)) } - it("should log the text from $comment keyword", function () { + it("should log the text from $comment keyword", () => { var schema = { properties: { foo: {$comment: "property foo"}, @@ -31,7 +31,7 @@ describe("$comment option", function () { var ajv = new Ajv({$comment: true}) var fullAjv = new Ajv({allErrors: true, $comment: true}) - ;[ajv, fullAjv].forEach(function (_ajv) { + ;[ajv, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) test({}, true, []) @@ -50,14 +50,14 @@ describe("$comment option", function () { }) }) - describe("function hook", function () { + describe("function hook", () => { var hookCalls function hook() { hookCalls.push(Array.prototype.slice.call(arguments)) } - it("should pass the text from $comment keyword to the hook", function () { + it("should pass the text from $comment keyword to the hook", () => { var schema = { properties: { foo: {$comment: "property foo"}, @@ -68,7 +68,7 @@ describe("$comment option", function () { var ajv = new Ajv({$comment: hook}) var fullAjv = new Ajv({allErrors: true, $comment: hook}) - ;[ajv, fullAjv].forEach(function (_ajv) { + ;[ajv, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) test({}, true, []) diff --git a/spec/options/meta_validateSchema.spec.js b/spec/options/meta_validateSchema.spec.js index 8229c67288..5aaf4f06e3 100644 --- a/spec/options/meta_validateSchema.spec.js +++ b/spec/options/meta_validateSchema.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("meta and validateSchema options", function () { - it("should add draft-7 meta schema by default", function () { +describe("meta and validateSchema options", () => { + it("should add draft-7 meta schema by default", () => { testOptionMeta(new Ajv()) testOptionMeta(new Ajv({meta: true})) @@ -14,41 +14,41 @@ describe("meta and validateSchema options", function () { .should.be.a("function") ajv.validateSchema({type: "integer"}).should.equal(true) ajv.validateSchema({type: 123}).should.equal(false) - should.not.throw(function () { + should.not.throw(() => { ajv.addSchema({type: "integer"}) }) - should.throw(function () { + should.throw(() => { ajv.addSchema({type: 123}) }) } }) - it("should throw if meta: false and validateSchema: true", function () { + it("should throw if meta: false and validateSchema: true", () => { var ajv = new Ajv({meta: false}) should.not.exist(ajv.getSchema("http://json-schema.org/draft-07/schema")) - should.not.throw(function () { + should.not.throw(() => { ajv.addSchema({type: "wrong_type"}, "integer") }) }) - it("should skip schema validation with validateSchema: false", function () { + it("should skip schema validation with validateSchema: false", () => { var ajv = new Ajv() - should.throw(function () { + should.throw(() => { ajv.addSchema({type: 123}, "integer") }) ajv = new Ajv({validateSchema: false}) - should.not.throw(function () { + should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) ajv = new Ajv({validateSchema: false, meta: false}) - should.not.throw(function () { + should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) }) - it('should not throw on invalid schema with validateSchema: "log"', function () { + it('should not throw on invalid schema with validateSchema: "log"', () => { var logError = console.error var loggedError = false console.error = function () { @@ -57,27 +57,27 @@ describe("meta and validateSchema options", function () { } var ajv = new Ajv({validateSchema: "log"}) - should.not.throw(function () { + should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) loggedError.should.equal(true) loggedError = false ajv = new Ajv({validateSchema: "log", meta: false}) - should.not.throw(function () { + should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) loggedError.should.equal(false) console.error = logError }) - it("should validate v6 schema", function () { + it("should validate v6 schema", () => { var ajv = new Ajv() ajv.validateSchema({contains: {minimum: 2}}).should.equal(true) ajv.validateSchema({contains: 2}).should.equal(false) }) - it("should use option meta as default meta schema", function () { + it("should use option meta as default meta schema", () => { var meta = { $schema: "http://json-schema.org/draft-07/schema", properties: { diff --git a/spec/options/nullable.spec.js b/spec/options/nullable.spec.js index 9fb3013b8a..bbf2df735e 100644 --- a/spec/options/nullable.spec.js +++ b/spec/options/nullable.spec.js @@ -3,17 +3,17 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("nullable option", function () { +describe("nullable option", () => { var ajv - describe("= true", function () { - beforeEach(function () { + describe("= true", () => { + beforeEach(() => { ajv = new Ajv({ nullable: true, }) }) - it('should add keyword "nullable"', function () { + it('should add keyword "nullable"', () => { testNullable({ type: "number", nullable: true, @@ -38,7 +38,7 @@ describe("nullable option", function () { testNotNullable({type: ["number"]}) }) - it('should respect "nullable" == false with opts.nullable == true', function () { + it('should respect "nullable" == false with opts.nullable == true', () => { testNotNullable({ type: "number", nullable: false, @@ -51,8 +51,8 @@ describe("nullable option", function () { }) }) - describe('without option "nullable"', function () { - it("should ignore keyword nullable", function () { + describe('without option "nullable"', () => { + it("should ignore keyword nullable", () => { ajv = new Ajv() testNotNullable({ @@ -74,7 +74,7 @@ describe("nullable option", function () { nullable: true, }) - should.not.throw(function () { + should.not.throw(() => { ajv.compile({nullable: false}) }) }) diff --git a/spec/options/options_add_schemas.spec.js b/spec/options/options_add_schemas.spec.js index 1574839a80..effe461ce1 100644 --- a/spec/options/options_add_schemas.spec.js +++ b/spec/options/options_add_schemas.spec.js @@ -3,9 +3,9 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("options to add schemas", function () { - describe("schemas", function () { - it("should add schemas from object", function () { +describe("options to add schemas", () => { + describe("schemas", () => { + it("should add schemas from object", () => { var ajv = new Ajv({ schemas: { int: {type: "integer"}, @@ -19,7 +19,7 @@ describe("options to add schemas", function () { ajv.validate("str", 123).should.equal(false) }) - it("should add schemas from array", function () { + it("should add schemas from array", () => { var ajv = new Ajv({ schemas: [ {$id: "int", type: "integer"}, @@ -34,17 +34,17 @@ describe("options to add schemas", function () { }) }) - describe("addUsedSchema", function () { - ;[true, undefined].forEach(function (optionValue) { - describe("= " + optionValue, function () { + describe("addUsedSchema", () => { + ;[true, undefined].forEach((optionValue) => { + describe("= " + optionValue, () => { var ajv - beforeEach(function () { + beforeEach(() => { ajv = new Ajv({addUsedSchema: optionValue}) }) - describe("compile and validate", function () { - it("should add schema", function () { + describe("compile and validate", () => { + it("should add schema", () => { var schema = {$id: "str", type: "string"} var validate = ajv.compile(schema) validate("abc").should.equal(true) @@ -57,16 +57,16 @@ describe("options to add schemas", function () { ajv.getSchema("int").should.be.a("function") }) - it("should throw with duplicate ID", function () { + it("should throw with duplicate ID", () => { ajv.compile({$id: "str", type: "string"}) - should.throw(function () { + should.throw(() => { ajv.compile({$id: "str", minLength: 2}) }) var schema = {$id: "int", type: "integer"} var schema2 = {$id: "int", minimum: 0} ajv.validate(schema, 1).should.equal(true) - should.throw(function () { + should.throw(() => { ajv.validate(schema2, 1) }) }) @@ -74,15 +74,15 @@ describe("options to add schemas", function () { }) }) - describe("= false", function () { + describe("= false", () => { var ajv - beforeEach(function () { + beforeEach(() => { ajv = new Ajv({addUsedSchema: false}) }) - describe("compile and validate", function () { - it("should NOT add schema", function () { + describe("compile and validate", () => { + it("should NOT add schema", () => { var schema = {$id: "str", type: "string"} var validate = ajv.compile(schema) validate("abc").should.equal(true) @@ -95,16 +95,16 @@ describe("options to add schemas", function () { should.equal(ajv.getSchema("int"), undefined) }) - it("should NOT throw with duplicate ID", function () { + it("should NOT throw with duplicate ID", () => { ajv.compile({$id: "str", type: "string"}) - should.not.throw(function () { + should.not.throw(() => { ajv.compile({$id: "str", minLength: 2}) }) var schema = {$id: "int", type: "integer"} var schema2 = {$id: "int", minimum: 0} ajv.validate(schema, 1).should.equal(true) - should.not.throw(function () { + should.not.throw(() => { ajv.validate(schema2, 1).should.equal(true) }) }) @@ -112,10 +112,10 @@ describe("options to add schemas", function () { }) }) - describe("serialize", function () { + describe("serialize", () => { var serializeCalled - it("should use custom function to serialize schema to string", function () { + it("should use custom function to serialize schema to string", () => { serializeCalled = undefined var ajv = new Ajv({serialize: serialize}) ajv.addSchema({type: "string"}) diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js index 14ae029f82..27b11bd7b0 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.js @@ -3,10 +3,10 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("code generation options", function () { - describe("sourceCode", function () { - describe("= true", function () { - it("should add source.code property", function () { +describe("code generation options", () => { + describe("sourceCode", () => { + describe("= true", () => { + it("should add source.code property", () => { test(new Ajv({sourceCode: true})) function test(ajv) { @@ -16,8 +16,8 @@ describe("code generation options", function () { }) }) - describe("= false and default", function () { - it("should not add source and sourceCode properties", function () { + describe("= false and default", () => { + it("should not add source and sourceCode properties", () => { test(new Ajv()) test(new Ajv({sourceCode: false})) @@ -30,8 +30,8 @@ describe("code generation options", function () { }) }) - describe("processCode", function () { - it("should process generated code", function () { + describe("processCode", () => { + it("should process generated code", () => { var ajv = new Ajv() var validate = ajv.compile({type: "string"}) validate.toString().split("\n").length.should.equal(1) @@ -45,34 +45,30 @@ describe("code generation options", function () { }) }) - describe("passContext option", function () { + describe("passContext option", () => { var ajv, contexts - beforeEach(function () { + beforeEach(() => { contexts = [] }) - describe("= true", function () { - it("should pass this value as context to custom keyword validation function", function () { + describe("= true", () => { + it("should pass this value as context to custom keyword validation function", () => { var validate = getValidate(true) var self = {} validate.call(self, {}) contexts.should.have.length(4) - contexts.forEach(function (ctx) { - ctx.should.equal(self) - }) + contexts.forEach((ctx) => ctx.should.equal(self)) }) }) - describe("= false", function () { - it("should pass ajv instance as context to custom keyword validation function", function () { + describe("= false", () => { + it("should pass ajv instance as context to custom keyword validation function", () => { var validate = getValidate(false) var self = {} validate.call(self, {}) contexts.should.have.length(4) - contexts.forEach(function (ctx) { - ctx.should.equal(ajv) - }) + contexts.forEach((ctx) => ctx.should.equal(ajv)) }) }) diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js index a5a68f8427..dae4f8c107 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.js @@ -3,36 +3,36 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("referenced schema options", function () { - describe("extendRefs", function () { - describe("= true", function () { - it("should allow extending $ref with other keywords", function () { +describe("referenced schema options", () => { + describe("extendRefs", () => { + describe("= true", () => { + it("should allow extending $ref with other keywords", () => { test(new Ajv({extendRefs: true}), true) }) - it("should NOT log warning if extendRefs is true", function () { + it("should NOT log warning if extendRefs is true", () => { testWarning(new Ajv({extendRefs: true})) }) }) - describe('= "ignore" and default', function () { - it("should ignore other keywords when $ref is used", function () { + describe('= "ignore" and default', () => { + it("should ignore other keywords when $ref is used", () => { test(new Ajv()) test(new Ajv({extendRefs: "ignore"}), false) }) - it("should log warning when other keywords are used with $ref", function () { + it("should log warning when other keywords are used with $ref", () => { testWarning(new Ajv(), /keywords\signored/) testWarning(new Ajv({extendRefs: "ignore"}), /keywords\signored/) }) }) - describe('= "fail"', function () { - it("should fail schema compilation if other keywords are used with $ref", function () { + describe('= "fail"', () => { + it("should fail schema compilation if other keywords are used with $ref", () => { testFail(new Ajv({extendRefs: "fail"})) function testFail(ajv) { - should.throw(function () { + should.throw(() => { var schema = { definitions: { int: {type: "integer"}, @@ -43,7 +43,7 @@ describe("referenced schema options", function () { ajv.compile(schema) }) - should.not.throw(function () { + should.not.throw(() => { var schema = { definitions: { int: {type: "integer"}, @@ -117,15 +117,15 @@ describe("referenced schema options", function () { } }) - describe("missingRefs", function () { - it("should throw if ref is missing without this option", function () { + describe("missingRefs", () => { + it("should throw if ref is missing without this option", () => { var ajv = new Ajv() - should.throw(function () { + should.throw(() => { ajv.compile({$ref: "missing_reference"}) }) }) - it('should not throw and pass validation with missingRef == "ignore"', function () { + it('should not throw and pass validation with missingRef == "ignore"', () => { testMissingRefsIgnore(new Ajv({missingRefs: "ignore"})) testMissingRefsIgnore(new Ajv({missingRefs: "ignore", allErrors: true})) @@ -135,7 +135,7 @@ describe("referenced schema options", function () { } }) - it('should not throw and fail validation with missingRef == "fail" if the ref is used', function () { + it('should not throw and fail validation with missingRef == "fail" if the ref is used', () => { testMissingRefsFail(new Ajv({missingRefs: "fail"})) testMissingRefsFail(new Ajv({missingRefs: "fail", verbose: true})) testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true})) diff --git a/spec/options/options_reporting.spec.js b/spec/options/options_reporting.spec.js index fcae23338c..450005e24a 100644 --- a/spec/options/options_reporting.spec.js +++ b/spec/options/options_reporting.spec.js @@ -3,9 +3,9 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("reporting options", function () { - describe("verbose", function () { - it("should add schema, parentSchema and data to errors with verbose option == true", function () { +describe("reporting options", () => { + describe("verbose", () => { + it("should add schema, parentSchema and data to errors with verbose option == true", () => { testVerbose(new Ajv({verbose: true})) testVerbose(new Ajv({verbose: true, allErrors: true})) @@ -26,8 +26,8 @@ describe("reporting options", function () { }) }) - describe("allErrors", function () { - it('should be disabled inside "not" keyword', function () { + describe("allErrors", () => { + it('should be disabled inside "not" keyword', () => { test(new Ajv(), false) test(new Ajv({allErrors: true}), true) @@ -35,12 +35,12 @@ describe("reporting options", function () { var format1called = false, format2called = false - ajv.addFormat("format1", function () { + ajv.addFormat("format1", () => { format1called = true return false }) - ajv.addFormat("format2", function () { + ajv.addFormat("format2", () => { format2called = true return false }) @@ -67,7 +67,7 @@ describe("reporting options", function () { }) }) - describe("logger", function () { + describe("logger", () => { /** * The logger option tests are based on the meta scenario which writes into the logger.warn */ @@ -75,18 +75,18 @@ describe("reporting options", function () { var origConsoleWarn = console.warn var consoleCalled - beforeEach(function () { + beforeEach(() => { consoleCalled = false - console.warn = function () { + console.warn = () => { consoleCalled = true } }) - afterEach(function () { + afterEach(() => { console.warn = origConsoleWarn }) - it("no custom logger is given - global console should be used", function () { + it("no custom logger is given - global console should be used", () => { var ajv = new Ajv({ meta: false, }) @@ -99,7 +99,7 @@ describe("reporting options", function () { should.equal(consoleCalled, true) }) - it("custom logger is an object - logs should only report to it", function () { + it("custom logger is an object - logs should only report to it", () => { var loggerCalled = false var logger = { @@ -126,7 +126,7 @@ describe("reporting options", function () { } }) - it("logger option is false - no logs should be reported", function () { + it("logger option is false - no logs should be reported", () => { var ajv = new Ajv({ meta: false, logger: false, @@ -140,16 +140,16 @@ describe("reporting options", function () { should.equal(consoleCalled, false) }) - it("logger option is an object without required methods - an error should be thrown", function () { - ;(function () { + it("logger option is an object without required methods - an error should be thrown", () => { + ;(() => { new Ajv({ meta: false, logger: {}, }) - }.should.throw( + }).should.throw( Error, /logger must implement log, warn and error methods/ - )) + ) }) }) }) diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index 07d667a07d..e23bb521c6 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -4,9 +4,9 @@ var Ajv = require("../ajv") require("../chai").should() var DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ -describe("validation options", function () { - describe("format", function () { - it("should not validate formats if option format == false", function () { +describe("validation options", () => { + describe("format", () => { + it("should not validate formats if option format == false", () => { var ajv = new Ajv({formats: {date: DATE_FORMAT}}), ajvFF = new Ajv({formats: {date: DATE_FORMAT}, format: false}) @@ -18,8 +18,8 @@ describe("validation options", function () { }) }) - describe("formats", function () { - it("should add formats from options", function () { + describe("formats", () => { + it("should add formats from options", () => { var ajv = new Ajv({ formats: { identifier: /^[a-z_$][a-z0-9_$]*$/i, @@ -35,8 +35,8 @@ describe("validation options", function () { }) }) - describe("keywords", function () { - it("should add keywords from options", function () { + describe("keywords", () => { + it("should add keywords from options", () => { var ajv = new Ajv({ keywords: { identifier: { @@ -57,8 +57,8 @@ describe("validation options", function () { }) }) - describe("uniqueItems", function () { - it("should not validate uniqueItems with uniqueItems option == false", function () { + describe("uniqueItems", () => { + it("should not validate uniqueItems with uniqueItems option == false", () => { testUniqueItems(new Ajv({uniqueItems: false})) testUniqueItems(new Ajv({uniqueItems: false, allErrors: true})) @@ -70,8 +70,8 @@ describe("validation options", function () { }) }) - describe("unicode", function () { - it("should use String.prototype.length with unicode option == false", function () { + describe("unicode", () => { + it("should use String.prototype.length with unicode option == false", () => { var ajvUnicode = new Ajv() testUnicode(new Ajv({unicode: false})) testUnicode(new Ajv({unicode: false, allErrors: true})) @@ -92,8 +92,8 @@ describe("validation options", function () { }) }) - describe("multipleOfPrecision", function () { - it("should allow for some deviation from 0 when validating multipleOf with value < 1", function () { + describe("multipleOfPrecision", () => { + it("should allow for some deviation from 0 when validating multipleOf with value < 1", () => { test(new Ajv({multipleOfPrecision: 7})) test(new Ajv({multipleOfPrecision: 7, allErrors: true})) diff --git a/spec/options/ownProperties.spec.js b/spec/options/ownProperties.spec.js index d72cf863cb..a1a84dc562 100644 --- a/spec/options/ownProperties.spec.js +++ b/spec/options/ownProperties.spec.js @@ -3,16 +3,16 @@ var Ajv = require("../ajv") require("../chai").should() -describe("ownProperties option", function () { +describe("ownProperties option", () => { var ajv, ajvOP, ajvOP1 - beforeEach(function () { + beforeEach(() => { ajv = new Ajv({allErrors: true}) ajvOP = new Ajv({ownProperties: true, allErrors: true}) ajvOP1 = new Ajv({ownProperties: true}) }) - it("should only validate own properties with additionalProperties", function () { + it("should only validate own properties with additionalProperties", () => { var schema = { properties: {a: {type: "number"}}, additionalProperties: false, @@ -23,7 +23,7 @@ describe("ownProperties option", function () { test(schema, obj, proto) }) - it("should only validate own properties with properties keyword", function () { + it("should only validate own properties with properties keyword", () => { var schema = { properties: { a: {type: "number"}, @@ -36,7 +36,7 @@ describe("ownProperties option", function () { test(schema, obj, proto) }) - it("should only validate own properties with required keyword", function () { + it("should only validate own properties with required keyword", () => { var schema = { required: ["a", "b"], } @@ -46,7 +46,7 @@ describe("ownProperties option", function () { test(schema, obj, proto, 1, true) }) - it("should only validate own properties with required keyword - many properties", function () { + it("should only validate own properties with required keyword - many properties", () => { ajv = new Ajv({allErrors: true, loopRequired: 1}) ajvOP = new Ajv({ownProperties: true, allErrors: true, loopRequired: 1}) ajvOP1 = new Ajv({ownProperties: true, loopRequired: 1}) @@ -60,7 +60,7 @@ describe("ownProperties option", function () { test(schema, obj, proto, 2, true) }) - it("should only validate own properties with required keyword as $data", function () { + it("should only validate own properties with required keyword as $data", () => { ajv = new Ajv({allErrors: true, $data: true}) ajvOP = new Ajv({ownProperties: true, allErrors: true, $data: true}) ajvOP1 = new Ajv({ownProperties: true, $data: true}) @@ -83,7 +83,7 @@ describe("ownProperties option", function () { test(schema, obj, proto, 1, true) }) - it("should only validate own properties with properties and required keyword", function () { + it("should only validate own properties with properties and required keyword", () => { var schema = { properties: { a: {type: "number"}, @@ -97,7 +97,7 @@ describe("ownProperties option", function () { test(schema, obj, proto, 1, true) }) - it("should only validate own properties with dependencies keyword", function () { + it("should only validate own properties with dependencies keyword", () => { var schema = { dependencies: { a: ["c"], @@ -114,7 +114,7 @@ describe("ownProperties option", function () { test(schema, obj, proto, 1, true) }) - it("should only validate own properties with schema dependencies", function () { + it("should only validate own properties with schema dependencies", () => { var schema = { dependencies: { a: {not: {required: ["c"]}}, @@ -131,7 +131,7 @@ describe("ownProperties option", function () { test(schema, obj, proto) }) - it("should only validate own properties with patternProperties", function () { + it("should only validate own properties with patternProperties", () => { var schema = { patternProperties: {"f.*o": {type: "integer"}}, } @@ -141,7 +141,7 @@ describe("ownProperties option", function () { test(schema, obj, proto) }) - it("should only validate own properties with propertyNames", function () { + it("should only validate own properties with propertyNames", () => { var schema = { propertyNames: { pattern: "foo", diff --git a/spec/options/removeAdditional.spec.js b/spec/options/removeAdditional.spec.js index 017f9b908b..6e488b578b 100644 --- a/spec/options/removeAdditional.spec.js +++ b/spec/options/removeAdditional.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("removeAdditional option", function () { - it("should remove all additional properties", function () { +describe("removeAdditional option", () => { + it("should remove all additional properties", () => { var ajv = new Ajv({removeAdditional: "all"}) ajv.addSchema({ @@ -24,7 +24,7 @@ describe("removeAdditional option", function () { object.should.not.have.property("baz") }) - it("should remove properties that would error when `additionalProperties = false`", function () { + it("should remove properties that would error when `additionalProperties = false`", () => { var ajv = new Ajv({removeAdditional: true}) ajv.addSchema({ @@ -45,7 +45,7 @@ describe("removeAdditional option", function () { object.should.not.have.property("baz") }) - it("should remove properties that would error when `additionalProperties = false` (many properties, boolean schema)", function () { + it("should remove properties that would error when `additionalProperties = false` (many properties, boolean schema)", () => { var ajv = new Ajv({removeAdditional: true}) var schema = { @@ -84,7 +84,7 @@ describe("removeAdditional option", function () { }) }) - it("should remove properties that would error when `additionalProperties` is a schema", function () { + it("should remove properties that would error when `additionalProperties` is a schema", () => { var ajv = new Ajv({removeAdditional: "failing"}) ajv.addSchema({ diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index 7473cfb4c4..367dc91dae 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -3,19 +3,19 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("removed schemaId option", function () { - it('should throw error if schemaId option is used and it is not equal to "$id"', function () { +describe("removed schemaId option", () => { + it('should throw error if schemaId option is used and it is not equal to "$id"', () => { new Ajv() new Ajv({schemaId: "$id"}) - should.throw(function () { + should.throw(() => { new Ajv({schemaId: "id"}) }) - should.throw(function () { + should.throw(() => { new Ajv({schemaId: "auto"}) }) }) - it("should use $id and ignore id", function () { + it("should use $id and ignore id", () => { test(new Ajv()) test(new Ajv({schemaId: "$id"})) diff --git a/spec/options/strictDefaults.spec.js b/spec/options/strictDefaults.spec.js index 7fc6f633d5..6bdac7c8cf 100644 --- a/spec/options/strictDefaults.spec.js +++ b/spec/options/strictDefaults.spec.js @@ -3,10 +3,10 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("strictDefaults option", function () { - describe("useDefaults = true", function () { - describe("strictDefaults = false", function () { - it("should NOT throw an error or log a warning given an ignored default", function () { +describe("strictDefaults option", () => { + describe("useDefaults = true", () => { + describe("strictDefaults = false", () => { + it("should NOT throw an error or log a warning given an ignored default", () => { var output = {} var ajv = new Ajv({ useDefaults: true, @@ -22,7 +22,7 @@ describe("strictDefaults option", function () { should.not.exist(output.warning) }) - it("should NOT throw an error or log a warning given an ignored default", function () { + it("should NOT throw an error or log a warning given an ignored default", () => { var output = {} var ajv = new Ajv({ useDefaults: true, @@ -47,19 +47,19 @@ describe("strictDefaults option", function () { }) }) - describe("strictDefaults = true", function () { - it("should throw an error given an ignored default in the schema root when strictDefaults is true", function () { + describe("strictDefaults = true", () => { + it("should throw an error given an ignored default in the schema root when strictDefaults is true", () => { var ajv = new Ajv({useDefaults: true, strictDefaults: true}) var schema = { default: 5, properties: {}, } - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) }) - it("should throw an error given an ignored default in oneOf when strictDefaults is true", function () { + it("should throw an error given an ignored default in oneOf when strictDefaults is true", () => { var ajv = new Ajv({useDefaults: true, strictDefaults: true}) var schema = { oneOf: [ @@ -73,14 +73,14 @@ describe("strictDefaults option", function () { }, ], } - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) }) }) - describe('strictDefaults = "log"', function () { - it('should log a warning given an ignored default in the schema root when strictDefaults is "log"', function () { + describe('strictDefaults = "log"', () => { + it('should log a warning given an ignored default in the schema root when strictDefaults is "log"', () => { var output = {} var ajv = new Ajv({ useDefaults: true, @@ -95,7 +95,7 @@ describe("strictDefaults option", function () { should.equal(output.warning, "default is ignored in the schema root") }) - it('should log a warning given an ignored default in oneOf when strictDefaults is "log"', function () { + it('should log a warning given an ignored default in oneOf when strictDefaults is "log"', () => { var output = {} var ajv = new Ajv({ useDefaults: true, @@ -120,20 +120,20 @@ describe("strictDefaults option", function () { }) }) - describe("useDefaults = false", function () { - describe("strictDefaults = true", function () { - it("should NOT throw an error given an ignored default in the schema root when useDefaults is false", function () { + describe("useDefaults = false", () => { + describe("strictDefaults = true", () => { + it("should NOT throw an error given an ignored default in the schema root when useDefaults is false", () => { var ajv = new Ajv({useDefaults: false, strictDefaults: true}) var schema = { default: 5, properties: {}, } - should.not.throw(function () { + should.not.throw(() => { ajv.compile(schema) }) }) - it("should NOT throw an error given an ignored default in oneOf when useDefaults is false", function () { + it("should NOT throw an error given an ignored default in oneOf when useDefaults is false", () => { var ajv = new Ajv({useDefaults: false, strictDefaults: true}) var schema = { oneOf: [ @@ -147,7 +147,7 @@ describe("strictDefaults option", function () { }, ], } - should.not.throw(function () { + should.not.throw(() => { ajv.compile(schema) }) }) @@ -156,13 +156,13 @@ describe("strictDefaults option", function () { function getLogger(output) { return { - log: function () { + log: () => { throw new Error("log should not be called") }, warn: function (warning) { output.warning = warning }, - error: function () { + error: () => { throw new Error("error should not be called") }, } diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js index 9143d74e6f..c45bbecbfe 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.js @@ -3,9 +3,9 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("strictKeywords option", function () { - describe("strictKeywords = false", function () { - it("should NOT throw an error or log a warning given an unknown keyword", function () { +describe("strictKeywords option", () => { + describe("strictKeywords = false", () => { + it("should NOT throw an error or log a warning given an unknown keyword", () => { var output = {} var ajv = new Ajv({ strictKeywords: false, @@ -21,21 +21,21 @@ describe("strictKeywords option", function () { }) }) - describe("strictKeywords = true", function () { - it("should throw an error given an unknown keyword in the schema root when strictKeywords is true", function () { + describe("strictKeywords = true", () => { + it("should throw an error given an unknown keyword in the schema root when strictKeywords is true", () => { var ajv = new Ajv({strictKeywords: true}) var schema = { properties: {}, unknownKeyword: 1, } - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) }) }) - describe('strictKeywords = "log"', function () { - it('should log a warning given an unknown keyword in the schema root when strictKeywords is "log"', function () { + describe('strictKeywords = "log"', () => { + it('should log a warning given an unknown keyword in the schema root when strictKeywords is "log"', () => { var output = {} var ajv = new Ajv({ strictKeywords: "log", @@ -50,8 +50,8 @@ describe("strictKeywords option", function () { }) }) - describe("unknown keyword inside schema that has no known keyword in compound keyword", function () { - it("should throw an error given an unknown keyword when strictKeywords is true", function () { + describe("unknown keyword inside schema that has no known keyword in compound keyword", () => { + it("should throw an error given an unknown keyword when strictKeywords is true", () => { var ajv = new Ajv({strictKeywords: true}) var schema = { anyOf: [ @@ -60,7 +60,7 @@ describe("strictKeywords option", function () { }, ], } - should.throw(function () { + should.throw(() => { ajv.compile(schema) }) }) @@ -68,13 +68,13 @@ describe("strictKeywords option", function () { function getLogger(output) { return { - log: function () { + log: () => { throw new Error("log should not be called") }, warn: function (warning) { output.warning = warning }, - error: function () { + error: () => { throw new Error("error should not be called") }, } diff --git a/spec/options/strictNumbers.spec.js b/spec/options/strictNumbers.spec.js index 9f939b2138..d5569d3590 100644 --- a/spec/options/strictNumbers.spec.js +++ b/spec/options/strictNumbers.spec.js @@ -2,19 +2,19 @@ var Ajv = require("../ajv") -describe("structNumbers option", function () { +describe("structNumbers option", () => { var ajv describe("strictNumbers default", testWithoutStrictNumbers(new Ajv())) describe( "strictNumbers = false", testWithoutStrictNumbers(new Ajv({strictNumbers: false})) ) - describe("strictNumbers = true", function () { - beforeEach(function () { + describe("strictNumbers = true", () => { + beforeEach(() => { ajv = new Ajv({strictNumbers: true}) }) - it("should fail validation for NaN/Infinity as type number", function () { + it("should fail validation for NaN/Infinity as type number", () => { var validate = ajv.compile({type: "number"}) validate("1.1").should.equal(false) validate(1.1).should.equal(true) @@ -23,7 +23,7 @@ describe("structNumbers option", function () { validate(Infinity).should.equal(false) }) - it("should fail validation for NaN as type integer", function () { + it("should fail validation for NaN as type integer", () => { var validate = ajv.compile({type: "integer"}) validate("1.1").should.equal(false) validate(1.1).should.equal(false) @@ -35,8 +35,8 @@ describe("structNumbers option", function () { }) function testWithoutStrictNumbers(_ajv) { - return function () { - it("should NOT fail validation for NaN/Infinity as type number", function () { + return () => { + it("should NOT fail validation for NaN/Infinity as type number", () => { var validate = _ajv.compile({type: "number"}) validate("1.1").should.equal(false) validate(1.1).should.equal(true) @@ -45,7 +45,7 @@ function testWithoutStrictNumbers(_ajv) { validate(Infinity).should.equal(true) }) - it("should NOT fail validation for NaN/Infinity as type integer", function () { + it("should NOT fail validation for NaN/Infinity as type integer", () => { var validate = _ajv.compile({type: "integer"}) validate("1.1").should.equal(false) validate(1.1).should.equal(false) diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.js index 54815983f5..2a544dd064 100644 --- a/spec/options/unknownFormats.spec.js +++ b/spec/options/unknownFormats.spec.js @@ -4,20 +4,20 @@ var Ajv = require("../ajv") var should = require("../chai").should() var DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ -describe("unknownFormats option", function () { - describe("= true (default)", function () { - it("should fail schema compilation if unknown format is used", function () { +describe("unknownFormats option", () => { + describe("= true (default)", () => { + it("should fail schema compilation if unknown format is used", () => { test(new Ajv()) test(new Ajv({unknownFormats: true})) function test(ajv) { - should.throw(function () { + should.throw(() => { ajv.compile({format: "unknown"}) }) } }) - it("should fail validation if unknown format is used via $data", function () { + it("should fail validation if unknown format is used via $data", () => { test(new Ajv({$data: true})) test(new Ajv({$data: true, unknownFormats: true})) @@ -40,8 +40,8 @@ describe("unknownFormats option", function () { }) }) - describe('= "ignore (default before 5.0.0)"', function () { - it("should pass schema compilation and be valid if unknown format is used", function () { + describe('= "ignore (default before 5.0.0)"', () => { + it("should pass schema compilation and be valid if unknown format is used", () => { test(new Ajv({unknownFormats: "ignore"})) function test(ajv) { @@ -50,7 +50,7 @@ describe("unknownFormats option", function () { } }) - it("should be valid if unknown format is used via $data", function () { + it("should be valid if unknown format is used via $data", () => { test(new Ajv({$data: true, unknownFormats: "ignore"})) function test(ajv) { @@ -71,21 +71,21 @@ describe("unknownFormats option", function () { }) }) - describe("= [String]", function () { - it("should pass schema compilation and be valid if allowed unknown format is used", function () { + describe("= [String]", () => { + it("should pass schema compilation and be valid if allowed unknown format is used", () => { test(new Ajv({unknownFormats: ["allowed"]})) function test(ajv) { var validate = ajv.compile({format: "allowed"}) validate("anything").should.equal(true) - should.throw(function () { + should.throw(() => { ajv.compile({format: "unknown"}) }) } }) - it("should be valid if allowed unknown format is used via $data", function () { + it("should be valid if allowed unknown format is used via $data", () => { test(new Ajv({$data: true, unknownFormats: ["allowed"]})) function test(ajv) { diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index b73676701a..5e9125107b 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -4,8 +4,8 @@ var Ajv = require("../ajv") var getAjvInstances = require("../ajv_instances") require("../chai").should() -describe("useDefaults options", function () { - it("should replace undefined property with default value", function () { +describe("useDefaults options", () => { + it("should replace undefined property with default value", () => { var instances = getAjvInstances( { allErrors: true, @@ -56,7 +56,7 @@ describe("useDefaults options", function () { } }) - it("should replace undefined item with default value", function () { + it("should replace undefined item with default value", () => { test(new Ajv({useDefaults: true})) test(new Ajv({useDefaults: true, allErrors: true})) @@ -87,7 +87,7 @@ describe("useDefaults options", function () { } }) - it('should apply default in "then" subschema (issue #635)', function () { + it('should apply default in "then" subschema (issue #635)', () => { test(new Ajv({useDefaults: true})) test(new Ajv({useDefaults: true, allErrors: true})) @@ -118,16 +118,16 @@ describe("useDefaults options", function () { } }) - describe("useDefaults: by value / by reference", function () { - describe("using by value", function () { - it("should NOT modify underlying defaults when modifying validated data", function () { + describe("useDefaults: by value / by reference", () => { + describe("using by value", () => { + it("should NOT modify underlying defaults when modifying validated data", () => { test("value", new Ajv({useDefaults: true})) test("value", new Ajv({useDefaults: true, allErrors: true})) }) }) - describe("using by reference", function () { - it("should modify underlying defaults when modifying validated data", function () { + describe("using by reference", () => { + it("should modify underlying defaults when modifying validated data", () => { test("reference", new Ajv({useDefaults: "shared"})) test("reference", new Ajv({useDefaults: "shared", allErrors: true})) }) @@ -165,10 +165,10 @@ describe("useDefaults options", function () { } }) - describe('defaults with "empty" values', function () { + describe('defaults with "empty" values', () => { var schema, data - beforeEach(function () { + beforeEach(() => { schema = { properties: { obj: { @@ -195,7 +195,7 @@ describe("useDefaults options", function () { } }) - it('should NOT assign defaults when useDefaults is true/"shared"', function () { + it('should NOT assign defaults when useDefaults is true/"shared"', () => { test(new Ajv({useDefaults: true})) test(new Ajv({useDefaults: "shared"})) @@ -214,7 +214,7 @@ describe("useDefaults options", function () { } }) - it('should assign defaults when useDefaults = "empty"', function () { + it('should assign defaults when useDefaults = "empty"', () => { var ajv = new Ajv({useDefaults: "empty"}) var validate = ajv.compile(schema) validate(data).should.equal(true) diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index a4f03247b9..dc2b9de86b 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -4,10 +4,10 @@ var Ajv = require("./ajv"), should = require("./chai").should(), getAjvInstances = require("./ajv_instances") -describe("resolve", function () { +describe("resolve", () => { var instances - beforeEach(function () { + beforeEach(() => { instances = getAjvInstances({ allErrors: true, verbose: true, @@ -15,8 +15,8 @@ describe("resolve", function () { }) }) - describe("resolve.ids method", function () { - it("should resolve ids in schema", function () { + describe("resolve.ids method", () => { + it("should resolve ids in schema", () => { // Example from http://json-schema.org/latest/json-schema-core.html#anchor29 var schema = { $id: "http://x.y.z/rootschema.json#", @@ -53,20 +53,20 @@ describe("resolve", function () { required: ["foo", "bar", "baz", "bax"], } - instances.forEach(function (ajv) { + instances.forEach((ajv) => { var validate = ajv.compile(schema) var data = {foo: 1, bar: "abc", baz: true, bax: null} validate(data).should.equal(true) }) }) - it("should throw if the same id resolves to two different schemas", function () { - instances.forEach(function (ajv) { + it("should throw if the same id resolves to two different schemas", () => { + instances.forEach((ajv) => { ajv.compile({ $id: "http://example.com/1.json", type: "integer", }) - should.throw(function () { + should.throw(() => { ajv.compile({ additionalProperties: { $id: "http://example.com/1.json", @@ -75,7 +75,7 @@ describe("resolve", function () { }) }) - should.throw(function () { + should.throw(() => { ajv.compile({ items: { $id: "#int", @@ -90,7 +90,7 @@ describe("resolve", function () { }) }) - it("should resolve ids defined as urn's (issue #423)", function () { + it("should resolve ids defined as urn's (issue #423)", () => { var schema = { type: "object", properties: { @@ -110,16 +110,16 @@ describe("resolve", function () { ip1: "0.0.0.0", ip2: "0.0.0.0", } - instances.forEach(function (ajv) { + instances.forEach((ajv) => { var validate = ajv.compile(schema) validate(data).should.equal(true) }) }) }) - describe("protocol-relative URIs", function () { - it("should resolve fragment", function () { - instances.forEach(function (ajv) { + describe("protocol-relative URIs", () => { + it("should resolve fragment", () => { + instances.forEach((ajv) => { var schema = { $id: "//e.com/types", definitions: { @@ -138,7 +138,7 @@ describe("resolve", function () { describe("missing schema error", function () { this.timeout(4000) - it("should contain missingRef and missingSchema", function () { + it("should contain missingRef and missingSchema", () => { testMissingSchemaError({ baseId: "http://example.com/1.json", ref: "http://another.com/int.json", @@ -147,7 +147,7 @@ describe("resolve", function () { }) }) - it("should resolve missingRef and missingSchema relative to base id", function () { + it("should resolve missingRef and missingSchema relative to base id", () => { testMissingSchemaError({ baseId: "http://example.com/folder/1.json", ref: "int.json", @@ -156,7 +156,7 @@ describe("resolve", function () { }) }) - it("should resolve missingRef and missingSchema relative to base id from root", function () { + it("should resolve missingRef and missingSchema relative to base id from root", () => { testMissingSchemaError({ baseId: "http://example.com/folder/1.json", ref: "/int.json", @@ -165,7 +165,7 @@ describe("resolve", function () { }) }) - it("missingRef should and missingSchema should NOT include JSON path (hash fragment)", function () { + it("missingRef should and missingSchema should NOT include JSON path (hash fragment)", () => { testMissingSchemaError({ baseId: "http://example.com/1.json", ref: "int.json#/definitions/positive", @@ -174,7 +174,7 @@ describe("resolve", function () { }) }) - it("should throw missing schema error if same path exist in the current schema but id is different (issue #220)", function () { + it("should throw missing schema error if same path exist in the current schema but id is different (issue #220)", () => { testMissingSchemaError({ baseId: "http://example.com/parent.json", ref: "object.json#/properties/a", @@ -184,7 +184,7 @@ describe("resolve", function () { }) function testMissingSchemaError(opts) { - instances.forEach(function (ajv) { + instances.forEach((ajv) => { try { ajv.compile({ $id: opts.baseId, @@ -198,7 +198,7 @@ describe("resolve", function () { } }) - describe("inline referenced schemas without refs in them", function () { + describe("inline referenced schemas without refs in them", () => { var schemas = [ {$id: "http://e.com/obj.json#", properties: {a: {$ref: "int.json#"}}}, {$id: "http://e.com/int.json#", type: "integer", minimum: 2, maximum: 4}, @@ -210,27 +210,27 @@ describe("resolve", function () { {$id: "http://e.com/list.json#", items: {$ref: "obj.json#"}}, ] - it("by default should inline schema if it doesn't contain refs", function () { + it("by default should inline schema if it doesn't contain refs", () => { var ajv = new Ajv({schemas: schemas}) testSchemas(ajv, true) }) - it("should NOT inline schema if option inlineRefs == false", function () { + it("should NOT inline schema if option inlineRefs == false", () => { var ajv = new Ajv({schemas: schemas, inlineRefs: false}) testSchemas(ajv, false) }) - it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", function () { + it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { var ajv = new Ajv({schemas: schemas, inlineRefs: 3}) testSchemas(ajv, true) }) - it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", function () { + it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { var ajv = new Ajv({schemas: schemas, inlineRefs: 2}) testSchemas(ajv, false) }) - it("should avoid schema substitution when refs are inlined (issue #77)", function () { + it("should avoid schema substitution when refs are inlined (issue #77)", () => { var ajv = new Ajv({verbose: true}) var schemaMessage = { From dd5a08b561d66e1522b4385f9c6043f9b5e0dc55 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 16:37:11 +0100 Subject: [PATCH 038/322] style: eqeqeq --- .eslintrc.yml | 2 +- lib/ajv.js | 8 ++++---- lib/compile/index.js | 8 ++++---- lib/compile/resolve.js | 16 ++++++++-------- lib/compile/util.ts | 4 ++-- lib/keyword.ts | 4 ++-- scripts/bundle.js | 4 ++-- scripts/compile-dots.js | 4 ++-- spec/async.spec.js | 2 +- spec/async_schemas.spec.js | 4 ++-- spec/async_validate.spec.js | 6 +++--- spec/browser_test_suite.js | 2 +- spec/custom.spec.js | 6 +++--- spec/errors.spec.js | 16 ++++++++-------- spec/options/useDefaults.spec.js | 4 ++-- 15 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 8229515a87..565f1fc850 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -39,6 +39,7 @@ rules: curly: [error, multi-line, consistent] dot-location: [error, property] dot-notation: error + eqeqeq: [error, smart] id-match: error linebreak-style: [error, unix] new-cap: error @@ -68,5 +69,4 @@ rules: no-control-regex: 0 no-useless-escape: error no-void: error - # eqeqeq: [error, smart] # no-var: error diff --git a/lib/ajv.js b/lib/ajv.js index 6aae75ef41..2f0d01d1fe 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -73,7 +73,7 @@ function Ajv(opts) { opts.loopRequired = opts.loopRequired || Infinity opts.loopEnum = opts.loopEnum || Infinity - if (opts.errorDataPath == "property") opts._errorDataPathProperty = true + if (opts.errorDataPath === "property") opts._errorDataPathProperty = true if (opts.serialize === undefined) opts.serialize = stableStringify this._metaOpts = getMetaSchemaOptions(this) @@ -185,7 +185,7 @@ function validateSchema(schema, throwOrLogError) { var valid = this.validate($schema, schema) if (!valid && throwOrLogError) { var message = "schema is invalid: " + this.errorsText() - if (this._opts.validateSchema == "log") this.logger.error(message) + if (this._opts.validateSchema === "log") this.logger.error(message) else throw new Error(message) } return valid @@ -314,7 +314,7 @@ function _addSchema(schema, skipValidation, meta, shouldAddSchema) { var recursiveMeta if ( willValidate && - !(recursiveMeta = id && id == resolve.normalizeId(schema.$schema)) + !(recursiveMeta = id && id === resolve.normalizeId(schema.$schema)) ) { this.validateSchema(schema, true) } @@ -329,7 +329,7 @@ function _addSchema(schema, skipValidation, meta, shouldAddSchema) { meta: meta, }) - if (id[0] != "#" && shouldAddSchema) this._refs[id] = schemaObj + if (id[0] !== "#" && shouldAddSchema) this._refs[id] = schemaObj this._cache.put(cacheKey, schemaObj) if (willValidate && recursiveMeta) this.validateSchema(schema, true) diff --git a/lib/compile/index.js b/lib/compile/index.js index 8f6ad6c14a..c9d19dc661 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -78,8 +78,8 @@ function compile(schema, root, localRefs, baseId) { } function localCompile(_schema, _root, localRefs, baseId) { - var isRoot = !_root || (_root && _root.schema == _schema) - if (_root.schema != root.schema) { + var isRoot = !_root || (_root && _root.schema === _schema) + if (_root.schema !== root.schema) { return compile.call(self, _schema, _root, localRefs, baseId) } @@ -279,7 +279,7 @@ function compile(schema, root, localRefs, baseId) { var message = "keyword schema is invalid: " + self.errorsText(validateSchema.errors) - if (self._opts.validateSchema == "log") self.logger.error(message) + if (self._opts.validateSchema === "log") self.logger.error(message) else throw new Error(message) } } @@ -362,7 +362,7 @@ function compIndex(schema, root, baseId) { /* jshint validthis: true */ for (var i = 0; i < this._compilations.length; i++) { var c = this._compilations[i] - if (c.schema == schema && c.root == root && c.baseId == baseId) return i + if (c.schema === schema && c.root === root && c.baseId === baseId) return i } return -1 } diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index 0db6153a89..9688507559 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -80,7 +80,7 @@ function resolveSchema(root, ref) { refVal = this._schemas[id] if (refVal instanceof SchemaObject) { if (!refVal.validate) this._compile(refVal) - if (id == normalizeId(ref)) { + if (id === normalizeId(ref)) { return {schema: refVal, root: root, baseId: baseId} } root = refVal @@ -119,7 +119,7 @@ var PREVENT_SCOPE_CHANGE = toHash([ function getJsonPointer(parsedRef, baseId, schema, root) { /* jshint validthis: true */ parsedRef.fragment = parsedRef.fragment || "" - if (parsedRef.fragment.slice(0, 1) != "/") return + if (parsedRef.fragment.slice(0, 1) !== "/") return var parts = parsedRef.fragment.split("/") for (var i = 1; i < parts.length; i++) { @@ -181,7 +181,7 @@ function checkNoRef(schema) { } } else { for (var key in schema) { - if (key == "$ref") return false + if (key === "$ref") return false item = schema[key] if (typeof item == "object" && !checkNoRef(item)) return false } @@ -196,17 +196,17 @@ function countKeys(schema) { for (var i = 0; i < schema.length; i++) { item = schema[i] if (typeof item == "object") count += countKeys(item) - if (count == Infinity) return Infinity + if (count === Infinity) return Infinity } } else { for (var key in schema) { - if (key == "$ref") return Infinity + if (key === "$ref") return Infinity if (SIMPLE_INLINED[key]) { count++ } else { item = schema[key] if (typeof item == "object") count += countKeys(item) + 1 - if (count == Infinity) return Infinity + if (count === Infinity) return Infinity } } } @@ -272,8 +272,8 @@ function resolveIds(schema) { if (!equal(sch, refVal.schema)) { throw new Error('id "' + id + '" resolves to more than one schema') } - } else if (id != normalizeId(fullPath)) { - if (id[0] == "#") { + } else if (id !== normalizeId(fullPath)) { + if (id[0] === "#") { if (localRefs[id] && !equal(sch, localRefs[id])) { throw new Error( 'id "' + id + '" resolves to more than one schema' diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 15d83e4bde..3aaf30ea45 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -151,7 +151,7 @@ export function schemaHasRulesExcept( exceptKeyword: string ): boolean | undefined { if (typeof schema == "boolean") return !schema && exceptKeyword !== "not" - for (const key in schema) if (key != exceptKeyword && rules[key]) return true + for (const key in schema) if (key !== exceptKeyword && rules[key]) return true } // TODO rules, schema? @@ -209,7 +209,7 @@ export function getData($data: string, lvl: number, paths: string[]): string { if (!matches) throw new Error("Invalid JSON-pointer: " + $data) const up: number = +matches[1] jsonPointer = matches[2] - if (jsonPointer == "#") { + if (jsonPointer === "#") { if (up >= lvl) { throw new Error( "Cannot access property/index " + diff --git a/lib/keyword.ts b/lib/keyword.ts index 027fb617c0..5556b8984e 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -102,7 +102,7 @@ export function addKeyword( var ruleGroup for (var i = 0; i < RULES.length; i++) { var rg = RULES[i] - if (rg.type == dataType) { + if (rg.type === dataType) { ruleGroup = rg break } @@ -265,7 +265,7 @@ export function removeKeyword(keyword: string): object { for (var i = 0; i < RULES.length; i++) { var rules = RULES[i].rules for (var j = 0; j < rules.length; j++) { - if (rules[j].keyword == keyword) { + if (rules[j].keyword === keyword) { rules.splice(j, 1) break } diff --git a/scripts/bundle.js b/scripts/bundle.js index 8325ec848d..06d883fd06 100644 --- a/scripts/bundle.js +++ b/scripts/bundle.js @@ -10,7 +10,7 @@ var pkg = process.argv[2], compress = process.argv[4] var packageDir = path.join(__dirname, "..") -if (pkg != ".") packageDir = path.join(packageDir, "node_modules", pkg) +if (pkg !== ".") packageDir = path.join(packageDir, "node_modules", pkg) var json = require(path.join(packageDir, "package.json")) @@ -47,7 +47,7 @@ browserify(bOpts) var compressOpts = compress.split(",") for (var i = 0, il = compressOpts.length; i < il; ++i) { var pair = compressOpts[i].split("=") - uglifyOpts.compress[pair[0]] = pair.length < 1 || pair[1] != "false" + uglifyOpts.compress[pair[0]] = pair.length < 1 || pair[1] !== "false" } } if (standalone) { diff --git a/scripts/compile-dots.js b/scripts/compile-dots.js index ccb92a39ce..a72e7ecb22 100644 --- a/scripts/compile-dots.js +++ b/scripts/compile-dots.js @@ -67,7 +67,7 @@ files.forEach((f) => { v = v.replace(/\$/g, "\\$$") var regexp = new RegExp(v + "[^A-Za-z0-9_$]", "g") var count = occurrences(regexp) - if (count == 1) { + if (count === 1) { regexp = new RegExp("var\\s+" + v + "\\s*=[^;]+;|var\\s+" + v + ";") code = code.replace(regexp, "") } @@ -76,7 +76,7 @@ files.forEach((f) => { function removeAlwaysFalsyInOr() { var countUsed = occurrences(ERROR_KEYWORD) var countOr = occurrences(ERROR_KEYWORD_OR) - if (countUsed == countOr + 1) code = code.replace(ERROR_KEYWORD_OR, "") + if (countUsed === countOr + 1) code = code.replace(ERROR_KEYWORD_OR, "") } function occurrences(regexp) { diff --git a/spec/async.spec.js b/spec/async.spec.js index 3c31d31f06..da3b83a7ee 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -196,7 +196,7 @@ describe("compileAsync method", () => { ajv.addKeyword("myFooBar", { type: "string", validate: function (sch, data) { - return sch == data + return sch === data }, }) diff --git a/spec/async_schemas.spec.js b/spec/async_schemas.spec.js index aab643bb63..2869ec9820 100644 --- a/spec/async_schemas.spec.js +++ b/spec/async_schemas.spec.js @@ -63,9 +63,9 @@ function addAsyncFormatsAndKeywords(ajv) { } function checkWordOnServer(str) { - return str == "tomorrow" + return str === "tomorrow" ? Promise.resolve(true) - : str == "manana" + : str === "manana" ? Promise.resolve(false) : Promise.reject(new Error("unknown word")) } diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index 7d53f86f62..83dfc61214 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -406,9 +406,9 @@ describe("async schemas, formats and keywords", function () { }) function checkWordOnServer(str) { - return str == "tomorrow" + return str === "tomorrow" ? Promise.resolve(true) - : str == "manana" + : str === "manana" ? Promise.resolve(false) : Promise.reject(new Error("unknown word")) } @@ -455,7 +455,7 @@ function checkNotValid(p) { }) .catch((err) => { err.should.be.instanceof(Error) - if (err.message == SHOULD_BE_INVALID) throw err + if (err.message === SHOULD_BE_INVALID) throw err return err }) } diff --git a/spec/browser_test_suite.js b/spec/browser_test_suite.js index d09b49b5e2..464cb95948 100644 --- a/spec/browser_test_suite.js +++ b/spec/browser_test_suite.js @@ -2,7 +2,7 @@ module.exports = function (suite) { suite.forEach((file) => { - if (file.name.indexOf("optional/format") == 0) { + if (file.name.indexOf("optional/format") === 0) { file.name = file.name.replace("optional/", "") } file.test = file.module diff --git a/spec/custom.spec.js b/spec/custom.spec.js index d0f944b84d..192c761e2b 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -364,7 +364,7 @@ describe("Custom keywords", () => { for (var prop in _schema) { var path = prop.split(".") var properties = {} - if (path.length == 1) { + if (path.length === 1) { properties[prop] = _schema[prop] } else { var deepProperties = {} @@ -374,7 +374,7 @@ describe("Custom keywords", () => { expanded.push({properties: properties}) } - return expanded.length == 1 ? expanded[0] : {allOf: expanded} + return expanded.length === 1 ? expanded[0] : {allOf: expanded} } }) }) @@ -983,7 +983,7 @@ describe("Custom keywords", () => { function validateRangeSchema(schema, parentSchema) { var schemaValid = Array.isArray(schema) && - schema.length == 2 && + schema.length === 2 && typeof schema[0] == "number" && typeof schema[1] == "number" if (!schemaValid) { diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 97db2a6081..b4e7be4517 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -118,9 +118,9 @@ describe("Validation errors", () => { {additionalProperty: "quux"} ) - if (errorDataPath == "property") { + if (errorDataPath === "property") { fullValidate.errors - .filter((err) => err.keyword == "additionalProperties") + .filter((err) => err.keyword === "additionalProperties") .map((err) => fullAjv._opts.jsonPointers ? err.dataPath.substr(1) @@ -607,20 +607,20 @@ describe("Validation errors", () => { function pathFunc(errorDataPath) { return function (dataPath) { - return errorDataPath == "property" ? dataPath : "" + return errorDataPath === "property" ? dataPath : "" } } function requiredFunc(errorDataPath) { return function (prop) { - return errorDataPath == "property" + return errorDataPath === "property" ? "is a required property" : "should have required property '" + prop + "'" } } function additionalFunc(errorDataPath) { - return errorDataPath == "property" + return errorDataPath === "property" ? "is an invalid additional property" : "should NOT have additional properties" } @@ -798,7 +798,7 @@ describe("Validation errors", () => { "", "property name 'foo' is invalid" ) - if (numErrors == 4) { + if (numErrors === 4) { shouldBeError( validate.errors[2], "pattern", @@ -1112,7 +1112,7 @@ describe("Validation errors", () => { {multipleOf: multipleOf} ) - if (numErrors == 2) { + if (numErrors === 2) { err = validate.errors[1] shouldBeError( err, @@ -1150,7 +1150,7 @@ describe("Validation errors", () => { var expectedErrors = _ajv._opts.allErrors ? 2 : 1 shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors) testTypeError(0, "/1") - if (expectedErrors == 2) testTypeError(1, "/2") + if (expectedErrors === 2) testTypeError(1, "/2") function testTypeError(i, dataPath) { var err = validate.errors[i] diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index 5e9125107b..6d1c1f8f67 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -155,9 +155,9 @@ describe("useDefaults options", () => { var data2 = {} validate(data2).should.equal(true) - if (useDefaultsMode == "reference") { + if (useDefaultsMode === "reference") { data2.items.should.eql(["a-default", "another-value"]) - } else if (useDefaultsMode == "value") { + } else if (useDefaultsMode === "value") { data2.items.should.eql(["a-default"]) } else { throw new Error("unknown useDefaults mode") From 39183068e384ef8409851041351d89ee163281e4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 22:02:56 +0100 Subject: [PATCH 039/322] refactor: uniqueItems to typescript --- lib/compile/index.js | 20 +-- lib/compile/rules.js | 12 +- lib/compile/util.ts | 49 ++----- lib/dot/errors.def | 3 - lib/dot/uniqueItems.jst | 62 --------- lib/dotjs/index.js | 1 - lib/keyword.ts | 61 ++++----- lib/types.ts | 31 ++--- lib/vocabularies/validation/index.ts | 1 + lib/vocabularies/validation/limit.ts | 5 +- lib/vocabularies/validation/limitLength.ts | 3 +- lib/vocabularies/validation/multipleOf.ts | 3 +- lib/vocabularies/validation/uniqueItems.ts | 146 +++++++++++++++++++++ spec/extras/$data/uniqueItems.json | 2 +- 14 files changed, 206 insertions(+), 193 deletions(-) delete mode 100644 lib/dot/uniqueItems.jst create mode 100644 lib/vocabularies/validation/uniqueItems.ts diff --git a/lib/compile/index.js b/lib/compile/index.js index c9d19dc661..56ff190387 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -263,22 +263,16 @@ function compile(schema, root, localRefs, baseId) { var deps = rule.definition.dependencies if ( deps && - !deps.every((keyword) => - Object.prototype.hasOwnProperty.call(parentSchema, keyword) - ) + !deps.every((keyword) => Object.prototype.hasOwnProperty.call(parentSchema, keyword)) ) { - throw new Error( - "parent schema must have all required keywords: " + deps.join(",") - ) + throw new Error("parent schema must have all required keywords: " + deps.join(",")) } var validateSchema = rule.definition.validateSchema if (validateSchema) { var valid = validateSchema(schema) if (!valid) { - var message = - "keyword schema is invalid: " + - self.errorsText(validateSchema.errors) + var message = "keyword schema is invalid: " + self.errorsText(validateSchema.errors) if (self._opts.validateSchema === "log") self.logger.error(message) else throw new Error(message) } @@ -368,9 +362,7 @@ function compIndex(schema, root, baseId) { } function patternCode(i, patterns) { - return ( - "var pattern" + i + " = new RegExp(" + toQuotedString(patterns[i]) + ");" - ) + return "var pattern" + i + " = new RegExp(" + toQuotedString(patterns[i]) + ");" } function defaultCode(i) { @@ -378,9 +370,7 @@ function defaultCode(i) { } function refValCode(i, refVal) { - return refVal[i] === undefined - ? "" - : "var refVal" + i + " = refVal[" + i + "];" + return refVal[i] === undefined ? "" : "var refVal" + i + " = refVal[" + i + "];" } function customRuleCode(i) { diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 574ff37a9a..7f8e071fc8 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -8,7 +8,7 @@ module.exports = function rules() { {type: "string", rules: ["format"]}, { type: "array", - rules: ["items", "contains", "uniqueItems"], + rules: ["items", "contains"], }, { type: "object", @@ -42,15 +42,7 @@ module.exports = function rules() { "then", "else", ] - var TYPES = [ - "number", - "integer", - "string", - "array", - "object", - "boolean", - "null", - ] + var TYPES = ["number", "integer", "string", "array", "object", "boolean", "null"] RULES.all = toHash(ALL) RULES.types = toHash(TYPES) diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 3aaf30ea45..a455449d1b 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -22,8 +22,8 @@ module.exports = { export function checkDataType( dataType: string, data: string, - strictNumbers: boolean, - negate: boolean + strictNumbers?: boolean, + negate?: boolean ): string { const EQ = negate ? " !== " : " === " const OK = negate ? "!" : "" @@ -33,10 +33,7 @@ export function checkDataType( case "array": return OK + `Array.isArray(${data})` case "object": - return ( - OK + - `(${data} && typeof ${data} === "object" && !Array.isArray(${data}))` - ) + return OK + `(${data} && typeof ${data} === "object" && !Array.isArray(${data}))` case "integer": return ( OK + @@ -44,21 +41,13 @@ export function checkDataType( (strictNumbers ? ` && isFinite(${data}))` : ")") ) case "number": - return ( - OK + - `(typeof ${data} === "number"` + - (strictNumbers ? `&& isFinite(${data}))` : ")") - ) + return OK + `(typeof ${data} === "number"` + (strictNumbers ? `&& isFinite(${data}))` : ")") default: return `typeof ${data} ${EQ} "${dataType}"` } } -export function checkDataTypes( - dataTypes: string[], - data: string, - strictNumbers: boolean -): string { +export function checkDataTypes(dataTypes: string[], data: string, strictNumbers?: boolean): string { if (dataTypes.length === 1) { return checkDataType(dataTypes[0], data, strictNumbers, true) } @@ -136,10 +125,7 @@ export function varReplace(str: string, dataVar: string, expr: string): string { } // TODO rules, schema? -export function schemaHasRules( - schema: object | boolean, - rules: object -): boolean | undefined { +export function schemaHasRules(schema: object | boolean, rules: object): boolean | undefined { if (typeof schema == "boolean") return !schema for (const key in schema) if (rules[key]) return true } @@ -155,10 +141,7 @@ export function schemaHasRulesExcept( } // TODO rules, schema? -export function schemaUnknownRules( - schema: object, - rules: object -): string | undefined { +export function schemaUnknownRules(schema: object, rules: object): string | undefined { if (typeof schema === "boolean") return for (const key in schema) if (!rules[key]) return key } @@ -174,19 +157,14 @@ export function getPathExpr( isNumber?: boolean ): string { const path = jsonPointers // false by default - ? `'/' + ${expr}` + - (isNumber ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") + ? `'/' + ${expr}` + (isNumber ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") : isNumber ? `'[' + ${expr} + ']'` : `'[\\'' + ${expr} + '\\']'` return joinPaths(currentPath, path) } -export function getPath( - currentPath: string, - prop: string, - jsonPointers?: boolean -): string { +export function getPath(currentPath: string, prop: string, jsonPointers?: boolean): string { const path = jsonPointers // false by default ? toQuotedString("/" + escapeJsonPointer(prop)) : toQuotedString(getProperty(prop)) @@ -212,19 +190,14 @@ export function getData($data: string, lvl: number, paths: string[]): string { if (jsonPointer === "#") { if (up >= lvl) { throw new Error( - "Cannot access property/index " + - up + - " levels up, current level is " + - lvl + "Cannot access property/index " + up + " levels up, current level is " + lvl ) } return paths[lvl - up] } if (up > lvl) { - throw new Error( - "Cannot access data " + up + " levels up, current level is " + lvl - ) + throw new Error("Cannot access data " + up + " levels up, current level is " + lvl) } data = "data" + (lvl - up || "") if (!jsonPointer) return data diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 7f018caaaa..32dcfaa39d 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -103,7 +103,6 @@ propertyNames: "'property name \\'{{=$invalidName}}\\' is invalid'", required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", type: "'should be {{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}'", - uniqueItems: "'should NOT have duplicate items (items ## ' + j + ' and ' + i + ' are identical)'", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", switch: "'should pass \"switch\" keyword validation'", @@ -130,7 +129,6 @@ propertyNames: "validate.schema{{=$schemaPath}}", required: "validate.schema{{=$schemaPath}}", type: "validate.schema{{=$schemaPath}}", - uniqueItems: "{{#def.schemaRefOrVal}}", custom: "validate.schema{{=$schemaPath}}", patternRequired: "validate.schema{{=$schemaPath}}", switch: "validate.schema{{=$schemaPath}}", @@ -156,7 +154,6 @@ propertyNames: "{ propertyName: '{{=$invalidName}}' }", required: "{ missingProperty: '{{=$missingProperty}}' }", type: "{ type: '{{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }", - uniqueItems: "{ i: i, j: j }", custom: "{ keyword: '{{=$rule.keyword}}' }", patternRequired: "{ missingPattern: '{{=$missingPattern}}' }", switch: "{ caseIndex: {{=$caseIndex}} }", diff --git a/lib/dot/uniqueItems.jst b/lib/dot/uniqueItems.jst deleted file mode 100644 index e69b8308d2..0000000000 --- a/lib/dot/uniqueItems.jst +++ /dev/null @@ -1,62 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - - -{{? ($schema || $isData) && it.opts.uniqueItems !== false }} - {{? $isData }} - var {{=$valid}}; - if ({{=$schemaValue}} === false || {{=$schemaValue}} === undefined) - {{=$valid}} = true; - else if (typeof {{=$schemaValue}} != 'boolean') - {{=$valid}} = false; - else { - {{?}} - - var i = {{=$data}}.length - , {{=$valid}} = true - , j; - if (i > 1) { - {{ - var $itemType = it.schema.items && it.schema.items.type - , $typeIsArray = Array.isArray($itemType); - }} - {{? !$itemType || $itemType == 'object' || $itemType == 'array' || - ($typeIsArray && ($itemType.indexOf('object') >= 0 || $itemType.indexOf('array') >= 0)) }} - outer: - for (;i--;) { - for (j = i; j--;) { - if (equal({{=$data}}[i], {{=$data}}[j])) { - {{=$valid}} = false; - break outer; - } - } - } - {{??}} - var itemIndices = {}, item; - for (;i--;) { - var item = {{=$data}}[i]; - {{ var $method = 'checkDataType' + ($typeIsArray ? 's' : ''); }} - if ({{= it.util[$method]($itemType, 'item', it.opts.strictNumbers, true) }}) continue; - {{? $typeIsArray}} - if (typeof item == 'string') item = '"' + item; - {{?}} - if (typeof itemIndices[item] == 'number') { - {{=$valid}} = false; - j = itemIndices[item]; - break; - } - itemIndices[item] = i; - } - {{?}} - } - - {{? $isData }} } {{?}} - - if (!{{=$valid}}) { - {{# def.error:'uniqueItems' }} - } {{? $breakOnError }} else { {{?}} -{{??}} - {{? $breakOnError }} if (true) { {{?}} -{{?}} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index a1fff68f09..caf3a34144 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -16,6 +16,5 @@ module.exports = { properties: require("./properties"), propertyNames: require("./propertyNames"), required: require("./required"), - uniqueItems: require("./uniqueItems"), validate: require("./validate"), } diff --git a/lib/keyword.ts b/lib/keyword.ts index 5556b8984e..22a21790cc 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -4,6 +4,7 @@ import { ErrorObject, ValidateFunction, CompilationContext, + KeywordContext, } from "./types" import {getData, getProperty, toQuotedString} from "./compile/util" @@ -20,16 +21,11 @@ const definitionSchema = require("./definition_schema") * @param {Boolean} _skipValidation skip definition validation * @return {Ajv} this for method chaining */ -export function addVocabulary( - definitions: Vocabulary, - _skipValidation?: boolean -): object { +export function addVocabulary(definitions: Vocabulary, _skipValidation?: boolean): object { // TODO return type Ajv for (const def of definitions) { if (!def.keyword) { - throw new Error( - 'Vocabulary keywords must have "keyword" property in definition' - ) + throw new Error('Vocabulary keywords must have "keyword" property in definition') } if (Array.isArray(def.keyword)) { for (const keyword of def.keyword) { @@ -134,47 +130,36 @@ export function addKeyword( * @param {String} keyword pre-defined or custom keyword. * @return {String} compiled rule code. */ -function ruleCode( - it: CompilationContext, - keyword: string /*, ruleType */ -): string { +function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): string { const schema = it.schema[keyword] - const { - schemaType, - code, - error, - $data: $defData, - }: KeywordDefinition = this.definition + const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition if (!code) throw new Error('"code" must be defined') let schemaCode: string | number | boolean let out = "" const $data = $defData && it.opts.$data && schema && schema.$data if ($data) { schemaCode = it.scope.getName("schema") - out += `const ${schemaCode} = ${getData( - $data, - it.dataLevel, - it.dataPathArr - )};` + out += `const ${schemaCode} = ${getData($data, it.dataLevel, it.dataPathArr)};` } else { if ( schemaType && - !(schemaType === "array" - ? Array.isArray(schema) - : typeof schema === schemaType) + !(schemaType === "array" ? Array.isArray(schema) : typeof schema === schemaType) ) { throw new Error(`${keyword} must be ${schemaType}`) } schemaCode = schemaRefOrVal() } const data = "data" + (it.dataLevel || "") - const cxt = { - fail, + const cxt: KeywordContext = { write, + fail, + ok, + errorParams, keyword, data, $data, schema, + parentSchema: it.schema, schemaCode, scope: it.scope, usePattern: it.usePattern, @@ -185,12 +170,23 @@ function ruleCode( return out function write(str: string): void { - out += str + out += str + "\n" } function fail(condition: string): void { out += `if (${condition}) { ${reportError()} }` if (!it.opts.allErrors) out += `else {` + out += "\n" + } + + function ok(condition?: string): void { + if (condition) out += `if (!(${condition})) { ${reportError()} }` + if (!it.opts.allErrors) out += condition ? `else {` : `if (true) {` + out += "\n" + } + + function errorParams(obj: any) { + cxt.params = obj } function reportError(): string { @@ -286,10 +282,7 @@ export interface KeywordValidator { * @param {Boolean} throwError true to throw exception if definition is invalid * @return {boolean} validation result */ -export const validateKeyword: KeywordValidator = function ( - definition, - throwError -) { +export const validateKeyword: KeywordValidator = function (definition, throwError) { validateKeyword.errors = null var v: ValidateFunction = (this._validateKeyword = this._validateKeyword || this.compile(definitionSchema, true)) @@ -297,9 +290,7 @@ export const validateKeyword: KeywordValidator = function ( if (v(definition)) return true validateKeyword.errors = v.errors if (throwError) { - throw new Error( - "custom keyword definition is invalid: " + this.errorsText(v.errors) - ) + throw new Error("custom keyword definition is invalid: " + this.errorsText(v.errors)) } return false } diff --git a/lib/types.ts b/lib/types.ts index 76c50de23e..ba842bc953 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -139,26 +139,13 @@ export interface KeywordDefinition { valid?: boolean // at least one of the following properties should be present validate?: SchemaValidateFunction | ValidateFunction - compile?: ( - schema: any, - parentSchema: object, - it: CompilationContext - ) => ValidateFunction - macro?: ( - schema: any, - parentSchema: object, - it: CompilationContext - ) => object | boolean - inline?: ( - it: CompilationContext, - keyword: string, - schema: any, - parentSchema: object - ) => string + compile?: (schema: any, parentSchema: object, it: CompilationContext) => ValidateFunction + macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean + inline?: (it: CompilationContext, keyword: string, schema: any, parentSchema: object) => string code?: (cxt: KeywordContext) => string | void error?: { - message: (cxt: KeywordContext) => string - params: (cxt: KeywordContext) => string + message: (cxt: KeywordContext, params?: any) => string + params: (cxt: KeywordContext, params?: any) => string } validateSchema?: ValidateFunction } @@ -167,15 +154,19 @@ export type Vocabulary = KeywordDefinition[] export interface KeywordContext { fail: (condition: string) => void + ok: (condition?: string) => void write: (str: string) => void usePattern: (str: string) => string + errorParams: (obj: any) => void scope: Scope keyword: string data: string $data?: string | false schema: any + parentSchema: any schemaCode: string | number | boolean opts: Options + params?: any } export type FormatMode = "fast" | "full" @@ -186,9 +177,7 @@ export type FormatValidator = (data: T) => boolean export type FormatCompare = (data1: T, data2: T) => boolean -export type AsyncFormatValidator = ( - data: T -) => PromiseLike +export type AsyncFormatValidator = (data: T) => PromiseLike export interface FormatDefinition { type: T extends string ? "string" : "number" diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 1f2ba75d4a..a5901e57f5 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -11,6 +11,7 @@ const validation: Vocabulary = [ require("./limitProperties"), // array require("./limitItems"), + require("./uniqueItems"), // any require("./const"), require("./enum"), diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index ee11d57800..94fb4ca305 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -15,13 +15,12 @@ const def: KeywordDefinition = { $data: true, code({fail, keyword, data, $data, schemaCode}) { const dnt = dataNotType(schemaCode, def.schemaType, $data) - fail(dnt + data + OPS[keyword].fail + schemaCode + ` || ${data}!==${data}`) + fail(dnt + data + OPS[keyword].fail + schemaCode + ` || isNaN(${data})`) }, error: { message: ({keyword, $data, schemaCode}) => `"should be ${OPS[keyword].ok} ${appendSchema(schemaCode, $data)}`, - params: ({keyword, schemaCode}) => - `{comparison: "${OPS[keyword].ok}", limit: ${schemaCode}}`, + params: ({keyword, schemaCode}) => `{comparison: "${OPS[keyword].ok}", limit: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 2914c5f589..123168bb5b 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -9,8 +9,7 @@ const def: KeywordDefinition = { code({fail, keyword, data, $data, schemaCode, opts}) { const op = keyword == "maxLength" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) - const len = - opts.unicode === false ? `${data}.length` : `ucs2length(${data})` + const len = opts.unicode === false ? `${data}.length` : `ucs2length(${data})` fail(dnt + len + op + schemaCode) }, error: { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 9522994868..39b9f7b972 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -17,8 +17,7 @@ const def: KeywordDefinition = { fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) }, error: { - message: ({$data, schemaCode}) => - `"should be multiple of ${appendSchema(schemaCode, $data)}`, + message: ({$data, schemaCode}) => `"should be multiple of ${appendSchema(schemaCode, $data)}`, params: ({schemaCode}) => `{multipleOf: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts new file mode 100644 index 0000000000..1943b06343 --- /dev/null +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -0,0 +1,146 @@ +// {{# def.definitions }} +// {{# def.errors }} +// {{# def.setupKeyword }} +// {{# def.$data }} + +// {{? ($schema || $isData) && it.opts.uniqueItems !== false }} +// {{? $isData }} +// var {{=$valid}}; +// if ({{=$schemaValue}} === false || {{=$schemaValue}} === undefined) +// {{=$valid}} = true; +// else if (typeof {{=$schemaValue}} != 'boolean') +// {{=$valid}} = false; +// else { +// {{?}} + +// var i = {{=$data}}.length +// , {{=$valid}} = true +// , j; +// if (i > 1) { +// {{ +// var $itemType = it.schema.items && it.schema.items.type +// , $typeIsArray = Array.isArray($itemType); +// }} +// {{? !$itemType || $itemType == 'object' || $itemType == 'array' || +// ($typeIsArray && ($itemType.indexOf('object') >= 0 || $itemType.indexOf('array') >= 0)) }} +// outer: +// for (;i--;) { +// for (j = i; j--;) { +// if (equal({{=$data}}[i], {{=$data}}[j])) { +// {{=$valid}} = false; +// break outer; +// } +// } +// } +// {{??}} +// var itemIndices = {}, item; +// for (;i--;) { +// var item = {{=$data}}[i]; +// {{ var $method = 'checkDataType' + ($typeIsArray ? 's' : ''); }} +// if ({{= it.util[$method]($itemType, 'item', it.opts.strictNumbers, true) }}) continue; +// {{? $typeIsArray}} +// if (typeof item == 'string') item = '"' + item; +// {{?}} +// if (typeof itemIndices[item] == 'number') { +// {{=$valid}} = false; +// j = itemIndices[item]; +// break; +// } +// itemIndices[item] = i; +// } +// {{?}} +// } + +// {{? $isData }} } {{?}} + +// if (!{{=$valid}}) { +// {{# def.error:'uniqueItems' }} +// } {{? $breakOnError }} else { {{?}} +// {{??}} +// {{? $breakOnError }} if (true) { {{?}} +// {{?}} + +import {KeywordDefinition} from "../../types" +import {checkDataType, checkDataTypes} from "../../compile/util" + +const def: KeywordDefinition = { + keyword: "uniqueItems", + type: "array", + schemaType: "boolean", + $data: true, + code({write, fail, ok, errorParams, scope, data, $data, schema, parentSchema, schemaCode, opts}) { + if (opts.uniqueItems === false || !($data || schema)) return ok() + const i = scope.getName("i") + const j = scope.getName("j") + errorParams({i, j}) + const valid = scope.getName("valid") + write(`let ${valid}, ${i}, ${j};`) + if ($data) { + write( + `if (${schemaCode} === false || ${schemaCode} === undefined) + ${valid} = true; + else if (typeof ${schemaCode} != "boolean") + ${valid} = false; + else {` + ) + } + const itemType = parentSchema.items?.type + write( + `${i} = ${data}.length; + ${j}; + ${valid} = true; + if (${i} > 1) { + ${canOptimize() ? loopN() : loopN2()} + }` + ) + if ($data) write("}") + fail(`!${valid}`) + + function canOptimize(): boolean { + return Array.isArray(itemType) + ? !itemType.some((t) => t === "object" || t === "array") + : itemType && itemType !== "object" && itemType !== "array" + } + + function loopN(): string { + const wrongType = (Array.isArray(itemType) ? checkDataTypes : checkDataType)( + itemType, + "item", + opts.strictNumbers, + true + ) + const indices = scope.getName("indices") + return `const ${indices} = {}; + for (;${i}--;) { + let item = ${data}[${i}]; + if (${wrongType}) continue; + ${Array.isArray(itemType) ? 'if (typeof item == "string") item += "_";' : ""} + if (typeof ${indices}[item] == "number") { + ${valid} = false; + ${j} = ${indices}[item]; + break; + } + ${indices}[item] = ${i}; + }` + } + + function loopN2(): string { + return `outer: + for (;${i}--;) { + for (${j} = ${i}; ${j}--;) { + if (equal(${data}[${i}], ${data}[${j}])) { + ${valid} = false; + break outer; + } + } + }` + } + }, + error: { + message: ({params: {i, j}}) => + `"should NOT have duplicate items (items ## " + ${j} + " and " + ${i} + " are identical)"`, + params: ({params: {i, j}}) => `{i: ${i}, j: ${j}}`, + }, +} + +module.exports = def diff --git a/spec/extras/$data/uniqueItems.json b/spec/extras/$data/uniqueItems.json index ac97f8b768..709fd9566e 100644 --- a/spec/extras/$data/uniqueItems.json +++ b/spec/extras/$data/uniqueItems.json @@ -19,7 +19,7 @@ "valid": true }, { - "description": "non-unique array of is invalid", + "description": "non-unique array is invalid", "data": { "list": [1, 1], "unique": true From 0720a49da6a1b7a2116c18f0df8d49bcffcd42c6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 11 Aug 2020 22:20:40 +0100 Subject: [PATCH 040/322] feat: different error message when uniqueItems fails because $data is not boolean --- lib/vocabularies/validation/uniqueItems.ts | 74 +++------------------- 1 file changed, 8 insertions(+), 66 deletions(-) diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 1943b06343..0baa791bb7 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,65 +1,3 @@ -// {{# def.definitions }} -// {{# def.errors }} -// {{# def.setupKeyword }} -// {{# def.$data }} - -// {{? ($schema || $isData) && it.opts.uniqueItems !== false }} -// {{? $isData }} -// var {{=$valid}}; -// if ({{=$schemaValue}} === false || {{=$schemaValue}} === undefined) -// {{=$valid}} = true; -// else if (typeof {{=$schemaValue}} != 'boolean') -// {{=$valid}} = false; -// else { -// {{?}} - -// var i = {{=$data}}.length -// , {{=$valid}} = true -// , j; -// if (i > 1) { -// {{ -// var $itemType = it.schema.items && it.schema.items.type -// , $typeIsArray = Array.isArray($itemType); -// }} -// {{? !$itemType || $itemType == 'object' || $itemType == 'array' || -// ($typeIsArray && ($itemType.indexOf('object') >= 0 || $itemType.indexOf('array') >= 0)) }} -// outer: -// for (;i--;) { -// for (j = i; j--;) { -// if (equal({{=$data}}[i], {{=$data}}[j])) { -// {{=$valid}} = false; -// break outer; -// } -// } -// } -// {{??}} -// var itemIndices = {}, item; -// for (;i--;) { -// var item = {{=$data}}[i]; -// {{ var $method = 'checkDataType' + ($typeIsArray ? 's' : ''); }} -// if ({{= it.util[$method]($itemType, 'item', it.opts.strictNumbers, true) }}) continue; -// {{? $typeIsArray}} -// if (typeof item == 'string') item = '"' + item; -// {{?}} -// if (typeof itemIndices[item] == 'number') { -// {{=$valid}} = false; -// j = itemIndices[item]; -// break; -// } -// itemIndices[item] = i; -// } -// {{?}} -// } - -// {{? $isData }} } {{?}} - -// if (!{{=$valid}}) { -// {{# def.error:'uniqueItems' }} -// } {{? $breakOnError }} else { {{?}} -// {{??}} -// {{? $breakOnError }} if (true) { {{?}} -// {{?}} - import {KeywordDefinition} from "../../types" import {checkDataType, checkDataTypes} from "../../compile/util" @@ -87,7 +25,6 @@ const def: KeywordDefinition = { const itemType = parentSchema.items?.type write( `${i} = ${data}.length; - ${j}; ${valid} = true; if (${i} > 1) { ${canOptimize() ? loopN() : loopN2()} @@ -137,9 +74,14 @@ const def: KeywordDefinition = { } }, error: { - message: ({params: {i, j}}) => - `"should NOT have duplicate items (items ## " + ${j} + " and " + ${i} + " are identical)"`, - params: ({params: {i, j}}) => `{i: ${i}, j: ${j}}`, + message: ({$data, params: {i, j}}) => { + const msg = `"should NOT have duplicate items (items ## " + ${j} + " and " + ${i} + " are identical)"` + return $data ? `(${i} === undefined ? "uniqueItems must be boolean ($data)" : ${msg})` : msg + }, + params: ({$data, params: {i, j}}) => { + const obj = `{i: ${i}, j: ${j}}` + return $data ? `(${i} === undefined ? {} : ${obj})` : obj + }, }, } From 3337b285c15158f89944e78272098ce327a3792d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 12 Aug 2020 15:55:09 +0100 Subject: [PATCH 041/322] chore: update @ajv-validator/config --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f5a2fe4b1..472ecdde04 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "uri-js": "^4.2.2" }, "devDependencies": { - "@ajv-validator/config": "^0.1.0", + "@ajv-validator/config": "^0.1.1", "@types/node": "^14.0.27", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", From d02a257fd1265b7242868a1ffd4acd0b34ba063d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 12 Aug 2020 22:16:31 +0100 Subject: [PATCH 042/322] refactor: validate, coerce to typescript (WIP) --- lib/compile/codegen.ts | 17 ++ lib/compile/errors.ts | 52 ++++ lib/compile/index.js | 5 +- lib/compile/scope.ts | 13 - lib/compile/util.ts | 13 +- lib/compile/validate/applicability.ts | 18 ++ lib/compile/validate/boolSchema.ts | 105 ++++++++ lib/compile/validate/dataType.ts | 172 +++++++++++++ lib/compile/validate/index.ts | 133 ++++++++++ lib/compile/validate/validate.jst | 277 +++++++++++++++++++++ lib/keyword.ts | 110 +++----- lib/types.ts | 42 ++-- lib/vocabularies/util.ts | 15 +- lib/vocabularies/validation/enum.ts | 23 +- lib/vocabularies/validation/limitLength.ts | 2 +- lib/vocabularies/validation/multipleOf.ts | 6 +- lib/vocabularies/validation/pattern.ts | 2 +- lib/vocabularies/validation/uniqueItems.ts | 18 +- package.json | 2 +- tsconfig.json | 3 +- 20 files changed, 886 insertions(+), 142 deletions(-) create mode 100644 lib/compile/codegen.ts create mode 100644 lib/compile/errors.ts delete mode 100644 lib/compile/scope.ts create mode 100644 lib/compile/validate/applicability.ts create mode 100644 lib/compile/validate/boolSchema.ts create mode 100644 lib/compile/validate/dataType.ts create mode 100644 lib/compile/validate/index.ts create mode 100644 lib/compile/validate/validate.jst diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts new file mode 100644 index 0000000000..a4999372ce --- /dev/null +++ b/lib/compile/codegen.ts @@ -0,0 +1,17 @@ +export default class CodeGen { + #names: {[key: string]: number} = {} + // TODO make private. Possibly stack? + _out = "" + + name(prefix: string): string { + if (!this.#names[prefix]) this.#names[prefix] = 0 + const num = this.#names[prefix]++ + return `${prefix}_${num}` + } + + code(str: string): CodeGen { + // TODO optionally strip whitespace + this._out += str + "\n" + return this + } +} diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts new file mode 100644 index 0000000000..6c5c5ae9d2 --- /dev/null +++ b/lib/compile/errors.ts @@ -0,0 +1,52 @@ +import {KeywordContext, KeywordErrorDefinition} from "../types" +import {toQuotedString} from "./util" + +export function reportError( + cxt: KeywordContext, + error: KeywordErrorDefinition, + allErrors?: boolean +): void { + const {gen, compositeRule, opts, async} = cxt.it + const errObj = errorObjectCode(cxt, error) + if (allErrors ?? (compositeRule || opts.allErrors)) { + gen.code( + `const err = ${errObj}; + if (vErrors === null) vErrors = [err]; + else vErrors.push(err); + errors++;` + ) + } else { + gen.code( + async + ? `throw new ValidationError([${errObj}]);` + : `validate.errors = [${errObj}]; + return false;` + ) + } +} + +function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): string { + const { + keyword, + data, + schemaValue, + it: {createErrors, schemaPath, errorPath, errSchemaPath, opts}, + } = cxt + if (createErrors === false) return "{}" + if (!error) throw new Error('keyword definition must have "error" property') + // TODO trim whitespace + let out = `{ + keyword: "${keyword}", + dataPath: (dataPath || "") + ${errorPath}, + schemaPath: ${toQuotedString(errSchemaPath + "/" + keyword)}, + params: ${error.params(cxt)},` + if (opts.messages !== false) out += `message: ${error.message(cxt)},` + if (opts.verbose) { + // TODO trim whitespace + out += ` + schema: ${schemaValue}, + parentSchema: validate.schema${schemaPath}, + data: ${data},` + } + return out + "}" +} diff --git a/lib/compile/index.js b/lib/compile/index.js index 56ff190387..adb7625539 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,4 +1,4 @@ -import Scope from "./scope" +import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" const equal = require("fast-deep-equal") @@ -94,7 +94,8 @@ function compile(schema, root, localRefs, baseId) { schemaPath: "", errSchemaPath: "#", errorPath: '""', - scope: new Scope(), + data: "data", // TODO get unique name when passed from applicator keywords + gen: new CodeGen(), MissingRefError, RULES: RULES, validate: validateGenerator, diff --git a/lib/compile/scope.ts b/lib/compile/scope.ts deleted file mode 100644 index ff4e5bb5f4..0000000000 --- a/lib/compile/scope.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default class Scope { - _names: {[key: string]: number} - - constructor() { - this._names = {} - } - - getName(prefix: string): string { - if (!this._names[prefix]) this._names[prefix] = 0 - const num = this._names[prefix]++ - return `${prefix}_${num}` - } -} diff --git a/lib/compile/util.ts b/lib/compile/util.ts index a455449d1b..348a659467 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -47,7 +47,12 @@ export function checkDataType( } } -export function checkDataTypes(dataTypes: string[], data: string, strictNumbers?: boolean): string { +export function checkDataTypes( + dataTypes: string[], + data: string, + strictNumbers?: boolean, + negate?: true +): string { if (dataTypes.length === 1) { return checkDataType(dataTypes[0], data, strictNumbers, true) } @@ -62,7 +67,7 @@ export function checkDataTypes(dataTypes: string[], data: string, strictNumbers? } if (types.number) delete types.integer for (const t in types) { - code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, true) + code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, negate) } return code } @@ -173,7 +178,7 @@ export function getPath(currentPath: string, prop: string, jsonPointers?: boolea const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ -export function getData($data: string, lvl: number, paths: string[]): string { +export function getData($data: string, lvl: number, paths: (string | undefined)[]): string { let jsonPointer, data if ($data === "") return "rootData" if ($data[0] === "/") { @@ -193,7 +198,7 @@ export function getData($data: string, lvl: number, paths: string[]): string { "Cannot access property/index " + up + " levels up, current level is " + lvl ) } - return paths[lvl - up] + return paths[lvl - up] || "" } if (up > lvl) { diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts new file mode 100644 index 0000000000..3814f02ad7 --- /dev/null +++ b/lib/compile/validate/applicability.ts @@ -0,0 +1,18 @@ +import {CompilationContext} from "../../types" + +export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: string) { + const group = RULES.types[ty] + return group && group !== true && shouldUseGroup(schema, group) +} + +function shouldUseGroup(schema, rulesGroup): boolean { + return rulesGroup.some((rule) => shouldUseRule(schema, rule)) +} + +function shouldUseRule(schema, rule): boolean { + return schema[rule.keyword] !== undefined || ruleImplementsSomeKeyword(schema, rule) +} + +function ruleImplementsSomeKeyword(schema, rule): boolean { + return rule.implements && rule.implements.some((kwd) => schema[kwd] !== undefined) +} diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts new file mode 100644 index 0000000000..9d7a3f3a98 --- /dev/null +++ b/lib/compile/validate/boolSchema.ts @@ -0,0 +1,105 @@ +import {KeywordErrorDefinition, CompilationContext, KeywordContext} from "../../types" +import {reportError} from "../errors" + +const boolError: KeywordErrorDefinition = { + message: () => '"boolean schema is false"', + params: () => "{}", +} + +export function booleanOrEmptySchema(it: CompilationContext): void { + const {gen, isTop, schema, level} = it + if (isTop) { + if (schema === false) { + falseSchemaError(it, false) + } else if (schema.$async === true) { + gen.code("return data;") + } else { + gen.code("validate.errors = null; return true;") + } + gen.code( + `}; + return validate;` + ) + } else { + if (schema === false) { + gen.code(`var valid${level} = false;`) // TODO level, var + falseSchemaError(it) + } else { + gen.code(`var valid${level} = true;`) // TODO level, var + } + } + + // if (schema === false) { + // if (!isTop) { + // gen.code(`var valid${level} = false;`) // TODO level, var + // } + // // TODO probably some other interface should be used for non-keyword validation errors... + // falseSchemaError(it, !isTop) + // } else { + // if (isTop) { + // gen.code(schema.$async === true ? `return data;` : `validate.errors = null; return true;`) + // } else { + // gen.code(`var valid${level} = true;`) // TODO level, var + // } + // } + // if (isTop) { + // gen.code( + // `}; + // return validate;` + // ) + // } +} + +function falseSchemaError(it: CompilationContext, allErrors?: boolean) { + const {gen, dataLevel} = it + // TODO maybe some other interface should be used for non-keyword validation errors... + const cxt: KeywordContext = { + gen, + fail: exception, + ok: exception, + errorParams: exception, + keyword: "false schema", + data: "data" + (dataLevel || ""), + $data: false, + schema: false, + schemaCode: false, + schemaValue: false, + parentSchema: false, + it, + } + reportError(cxt, boolError, allErrors) +} + +function exception() { + throw new Error("this function can only be used in keyword") +} + +// {{ var $keyword = 'false schema'; }} +// {{# def.setupKeyword }} +// {{? it.schema === false}} +// {{? it.isTop}} +// {{ $breakOnError = true; }} +// {{??}} +// var {{=$valid}} = false; +// {{?}} +// {{# def.error:'false schema' }} +// {{??}} +// {{? it.isTop}} +// {{? $async }} +// return data; +// {{??}} +// validate.errors = null; +// return true; +// {{?}} +// {{??}} +// var {{=$valid}} = true; +// {{?}} +// {{?}} + +// {{? it.isTop}} +// }; +// return validate; +// {{?}} + +// {{ return out; }} +// {{?}} diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts new file mode 100644 index 0000000000..5b0bcb16f7 --- /dev/null +++ b/lib/compile/validate/dataType.ts @@ -0,0 +1,172 @@ +import {CompilationContext} from "../../types" +import {toHash, checkDataTypes} from "../util" +import {schemaHasRulesForType} from "./applicability" + +export function getSchemaTypes({schema, opts}: CompilationContext): string[] { + const t = schema.type + const types: string[] = Array.isArray(t) ? t : t || [] + types.forEach(checkType) + if (opts.nullable) { + const hasNull = types.includes("null") + if (hasNull && schema.nullable === false) { + throw new Error('{"type": "null"} contradicts {"nullable": "false"}') + } else if (!hasNull && schema.nullable === true) { + types.push("null") + } + } + return types + + function checkType(t: string): void { + // TODO check that type is allowed + if (typeof t != "string") throw new Error('"type" keyword must be string or string[]') + } +} + +export function coerceAndCheckDataType(it: CompilationContext, types: string[]): void { + const { + gen, + dataLevel, + opts: {coerceTypes, strictNumbers}, + } = it + let coerceTo = coerceToTypes(types, coerceTypes) + if (coerceTo.length || types.length > 1 || !schemaHasRulesForType(it, types[0])) { + const wrongType = checkDataTypes(types, `data${dataLevel || ""}`, strictNumbers, true) + gen.code(`if (${wrongType}) {`) + if (coerceTo.length) coerceData(it, coerceTo) + else reportTypeError(it) + gen.code("}") + } +} + +const COERCIBLE = toHash(["string", "number", "integer", "boolean", "null"]) +function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string[] { + return coerceTypes + ? types.filter((t) => COERCIBLE[t] || (coerceTypes === "array" && t === "array")) + : [] +} + +const coerceCode = { + number: ({dataType, data, coerced}) => + `if (${dataType} === "boolean" || ${data} === null + || (${dataType} === "string' && ${data} && ${data} == +${data})) { + ${coerced} = +${data}; + }`, + integer: ({dataType, data, coerced}) => + `if (${dataType} === "boolean" || ${data} === null + || (${dataType} === "string' && ${data} && ${data} == +${data} && !(${data} % 1))) { + ${coerced} = +${data}; + }`, + boolean: ({data, coerced}) => + `if (${data} === "false" || ${data} === 0 || ${data} === null) { + ${coerced} = false; + } else if (${data} === "true" || ${data} === 1) { + ${coerced} = true; + }`, + null: ({data, coerced}) => + `if (${data} === "" || ${data} === 0 || ${data} === false) { + ${coerced} = null; + }`, + array: ({dataType, data, coerced}) => + `if (${dataType} == 'string' || ${dataType} == 'number' || ${dataType} == 'boolean' || ${data} == null) { + ${coerced} = [${data}]; + }`, +} + +export function coerceData( + {gen, dataLevel, opts: {coerceTypes}}: CompilationContext, + coerceTo: string[] +): void { + // TODO add "data" to CompilationContext + const data = `data${dataLevel || ""}` + const dataType = gen.name("dataType") + const coerced = gen.name("coerced") + gen.code(`let coerced;`) + gen.code(`let ${dataType} = typeof ${data};`) + if (coerceTypes === "array" && !coerceTo.includes("array")) { + gen.code( + `if (${dataType} === "object" && Array.isArray(${data}) && ${data}.length === 1) { + ${coerced} = ${data} = ${data}[0]; + ${dataType} = typeof ${data}; + }` + ) + } + let closeBraces = "" + coerceTo.forEach((t, i) => { + if (i) { + gen.code(`if (${coerced} === undefined) {`) + closeBraces += "}" + } + if (coerceTypes === "array" && t !== "array") { + gen.code( + `if (${dataType} === "array" && ${data}.length === 1) { + ${coerced} = ${data} = ${data}[0]; + ${dataType} = typeof ${data}; + /*if (${dataType} == 'object' && Array.isArray(${data})) ${dataType} = 'array';*/ + }` + ) + } + if (t in coerceCode && (t !== "array" || coerceTypes === "array")) { + gen.code(coerceCode[t]({dataType, data, coerced})) + } + }) +} + +// {{## def.coerceType: +// {{ +// var $dataType = 'dataType' + $lvl +// , $coerced = 'coerced' + $lvl; +// }} +// var {{=$dataType}} = typeof {{=$data}}; +// var {{=$coerced}} = undefined; + +// {{? it.opts.coerceTypes == 'array' && !$coerceToTypes.includes('array') }} +// if ({{=$dataType}} == 'object' && Array.isArray({{=$data}}) && {{=$data}}.length == 1) { +// {{=$coerced}} = {{=$data}} = {{=$data}}[0]; +// {{=$dataType}} = typeof {{=$data}}; +// /*if ({{=$dataType}} == 'object' && Array.isArray({{=$data}})) {{=$dataType}} = 'array';*/ +// } +// {{?}} + +// {{ var $bracesCoercion = ''; }} +// {{~ $coerceToTypes:$type:$i }} +// {{? $i }} +// if (${coerced} === undefined) { +// {{ $bracesCoercion += '}'; }} +// {{?}} + +// {{? $type == 'string' }} +// if (${dataType} == 'number' || ${dataType} == 'boolean') +// ${coerced} = '' + ${data}; +// else if (${data} === null) ${coerced} = ''; +// {{?? $type == 'number' || $type == 'integer' }} +// if (${dataType} == 'boolean' || ${data} === null +// || (${dataType} == 'string' && ${data} && ${data} == +${data} +// {{? $type == 'integer' }} && !(${data} % 1){{?}})) +// ${coerced} = +${data}; +// {{?? $type == 'boolean' }} +// if (${data} === 'false' || ${data} === 0 || ${data} === null) +// ${coerced} = false; +// else if (${data} === 'true' || ${data} === 1) +// ${coerced} = true; +// {{?? $type == 'null' }} +// if (${data} === '' || ${data} === 0 || ${data} === false) +// ${coerced} = null; +// {{?? it.opts.coerceTypes == 'array' && $type == 'array' }} +// if (${dataType} == 'string' || ${dataType} == 'number' || ${dataType} == 'boolean' || ${data} == null) +// ${coerced} = [${data}]; +// {{?}} +// {{~}} + +// {{= $bracesCoercion }} + +// if (${coerced} === undefined) { +// {{# def.error:'type' }} +// } else { +// {{# def.setParentData }} +// ${data} = ${coerced}; +// {{? !$dataLvl }}if ({{=$parentData}} !== undefined){{?}} +// {{=$parentData}}[{{=$parentDataProperty}}] = ${coerced}; +// } +// #}} + +function reportTypeError(_: CompilationContext) {} diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts new file mode 100644 index 0000000000..3944fd3a62 --- /dev/null +++ b/lib/compile/validate/index.ts @@ -0,0 +1,133 @@ +import {CompilationContext} from "../../types" +import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util" +import {quotedString} from "../../vocabularies/util" +import {booleanOrEmptySchema} from "./boolSchema" +import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" + +const resolve = require("../resolve") + +export default function validateCode(it: CompilationContext): void { + const { + isTop, + schema, + RULES, + level, + gen, + opts: {$comment}, + } = it + if (schema.$async === true) it.async = true + checkUnknownKeywords(it) + if (isTop) startFunction(it) + if (typeof schema == "boolean" || !schemaHasRules(schema, RULES)) { + booleanOrEmptySchema(it) + return + } + if (isTop) { + updateTopContext(it) + checkNoDefault(it) + initializeTop(it) + } else { + updateContext(it) + checkAsync(it) + gen.code(`let errs_${level} = errors;`) + } + checkRefsAndKeywords(it) + if ($comment) commentKeyword(it) + const types = getSchemaTypes(it) + coerceAndCheckDataType(it, types) +} + +function checkUnknownKeywords({ + schema, + RULES, + opts: {strictKeywords}, + logger, +}: CompilationContext): void { + if (strictKeywords) { + const unknownKeyword = schemaUnknownRules(schema, RULES.keywords) + if (unknownKeyword) { + const msg = `unknown keyword: "${unknownKeyword}"` + if (strictKeywords === "log") logger.warn(msg) + else throw new Error(msg) + } + } +} + +function startFunction({ + gen, + schema, + async, + opts: {sourceCode, processCode}, +}: CompilationContext): void { + const asyncFunc = async ? "async" : "" + const sourceUrl = + schema.$id && (sourceCode || processCode) ? `/*# sourceURL=${schema.$id} */` : "" + gen.code( + `const validate = ${asyncFunc} function(data, dataPath, parentData, parentDataProperty, rootData) { + 'use strict'; + ${sourceUrl}` + ) +} + +function updateTopContext(it: CompilationContext): void { + it.rootId = resolve.fullPath(it.root.schema.$id) + it.baseId = it.baseId || it.rootId + delete it.isTop + + it.dataPathArr = [undefined] +} + +function checkNoDefault({ + schema, + opts: {useDefaults, strictDefaults}, + logger, +}: CompilationContext): void { + if (schema.default !== undefined && useDefaults && strictDefaults) { + const msg = "default is ignored in the schema root" + if (strictDefaults === "log") logger.warn(msg) + else throw new Error(msg) + } +} + +function initializeTop({gen}: CompilationContext): void { + // TODO old comment: "don't edit, used in replace". Should be removed? + gen.code( + `let vErrors = null; + let errors = 0; + if (rootData === undefined) rootData = data;` + ) +} + +function updateContext(it: CompilationContext): void { + if (it.schema.$id) it.baseId = resolve.url(it.baseId, it.schema.$id) +} + +function checkAsync(it: CompilationContext): void { + if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") +} + +function checkRefsAndKeywords({ + schema, + errSchemaPath, + RULES, + opts: {extendRefs}, + logger, +}: CompilationContext): void { + if (schema.$ref && schemaHasRulesExcept(schema, RULES.all, "$ref")) { + if (extendRefs === "fail") { + throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`) + } else if (extendRefs !== true) { + logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) + } + } +} + +function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: CompilationContext): void { + const msg = quotedString(schema.$comment) + if ($comment === true) { + gen.code(`console.log(${msg})`) + } else if (typeof $comment == "function") { + const schemaPath = quotedString(errSchemaPath + "/$comment") + gen.code(`self._opts.$comment(${msg}, ${schemaPath}, validate.root.schema)`) + } +} diff --git a/lib/compile/validate/validate.jst b/lib/compile/validate/validate.jst new file mode 100644 index 0000000000..5f0f060ab0 --- /dev/null +++ b/lib/compile/validate/validate.jst @@ -0,0 +1,277 @@ +{{# def.definitions }} +{{# def.errors }} +{{# def.defaults }} +{{# def.coerce }} + +{{ /** + * schema compilation (render) time: + * it = { schema, RULES, _validate, opts } + * it.validate - this template function, + * it is used recursively to generate code for subschemas + * + * runtime: + * "validate" is a variable name to which this function will be assigned + * validateRef etc. are defined in the parent scope in index.js + */ }} + +{{ + var $async = it.schema.$async === true + , $refKeywords = it.util.schemaHasRulesExcept(it.schema, it.RULES.all, '$ref') + , $id = it.schema.$id; +}} + +// {{ +// if (it.opts.strictKeywords) { +// var $unknownKwd = it.util.schemaUnknownRules(it.schema, it.RULES.keywords); +// if ($unknownKwd) { +// var $keywordsMsg = 'unknown keyword: ' + $unknownKwd; +// if (it.opts.strictKeywords === 'log') it.logger.warn($keywordsMsg); +// else throw new Error($keywordsMsg); +// } +// } +// }} + +// {{? it.isTop }} +// var validate = {{?$async}}{{it.async = true;}}async {{?}}function(data, dataPath, parentData, parentDataProperty, rootData) { +// 'use strict'; +// {{? $id && (it.opts.sourceCode || it.opts.processCode) }} +// {{= '/\*# sourceURL=' + $id + ' */' }} +// {{?}} +// {{?}} + +// {{? typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref) }} +// {{ var $keyword = 'false schema'; }} +// {{# def.setupKeyword }} +// {{? it.schema === false}} +// {{? it.isTop}} +// {{ $breakOnError = true; }} +// {{??}} +// var {{=$valid}} = false; +// {{?}} +// {{# def.error:'false schema' }} +// {{??}} +// {{? it.isTop}} +// {{? $async }} +// return data; +// {{??}} +// validate.errors = null; +// return true; +// {{?}} +// {{??}} +// var {{=$valid}} = true; +// {{?}} +// {{?}} + +// {{? it.isTop}} +// }; +// return validate; +// {{?}} + +// {{ return out; }} +// {{?}} + +{{? it.isTop }} + // {{ + // var $top = it.isTop + // , $lvl = it.level = 0 + // , $dataLvl = it.dataLevel = 0 + // , $data = 'data'; + // it.rootId = it.resolve.fullPath(it.root.schema.$id); + // it.baseId = it.baseId || it.rootId; + // delete it.isTop; + + // it.dataPathArr = [undefined]; + + // if (it.schema.default !== undefined && it.opts.useDefaults && it.opts.strictDefaults) { + // var $defaultMsg = 'default is ignored in the schema root'; + // if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + // else throw new Error($defaultMsg); + // } + // }} + + // var vErrors = null; {{ /* don't edit, used in replace */ }} + // var errors = 0; {{ /* don't edit, used in replace */ }} + // if (rootData === undefined) rootData = data; {{ /* don't edit, used in replace */ }} +{{??}} + {{ + var $lvl = it.level + , $dataLvl = it.dataLevel + , $data = 'data' + ($dataLvl || ''); + + // if ($id) it.baseId = it.resolve.url(it.baseId, $id); + + // if ($async && !it.async) throw new Error('async schema in sync schema'); + }} + + // var errs_{{=$lvl}} = errors; +{{?}} + +{{ + var $valid = 'valid' + $lvl + , $breakOnError = !it.opts.allErrors + , $closingBraces1 = '' + , $closingBraces2 = ''; + + var $errorKeyword; + // var $typeSchema = it.schema.type + // , $typeIsArray = Array.isArray($typeSchema); + + // if ($typeSchema && it.opts.nullable && it.schema.nullable === true) { + // if ($typeIsArray) { + // if ($typeSchema.indexOf('null') == -1) + // $typeSchema = $typeSchema.concat('null'); + // } else if ($typeSchema != 'null') { + // $typeSchema = [$typeSchema, 'null']; + // $typeIsArray = true; + // } + // } + + // if ($typeIsArray && $typeSchema.length == 1) { + // $typeSchema = $typeSchema[0]; + // $typeIsArray = false; + // } +}} + +{{## def.checkType: + {{ + var $schemaPath = it.schemaPath + '.type' + , $errSchemaPath = it.errSchemaPath + '/type' + , $method = $typeIsArray ? 'checkDataTypes' : 'checkDataType'; + }} + + if ({{= it.util[$method]($typeSchema, $data, it.opts.strictNumbers, true) }}) { +#}} + +// {{? it.schema.$ref && $refKeywords }} +// {{? it.opts.extendRefs == 'fail' }} +// {{ throw new Error('$ref: validation keywords used in schema at path "' + it.errSchemaPath + '" (see option extendRefs)'); }} +// {{?? it.opts.extendRefs !== true }} +// {{ +// $refKeywords = false; +// it.logger.warn('$ref: keywords ignored in schema at path "' + it.errSchemaPath + '"'); +// }} +// {{?}} +// {{?}} + +// {{? it.schema.$comment && it.opts.$comment }} +// {{= it.RULES.all.$comment.code(it, '$comment') }} +// {{?}} + +{{? $typeSchema }} + {{? it.opts.coerceTypes }} + {{ var $coerceToTypes = it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema); }} + {{?}} + + {{ var $rulesGroup = it.RULES.types[$typeSchema]; }} + +{{ /* // This condition means one of the following: +// - opts.coerceTypes in on and there are types to which data can be coerced +// - or, there are multiple types allowed +// - or, no known keywords for this type (e.g. boolean or null) +// - or, no keywords for this type in the current schema +// +// this block checks and coerces types separately from keyword validation below */ }} + {{? $coerceToTypes || $typeIsArray || $rulesGroup === true || + ($rulesGroup && !shouldUseGroup($rulesGroup)) }} + {{ + var $schemaPath = it.schemaPath + '.type' + , $errSchemaPath = it.errSchemaPath + '/type'; + }} + {{# def.checkType }} + {{? $coerceToTypes }} + {{# def.coerceType }} + {{??}} + {{# def.error:'type' }} + {{?}} + } + {{?}} +{{?}} + +{{? it.schema.$ref && !$refKeywords }} + {{= it.RULES.all.$ref.code(it, '$ref') }} + {{? $breakOnError }} + } + if (errors === {{?$top}}0{{??}}errs_{{=$lvl}}{{?}}) { + {{ $closingBraces2 += '}'; }} + {{?}} +{{??}} + {{~ it.RULES:$rulesGroup }} + {{? shouldUseGroup($rulesGroup) }} + {{? $rulesGroup.type }} + if ({{= it.util.checkDataType($rulesGroup.type, $data, it.opts.strictNumbers) }}) { + {{?}} + {{? it.opts.useDefaults }} + {{? $rulesGroup.type == 'object' && it.schema.properties }} + {{# def.defaultProperties }} + {{?? $rulesGroup.type == 'array' && Array.isArray(it.schema.items) }} + {{# def.defaultItems }} + {{?}} + {{?}} + {{~ $rulesGroup.rules:$rule }} + {{? shouldUseRule($rule) }} + {{ var $code = $rule.code(it, $rule.keyword, $rulesGroup.type); }} + {{? $code }} + {{= $code }} + {{? $breakOnError }} + {{ $closingBraces1 += '}'; }} + {{?}} + {{?}} + {{?}} + {{~}} + {{? $breakOnError }} + {{= $closingBraces1 }} + {{ $closingBraces1 = ''; }} + {{?}} + {{? $rulesGroup.type }} + } + {{? $typeSchema && $typeSchema === $rulesGroup.type && !$coerceToTypes }} + else { + {{ + var $schemaPath = it.schemaPath + '.type' + , $errSchemaPath = it.errSchemaPath + '/type'; + }} + {{# def.error:'type' }} + } + {{?}} + {{?}} + + {{? $breakOnError }} + if (errors === {{?$top}}0{{??}}errs_{{=$lvl}}{{?}}) { + {{ $closingBraces2 += '}'; }} + {{?}} + {{?}} + {{~}} +{{?}} + +{{? $breakOnError }} {{= $closingBraces2 }} {{?}} + +{{? $top }} + {{? $async }} + if (errors === 0) return data; {{ /* don't edit, used in replace */ }} + else throw new ValidationError(vErrors); {{ /* don't edit, used in replace */ }} + {{??}} + validate.errors = vErrors; {{ /* don't edit, used in replace */ }} + return errors === 0; {{ /* don't edit, used in replace */ }} + {{?}} + }; + + return validate; +{{??}} + var {{=$valid}} = errors === errs_{{=$lvl}}; +{{?}} + +{{ + function shouldUseGroup(rulesGroup) { + return rulesGroup.some(shouldUseRule) + } + + function shouldUseRule(rule) { + return it.schema[rule.keyword] !== undefined + || ruleImplementsSomeKeyword(rule); + } + + function ruleImplementsSomeKeyword(rule) { + return rule.implements && + && rule.implements.some(kwd => it.schema[kwd] !== undefined) + } +}} diff --git a/lib/keyword.ts b/lib/keyword.ts index 22a21790cc..2e330742e6 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,5 +1,6 @@ import { KeywordDefinition, + KeywordErrorDefinition, Vocabulary, ErrorObject, ValidateFunction, @@ -7,8 +8,9 @@ import { KeywordContext, } from "./types" -import {getData, getProperty, toQuotedString} from "./compile/util" -import {quotedString} from "./vocabularies/util" +import {reportError} from "./compile/errors" +import {getData} from "./compile/util" +import {schemaRefOrVal} from "./vocabularies/util" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i const customRuleCode = require("./dotjs/custom") @@ -133,25 +135,15 @@ export function addKeyword( function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): string { const schema = it.schema[keyword] const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition - if (!code) throw new Error('"code" must be defined') - let schemaCode: string | number | boolean - let out = "" - const $data = $defData && it.opts.$data && schema && schema.$data - if ($data) { - schemaCode = it.scope.getName("schema") - out += `const ${schemaCode} = ${getData($data, it.dataLevel, it.dataPathArr)};` - } else { - if ( - schemaType && - !(schemaType === "array" ? Array.isArray(schema) : typeof schema === schemaType) - ) { - throw new Error(`${keyword} must be ${schemaType}`) - } - schemaCode = schemaRefOrVal() - } - const data = "data" + (it.dataLevel || "") + const {gen, opts, dataLevel, schemaPath, dataPathArr} = it + if (!code) throw new Error('"code" and "error" must be defined') + // TODO _out + gen._out = "" + const $data = $defData && opts.$data && schema && schema.$data + const data = "data" + (dataLevel || "") + const schemaValue = schemaRefOrVal(schema, schemaPath, keyword, $data) const cxt: KeywordContext = { - write, + gen, fail, ok, errorParams, @@ -159,78 +151,40 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): stri data, $data, schema, + schemaCode: $data ? gen.name("schema") : schemaValue, + schemaValue, parentSchema: it.schema, - schemaCode, - scope: it.scope, - usePattern: it.usePattern, - opts: it.opts, + it, + } + if ($data) { + gen.code(`const ${cxt.schemaCode} = ${getData($data, dataLevel, dataPathArr)};`) + } else { + if ( + schemaType && + !(schemaType === "array" ? Array.isArray(schema) : typeof schema === schemaType) + ) { + throw new Error(`${keyword} must be ${schemaType}`) + } } // TODO check that code called "fail" or another valid way to return code code(cxt) - return out - - function write(str: string): void { - out += str + "\n" - } + // TODO + return gen._out function fail(condition: string): void { - out += `if (${condition}) { ${reportError()} }` - if (!it.opts.allErrors) out += `else {` - out += "\n" + gen.code(`if (${condition}) {`) + reportError(cxt, error as KeywordErrorDefinition) + gen.code(opts.allErrors ? "}" : "} else {") } function ok(condition?: string): void { - if (condition) out += `if (!(${condition})) { ${reportError()} }` - if (!it.opts.allErrors) out += condition ? `else {` : `if (true) {` - out += "\n" + if (condition) fail(`!(${condition})`) + else if (!opts.allErrors) gen.code("if (true) {") } function errorParams(obj: any) { cxt.params = obj } - - function reportError(): string { - const errCode = errorObjectCode() - if (!it.compositeRule && !it.opts.allErrors) { - // TODO trim whitespace - return it.async - ? `throw new ValidationError([${errCode}]);` - : `validate.errors = [${errCode}]; - return false;` - } - return `const err = ${errCode}; - if (vErrors === null) vErrors = [err]; - else vErrors.push(err); - errors++;` - } - - function errorObjectCode() { - if (it.createErrors === false) return "{}" - if (!error) throw new Error('keyword definition must have "error" property') - // TODO trim whitespace - let out = `{ - keyword: "${keyword}", - dataPath: (dataPath || "") + ${it.errorPath}, - schemaPath: ${toQuotedString(it.errSchemaPath + "/" + keyword)}, - params: ${error.params(cxt)},` - if (it.opts.messages !== false) out += `message: ${error.message(cxt)},` - if (it.opts.verbose) { - // TODO trim whitespace - out += ` - schema: ${schemaRefOrVal()}, - parentSchema: validate.schema${it.schemaPath}, - data: ${data},` - } - return out + "}" - } - - function schemaRefOrVal(): string | number | boolean { - if (!$data) { - if (schemaType === "number" || schemaType === "boolean") return schema - if (schemaType === "string") return quotedString(schema) - } - return `validate.schema${it.schemaPath + getProperty(keyword)}` - } } /** diff --git a/lib/types.ts b/lib/types.ts index ba842bc953..b7963d504f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,7 @@ import Cache from "./cache" -import Scope from "./compile/scope" +import CodeGen from "./compile/codegen" -interface Options { +export interface Options { $data?: boolean allErrors?: boolean verbose?: boolean @@ -43,7 +43,8 @@ interface Options { cache?: Cache logger?: Logger | false nullable?: boolean - serialize?: ((schema: object | boolean) => any) | false + serialize?: false | ((schema: object | boolean) => any) + $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) } interface Logger { @@ -100,14 +101,15 @@ export interface ErrorObject { export interface CompilationContext { level: number dataLevel: number - dataPathArr: string[] + data: string + dataPathArr: (string | undefined)[] schema: any schemaPath: string errorPath: string errSchemaPath: string - scope: Scope + gen: CodeGen createErrors?: boolean // TODO maybe remove later - baseId: string + baseId?: string // TODO probably not optional async: boolean opts: Options formats: { @@ -121,6 +123,17 @@ export interface CompilationContext { usePattern: (str: string) => string util: object // TODO self: object // TODO + RULES: any // TODO replace? + logger: Logger // TODO ? + isTop: boolean // TODO ? + root: SchemaRoot // TODO ? + rootId?: string // TODO ? +} + +interface SchemaRoot { + schema: any + refVal: (string | undefined)[] // TODO + refs: {[key: string]: any} // TODO } export interface KeywordDefinition { @@ -143,30 +156,31 @@ export interface KeywordDefinition { macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean inline?: (it: CompilationContext, keyword: string, schema: any, parentSchema: object) => string code?: (cxt: KeywordContext) => string | void - error?: { - message: (cxt: KeywordContext, params?: any) => string - params: (cxt: KeywordContext, params?: any) => string - } + error?: KeywordErrorDefinition validateSchema?: ValidateFunction } +export interface KeywordErrorDefinition { + message: (cxt: KeywordContext, params?: any) => string + params: (cxt: KeywordContext, params?: any) => string +} + export type Vocabulary = KeywordDefinition[] export interface KeywordContext { + gen: CodeGen fail: (condition: string) => void ok: (condition?: string) => void - write: (str: string) => void - usePattern: (str: string) => string errorParams: (obj: any) => void - scope: Scope keyword: string data: string $data?: string | false schema: any parentSchema: any schemaCode: string | number | boolean - opts: Options + schemaValue: string | number | boolean params?: any + it: CompilationContext } export type FormatMode = "fast" | "full" diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index c3c53ead43..f3efef973e 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,3 +1,5 @@ +import {getProperty} from "../compile/util" + export function appendSchema( schemaCode: string | number | boolean, $data?: string | false @@ -23,7 +25,14 @@ export function dataNotType( schemaType?: string, $data?: string | false ): string { - return $data - ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` - : "" + return $data ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` : "" +} + +export function schemaRefOrVal(schema, schemaPath, keyword, $data): string | number | boolean { + const t = typeof schema + if (!$data) { + if (t === "number" || t === "boolean") return schema + if (t === "string") return quotedString(schema) + } + return `validate.schema${schemaPath + getProperty(keyword)}` } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index ee0c743b53..921d4a5ca7 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -5,27 +5,26 @@ const def: KeywordDefinition = { keyword: "enum", schemaType: "array", $data: true, - code({write, fail, scope, data, $data, schema, schemaCode, opts}) { + code({gen, fail, data, $data, schema, schemaCode, it: {opts}}) { if ($data) { - const valid = scope.getName("valid") - write(`let ${valid};`) - // TODO trim whitespace - write( - `if (${schemaCode} === undefined) ${valid} = true; + const valid = gen.name("valid") + gen.code( + `let ${valid}; + if (${schemaCode} === undefined) ${valid} = true; else { ${valid} = false; if (Array.isArray(${schemaCode})) {` ) loopEnum(schemaCode, valid) - write("}}") + gen.code("}}") fail(`!${valid}`) } else { if (schema.length === 0) throw new Error("enum must have non-empty array") - const vSchema = scope.getName("schema") - write(`const ${vSchema} = ${schemaCode};`) + const vSchema = gen.name("schema") + gen.code(`const ${vSchema} = ${schemaCode};`) if (schema.length > (opts.loopEnum as number)) { - const valid = scope.getName("valid") - write(`let ${valid} = false;`) + const valid = gen.name("valid") + gen.code(`let ${valid} = false;`) loopEnum(vSchema, valid) fail(`!${valid}`) } else { @@ -39,7 +38,7 @@ const def: KeywordDefinition = { function loopEnum(sch: string, valid: string): void { // TODO trim whitespace - write( + gen.code( `for (const v of ${sch}) { if (equal(${data}, v)) { ${valid} = true; diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 123168bb5b..1670c8049f 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -6,7 +6,7 @@ const def: KeywordDefinition = { type: "string", schemaType: "number", $data: true, - code({fail, keyword, data, $data, schemaCode, opts}) { + code({fail, keyword, data, $data, schemaCode, it: {opts}}) { const op = keyword == "maxLength" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) const len = opts.unicode === false ? `${data}.length` : `ucs2length(${data})` diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 39b9f7b972..09200caa07 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -6,14 +6,14 @@ const def: KeywordDefinition = { type: "number", schemaType: "number", $data: true, - code({write, fail, scope, data, $data, schemaCode, opts}) { + code({gen, fail, data, $data, schemaCode, it: {opts}}) { const dnt = dataNotType(schemaCode, def.schemaType, $data) - const res = scope.getName("res") + const res = gen.name("res") const prec = opts.multipleOfPrecision const invalid = prec ? `Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : `${res} !== parseInt(${res})` - write(`let ${res};`) + gen.code(`let ${res};`) fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) }, error: { diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index eea214c4c0..f4f1a546e5 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -6,7 +6,7 @@ const def: KeywordDefinition = { type: "string", schemaType: "string", $data: true, - code({fail, usePattern, data, $data, schema, schemaCode}) { + code({fail, data, $data, schema, schemaCode, it: {usePattern}}) { const dnt = dataNotType(schemaCode, def.schemaType, $data) const regExp = $data ? `(new RegExp(${schemaCode}))` : usePattern(schema) fail(dnt + `!${regExp}.test(${data})`) diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 0baa791bb7..c7ed32c814 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -6,15 +6,15 @@ const def: KeywordDefinition = { type: "array", schemaType: "boolean", $data: true, - code({write, fail, ok, errorParams, scope, data, $data, schema, parentSchema, schemaCode, opts}) { + code({gen, fail, ok, errorParams, data, $data, schema, parentSchema, schemaCode, it: {opts}}) { if (opts.uniqueItems === false || !($data || schema)) return ok() - const i = scope.getName("i") - const j = scope.getName("j") + const i = gen.name("i") + const j = gen.name("j") errorParams({i, j}) - const valid = scope.getName("valid") - write(`let ${valid}, ${i}, ${j};`) + const valid = gen.name("valid") + gen.code(`let ${valid}, ${i}, ${j};`) if ($data) { - write( + gen.code( `if (${schemaCode} === false || ${schemaCode} === undefined) ${valid} = true; else if (typeof ${schemaCode} != "boolean") @@ -23,14 +23,14 @@ const def: KeywordDefinition = { ) } const itemType = parentSchema.items?.type - write( + gen.code( `${i} = ${data}.length; ${valid} = true; if (${i} > 1) { ${canOptimize() ? loopN() : loopN2()} }` ) - if ($data) write("}") + if ($data) gen.code("}") fail(`!${valid}`) function canOptimize(): boolean { @@ -46,7 +46,7 @@ const def: KeywordDefinition = { opts.strictNumbers, true ) - const indices = scope.getName("indices") + const indices = gen.name("indices") return `const ${indices} = {}; for (;${i}--;) { let item = ${data}[${i}]; diff --git a/package.json b/package.json index 472ecdde04..2c1555e1c7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", "bundle-beautify": "node ./scripts/bundle.js js-beautify", "dot": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", - "tsc": "tsc && cp -r lib/refs dist/refs", + "tsc": "tsc || true && cp -r lib/refs dist/refs", "build": "npm run dot && npm run tsc", "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", diff --git a/tsconfig.json b/tsconfig.json index 1021121369..7c5425b5b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "noImplicitThis": false, "noImplicitReturns": false, "allowJs": true, - "declaration": false + "declaration": false, + "target": "ES2018" } } From 3b267b0ec7d58b5cc7729f1abf577a13300f5988 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 15 Aug 2020 13:20:06 +0100 Subject: [PATCH 043/322] refactor: coerce.def to typescript --- lib/compile/index.js | 3 + lib/compile/util.ts | 4 +- lib/compile/validate/boolSchema.ts | 2 +- lib/compile/validate/dataType.ts | 190 +++++++++++++---------------- lib/compile/validate/index.ts | 2 +- lib/compile/validate/validate.jst | 4 +- lib/dot/coerce.def | 51 -------- lib/dot/validate.jst | 8 +- lib/keyword.ts | 2 +- lib/types.ts | 2 +- lib/vocabularies/util.ts | 7 +- 11 files changed, 111 insertions(+), 164 deletions(-) delete mode 100644 lib/dot/coerce.def diff --git a/lib/compile/index.js b/lib/compile/index.js index adb7625539..45854d56c0 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,6 +1,8 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" +import {coerceData} from "./validate/dataType" + const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -99,6 +101,7 @@ function compile(schema, root, localRefs, baseId) { MissingRefError, RULES: RULES, validate: validateGenerator, + coerceData, // TODO remove when validate is replaced util: util, resolve: resolve, resolveRef: resolveRef, diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 348a659467..45248057ea 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -178,7 +178,7 @@ export function getPath(currentPath: string, prop: string, jsonPointers?: boolea const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ -export function getData($data: string, lvl: number, paths: (string | undefined)[]): string { +export function getData($data: string, lvl: number, paths: string[]): string { let jsonPointer, data if ($data === "") return "rootData" if ($data[0] === "/") { @@ -198,7 +198,7 @@ export function getData($data: string, lvl: number, paths: (string | undefined)[ "Cannot access property/index " + up + " levels up, current level is " + lvl ) } - return paths[lvl - up] || "" + return paths[lvl - up] } if (up > lvl) { diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 9d7a3f3a98..db0634b926 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -60,7 +60,6 @@ function falseSchemaError(it: CompilationContext, allErrors?: boolean) { errorParams: exception, keyword: "false schema", data: "data" + (dataLevel || ""), - $data: false, schema: false, schemaCode: false, schemaValue: false, @@ -70,6 +69,7 @@ function falseSchemaError(it: CompilationContext, allErrors?: boolean) { reportError(cxt, boolError, allErrors) } +// TODO combine with exception from dataType function exception() { throw new Error("this function can only be used in keyword") } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 5b0bcb16f7..e66226295d 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,6 +1,8 @@ -import {CompilationContext} from "../../types" -import {toHash, checkDataTypes} from "../util" +import {CompilationContext, KeywordContext, KeywordErrorDefinition} from "../../types" +import {toHash, checkDataType, checkDataTypes} from "../util" +import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" +import {reportError} from "../errors" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { const t = schema.type @@ -46,127 +48,111 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string } const coerceCode = { + string: ({dataType, data, coerced}) => + `else if (${dataType} == "number" || ${dataType} == "boolean") + ${coerced} = "" + ${data}; + else if (${data} === null) + ${coerced} = "";`, number: ({dataType, data, coerced}) => - `if (${dataType} === "boolean" || ${data} === null - || (${dataType} === "string' && ${data} && ${data} == +${data})) { - ${coerced} = +${data}; - }`, + `else if (${dataType} == "boolean" || ${data} === null + || (${dataType} == "string" && ${data} && ${data} == +${data})) + ${coerced} = +${data};`, integer: ({dataType, data, coerced}) => - `if (${dataType} === "boolean" || ${data} === null - || (${dataType} === "string' && ${data} && ${data} == +${data} && !(${data} % 1))) { - ${coerced} = +${data}; - }`, + `else if (${dataType} === "boolean" || ${data} === null + || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))) + ${coerced} = +${data};`, boolean: ({data, coerced}) => - `if (${data} === "false" || ${data} === 0 || ${data} === null) { + `else if (${data} === "false" || ${data} === 0 || ${data} === null) ${coerced} = false; - } else if (${data} === "true" || ${data} === 1) { - ${coerced} = true; - }`, + else if (${data} === "true" || ${data} === 1) + ${coerced} = true;`, null: ({data, coerced}) => - `if (${data} === "" || ${data} === 0 || ${data} === false) { - ${coerced} = null; - }`, + `else if (${data} === "" || ${data} === 0 || ${data} === false) + ${coerced} = null;`, array: ({dataType, data, coerced}) => - `if (${dataType} == 'string' || ${dataType} == 'number' || ${dataType} == 'boolean' || ${data} == null) { - ${coerced} = [${data}]; - }`, + `else if (${dataType} === "string" || ${dataType} === "number" || ${dataType} === "boolean" || ${data} === null) + ${coerced} = [${data}];`, } -export function coerceData( - {gen, dataLevel, opts: {coerceTypes}}: CompilationContext, - coerceTo: string[] -): void { - // TODO add "data" to CompilationContext +export function coerceData(it: CompilationContext, coerceTo: string[]): void { + const { + gen, + schema, + dataLevel, + opts: {coerceTypes, strictNumbers}, + } = it + // TODO use "data" to CompilationContext const data = `data${dataLevel || ""}` const dataType = gen.name("dataType") const coerced = gen.name("coerced") - gen.code(`let coerced;`) + gen.code(`let ${coerced};`) gen.code(`let ${dataType} = typeof ${data};`) - if (coerceTypes === "array" && !coerceTo.includes("array")) { + if (coerceTypes === "array") { gen.code( - `if (${dataType} === "object" && Array.isArray(${data}) && ${data}.length === 1) { - ${coerced} = ${data} = ${data}[0]; + `if (${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1) { + ${data} = ${data}[0]; ${dataType} = typeof ${data}; + if (${checkDataType(schema.type, data, strictNumbers)}) + ${coerced} = ${data}; }` ) } - let closeBraces = "" - coerceTo.forEach((t, i) => { - if (i) { - gen.code(`if (${coerced} === undefined) {`) - closeBraces += "}" - } - if (coerceTypes === "array" && t !== "array") { - gen.code( - `if (${dataType} === "array" && ${data}.length === 1) { - ${coerced} = ${data} = ${data}[0]; - ${dataType} = typeof ${data}; - /*if (${dataType} == 'object' && Array.isArray(${data})) ${dataType} = 'array';*/ - }` - ) - } + gen.code(`if (${coerced} !== undefined) ;`) + const args = {dataType, data, coerced} + for (const t of coerceTo) { if (t in coerceCode && (t !== "array" || coerceTypes === "array")) { - gen.code(coerceCode[t]({dataType, data, coerced})) + gen.code(coerceCode[t](args)) } - }) -} - -// {{## def.coerceType: -// {{ -// var $dataType = 'dataType' + $lvl -// , $coerced = 'coerced' + $lvl; -// }} -// var {{=$dataType}} = typeof {{=$data}}; -// var {{=$coerced}} = undefined; - -// {{? it.opts.coerceTypes == 'array' && !$coerceToTypes.includes('array') }} -// if ({{=$dataType}} == 'object' && Array.isArray({{=$data}}) && {{=$data}}.length == 1) { -// {{=$coerced}} = {{=$data}} = {{=$data}}[0]; -// {{=$dataType}} = typeof {{=$data}}; -// /*if ({{=$dataType}} == 'object' && Array.isArray({{=$data}})) {{=$dataType}} = 'array';*/ -// } -// {{?}} + } + gen.code(`else {`) + reportTypeError(it) + gen.code(`}`) -// {{ var $bracesCoercion = ''; }} -// {{~ $coerceToTypes:$type:$i }} -// {{? $i }} -// if (${coerced} === undefined) { -// {{ $bracesCoercion += '}'; }} -// {{?}} + gen.code( + `if (${coerced} !== undefined) { + ${data} = ${coerced}; + ${assignParentData(it, coerced)} + }` + ) +} -// {{? $type == 'string' }} -// if (${dataType} == 'number' || ${dataType} == 'boolean') -// ${coerced} = '' + ${data}; -// else if (${data} === null) ${coerced} = ''; -// {{?? $type == 'number' || $type == 'integer' }} -// if (${dataType} == 'boolean' || ${data} === null -// || (${dataType} == 'string' && ${data} && ${data} == +${data} -// {{? $type == 'integer' }} && !(${data} % 1){{?}})) -// ${coerced} = +${data}; -// {{?? $type == 'boolean' }} -// if (${data} === 'false' || ${data} === 0 || ${data} === null) -// ${coerced} = false; -// else if (${data} === 'true' || ${data} === 1) -// ${coerced} = true; -// {{?? $type == 'null' }} -// if (${data} === '' || ${data} === 0 || ${data} === false) -// ${coerced} = null; -// {{?? it.opts.coerceTypes == 'array' && $type == 'array' }} -// if (${dataType} == 'string' || ${dataType} == 'number' || ${dataType} == 'boolean' || ${data} == null) -// ${coerced} = [${data}]; -// {{?}} -// {{~}} +function assignParentData({dataLevel, dataPathArr}: CompilationContext, expr: string): string { + // TODO replace dataLevel + if (dataLevel) { + const parentData = "data" + (dataLevel - 1 || "") + return `${parentData}[${dataPathArr[dataLevel]}] = ${expr};` + } + return `if (parentData !== undefined) parentData[parentDataProperty] = ${expr};` +} -// {{= $bracesCoercion }} +const typeError: KeywordErrorDefinition = { + message: ({schema}) => `"should be ${Array.isArray(schema) ? schema.join(",") : schema}"`, + // TODO change: return type as array here + params: ({schema}) => `{type: "${Array.isArray(schema) ? schema.join(",") : schema}"}`, +} -// if (${coerced} === undefined) { -// {{# def.error:'type' }} -// } else { -// {{# def.setParentData }} -// ${data} = ${coerced}; -// {{? !$dataLvl }}if ({{=$parentData}} !== undefined){{?}} -// {{=$parentData}}[{{=$parentDataProperty}}] = ${coerced}; -// } -// #}} +// TODO maybe combine with boolSchemaError +// TODO refactor type keyword context creation +function reportTypeError(it: CompilationContext) { + const {gen, schema, schemaPath, dataLevel} = it + const schemaCode = schemaRefOrVal(schema, schemaPath, "type") + const cxt: KeywordContext = { + gen, + fail: exception, + ok: exception, + errorParams: exception, + keyword: "type", + data: "data" + (dataLevel || ""), + schema: schema.type, + schemaCode, + schemaValue: schemaCode, + parentSchema: schema, + it, + } + reportError(cxt, typeError) +} -function reportTypeError(_: CompilationContext) {} +// TODO combine with exception from boolSchema +function exception() { + throw new Error("this function can only be used in keyword") +} diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 3944fd3a62..c590391597 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -74,7 +74,7 @@ function updateTopContext(it: CompilationContext): void { it.baseId = it.baseId || it.rootId delete it.isTop - it.dataPathArr = [undefined] + it.dataPathArr = [""] } function checkNoDefault({ diff --git a/lib/compile/validate/validate.jst b/lib/compile/validate/validate.jst index 5f0f060ab0..a3cc724668 100644 --- a/lib/compile/validate/validate.jst +++ b/lib/compile/validate/validate.jst @@ -80,7 +80,7 @@ // it.baseId = it.baseId || it.rootId; // delete it.isTop; - // it.dataPathArr = [undefined]; + // it.dataPathArr = [""]; // if (it.schema.default !== undefined && it.opts.useDefaults && it.opts.strictDefaults) { // var $defaultMsg = 'default is ignored in the schema root'; @@ -179,7 +179,7 @@ }} {{# def.checkType }} {{? $coerceToTypes }} - {{# def.coerceType }} + // {{# def.coerceType }} {{??}} {{# def.error:'type' }} {{?}} diff --git a/lib/dot/coerce.def b/lib/dot/coerce.def deleted file mode 100644 index c947ed6af8..0000000000 --- a/lib/dot/coerce.def +++ /dev/null @@ -1,51 +0,0 @@ -{{## def.coerceType: - {{ - var $dataType = 'dataType' + $lvl - , $coerced = 'coerced' + $lvl; - }} - var {{=$dataType}} = typeof {{=$data}}; - var {{=$coerced}} = undefined; - - {{? it.opts.coerceTypes == 'array' }} - if ({{=$dataType}} == 'object' && Array.isArray({{=$data}}) && {{=$data}}.length == 1) { - {{=$data}} = {{=$data}}[0]; - {{=$dataType}} = typeof {{=$data}}; - if ({{=it.util.checkDataType(it.schema.type, $data, it.opts.strictNumbers)}}) {{=$coerced}} = {{=$data}}; - } - {{?}} - - if ({{=$coerced}} !== undefined) ; - {{~ $coerceToTypes:$type:$i }} - {{? $type == 'string' }} - else if ({{=$dataType}} == 'number' || {{=$dataType}} == 'boolean') - {{=$coerced}} = '' + {{=$data}}; - else if ({{=$data}} === null) {{=$coerced}} = ''; - {{?? $type == 'number' || $type == 'integer' }} - else if ({{=$dataType}} == 'boolean' || {{=$data}} === null - || ({{=$dataType}} == 'string' && {{=$data}} && {{=$data}} == +{{=$data}} - {{? $type == 'integer' }} && !({{=$data}} % 1){{?}})) - {{=$coerced}} = +{{=$data}}; - {{?? $type == 'boolean' }} - else if ({{=$data}} === 'false' || {{=$data}} === 0 || {{=$data}} === null) - {{=$coerced}} = false; - else if ({{=$data}} === 'true' || {{=$data}} === 1) - {{=$coerced}} = true; - {{?? $type == 'null' }} - else if ({{=$data}} === '' || {{=$data}} === 0 || {{=$data}} === false) - {{=$coerced}} = null; - {{?? it.opts.coerceTypes == 'array' && $type == 'array' }} - else if ({{=$dataType}} == 'string' || {{=$dataType}} == 'number' || {{=$dataType}} == 'boolean' || {{=$data}} == null) - {{=$coerced}} = [{{=$data}}]; - {{?}} - {{~}} - else { - {{# def.error:'type' }} - } - - if ({{=$coerced}} !== undefined) { - {{# def.setParentData }} - {{=$data}} = {{=$coerced}}; - {{? !$dataLvl }}if ({{=$parentData}} !== undefined){{?}} - {{=$parentData}}[{{=$parentDataProperty}}] = {{=$coerced}}; - } -#}} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index a671490682..410be3939e 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -1,7 +1,6 @@ {{# def.definitions }} {{# def.errors }} {{# def.defaults }} -{{# def.coerce }} {{ /** * schema compilation (render) time: @@ -172,7 +171,12 @@ }} {{# def.checkType }} {{? $coerceToTypes }} - {{# def.coerceType }} + {{ + /* TODO do not clear _out */ + it.gen._out = ""; + it.coerceData(it, $coerceToTypes); + }} + {{= it.gen._out }} {{??}} {{# def.error:'type' }} {{?}} diff --git a/lib/keyword.ts b/lib/keyword.ts index 2e330742e6..67bc32832a 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -137,7 +137,7 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): stri const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition const {gen, opts, dataLevel, schemaPath, dataPathArr} = it if (!code) throw new Error('"code" and "error" must be defined') - // TODO _out + // TODO do not clear _out gen._out = "" const $data = $defData && opts.$data && schema && schema.$data const data = "data" + (dataLevel || "") diff --git a/lib/types.ts b/lib/types.ts index b7963d504f..01c4ca560f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -102,7 +102,7 @@ export interface CompilationContext { level: number dataLevel: number data: string - dataPathArr: (string | undefined)[] + dataPathArr: string[] schema: any schemaPath: string errorPath: string diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index f3efef973e..f3af003b95 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -28,7 +28,12 @@ export function dataNotType( return $data ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` : "" } -export function schemaRefOrVal(schema, schemaPath, keyword, $data): string | number | boolean { +export function schemaRefOrVal( + schema, + schemaPath: string, + keyword: string, + $data?: string | false +): string | number | boolean { const t = typeof schema if (!$data) { if (t === "number" || t === "boolean") return schema From c974266e5193358107327fa6f1c52dd6279298c9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 15 Aug 2020 14:06:44 +0100 Subject: [PATCH 044/322] refactor(validate): boolean schema to typescript --- lib/compile/errors.ts | 7 ++- lib/compile/index.js | 12 +++- lib/compile/util.ts | 2 + lib/compile/validate/applicability.ts | 4 +- lib/compile/validate/boolSchema.ts | 50 ---------------- lib/compile/validate/dataType.ts | 11 ++-- lib/compile/validate/index.ts | 8 +-- lib/compile/validate/validate.jst | 56 ++++++++--------- lib/dot/validate.jst | 86 ++++++--------------------- spec/options/options_code.spec.js | 6 +- 10 files changed, 79 insertions(+), 163 deletions(-) diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 6c5c5ae9d2..c405ad4552 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -9,10 +9,11 @@ export function reportError( const {gen, compositeRule, opts, async} = cxt.it const errObj = errorObjectCode(cxt, error) if (allErrors ?? (compositeRule || opts.allErrors)) { + const err = gen.name("err") gen.code( - `const err = ${errObj}; - if (vErrors === null) vErrors = [err]; - else vErrors.push(err); + `const ${err} = ${errObj}; + if (vErrors === null) vErrors = [${err}]; + else vErrors.push(${err}); errors++;` ) } else { diff --git a/lib/compile/index.js b/lib/compile/index.js index 45854d56c0..43c686a151 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,7 +1,9 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" -import {coerceData} from "./validate/dataType" +import {getSchemaTypes, coerceAndCheckDataType} from "./validate/dataType" +import {updateTopContext, checkNoDefault, updateContext, checkAsync} from "./validate" +import {booleanOrEmptySchema} from "./validate/boolSchema" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -101,7 +103,13 @@ function compile(schema, root, localRefs, baseId) { MissingRefError, RULES: RULES, validate: validateGenerator, - coerceData, // TODO remove when validate is replaced + getSchemaTypes, // TODO remove when validate is replaced + coerceAndCheckDataType, // TODO remove when validate is replaced + updateTopContext, // TODO remove when validate is replaced + checkNoDefault, // TODO remove when validate is replaced + updateContext, // TODO remove when validate is replaced + checkAsync, // TODO remove when validate is replaced + booleanOrEmptySchema, // TODO remove when validate is replaced util: util, resolve: resolve, resolveRef: resolveRef, diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 45248057ea..a716d9908e 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -2,6 +2,7 @@ module.exports = { checkDataType, checkDataTypes, + // TODO remove when validate is refactored coerceToTypes, toHash, escapeQuotes, @@ -72,6 +73,7 @@ export function checkDataTypes( return code } +// TODO remove when validate is refactored const COERCE_TYPES = toHash(["string", "number", "integer", "boolean", "null"]) export function coerceToTypes( optionCoerceTypes: undefined | boolean | "array", diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 3814f02ad7..22854f620a 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -5,8 +5,8 @@ export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: s return group && group !== true && shouldUseGroup(schema, group) } -function shouldUseGroup(schema, rulesGroup): boolean { - return rulesGroup.some((rule) => shouldUseRule(schema, rule)) +function shouldUseGroup(schema, group): boolean { + return group.rules.some((rule) => shouldUseRule(schema, rule)) } function shouldUseRule(schema, rule): boolean { diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index db0634b926..0f93c75b80 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -28,26 +28,6 @@ export function booleanOrEmptySchema(it: CompilationContext): void { gen.code(`var valid${level} = true;`) // TODO level, var } } - - // if (schema === false) { - // if (!isTop) { - // gen.code(`var valid${level} = false;`) // TODO level, var - // } - // // TODO probably some other interface should be used for non-keyword validation errors... - // falseSchemaError(it, !isTop) - // } else { - // if (isTop) { - // gen.code(schema.$async === true ? `return data;` : `validate.errors = null; return true;`) - // } else { - // gen.code(`var valid${level} = true;`) // TODO level, var - // } - // } - // if (isTop) { - // gen.code( - // `}; - // return validate;` - // ) - // } } function falseSchemaError(it: CompilationContext, allErrors?: boolean) { @@ -73,33 +53,3 @@ function falseSchemaError(it: CompilationContext, allErrors?: boolean) { function exception() { throw new Error("this function can only be used in keyword") } - -// {{ var $keyword = 'false schema'; }} -// {{# def.setupKeyword }} -// {{? it.schema === false}} -// {{? it.isTop}} -// {{ $breakOnError = true; }} -// {{??}} -// var {{=$valid}} = false; -// {{?}} -// {{# def.error:'false schema' }} -// {{??}} -// {{? it.isTop}} -// {{? $async }} -// return data; -// {{??}} -// validate.errors = null; -// return true; -// {{?}} -// {{??}} -// var {{=$valid}} = true; -// {{?}} -// {{?}} - -// {{? it.isTop}} -// }; -// return validate; -// {{?}} - -// {{ return out; }} -// {{?}} diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index e66226295d..f938fbf49b 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -5,8 +5,8 @@ import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { - const t = schema.type - const types: string[] = Array.isArray(t) ? t : t || [] + const t: undefined | string | string[] = schema.type + const types: string[] = Array.isArray(t) ? t : t ? [t] : [] types.forEach(checkType) if (opts.nullable) { const hasNull = types.includes("null") @@ -20,7 +20,7 @@ export function getSchemaTypes({schema, opts}: CompilationContext): string[] { function checkType(t: string): void { // TODO check that type is allowed - if (typeof t != "string") throw new Error('"type" keyword must be string or string[]') + if (typeof t != "string") throw new Error('"type" keyword must be string or string[]: ' + t) } } @@ -31,7 +31,10 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]): opts: {coerceTypes, strictNumbers}, } = it let coerceTo = coerceToTypes(types, coerceTypes) - if (coerceTo.length || types.length > 1 || !schemaHasRulesForType(it, types[0])) { + if ( + types.length && + (coerceTo.length || types.length > 1 || !schemaHasRulesForType(it, types[0])) + ) { const wrongType = checkDataTypes(types, `data${dataLevel || ""}`, strictNumbers, true) gen.code(`if (${wrongType}) {`) if (coerceTo.length) coerceData(it, coerceTo) diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index c590391597..0267e64d93 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -69,7 +69,7 @@ function startFunction({ ) } -function updateTopContext(it: CompilationContext): void { +export function updateTopContext(it: CompilationContext): void { it.rootId = resolve.fullPath(it.root.schema.$id) it.baseId = it.baseId || it.rootId delete it.isTop @@ -77,7 +77,7 @@ function updateTopContext(it: CompilationContext): void { it.dataPathArr = [""] } -function checkNoDefault({ +export function checkNoDefault({ schema, opts: {useDefaults, strictDefaults}, logger, @@ -98,11 +98,11 @@ function initializeTop({gen}: CompilationContext): void { ) } -function updateContext(it: CompilationContext): void { +export function updateContext(it: CompilationContext): void { if (it.schema.$id) it.baseId = resolve.url(it.baseId, it.schema.$id) } -function checkAsync(it: CompilationContext): void { +export function checkAsync(it: CompilationContext): void { if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") } diff --git a/lib/compile/validate/validate.jst b/lib/compile/validate/validate.jst index a3cc724668..05c245a2fc 100644 --- a/lib/compile/validate/validate.jst +++ b/lib/compile/validate/validate.jst @@ -40,34 +40,34 @@ // {{?}} // {{? typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref) }} -// {{ var $keyword = 'false schema'; }} -// {{# def.setupKeyword }} -// {{? it.schema === false}} -// {{? it.isTop}} -// {{ $breakOnError = true; }} -// {{??}} -// var {{=$valid}} = false; -// {{?}} -// {{# def.error:'false schema' }} -// {{??}} -// {{? it.isTop}} -// {{? $async }} -// return data; -// {{??}} -// validate.errors = null; -// return true; -// {{?}} -// {{??}} -// var {{=$valid}} = true; -// {{?}} -// {{?}} - -// {{? it.isTop}} -// }; -// return validate; -// {{?}} - -// {{ return out; }} + // {{ var $keyword = 'false schema'; }} + // {{# def.setupKeyword }} + // {{? it.schema === false}} + // {{? it.isTop}} + // {{ $breakOnError = true; }} + // {{??}} + // var {{=$valid}} = false; + // {{?}} + // {{# def.error:'false schema' }} + // {{??}} + // {{? it.isTop}} + // {{? $async }} + // return data; + // {{??}} + // validate.errors = null; + // return true; + // {{?}} + // {{??}} + // var {{=$valid}} = true; + // {{?}} + // {{?}} + + // {{? it.isTop}} + // }; + // return validate; + // {{?}} + + // {{ return out; }} // {{?}} {{? it.isTop }} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index 410be3939e..eaa7f3279c 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -39,34 +39,12 @@ {{?}} {{? typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref) }} - {{ var $keyword = 'false schema'; }} - {{# def.setupKeyword }} - {{? it.schema === false}} - {{? it.isTop}} - {{ $breakOnError = true; }} - {{??}} - var {{=$valid}} = false; - {{?}} - {{# def.error:'false schema' }} - {{??}} - {{? it.isTop}} - {{? $async }} - return data; - {{??}} - validate.errors = null; - return true; - {{?}} - {{??}} - var {{=$valid}} = true; - {{?}} - {{?}} - - {{? it.isTop}} - }; - return validate; - {{?}} - - {{ return out; }} + {{ + /* TODO do not clear _out */ + it.gen._out = ""; + it.booleanOrEmptySchema(it); + return out + it.gen._out; + }} {{?}} @@ -76,17 +54,9 @@ , $lvl = it.level = 0 , $dataLvl = it.dataLevel = 0 , $data = 'data'; - it.rootId = it.resolve.fullPath(it.root.schema.$id); - it.baseId = it.baseId || it.rootId; - delete it.isTop; - - it.dataPathArr = [undefined]; - if (it.schema.default !== undefined && it.opts.useDefaults && it.opts.strictDefaults) { - var $defaultMsg = 'default is ignored in the schema root'; - if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); - else throw new Error($defaultMsg); - } + it.updateTopContext(it); + it.checkNoDefault(it); }} var vErrors = null; {{ /* don't edit, used in replace */ }} @@ -98,9 +68,8 @@ , $dataLvl = it.dataLevel , $data = 'data' + ($dataLvl || ''); - if ($id) it.baseId = it.resolve.url(it.baseId, $id); - - if ($async && !it.async) throw new Error('async schema in sync schema'); + it.updateContext(it); + it.checkAsync(it); }} var errs_{{=$lvl}} = errors; @@ -157,33 +126,14 @@ {{= it.RULES.all.$comment.code(it, '$comment') }} {{?}} -{{? $typeSchema }} - {{? it.opts.coerceTypes }} - {{ var $coerceToTypes = it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema); }} - {{?}} - - {{ var $rulesGroup = it.RULES.types[$typeSchema]; }} - {{? $coerceToTypes || $typeIsArray || $rulesGroup === true || - ($rulesGroup && !$shouldUseGroup($rulesGroup)) }} - {{ - var $schemaPath = it.schemaPath + '.type' - , $errSchemaPath = it.errSchemaPath + '/type'; - }} - {{# def.checkType }} - {{? $coerceToTypes }} - {{ - /* TODO do not clear _out */ - it.gen._out = ""; - it.coerceData(it, $coerceToTypes); - }} - {{= it.gen._out }} - {{??}} - {{# def.error:'type' }} - {{?}} - } - {{?}} -{{?}} +{{ + const types = it.getSchemaTypes(it); + /* TODO do not clear _out */ + it.gen._out = ""; + it.coerceAndCheckDataType(it, types); +}} +{{= it.gen._out }} {{? it.schema.$ref && !$refKeywords }} {{= it.RULES.all.$ref.code(it, '$ref') }} @@ -222,7 +172,7 @@ {{?}} {{? $rulesGroup.type }} } - {{? $typeSchema && $typeSchema === $rulesGroup.type && !$coerceToTypes }} + {{? $typeSchema && $typeSchema === $rulesGroup.type && !(it.opts.coerceTypes && it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema)) }} else { {{ var $schemaPath = it.schemaPath + '.type' diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js index 27b11bd7b0..09750ab0e4 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.js @@ -34,12 +34,14 @@ describe("code generation options", () => { it("should process generated code", () => { var ajv = new Ajv() var validate = ajv.compile({type: "string"}) - validate.toString().split("\n").length.should.equal(1) + // TODO re-enable this test when option to strip whitespace is added + // validate.toString().split("\n").length.should.equal(1) + const unprocessedLines = validate.toString().split("\n").length var beautify = require("js-beautify").js_beautify var ajvPC = new Ajv({processCode: beautify}) validate = ajvPC.compile({type: "string"}) - validate.toString().split("\n").length.should.be.above(1) + validate.toString().split("\n").length.should.be.above(unprocessedLines) validate("foo").should.equal(true) validate(1).should.equal(false) }) From 9765bcb1d26d83484964a33f26f92aa112817ecd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 15 Aug 2020 17:40:07 +0100 Subject: [PATCH 045/322] refactor(validate): initialization, checks, $comment keyword --- lib/compile/index.js | 18 +++++++++- lib/compile/rules.js | 5 --- lib/compile/validate/index.ts | 18 ++++++---- lib/dot/comment.jst | 9 ----- lib/dot/validate.jst | 54 +++++++++++------------------ lib/dotjs/index.js | 1 - spec/options/strictKeywords.spec.js | 2 +- 7 files changed, 50 insertions(+), 57 deletions(-) delete mode 100644 lib/dot/comment.jst diff --git a/lib/compile/index.js b/lib/compile/index.js index 43c686a151..38c4fe613c 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,8 +1,18 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" +import { + startFunction, + checkUnknownKeywords, + updateTopContext, + checkNoDefault, + initializeTop, + updateContext, + checkAsync, + checkRefsAndKeywords, + commentKeyword, +} from "./validate" import {getSchemaTypes, coerceAndCheckDataType} from "./validate/dataType" -import {updateTopContext, checkNoDefault, updateContext, checkAsync} from "./validate" import {booleanOrEmptySchema} from "./validate/boolSchema" const equal = require("fast-deep-equal") @@ -91,6 +101,7 @@ function compile(schema, root, localRefs, baseId) { var sourceCode = validateGenerator({ isTop: true, + async: _schema.$async === true, schema: _schema, isRoot: isRoot, baseId: baseId, @@ -103,13 +114,18 @@ function compile(schema, root, localRefs, baseId) { MissingRefError, RULES: RULES, validate: validateGenerator, + startFunction, // TODO remove when validate is replaced + checkUnknownKeywords, // TODO remove when validate is replaced getSchemaTypes, // TODO remove when validate is replaced coerceAndCheckDataType, // TODO remove when validate is replaced updateTopContext, // TODO remove when validate is replaced checkNoDefault, // TODO remove when validate is replaced + initializeTop, // TODO remove when validate is replaced updateContext, // TODO remove when validate is replaced checkAsync, // TODO remove when validate is replaced + checkRefsAndKeywords, // TODO remove when validate is replaced booleanOrEmptySchema, // TODO remove when validate is replaced + commentKeyword, // TODO remove when validate is replaced util: util, resolve: resolve, resolveRef: resolveRef, diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 7f8e071fc8..8c93911a56 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -67,11 +67,6 @@ module.exports = function rules() { return rule }) - RULES.all.$comment = { - keyword: "$comment", - code: ruleModules.$comment, - } - if (group.type) RULES.types[group.type] = group }) diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 0267e64d93..a53e0143a2 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -15,7 +15,6 @@ export default function validateCode(it: CompilationContext): void { gen, opts: {$comment}, } = it - if (schema.$async === true) it.async = true checkUnknownKeywords(it) if (isTop) startFunction(it) if (typeof schema == "boolean" || !schemaHasRules(schema, RULES)) { @@ -32,12 +31,12 @@ export default function validateCode(it: CompilationContext): void { gen.code(`let errs_${level} = errors;`) } checkRefsAndKeywords(it) - if ($comment) commentKeyword(it) + if ($comment && schema.$comment) commentKeyword(it) const types = getSchemaTypes(it) coerceAndCheckDataType(it, types) } -function checkUnknownKeywords({ +export function checkUnknownKeywords({ schema, RULES, opts: {strictKeywords}, @@ -53,7 +52,7 @@ function checkUnknownKeywords({ } } -function startFunction({ +export function startFunction({ gen, schema, async, @@ -89,7 +88,7 @@ export function checkNoDefault({ } } -function initializeTop({gen}: CompilationContext): void { +export function initializeTop({gen}: CompilationContext): void { // TODO old comment: "don't edit, used in replace". Should be removed? gen.code( `let vErrors = null; @@ -106,7 +105,7 @@ export function checkAsync(it: CompilationContext): void { if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") } -function checkRefsAndKeywords({ +export function checkRefsAndKeywords({ schema, errSchemaPath, RULES, @@ -122,7 +121,12 @@ function checkRefsAndKeywords({ } } -function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: CompilationContext): void { +export function commentKeyword({ + gen, + schema, + errSchemaPath, + opts: {$comment}, +}: CompilationContext): void { const msg = quotedString(schema.$comment) if ($comment === true) { gen.code(`console.log(${msg})`) diff --git a/lib/dot/comment.jst b/lib/dot/comment.jst deleted file mode 100644 index f95915035c..0000000000 --- a/lib/dot/comment.jst +++ /dev/null @@ -1,9 +0,0 @@ -{{# def.definitions }} -{{# def.setupKeyword }} - -{{ var $comment = it.util.toQuotedString($schema); }} -{{? it.opts.$comment === true }} - console.log({{=$comment}}); -{{?? typeof it.opts.$comment == 'function' }} - self._opts.$comment({{=$comment}}, {{=it.util.toQuotedString($errSchemaPath)}}, validate.root.schema); -{{?}} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index eaa7f3279c..1541bc8fdd 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -19,23 +19,14 @@ , $id = it.schema.$id; }} -{{ - if (it.opts.strictKeywords) { - var $unknownKwd = it.util.schemaUnknownRules(it.schema, it.RULES.keywords); - if ($unknownKwd) { - var $keywordsMsg = 'unknown keyword: ' + $unknownKwd; - if (it.opts.strictKeywords === 'log') it.logger.warn($keywordsMsg); - else throw new Error($keywordsMsg); - } - } -}} +{{ it.checkUnknownKeywords(it); }} {{? it.isTop }} - var validate = {{?$async}}{{it.async = true;}}async {{?}}function(data, dataPath, parentData, parentDataProperty, rootData) { - 'use strict'; - {{? $id && (it.opts.sourceCode || it.opts.processCode) }} - {{= '/\*# sourceURL=' + $id + ' */' }} - {{?}} + {{ + it.gen._out = ""; + it.startFunction(it); + }} + {{=it.gen._out}} {{?}} {{? typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref) }} @@ -59,9 +50,11 @@ it.checkNoDefault(it); }} - var vErrors = null; {{ /* don't edit, used in replace */ }} - var errors = 0; {{ /* don't edit, used in replace */ }} - if (rootData === undefined) rootData = data; {{ /* don't edit, used in replace */ }} + {{ + it.gen._out = ""; + it.initializeTop(it); + }} + {{=it.gen._out}} {{??}} {{ var $lvl = it.level @@ -111,21 +104,16 @@ if ({{= it.util[$method]($typeSchema, $data, it.opts.strictNumbers, true) }}) { #}} -{{? it.schema.$ref && $refKeywords }} - {{? it.opts.extendRefs == 'fail' }} - {{ throw new Error('$ref: validation keywords used in schema at path "' + it.errSchemaPath + '" (see option extendRefs)'); }} - {{?? it.opts.extendRefs !== true }} - {{ - $refKeywords = false; - it.logger.warn('$ref: keywords ignored in schema at path "' + it.errSchemaPath + '"'); - }} - {{?}} -{{?}} - -{{? it.schema.$comment && it.opts.$comment }} - {{= it.RULES.all.$comment.code(it, '$comment') }} -{{?}} - +{{ + it.gen._out = ""; + it.checkRefsAndKeywords(it); + /* TODO remove below once $refKeywords no longer used */ + if (it.opts.extendRefs !== true) $refKeywords = false; + if (it.opts.$comment && it.schema.$comment) { + it.commentKeyword(it) + } +}} +{{=it.gen._out}} {{ const types = it.getSchemaTypes(it); diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index caf3a34144..7bab7a1078 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -5,7 +5,6 @@ module.exports = { $ref: require("./ref"), allOf: require("./allOf"), anyOf: require("./anyOf"), - $comment: require("./comment"), contains: require("./contains"), dependencies: require("./dependencies"), format: require("./format"), diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js index c45bbecbfe..96c7ea2162 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.js @@ -46,7 +46,7 @@ describe("strictKeywords option", () => { unknownKeyword: 1, } ajv.compile(schema) - should.equal(output.warning, "unknown keyword: unknownKeyword") + should.equal(output.warning, 'unknown keyword: "unknownKeyword"') }) }) From 822336ca48fbe62bb5c252fb5752c43a19802095 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 15 Aug 2020 19:57:33 +0100 Subject: [PATCH 046/322] refactor(validate): defaults.def to typescript --- lib/compile/index.js | 2 ++ lib/compile/validate/applicability.ts | 2 +- lib/compile/validate/defaults.ts | 45 +++++++++++++++++++++++++ lib/dot/defaults.def | 47 --------------------------- lib/dot/validate.jst | 11 +++---- lib/types.ts | 1 + spec/options/useDefaults.spec.js | 2 +- 7 files changed, 55 insertions(+), 55 deletions(-) create mode 100644 lib/compile/validate/defaults.ts delete mode 100644 lib/dot/defaults.def diff --git a/lib/compile/index.js b/lib/compile/index.js index 38c4fe613c..72e6daa040 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -14,6 +14,7 @@ import { } from "./validate" import {getSchemaTypes, coerceAndCheckDataType} from "./validate/dataType" import {booleanOrEmptySchema} from "./validate/boolSchema" +import {assignDefaults} from "./validate/defaults" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -126,6 +127,7 @@ function compile(schema, root, localRefs, baseId) { checkRefsAndKeywords, // TODO remove when validate is replaced booleanOrEmptySchema, // TODO remove when validate is replaced commentKeyword, // TODO remove when validate is replaced + assignDefaults, // TODO remove when validate is replaced util: util, resolve: resolve, resolveRef: resolveRef, diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 22854f620a..31f32628d8 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -5,7 +5,7 @@ export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: s return group && group !== true && shouldUseGroup(schema, group) } -function shouldUseGroup(schema, group): boolean { +export function shouldUseGroup(schema, group): boolean { return group.rules.some((rule) => shouldUseRule(schema, rule)) } diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts new file mode 100644 index 0000000000..a3925323ca --- /dev/null +++ b/lib/compile/validate/defaults.ts @@ -0,0 +1,45 @@ +import {CompilationContext} from "../../types" +import {getProperty} from "../util" + +export function assignDefaults(it: CompilationContext, group) { + const {properties, items} = it.schema + if (group.type === "object" && properties) { + for (const key in properties) { + assignDefault(it, key, properties[key].default) + } + } else if (group.type === "array" && Array.isArray(items)) { + items.forEach((sch, i: number) => assignDefault(it, i, sch.default)) + } +} + +function assignDefault( + { + gen, + compositeRule, + dataLevel, + useDefault, + opts: {strictDefaults, useDefaults}, + logger, + }: CompilationContext, + prop: string | number, + defaultValue: any +) { + if (defaultValue === undefined) return + // TODO refactor `data${dataLevel || ""}` + const data = "data" + (dataLevel || "") + getProperty(prop) + if (compositeRule) { + if (strictDefaults) { + const msg = `default is ignored for: ${data}` + if (strictDefaults === "log") logger.warn(msg) + else throw new Error(msg) + } + return + } + + let condition = + `${data} === undefined` + + (useDefaults === "empty" ? ` || ${data} === null || ${data} === ""` : "") + // TODO remove option `useDefaults === "shared"` + const defaultExpr = useDefaults === "shared" ? useDefault : JSON.stringify + gen.code(`if (${condition}) ${data} = ${defaultExpr(defaultValue)};`) +} diff --git a/lib/dot/defaults.def b/lib/dot/defaults.def deleted file mode 100644 index a844cf2854..0000000000 --- a/lib/dot/defaults.def +++ /dev/null @@ -1,47 +0,0 @@ -{{## def.assignDefault: - {{? it.compositeRule }} - {{ - if (it.opts.strictDefaults) { - var $defaultMsg = 'default is ignored for: ' + $passData; - if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); - else throw new Error($defaultMsg); - } - }} - {{??}} - if ({{=$passData}} === undefined - {{? it.opts.useDefaults == 'empty' }} - || {{=$passData}} === null - || {{=$passData}} === '' - {{?}} - ) - {{=$passData}} = {{? it.opts.useDefaults == 'shared' }} - {{= it.useDefault($sch.default) }} - {{??}} - {{= JSON.stringify($sch.default) }} - {{?}}; - {{?}} -#}} - - -{{## def.defaultProperties: - {{ - var $schema = it.schema.properties - , $schemaKeys = Object.keys($schema); }} - {{~ $schemaKeys:$propertyKey }} - {{ var $sch = $schema[$propertyKey]; }} - {{? $sch.default !== undefined }} - {{ var $passData = $data + it.util.getProperty($propertyKey); }} - {{# def.assignDefault }} - {{?}} - {{~}} -#}} - - -{{## def.defaultItems: - {{~ it.schema.items:$sch:$i }} - {{? $sch.default !== undefined }} - {{ var $passData = $data + '[' + $i + ']'; }} - {{# def.assignDefault }} - {{?}} - {{~}} -#}} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index 1541bc8fdd..2af57098b3 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -1,6 +1,5 @@ {{# def.definitions }} {{# def.errors }} -{{# def.defaults }} {{ /** * schema compilation (render) time: @@ -137,11 +136,11 @@ if ({{= it.util.checkDataType($rulesGroup.type, $data, it.opts.strictNumbers) }}) { {{?}} {{? it.opts.useDefaults }} - {{? $rulesGroup.type == 'object' && it.schema.properties }} - {{# def.defaultProperties }} - {{?? $rulesGroup.type == 'array' && Array.isArray(it.schema.items) }} - {{# def.defaultItems }} - {{?}} + {{ + it.gen._out = ""; + it.assignDefaults(it, $rulesGroup); + }} + {{=it.gen._out}} {{?}} {{~ $rulesGroup.rules:$rule }} {{? $shouldUseRule($rule) }} diff --git a/lib/types.ts b/lib/types.ts index 01c4ca560f..c7ccb5ccc0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -121,6 +121,7 @@ export interface CompilationContext { compositeRule: boolean validate: (schema: object) => boolean usePattern: (str: string) => string + useDefault: (value: any) => string util: object // TODO self: object // TODO RULES: any // TODO replace? diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index 6d1c1f8f67..3c56dd9c30 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -4,7 +4,7 @@ var Ajv = require("../ajv") var getAjvInstances = require("../ajv_instances") require("../chai").should() -describe("useDefaults options", () => { +describe("useDefaults option", () => { it("should replace undefined property with default value", () => { var instances = getAjvInstances( { From 55eb336985b5ac256441497b0d0e89e93e0eb795 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 15 Aug 2020 21:07:05 +0100 Subject: [PATCH 047/322] refactor(validate): keywords validation to typescript --- lib/compile/index.js | 4 +- lib/compile/validate/applicability.ts | 2 +- lib/compile/validate/dataType.ts | 14 +-- lib/compile/validate/keywords.ts | 78 ++++++++++++++++ lib/dot/errors.def | 6 -- lib/dot/validate.jst | 123 +------------------------- spec/options/options_refs.spec.js | 4 +- 7 files changed, 95 insertions(+), 136 deletions(-) create mode 100644 lib/compile/validate/keywords.ts diff --git a/lib/compile/index.js b/lib/compile/index.js index 72e6daa040..0486b6a7f2 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -15,6 +15,7 @@ import { import {getSchemaTypes, coerceAndCheckDataType} from "./validate/dataType" import {booleanOrEmptySchema} from "./validate/boolSchema" import {assignDefaults} from "./validate/defaults" +import schemaKeywords from "./validate/keywords" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -128,6 +129,7 @@ function compile(schema, root, localRefs, baseId) { booleanOrEmptySchema, // TODO remove when validate is replaced commentKeyword, // TODO remove when validate is replaced assignDefaults, // TODO remove when validate is replaced + schemaKeywords, // TODO remove when validate is replaced util: util, resolve: resolve, resolveRef: resolveRef, @@ -148,7 +150,7 @@ function compile(schema, root, localRefs, baseId) { sourceCode if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) - // console.log('\n\n\n *** \n', JSON.stringify(sourceCode)); + // console.log("\n\n\n *** \n", sourceCode) var validate try { var makeValidate = new Function( diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 31f32628d8..a42c95627b 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -9,7 +9,7 @@ export function shouldUseGroup(schema, group): boolean { return group.rules.some((rule) => shouldUseRule(schema, rule)) } -function shouldUseRule(schema, rule): boolean { +export function shouldUseRule(schema, rule): boolean { return schema[rule.keyword] !== undefined || ruleImplementsSomeKeyword(schema, rule) } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index f938fbf49b..6eb1228b6d 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -24,23 +24,25 @@ export function getSchemaTypes({schema, opts}: CompilationContext): string[] { } } -export function coerceAndCheckDataType(it: CompilationContext, types: string[]): void { +export function coerceAndCheckDataType(it: CompilationContext, types: string[]): boolean { const { gen, dataLevel, opts: {coerceTypes, strictNumbers}, } = it let coerceTo = coerceToTypes(types, coerceTypes) - if ( - types.length && - (coerceTo.length || types.length > 1 || !schemaHasRulesForType(it, types[0])) - ) { + const checkTypes = + types.length > 0 && + (coerceTo.length > 0 || types.length > 1 || !schemaHasRulesForType(it, types[0])) + if (checkTypes) { + // TODO refactor `data${dataLevel || ""}` const wrongType = checkDataTypes(types, `data${dataLevel || ""}`, strictNumbers, true) gen.code(`if (${wrongType}) {`) if (coerceTo.length) coerceData(it, coerceTo) else reportTypeError(it) gen.code("}") } + return checkTypes } const COERCIBLE = toHash(["string", "number", "integer", "boolean", "null"]) @@ -136,7 +138,7 @@ const typeError: KeywordErrorDefinition = { // TODO maybe combine with boolSchemaError // TODO refactor type keyword context creation -function reportTypeError(it: CompilationContext) { +export function reportTypeError(it: CompilationContext) { const {gen, schema, schemaPath, dataLevel} = it const schemaCode = schemaRefOrVal(schema, schemaPath, "type") const cxt: KeywordContext = { diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts new file mode 100644 index 0000000000..77f156ebc1 --- /dev/null +++ b/lib/compile/validate/keywords.ts @@ -0,0 +1,78 @@ +import {CompilationContext} from "../../types" +import {shouldUseGroup, shouldUseRule} from "./applicability" +import {checkDataType, schemaHasRulesExcept} from "../util" +import {assignDefaults} from "./defaults" +import {reportTypeError} from "./dataType" + +export default function schemaKeywords( + it: CompilationContext, + types: string[], + typeErrors: boolean, + top: boolean +) { + const { + gen, + schema, + level, + dataLevel, + RULES, + opts: {allErrors, extendRefs, strictNumbers, useDefaults}, + } = it + let closingBraces2 = "" + if (schema.$ref && !(extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref"))) { + // TODO _out + const _out = gen._out + const code = RULES.all.$ref.code(it, "$ref") + gen._out = _out + gen.code(code) + if (!allErrors) { + // TODO refactor with below + const errCount = top ? "0" : `errs_${level}` + gen.code( + `} + if (errors === ${errCount}) {` + ) + closingBraces2 += "}" + } + } else { + for (const group of RULES) { + if (shouldUseGroup(schema, group)) { + if (group.type) { + // TODO refactor `data${dataLevel || ""}` + const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) + gen.code(`if (${checkType}) {`) + } + if (useDefaults) assignDefaults(it, group) + let closingBraces1 = "" + for (const rule of group.rules) { + if (shouldUseRule(schema, rule)) { + // TODO _out + const _out = gen._out + gen._out = "" + const code = rule.code(it, rule.keyword, group.type) + gen._out = _out + if (code) { + gen.code(code) + if (!allErrors) closingBraces1 += "}" + } + } + } + if (!allErrors) gen.code(closingBraces1) + if (group.type) { + gen.code("}") + if (types.length === 1 && types[0] === group.type && typeErrors) { + gen.code(`else {`) + reportTypeError(it) + gen.code(`}`) + } + } + if (!allErrors) { + const errCount = top ? "0" : `errs_${level}` + gen.code(`if (errors === ${errCount}) {`) + closingBraces2 += "}" + } + } + } + } + if (!allErrors) gen.code(closingBraces2) +} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 32dcfaa39d..c0a462e2ee 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -89,7 +89,6 @@ {{## def.concatSchemaEQ:{{?$isData}}' + {{=$schemaValue}} + '{{??}}{{=it.util.escapeQuotes($schema)}}{{?}}#}} {{## def._errorMessages = { - 'false schema': "'boolean schema is false'", $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", additionalItems: "'should NOT have more than {{=$schema.length}} items'", additionalProperties: "'{{? it.opts._errorDataPathProperty }}is an invalid additional property{{??}}should NOT have additional properties{{?}}'", @@ -102,7 +101,6 @@ oneOf: "'should match exactly one schema in oneOf'", propertyNames: "'property name \\'{{=$invalidName}}\\' is invalid'", required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", - type: "'should be {{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}'", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", switch: "'should pass \"switch\" keyword validation'", @@ -115,7 +113,6 @@ {{## def.schemaRefOrQS: {{?$isData}}validate.schema{{=$schemaPath}}{{??}}{{=it.util.toQuotedString($schema)}}{{?}} #}} {{## def._errorSchemas = { - 'false schema': "false", $ref: "{{=it.util.toQuotedString($schema)}}", additionalItems: "false", additionalProperties: "false", @@ -128,7 +125,6 @@ oneOf: "validate.schema{{=$schemaPath}}", propertyNames: "validate.schema{{=$schemaPath}}", required: "validate.schema{{=$schemaPath}}", - type: "validate.schema{{=$schemaPath}}", custom: "validate.schema{{=$schemaPath}}", patternRequired: "validate.schema{{=$schemaPath}}", switch: "validate.schema{{=$schemaPath}}", @@ -140,7 +136,6 @@ {{## def.schemaValueQS: {{?$isData}}{{=$schemaValue}}{{??}}{{=it.util.toQuotedString($schema)}}{{?}} #}} {{## def._errorParams = { - 'false schema': "{}", $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", additionalItems: "{ limit: {{=$schema.length}} }", additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", @@ -153,7 +148,6 @@ oneOf: "{ passingSchemas: {{=$passingSchemas}} }", propertyNames: "{ propertyName: '{{=$invalidName}}' }", required: "{ missingProperty: '{{=$missingProperty}}' }", - type: "{ type: '{{? $typeIsArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }", custom: "{ keyword: '{{=$rule.keyword}}' }", patternRequired: "{ missingPattern: '{{=$missingPattern}}' }", switch: "{ caseIndex: {{=$caseIndex}} }", diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index 2af57098b3..8857d10590 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -14,7 +14,6 @@ {{ var $async = it.schema.$async === true - , $refKeywords = it.util.schemaHasRulesExcept(it.schema, it.RULES.all, '$ref') , $id = it.schema.$id; }} @@ -28,7 +27,7 @@ {{=it.gen._out}} {{?}} -{{? typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref) }} +{{? typeof it.schema == 'boolean' || !it.util.schemaHasRules(it.schema, it.RULES.all) }} {{ /* TODO do not clear _out */ it.gen._out = ""; @@ -67,47 +66,11 @@ var errs_{{=$lvl}} = errors; {{?}} -{{ - var $valid = 'valid' + $lvl - , $breakOnError = !it.opts.allErrors - , $closingBraces1 = '' - , $closingBraces2 = ''; - - var $errorKeyword; - var $typeSchema = it.schema.type - , $typeIsArray = Array.isArray($typeSchema); - - if ($typeSchema && it.opts.nullable && it.schema.nullable === true) { - if ($typeIsArray) { - if ($typeSchema.indexOf('null') == -1) - $typeSchema = $typeSchema.concat('null'); - } else if ($typeSchema != 'null') { - $typeSchema = [$typeSchema, 'null']; - $typeIsArray = true; - } - } - - if ($typeIsArray && $typeSchema.length == 1) { - $typeSchema = $typeSchema[0]; - $typeIsArray = false; - } -}} - -{{## def.checkType: - {{ - var $schemaPath = it.schemaPath + '.type' - , $errSchemaPath = it.errSchemaPath + '/type' - , $method = $typeIsArray ? 'checkDataTypes' : 'checkDataType'; - }} - - if ({{= it.util[$method]($typeSchema, $data, it.opts.strictNumbers, true) }}) { -#}} +{{ var $valid = 'valid' + $lvl; }} {{ it.gen._out = ""; it.checkRefsAndKeywords(it); - /* TODO remove below once $refKeywords no longer used */ - if (it.opts.extendRefs !== true) $refKeywords = false; if (it.opts.$comment && it.schema.$comment) { it.commentKeyword(it) } @@ -118,68 +81,11 @@ const types = it.getSchemaTypes(it); /* TODO do not clear _out */ it.gen._out = ""; - it.coerceAndCheckDataType(it, types); + const checkedTypes = it.coerceAndCheckDataType(it, types); + it.schemaKeywords(it, types, !checkedTypes, $top); }} {{= it.gen._out }} -{{? it.schema.$ref && !$refKeywords }} - {{= it.RULES.all.$ref.code(it, '$ref') }} - {{? $breakOnError }} - } - if (errors === {{?$top}}0{{??}}errs_{{=$lvl}}{{?}}) { - {{ $closingBraces2 += '}'; }} - {{?}} -{{??}} - {{~ it.RULES:$rulesGroup }} - {{? $shouldUseGroup($rulesGroup) }} - {{? $rulesGroup.type }} - if ({{= it.util.checkDataType($rulesGroup.type, $data, it.opts.strictNumbers) }}) { - {{?}} - {{? it.opts.useDefaults }} - {{ - it.gen._out = ""; - it.assignDefaults(it, $rulesGroup); - }} - {{=it.gen._out}} - {{?}} - {{~ $rulesGroup.rules:$rule }} - {{? $shouldUseRule($rule) }} - {{ var $code = $rule.code(it, $rule.keyword, $rulesGroup.type); }} - {{? $code }} - {{= $code }} - {{? $breakOnError }} - {{ $closingBraces1 += '}'; }} - {{?}} - {{?}} - {{?}} - {{~}} - {{? $breakOnError }} - {{= $closingBraces1 }} - {{ $closingBraces1 = ''; }} - {{?}} - {{? $rulesGroup.type }} - } - {{? $typeSchema && $typeSchema === $rulesGroup.type && !(it.opts.coerceTypes && it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema)) }} - else { - {{ - var $schemaPath = it.schemaPath + '.type' - , $errSchemaPath = it.errSchemaPath + '/type'; - }} - {{# def.error:'type' }} - } - {{?}} - {{?}} - - {{? $breakOnError }} - if (errors === {{?$top}}0{{??}}errs_{{=$lvl}}{{?}}) { - {{ $closingBraces2 += '}'; }} - {{?}} - {{?}} - {{~}} -{{?}} - -{{? $breakOnError }} {{= $closingBraces2 }} {{?}} - {{? $top }} {{? $async }} if (errors === 0) return data; {{ /* don't edit, used in replace */ }} @@ -194,24 +100,3 @@ {{??}} var {{=$valid}} = errors === errs_{{=$lvl}}; {{?}} - -{{ - function $shouldUseGroup($rulesGroup) { - var rules = $rulesGroup.rules; - for (var i=0; i < rules.length; i++) - if ($shouldUseRule(rules[i])) - return true; - } - - function $shouldUseRule($rule) { - return it.schema[$rule.keyword] !== undefined || - ($rule.implements && $ruleImplementsSomeKeyword($rule)); - } - - function $ruleImplementsSomeKeyword($rule) { - var impl = $rule.implements; - for (var i=0; i < impl.length; i++) - if (it.schema[impl[i]] !== undefined) - return true; - } -}} diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js index dae4f8c107..48f7a16b46 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.js @@ -139,9 +139,7 @@ describe("referenced schema options", () => { testMissingRefsFail(new Ajv({missingRefs: "fail"})) testMissingRefsFail(new Ajv({missingRefs: "fail", verbose: true})) testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true})) - testMissingRefsFail( - new Ajv({missingRefs: "fail", allErrors: true, verbose: true}) - ) + testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true, verbose: true})) function testMissingRefsFail(ajv) { var validate = ajv.compile({ From f2d2416078b299c15beeb1530e7a7b0a0f609f08 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 16 Aug 2020 17:38:19 +0100 Subject: [PATCH 048/322] refactor: validator generation to typescript --- lib/compile/index.js | 14 +++--- lib/compile/validate/index.ts | 34 +++++++++++-- lib/compile/validate/keywords.ts | 2 +- lib/dot/allOf.jst | 2 + lib/dot/anyOf.jst | 2 + lib/dot/contains.jst | 2 + lib/dot/custom.jst | 2 + lib/dot/definitions.def | 2 + lib/dot/dependencies.jst | 2 + lib/dot/format.jst | 2 + lib/dot/if.jst | 1 + lib/dot/items.jst | 2 + lib/dot/missing.def | 2 + lib/dot/not.jst | 2 + lib/dot/oneOf.jst | 2 + lib/dot/properties.jst | 2 + lib/dot/propertyNames.jst | 2 + lib/dot/ref.jst | 2 + lib/dot/required.jst | 2 + lib/dot/validate.jst | 86 +++++++------------------------- 20 files changed, 87 insertions(+), 80 deletions(-) diff --git a/lib/compile/index.js b/lib/compile/index.js index 0486b6a7f2..8566e8ad69 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,7 +1,7 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" -import { +import validateCode, { startFunction, checkUnknownKeywords, updateTopContext, @@ -11,11 +11,12 @@ import { checkAsync, checkRefsAndKeywords, commentKeyword, + endFunction, } from "./validate" import {getSchemaTypes, coerceAndCheckDataType} from "./validate/dataType" import {booleanOrEmptySchema} from "./validate/boolSchema" import {assignDefaults} from "./validate/defaults" -import schemaKeywords from "./validate/keywords" +import {schemaKeywords} from "./validate/keywords" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -24,8 +25,6 @@ var resolve = require("./resolve"), util = require("./util"), stableStringify = require("fast-json-stable-stringify") -var validateGenerator = require("../dotjs/validate") - /** * Functions below are used inside compiled validations function */ @@ -101,7 +100,7 @@ function compile(schema, root, localRefs, baseId) { var $async = _schema.$async === true - var sourceCode = validateGenerator({ + var sourceCode = validateCode({ isTop: true, async: _schema.$async === true, schema: _schema, @@ -111,11 +110,13 @@ function compile(schema, root, localRefs, baseId) { schemaPath: "", errSchemaPath: "#", errorPath: '""', + level: 0, + dataLevel: 0, data: "data", // TODO get unique name when passed from applicator keywords gen: new CodeGen(), MissingRefError, RULES: RULES, - validate: validateGenerator, + validate: validateCode, startFunction, // TODO remove when validate is replaced checkUnknownKeywords, // TODO remove when validate is replaced getSchemaTypes, // TODO remove when validate is replaced @@ -130,6 +131,7 @@ function compile(schema, root, localRefs, baseId) { commentKeyword, // TODO remove when validate is replaced assignDefaults, // TODO remove when validate is replaced schemaKeywords, // TODO remove when validate is replaced + endFunction, // TODO remove when validate is replaced util: util, resolve: resolve, resolveRef: resolveRef, diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index a53e0143a2..c01893bc07 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -3,10 +3,11 @@ import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util" import {quotedString} from "../../vocabularies/util" import {booleanOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" +import {schemaKeywords} from "./keywords" const resolve = require("../resolve") -export default function validateCode(it: CompilationContext): void { +export default function validateCode(it: CompilationContext): string { const { isTop, schema, @@ -15,11 +16,14 @@ export default function validateCode(it: CompilationContext): void { gen, opts: {$comment}, } = it + // TODO _out + gen._out = "" checkUnknownKeywords(it) if (isTop) startFunction(it) - if (typeof schema == "boolean" || !schemaHasRules(schema, RULES)) { + if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { booleanOrEmptySchema(it) - return + // TODO _out + return gen._out } if (isTop) { updateTopContext(it) @@ -28,12 +32,17 @@ export default function validateCode(it: CompilationContext): void { } else { updateContext(it) checkAsync(it) - gen.code(`let errs_${level} = errors;`) + gen.code(`var errs_${level} = errors;`) } checkRefsAndKeywords(it) if ($comment && schema.$comment) commentKeyword(it) const types = getSchemaTypes(it) - coerceAndCheckDataType(it, types) + const checkedTypes = coerceAndCheckDataType(it, types) + schemaKeywords(it, types, !checkedTypes, isTop) + if (isTop) endFunction(it) + else gen.code(`var valid${level} = errors === errs_${level};`) + // TODO _out + return gen._out } export function checkUnknownKeywords({ @@ -135,3 +144,18 @@ export function commentKeyword({ gen.code(`self._opts.$comment(${msg}, ${schemaPath}, validate.root.schema)`) } } + +export function endFunction({gen, async}: CompilationContext) { + // TODO old comment: "don't edit, used in replace". Should be removed? + gen.code( + async + ? `if (errors === 0) return data; + else throw new ValidationError(vErrors);` + : `validate.errors = vErrors; + return errors === 0;` + ) + gen.code( + `}; + return validate;` + ) +} diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index 77f156ebc1..9f0a7595b6 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -4,7 +4,7 @@ import {checkDataType, schemaHasRulesExcept} from "../util" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" -export default function schemaKeywords( +export function schemaKeywords( it: CompilationContext, types: string[], typeErrors: boolean, diff --git a/lib/dot/allOf.jst b/lib/dot/allOf.jst index 0e782fe98e..8045586d31 100644 --- a/lib/dot/allOf.jst +++ b/lib/dot/allOf.jst @@ -30,3 +30,5 @@ {{= $closingBraces.slice(0,-1) }} {{?}} {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/anyOf.jst b/lib/dot/anyOf.jst index ea909ee628..3ae85df199 100644 --- a/lib/dot/anyOf.jst +++ b/lib/dot/anyOf.jst @@ -44,3 +44,5 @@ if (true) { {{?}} {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/contains.jst b/lib/dot/contains.jst index 4dc9967418..94da7a2ef5 100644 --- a/lib/dot/contains.jst +++ b/lib/dot/contains.jst @@ -53,3 +53,5 @@ var {{=$valid}}; {{# def.resetErrors }} {{?}} {{? it.opts.allErrors }} } {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst index d30588fb0b..7ddb02f4e2 100644 --- a/lib/dot/custom.jst +++ b/lib/dot/custom.jst @@ -189,3 +189,5 @@ var {{=$valid}}; } {{? $breakOnError }} else { {{?}} {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index 745382f1d1..6144eae46d 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -191,3 +191,5 @@ {{## def.isOwnProperty: Object.prototype.hasOwnProperty.call({{=$data}}, '{{=it.util.escapeQuotes($propertyKey)}}') #}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/dependencies.jst b/lib/dot/dependencies.jst index e4bdddec8e..00a5e67a32 100644 --- a/lib/dot/dependencies.jst +++ b/lib/dot/dependencies.jst @@ -77,3 +77,5 @@ var missing{{=$lvl}}; {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/format.jst b/lib/dot/format.jst index 37f14da80e..24c05e5229 100644 --- a/lib/dot/format.jst +++ b/lib/dot/format.jst @@ -104,3 +104,5 @@ {{?}} {{# def.error:'format' }} } {{? $breakOnError }} else { {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/if.jst b/lib/dot/if.jst index adb5036124..56148d28e7 100644 --- a/lib/dot/if.jst +++ b/lib/dot/if.jst @@ -71,3 +71,4 @@ {{?}} {{?}} +{{ it.gen.code(out); }} diff --git a/lib/dot/items.jst b/lib/dot/items.jst index acc932a26e..dcc2b696fe 100644 --- a/lib/dot/items.jst +++ b/lib/dot/items.jst @@ -96,3 +96,5 @@ var {{=$valid}}; {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/missing.def b/lib/dot/missing.def index a73b9f966e..40973df356 100644 --- a/lib/dot/missing.def +++ b/lib/dot/missing.def @@ -37,3 +37,5 @@ {{# def.addError:_error }} } #}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/not.jst b/lib/dot/not.jst index e03185ae87..b49b85d05c 100644 --- a/lib/dot/not.jst +++ b/lib/dot/not.jst @@ -41,3 +41,5 @@ if (false) { {{?}} {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/oneOf.jst b/lib/dot/oneOf.jst index bcce2c6ed5..86094a0005 100644 --- a/lib/dot/oneOf.jst +++ b/lib/dot/oneOf.jst @@ -52,3 +52,5 @@ if (!{{=$valid}}) { } else { {{# def.resetErrors }} {{? it.opts.allErrors }} } {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 5cebb9b12e..3da5ecb141 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -243,3 +243,5 @@ var {{=$nextValid}} = true; {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/propertyNames.jst b/lib/dot/propertyNames.jst index d456ccafc4..4d44ff26db 100644 --- a/lib/dot/propertyNames.jst +++ b/lib/dot/propertyNames.jst @@ -50,3 +50,5 @@ var {{=$errs}} = errors; {{= $closingBraces }} if ({{=$errs}} == errors) { {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/ref.jst b/lib/dot/ref.jst index 253e3507cb..622bf93dc9 100644 --- a/lib/dot/ref.jst +++ b/lib/dot/ref.jst @@ -83,3 +83,5 @@ } {{? $breakOnError }} else { {{?}} {{?}} {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/required.jst b/lib/dot/required.jst index 80fde35e8c..4fedc4eb83 100644 --- a/lib/dot/required.jst +++ b/lib/dot/required.jst @@ -106,3 +106,5 @@ {{?? $breakOnError }} if (true) { {{?}} + +{{ it.gen.code(out); }} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index 8857d10590..c106fadf2f 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -1,6 +1,3 @@ -{{# def.definitions }} -{{# def.errors }} - {{ /** * schema compilation (render) time: * it = { schema, RULES, _validate, opts } @@ -10,93 +7,46 @@ * runtime: * "validate" is a variable name to which this function will be assigned * validateRef etc. are defined in the parent scope in index.js - */ }} + */ -{{ - var $async = it.schema.$async === true - , $id = it.schema.$id; -}} + /* TODO do not clear _out */ + it.gen._out = ""; -{{ it.checkUnknownKeywords(it); }} + it.checkUnknownKeywords(it); -{{? it.isTop }} - {{ - it.gen._out = ""; - it.startFunction(it); - }} - {{=it.gen._out}} -{{?}} + if (it.isTop) it.startFunction(it); -{{? typeof it.schema == 'boolean' || !it.util.schemaHasRules(it.schema, it.RULES.all) }} - {{ - /* TODO do not clear _out */ - it.gen._out = ""; + if (typeof it.schema == 'boolean' || !it.util.schemaHasRules(it.schema, it.RULES.all)) { it.booleanOrEmptySchema(it); - return out + it.gen._out; - }} -{{?}} - + return it.gen._out; + } -{{? it.isTop }} - {{ + if (it.isTop) { var $top = it.isTop - , $lvl = it.level = 0 - , $dataLvl = it.dataLevel = 0 - , $data = 'data'; + , $lvl = it.level; it.updateTopContext(it); it.checkNoDefault(it); - }} - - {{ - it.gen._out = ""; it.initializeTop(it); - }} - {{=it.gen._out}} -{{??}} - {{ - var $lvl = it.level - , $dataLvl = it.dataLevel - , $data = 'data' + ($dataLvl || ''); + } else { + var $lvl = it.level; it.updateContext(it); it.checkAsync(it); - }} - - var errs_{{=$lvl}} = errors; -{{?}} - -{{ var $valid = 'valid' + $lvl; }} + it.gen.code(`var errs_${$lvl} = errors;`); + } -{{ - it.gen._out = ""; it.checkRefsAndKeywords(it); if (it.opts.$comment && it.schema.$comment) { it.commentKeyword(it) } -}} -{{=it.gen._out}} -{{ const types = it.getSchemaTypes(it); - /* TODO do not clear _out */ - it.gen._out = ""; const checkedTypes = it.coerceAndCheckDataType(it, types); it.schemaKeywords(it, types, !checkedTypes, $top); -}} -{{= it.gen._out }} -{{? $top }} - {{? $async }} - if (errors === 0) return data; {{ /* don't edit, used in replace */ }} - else throw new ValidationError(vErrors); {{ /* don't edit, used in replace */ }} - {{??}} - validate.errors = vErrors; {{ /* don't edit, used in replace */ }} - return errors === 0; {{ /* don't edit, used in replace */ }} - {{?}} - }; + if ($top) it.endFunction(it); + else it.gen.code(`var valid${$lvl} = errors === errs_${$lvl};`); - return validate; -{{??}} - var {{=$valid}} = errors === errs_{{=$lvl}}; -{{?}} + return it.gen._out; +}} From 5d7e4848df439c8fb0c2d5a0adb5b6e397f39973 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 16 Aug 2020 18:17:03 +0100 Subject: [PATCH 049/322] refactor: remove unused validate generation --- lib/compile/codegen.ts | 4 +- lib/compile/index.js | 32 +--- lib/compile/validate/index.ts | 50 ++++-- lib/compile/validate/keywords.ts | 17 +- lib/compile/validate/validate.jst | 277 ------------------------------ lib/dot/validate.jst | 52 ------ lib/dotjs/index.js | 1 - lib/keyword.ts | 7 +- 8 files changed, 40 insertions(+), 400 deletions(-) delete mode 100644 lib/compile/validate/validate.jst delete mode 100644 lib/dot/validate.jst diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index a4999372ce..bcd00f2573 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -9,9 +9,9 @@ export default class CodeGen { return `${prefix}_${num}` } - code(str: string): CodeGen { + code(str?: string): CodeGen { // TODO optionally strip whitespace - this._out += str + "\n" + if (str) this._out += str + "\n" return this } } diff --git a/lib/compile/index.js b/lib/compile/index.js index 8566e8ad69..351f208960 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,22 +1,7 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" -import validateCode, { - startFunction, - checkUnknownKeywords, - updateTopContext, - checkNoDefault, - initializeTop, - updateContext, - checkAsync, - checkRefsAndKeywords, - commentKeyword, - endFunction, -} from "./validate" -import {getSchemaTypes, coerceAndCheckDataType} from "./validate/dataType" -import {booleanOrEmptySchema} from "./validate/boolSchema" -import {assignDefaults} from "./validate/defaults" -import {schemaKeywords} from "./validate/keywords" +import validateCode from "./validate" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -117,21 +102,6 @@ function compile(schema, root, localRefs, baseId) { MissingRefError, RULES: RULES, validate: validateCode, - startFunction, // TODO remove when validate is replaced - checkUnknownKeywords, // TODO remove when validate is replaced - getSchemaTypes, // TODO remove when validate is replaced - coerceAndCheckDataType, // TODO remove when validate is replaced - updateTopContext, // TODO remove when validate is replaced - checkNoDefault, // TODO remove when validate is replaced - initializeTop, // TODO remove when validate is replaced - updateContext, // TODO remove when validate is replaced - checkAsync, // TODO remove when validate is replaced - checkRefsAndKeywords, // TODO remove when validate is replaced - booleanOrEmptySchema, // TODO remove when validate is replaced - commentKeyword, // TODO remove when validate is replaced - assignDefaults, // TODO remove when validate is replaced - schemaKeywords, // TODO remove when validate is replaced - endFunction, // TODO remove when validate is replaced util: util, resolve: resolve, resolveRef: resolveRef, diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index c01893bc07..675485df58 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -7,6 +7,16 @@ import {schemaKeywords} from "./keywords" const resolve = require("../resolve") +/** + * schema compilation (render) time: + * it = { schema, RULES, _validate, opts } + * it.validate - this function (validateCode), + * it is used recursively to generate code for sub-schemas + * + * runtime: + * "validate" is a variable name to which this function will be assigned + * validateRef etc. are defined in the parent scope in index.js + */ export default function validateCode(it: CompilationContext): string { const { isTop, @@ -17,13 +27,15 @@ export default function validateCode(it: CompilationContext): string { opts: {$comment}, } = it // TODO _out + let _out = gen._out gen._out = "" checkUnknownKeywords(it) if (isTop) startFunction(it) if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { booleanOrEmptySchema(it) // TODO _out - return gen._out + ;[_out, gen._out] = [gen._out, _out] + return _out } if (isTop) { updateTopContext(it) @@ -39,13 +51,18 @@ export default function validateCode(it: CompilationContext): string { const types = getSchemaTypes(it) const checkedTypes = coerceAndCheckDataType(it, types) schemaKeywords(it, types, !checkedTypes, isTop) - if (isTop) endFunction(it) - else gen.code(`var valid${level} = errors === errs_${level};`) + if (isTop) { + endFunction(it) + } else { + gen.code(`var valid${level} = errors === errs_${level};`) + } + // TODO _out - return gen._out + ;[_out, gen._out] = [gen._out, _out] + return _out } -export function checkUnknownKeywords({ +function checkUnknownKeywords({ schema, RULES, opts: {strictKeywords}, @@ -61,7 +78,7 @@ export function checkUnknownKeywords({ } } -export function startFunction({ +function startFunction({ gen, schema, async, @@ -77,7 +94,7 @@ export function startFunction({ ) } -export function updateTopContext(it: CompilationContext): void { +function updateTopContext(it: CompilationContext): void { it.rootId = resolve.fullPath(it.root.schema.$id) it.baseId = it.baseId || it.rootId delete it.isTop @@ -85,7 +102,7 @@ export function updateTopContext(it: CompilationContext): void { it.dataPathArr = [""] } -export function checkNoDefault({ +function checkNoDefault({ schema, opts: {useDefaults, strictDefaults}, logger, @@ -97,7 +114,7 @@ export function checkNoDefault({ } } -export function initializeTop({gen}: CompilationContext): void { +function initializeTop({gen}: CompilationContext): void { // TODO old comment: "don't edit, used in replace". Should be removed? gen.code( `let vErrors = null; @@ -106,15 +123,15 @@ export function initializeTop({gen}: CompilationContext): void { ) } -export function updateContext(it: CompilationContext): void { +function updateContext(it: CompilationContext): void { if (it.schema.$id) it.baseId = resolve.url(it.baseId, it.schema.$id) } -export function checkAsync(it: CompilationContext): void { +function checkAsync(it: CompilationContext): void { if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") } -export function checkRefsAndKeywords({ +function checkRefsAndKeywords({ schema, errSchemaPath, RULES, @@ -130,12 +147,7 @@ export function checkRefsAndKeywords({ } } -export function commentKeyword({ - gen, - schema, - errSchemaPath, - opts: {$comment}, -}: CompilationContext): void { +function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: CompilationContext): void { const msg = quotedString(schema.$comment) if ($comment === true) { gen.code(`console.log(${msg})`) @@ -145,7 +157,7 @@ export function commentKeyword({ } } -export function endFunction({gen, async}: CompilationContext) { +function endFunction({gen, async}: CompilationContext) { // TODO old comment: "don't edit, used in replace". Should be removed? gen.code( async diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index 9f0a7595b6..2385f7890d 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -20,11 +20,7 @@ export function schemaKeywords( } = it let closingBraces2 = "" if (schema.$ref && !(extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref"))) { - // TODO _out - const _out = gen._out - const code = RULES.all.$ref.code(it, "$ref") - gen._out = _out - gen.code(code) + RULES.all.$ref.code(it, "$ref") if (!allErrors) { // TODO refactor with below const errCount = top ? "0" : `errs_${level}` @@ -46,13 +42,10 @@ export function schemaKeywords( let closingBraces1 = "" for (const rule of group.rules) { if (shouldUseRule(schema, rule)) { - // TODO _out - const _out = gen._out - gen._out = "" - const code = rule.code(it, rule.keyword, group.type) - gen._out = _out - if (code) { - gen.code(code) + // TODO _outLen + const _outLen = gen._out.length + rule.code(it, rule.keyword, group.type) + if (_outLen < gen._out.length) { if (!allErrors) closingBraces1 += "}" } } diff --git a/lib/compile/validate/validate.jst b/lib/compile/validate/validate.jst deleted file mode 100644 index 05c245a2fc..0000000000 --- a/lib/compile/validate/validate.jst +++ /dev/null @@ -1,277 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.defaults }} -{{# def.coerce }} - -{{ /** - * schema compilation (render) time: - * it = { schema, RULES, _validate, opts } - * it.validate - this template function, - * it is used recursively to generate code for subschemas - * - * runtime: - * "validate" is a variable name to which this function will be assigned - * validateRef etc. are defined in the parent scope in index.js - */ }} - -{{ - var $async = it.schema.$async === true - , $refKeywords = it.util.schemaHasRulesExcept(it.schema, it.RULES.all, '$ref') - , $id = it.schema.$id; -}} - -// {{ -// if (it.opts.strictKeywords) { -// var $unknownKwd = it.util.schemaUnknownRules(it.schema, it.RULES.keywords); -// if ($unknownKwd) { -// var $keywordsMsg = 'unknown keyword: ' + $unknownKwd; -// if (it.opts.strictKeywords === 'log') it.logger.warn($keywordsMsg); -// else throw new Error($keywordsMsg); -// } -// } -// }} - -// {{? it.isTop }} -// var validate = {{?$async}}{{it.async = true;}}async {{?}}function(data, dataPath, parentData, parentDataProperty, rootData) { -// 'use strict'; -// {{? $id && (it.opts.sourceCode || it.opts.processCode) }} -// {{= '/\*# sourceURL=' + $id + ' */' }} -// {{?}} -// {{?}} - -// {{? typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref) }} - // {{ var $keyword = 'false schema'; }} - // {{# def.setupKeyword }} - // {{? it.schema === false}} - // {{? it.isTop}} - // {{ $breakOnError = true; }} - // {{??}} - // var {{=$valid}} = false; - // {{?}} - // {{# def.error:'false schema' }} - // {{??}} - // {{? it.isTop}} - // {{? $async }} - // return data; - // {{??}} - // validate.errors = null; - // return true; - // {{?}} - // {{??}} - // var {{=$valid}} = true; - // {{?}} - // {{?}} - - // {{? it.isTop}} - // }; - // return validate; - // {{?}} - - // {{ return out; }} -// {{?}} - -{{? it.isTop }} - // {{ - // var $top = it.isTop - // , $lvl = it.level = 0 - // , $dataLvl = it.dataLevel = 0 - // , $data = 'data'; - // it.rootId = it.resolve.fullPath(it.root.schema.$id); - // it.baseId = it.baseId || it.rootId; - // delete it.isTop; - - // it.dataPathArr = [""]; - - // if (it.schema.default !== undefined && it.opts.useDefaults && it.opts.strictDefaults) { - // var $defaultMsg = 'default is ignored in the schema root'; - // if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); - // else throw new Error($defaultMsg); - // } - // }} - - // var vErrors = null; {{ /* don't edit, used in replace */ }} - // var errors = 0; {{ /* don't edit, used in replace */ }} - // if (rootData === undefined) rootData = data; {{ /* don't edit, used in replace */ }} -{{??}} - {{ - var $lvl = it.level - , $dataLvl = it.dataLevel - , $data = 'data' + ($dataLvl || ''); - - // if ($id) it.baseId = it.resolve.url(it.baseId, $id); - - // if ($async && !it.async) throw new Error('async schema in sync schema'); - }} - - // var errs_{{=$lvl}} = errors; -{{?}} - -{{ - var $valid = 'valid' + $lvl - , $breakOnError = !it.opts.allErrors - , $closingBraces1 = '' - , $closingBraces2 = ''; - - var $errorKeyword; - // var $typeSchema = it.schema.type - // , $typeIsArray = Array.isArray($typeSchema); - - // if ($typeSchema && it.opts.nullable && it.schema.nullable === true) { - // if ($typeIsArray) { - // if ($typeSchema.indexOf('null') == -1) - // $typeSchema = $typeSchema.concat('null'); - // } else if ($typeSchema != 'null') { - // $typeSchema = [$typeSchema, 'null']; - // $typeIsArray = true; - // } - // } - - // if ($typeIsArray && $typeSchema.length == 1) { - // $typeSchema = $typeSchema[0]; - // $typeIsArray = false; - // } -}} - -{{## def.checkType: - {{ - var $schemaPath = it.schemaPath + '.type' - , $errSchemaPath = it.errSchemaPath + '/type' - , $method = $typeIsArray ? 'checkDataTypes' : 'checkDataType'; - }} - - if ({{= it.util[$method]($typeSchema, $data, it.opts.strictNumbers, true) }}) { -#}} - -// {{? it.schema.$ref && $refKeywords }} -// {{? it.opts.extendRefs == 'fail' }} -// {{ throw new Error('$ref: validation keywords used in schema at path "' + it.errSchemaPath + '" (see option extendRefs)'); }} -// {{?? it.opts.extendRefs !== true }} -// {{ -// $refKeywords = false; -// it.logger.warn('$ref: keywords ignored in schema at path "' + it.errSchemaPath + '"'); -// }} -// {{?}} -// {{?}} - -// {{? it.schema.$comment && it.opts.$comment }} -// {{= it.RULES.all.$comment.code(it, '$comment') }} -// {{?}} - -{{? $typeSchema }} - {{? it.opts.coerceTypes }} - {{ var $coerceToTypes = it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema); }} - {{?}} - - {{ var $rulesGroup = it.RULES.types[$typeSchema]; }} - -{{ /* // This condition means one of the following: -// - opts.coerceTypes in on and there are types to which data can be coerced -// - or, there are multiple types allowed -// - or, no known keywords for this type (e.g. boolean or null) -// - or, no keywords for this type in the current schema -// -// this block checks and coerces types separately from keyword validation below */ }} - {{? $coerceToTypes || $typeIsArray || $rulesGroup === true || - ($rulesGroup && !shouldUseGroup($rulesGroup)) }} - {{ - var $schemaPath = it.schemaPath + '.type' - , $errSchemaPath = it.errSchemaPath + '/type'; - }} - {{# def.checkType }} - {{? $coerceToTypes }} - // {{# def.coerceType }} - {{??}} - {{# def.error:'type' }} - {{?}} - } - {{?}} -{{?}} - -{{? it.schema.$ref && !$refKeywords }} - {{= it.RULES.all.$ref.code(it, '$ref') }} - {{? $breakOnError }} - } - if (errors === {{?$top}}0{{??}}errs_{{=$lvl}}{{?}}) { - {{ $closingBraces2 += '}'; }} - {{?}} -{{??}} - {{~ it.RULES:$rulesGroup }} - {{? shouldUseGroup($rulesGroup) }} - {{? $rulesGroup.type }} - if ({{= it.util.checkDataType($rulesGroup.type, $data, it.opts.strictNumbers) }}) { - {{?}} - {{? it.opts.useDefaults }} - {{? $rulesGroup.type == 'object' && it.schema.properties }} - {{# def.defaultProperties }} - {{?? $rulesGroup.type == 'array' && Array.isArray(it.schema.items) }} - {{# def.defaultItems }} - {{?}} - {{?}} - {{~ $rulesGroup.rules:$rule }} - {{? shouldUseRule($rule) }} - {{ var $code = $rule.code(it, $rule.keyword, $rulesGroup.type); }} - {{? $code }} - {{= $code }} - {{? $breakOnError }} - {{ $closingBraces1 += '}'; }} - {{?}} - {{?}} - {{?}} - {{~}} - {{? $breakOnError }} - {{= $closingBraces1 }} - {{ $closingBraces1 = ''; }} - {{?}} - {{? $rulesGroup.type }} - } - {{? $typeSchema && $typeSchema === $rulesGroup.type && !$coerceToTypes }} - else { - {{ - var $schemaPath = it.schemaPath + '.type' - , $errSchemaPath = it.errSchemaPath + '/type'; - }} - {{# def.error:'type' }} - } - {{?}} - {{?}} - - {{? $breakOnError }} - if (errors === {{?$top}}0{{??}}errs_{{=$lvl}}{{?}}) { - {{ $closingBraces2 += '}'; }} - {{?}} - {{?}} - {{~}} -{{?}} - -{{? $breakOnError }} {{= $closingBraces2 }} {{?}} - -{{? $top }} - {{? $async }} - if (errors === 0) return data; {{ /* don't edit, used in replace */ }} - else throw new ValidationError(vErrors); {{ /* don't edit, used in replace */ }} - {{??}} - validate.errors = vErrors; {{ /* don't edit, used in replace */ }} - return errors === 0; {{ /* don't edit, used in replace */ }} - {{?}} - }; - - return validate; -{{??}} - var {{=$valid}} = errors === errs_{{=$lvl}}; -{{?}} - -{{ - function shouldUseGroup(rulesGroup) { - return rulesGroup.some(shouldUseRule) - } - - function shouldUseRule(rule) { - return it.schema[rule.keyword] !== undefined - || ruleImplementsSomeKeyword(rule); - } - - function ruleImplementsSomeKeyword(rule) { - return rule.implements && - && rule.implements.some(kwd => it.schema[kwd] !== undefined) - } -}} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst deleted file mode 100644 index c106fadf2f..0000000000 --- a/lib/dot/validate.jst +++ /dev/null @@ -1,52 +0,0 @@ -{{ /** - * schema compilation (render) time: - * it = { schema, RULES, _validate, opts } - * it.validate - this template function, - * it is used recursively to generate code for subschemas - * - * runtime: - * "validate" is a variable name to which this function will be assigned - * validateRef etc. are defined in the parent scope in index.js - */ - - /* TODO do not clear _out */ - it.gen._out = ""; - - it.checkUnknownKeywords(it); - - if (it.isTop) it.startFunction(it); - - if (typeof it.schema == 'boolean' || !it.util.schemaHasRules(it.schema, it.RULES.all)) { - it.booleanOrEmptySchema(it); - return it.gen._out; - } - - if (it.isTop) { - var $top = it.isTop - , $lvl = it.level; - - it.updateTopContext(it); - it.checkNoDefault(it); - it.initializeTop(it); - } else { - var $lvl = it.level; - - it.updateContext(it); - it.checkAsync(it); - it.gen.code(`var errs_${$lvl} = errors;`); - } - - it.checkRefsAndKeywords(it); - if (it.opts.$comment && it.schema.$comment) { - it.commentKeyword(it) - } - - const types = it.getSchemaTypes(it); - const checkedTypes = it.coerceAndCheckDataType(it, types); - it.schemaKeywords(it, types, !checkedTypes, $top); - - if ($top) it.endFunction(it); - else it.gen.code(`var valid${$lvl} = errors === errs_${$lvl};`); - - return it.gen._out; -}} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 7bab7a1078..e434806668 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -15,5 +15,4 @@ module.exports = { properties: require("./properties"), propertyNames: require("./propertyNames"), required: require("./required"), - validate: require("./validate"), } diff --git a/lib/keyword.ts b/lib/keyword.ts index 67bc32832a..4e587a7f55 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -130,15 +130,12 @@ export function addKeyword( * @this rule * @param {Object} it schema compilation context. * @param {String} keyword pre-defined or custom keyword. - * @return {String} compiled rule code. */ -function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): string { +function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void { const schema = it.schema[keyword] const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition const {gen, opts, dataLevel, schemaPath, dataPathArr} = it if (!code) throw new Error('"code" and "error" must be defined') - // TODO do not clear _out - gen._out = "" const $data = $defData && opts.$data && schema && schema.$data const data = "data" + (dataLevel || "") const schemaValue = schemaRefOrVal(schema, schemaPath, keyword, $data) @@ -168,8 +165,6 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): stri } // TODO check that code called "fail" or another valid way to return code code(cxt) - // TODO - return gen._out function fail(condition: string): void { gen.code(`if (${condition}) {`) From 0ac62587cfe51f3d24fec45a61a0fc198cc29619 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 16 Aug 2020 20:03:31 +0100 Subject: [PATCH 050/322] refactor: rules to typescript --- lib/ajv.js | 18 ++---- lib/compile/rules.js | 77 ----------------------- lib/compile/rules.ts | 105 +++++++++++++++++++++++++++++++ lib/compile/validate/keywords.ts | 2 +- lib/keyword.ts | 48 ++++++-------- lib/types.ts | 1 + 6 files changed, 130 insertions(+), 121 deletions(-) delete mode 100644 lib/compile/rules.js create mode 100644 lib/compile/rules.ts diff --git a/lib/ajv.js b/lib/ajv.js index 2f0d01d1fe..1b3f3b2565 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -1,11 +1,11 @@ import SchemaObject from "./compile/schema_obj" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" +import rules from "./compile/rules" var compileSchema = require("./compile"), resolve = require("./compile/resolve"), stableStringify = require("fast-json-stable-stringify"), - rules = require("./compile/rules"), $dataMetaSchema = require("./data"), validationVocabulary = require("./vocabularies/validation") @@ -38,12 +38,7 @@ Ajv.$dataMetaSchema = $dataMetaSchema var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" -var META_IGNORE_OPTIONS = [ - "removeAdditional", - "useDefaults", - "coerceTypes", - "strictDefaults", -] +var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes", "strictDefaults"] var META_SUPPORT_DATA = ["/properties"] /** @@ -312,10 +307,7 @@ function _addSchema(schema, skipValidation, meta, shouldAddSchema) { var willValidate = this._opts.validateSchema !== false && !skipValidation var recursiveMeta - if ( - willValidate && - !(recursiveMeta = id && id === resolve.normalizeId(schema.$schema)) - ) { + if (willValidate && !(recursiveMeta = id && id === resolve.normalizeId(schema.$schema))) { this.validateSchema(schema, true) } @@ -473,9 +465,7 @@ function setLogger(self) { self.logger = {log: noop, warn: noop, error: noop} } else { if (logger === undefined) logger = console - if ( - !(typeof logger == "object" && logger.log && logger.warn && logger.error) - ) { + if (!(typeof logger == "object" && logger.log && logger.warn && logger.error)) { throw new Error("logger must implement log, warn and error methods") } self.logger = logger diff --git a/lib/compile/rules.js b/lib/compile/rules.js deleted file mode 100644 index 8c93911a56..0000000000 --- a/lib/compile/rules.js +++ /dev/null @@ -1,77 +0,0 @@ -import {toHash} from "./util" - -var ruleModules = require("../dotjs") - -module.exports = function rules() { - var RULES = [ - {type: "number", rules: ["format"]}, - {type: "string", rules: ["format"]}, - { - type: "array", - rules: ["items", "contains"], - }, - { - type: "object", - rules: [ - "required", - "dependencies", - "propertyNames", - {properties: ["additionalProperties", "patternProperties"]}, - ], - }, - {rules: ["$ref", "not", "anyOf", "oneOf", "allOf", "if"]}, - ] - - var ALL = ["type", "$comment"] - var KEYWORDS = [ - "$schema", - "$id", - "id", - "$data", - "$async", - "title", - "description", - "default", - "definitions", - "examples", - "readOnly", - "writeOnly", - "contentMediaType", - "contentEncoding", - "additionalItems", - "then", - "else", - ] - var TYPES = ["number", "integer", "string", "array", "object", "boolean", "null"] - RULES.all = toHash(ALL) - RULES.types = toHash(TYPES) - - RULES.forEach((group) => { - group.rules = group.rules.map((keyword) => { - var implKeywords - if (typeof keyword == "object") { - var key = Object.keys(keyword)[0] - implKeywords = keyword[key] - keyword = key - implKeywords.forEach((k) => { - ALL.push(k) - RULES.all[k] = true - }) - } - ALL.push(keyword) - var rule = (RULES.all[keyword] = { - keyword: keyword, - code: ruleModules[keyword], - implements: implKeywords, - }) - return rule - }) - - if (group.type) RULES.types[group.type] = group - }) - - RULES.keywords = toHash(ALL.concat(KEYWORDS)) - RULES.custom = {} - - return RULES -} diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts new file mode 100644 index 0000000000..7bd5ca71fd --- /dev/null +++ b/lib/compile/rules.ts @@ -0,0 +1,105 @@ +import {toHash} from "./util" +import {CompilationContext, KeywordDefinition} from "../types" + +const ruleModules = require("../dotjs") + +export interface ValidationRules { + rules: GroupOfRules[] + all: {[key: string]: boolean | Rule} + keywords: {[key: string]: boolean} + types: {[key: string]: boolean | GroupOfRules} + custom: {[key: string]: Rule} +} + +interface GroupOfRules { + type?: string + rules: RuleDef[] +} + +type RuleDef = string | {[key: string]: string[]} | Rule + +export interface Rule { + keyword: string + code: (it: CompilationContext, keyword?: string, ruleType?: string) => void + implements?: string[] + definition?: KeywordDefinition + custom?: true +} + +export default function rules() { + const ALL = ["type", "$comment"] + const KEYWORDS = [ + "$schema", + "$id", + "id", + "$data", + "$async", + "title", + "description", + "default", + "definitions", + "examples", + "readOnly", + "writeOnly", + "contentMediaType", + "contentEncoding", + "additionalItems", + "then", + "else", + ] + const TYPES = ["number", "integer", "string", "array", "object", "boolean", "null"] + + const RULES: ValidationRules = { + rules: [ + {type: "number", rules: ["format"]}, + {type: "string", rules: ["format"]}, + { + type: "array", + rules: ["items", "contains"], + }, + { + type: "object", + rules: [ + "required", + "dependencies", + "propertyNames", + {properties: ["additionalProperties", "patternProperties"]}, + ], + }, + {rules: ["$ref", "not", "anyOf", "oneOf", "allOf", "if"]}, + ], + all: toHash(ALL), + keywords: {}, + types: toHash(TYPES), + custom: {}, + } + + RULES.rules.forEach((group) => { + group.rules = group.rules.map((keyword: string | object) => { + let implKeywords: string[] | undefined + if (typeof keyword == "object") { + const key: string = Object.keys(keyword)[0] + implKeywords = keyword[key] + keyword = key + ;(implKeywords).forEach((k) => { + ALL.push(k) + RULES.all[k] = true + }) + } + ALL.push(keyword) + const rule: Rule = (RULES.all[keyword] = { + keyword: keyword, + code: ruleModules[keyword], + implements: implKeywords, + }) + return rule + }) + + if (group.type) RULES.types[group.type] = group + }) + + RULES.keywords = toHash(ALL.concat(KEYWORDS)) + RULES.custom = {} + + return RULES +} diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index 2385f7890d..0790372ced 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -31,7 +31,7 @@ export function schemaKeywords( closingBraces2 += "}" } } else { - for (const group of RULES) { + for (const group of RULES.rules) { if (shouldUseGroup(schema, group)) { if (group.type) { // TODO refactor `data${dataLevel || ""}` diff --git a/lib/keyword.ts b/lib/keyword.ts index 4e587a7f55..a1a57aa086 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -8,6 +8,7 @@ import { KeywordContext, } from "./types" +import {ValidationRules, Rule} from "./compile/rules" import {reportError} from "./compile/errors" import {getData} from "./compile/util" import {schemaRefOrVal} from "./vocabularies/util" @@ -56,7 +57,7 @@ export function addKeyword( ): object { // TODO return type Ajv /* eslint no-shadow: 0 */ - var RULES = this.RULES + const RULES: ValidationRules = this.RULES if (RULES.keywords[keyword]) { throw new Error("Keyword " + keyword + " is already defined") } @@ -68,16 +69,16 @@ export function addKeyword( if (definition) { if (!_skipValidation) this.validateKeyword(definition, true) - var dataType = definition.type + const dataType = definition.type if (Array.isArray(dataType)) { - for (var i = 0; i < dataType.length; i++) { - _addRule(keyword, dataType[i], definition) + for (const t of dataType) { + _addRule(keyword, t, definition) } } else { _addRule(keyword, dataType, definition) } - var metaSchema = definition.metaSchema + let metaSchema = definition.metaSchema if (metaSchema) { if (definition.$data && this._opts.$data) { metaSchema = { @@ -96,24 +97,17 @@ export function addKeyword( RULES.keywords[keyword] = RULES.all[keyword] = true - function _addRule(keyword, dataType, definition) { - var ruleGroup - for (var i = 0; i < RULES.length; i++) { - var rg = RULES[i] - if (rg.type === dataType) { - ruleGroup = rg - break - } - } + function _addRule(keyword: string, dataType: string | undefined, definition: KeywordDefinition) { + let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) if (!ruleGroup) { ruleGroup = {type: dataType, rules: []} - RULES.push(ruleGroup) + RULES.rules.push(ruleGroup) } - var rule = { - keyword: keyword, - definition: definition, + const rule: Rule = { + keyword, + definition, custom: true, code: definition.code ? ruleCode : customRuleCode, implements: definition.implements, @@ -190,7 +184,7 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void */ export function getKeyword(keyword: string): KeywordDefinition | boolean { /* jshint validthis: true */ - var rule = this.RULES.custom[keyword] + const rule = this.RULES.custom[keyword] return rule ? rule.definition : this.RULES.keywords[keyword] || false } @@ -203,18 +197,14 @@ export function getKeyword(keyword: string): KeywordDefinition | boolean { export function removeKeyword(keyword: string): object { // TODO return type should be Ajv /* jshint validthis: true */ - var RULES = this.RULES + const RULES: ValidationRules = this.RULES delete RULES.keywords[keyword] delete RULES.all[keyword] delete RULES.custom[keyword] - for (var i = 0; i < RULES.length; i++) { - var rules = RULES[i].rules - for (var j = 0; j < rules.length; j++) { - if (rules[j].keyword === keyword) { - rules.splice(j, 1) - break - } - } + for (const group of RULES.rules) { + // TODO remove type cast once all rules migrated + const i = group.rules.findIndex((rule) => (rule).keyword === keyword) + if (i >= 0) group.rules.splice(i, 1) } return this } @@ -233,7 +223,7 @@ export interface KeywordValidator { */ export const validateKeyword: KeywordValidator = function (definition, throwError) { validateKeyword.errors = null - var v: ValidateFunction = (this._validateKeyword = + const v: ValidateFunction = (this._validateKeyword = this._validateKeyword || this.compile(definitionSchema, true)) if (v(definition)) return true diff --git a/lib/types.ts b/lib/types.ts index c7ccb5ccc0..27ca2ccf7b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -159,6 +159,7 @@ export interface KeywordDefinition { code?: (cxt: KeywordContext) => string | void error?: KeywordErrorDefinition validateSchema?: ValidateFunction + implements?: string[] } export interface KeywordErrorDefinition { From a241ea30a56f114b7d01e730d723c42324aeecd5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 16 Aug 2020 20:48:20 +0100 Subject: [PATCH 051/322] optimize validate code - reduce empty branches --- lib/compile/rules.ts | 6 +-- lib/compile/validate/defaults.ts | 8 +-- lib/compile/validate/keywords.ts | 93 ++++++++++++++++---------------- lib/types.ts | 3 +- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 7bd5ca71fd..dd4cb9293c 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -4,14 +4,14 @@ import {CompilationContext, KeywordDefinition} from "../types" const ruleModules = require("../dotjs") export interface ValidationRules { - rules: GroupOfRules[] + rules: RuleGroup[] all: {[key: string]: boolean | Rule} keywords: {[key: string]: boolean} - types: {[key: string]: boolean | GroupOfRules} + types: {[key: string]: boolean | RuleGroup} custom: {[key: string]: Rule} } -interface GroupOfRules { +export interface RuleGroup { type?: string rules: RuleDef[] } diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index a3925323ca..6915c5500d 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,13 +1,13 @@ import {CompilationContext} from "../../types" import {getProperty} from "../util" -export function assignDefaults(it: CompilationContext, group) { +export function assignDefaults(it: CompilationContext, ty?: string): void { const {properties, items} = it.schema - if (group.type === "object" && properties) { + if (ty === "object" && properties) { for (const key in properties) { assignDefault(it, key, properties[key].default) } - } else if (group.type === "array" && Array.isArray(items)) { + } else if (ty === "array" && Array.isArray(items)) { items.forEach((sch, i: number) => assignDefault(it, i, sch.default)) } } @@ -23,7 +23,7 @@ function assignDefault( }: CompilationContext, prop: string | number, defaultValue: any -) { +): void { if (defaultValue === undefined) return // TODO refactor `data${dataLevel || ""}` const data = "data" + (dataLevel || "") + getProperty(prop) diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index 0790372ced..eb519f7793 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -3,6 +3,7 @@ import {shouldUseGroup, shouldUseRule} from "./applicability" import {checkDataType, schemaHasRulesExcept} from "../util" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" +import {RuleGroup, Rule} from "../rules" export function schemaKeywords( it: CompilationContext, @@ -16,56 +17,58 @@ export function schemaKeywords( level, dataLevel, RULES, - opts: {allErrors, extendRefs, strictNumbers, useDefaults}, + opts: {allErrors, extendRefs, strictNumbers}, } = it - let closingBraces2 = "" if (schema.$ref && !(extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref"))) { - RULES.all.$ref.code(it, "$ref") - if (!allErrors) { - // TODO refactor with below + // TODO remove Rule type cast + ;(RULES.all.$ref as Rule).code(it, "$ref") + if (!allErrors) gen.code("}") + return + } + let closeBlocks = "" + const ruleGroups = RULES.rules.filter((group) => shouldUseGroup(schema, group)) + const last = ruleGroups.length - 1 + ruleGroups.forEach((group, i) => { + if (group.type) { + // TODO refactor `data${dataLevel || ""}` + const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) + gen.code(`if (${checkType}) {`) + iterateKeywords(it, group) + if (types.length === 1 && types[0] === group.type && typeErrors) { + gen.code(`} else {`) + reportTypeError(it) + } + gen.code("}") + } else { + iterateKeywords(it, group) + } + if (!allErrors && i < last) { const errCount = top ? "0" : `errs_${level}` - gen.code( - `} - if (errors === ${errCount}) {` - ) - closingBraces2 += "}" + gen.code(`if (errors === ${errCount}) {`) + closeBlocks += "}" } - } else { - for (const group of RULES.rules) { - if (shouldUseGroup(schema, group)) { - if (group.type) { - // TODO refactor `data${dataLevel || ""}` - const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) - gen.code(`if (${checkType}) {`) - } - if (useDefaults) assignDefaults(it, group) - let closingBraces1 = "" - for (const rule of group.rules) { - if (shouldUseRule(schema, rule)) { - // TODO _outLen - const _outLen = gen._out.length - rule.code(it, rule.keyword, group.type) - if (_outLen < gen._out.length) { - if (!allErrors) closingBraces1 += "}" - } - } - } - if (!allErrors) gen.code(closingBraces1) - if (group.type) { - gen.code("}") - if (types.length === 1 && types[0] === group.type && typeErrors) { - gen.code(`else {`) - reportTypeError(it) - gen.code(`}`) - } - } - if (!allErrors) { - const errCount = top ? "0" : `errs_${level}` - gen.code(`if (errors === ${errCount}) {`) - closingBraces2 += "}" - } + }) + if (!allErrors) gen.code(closeBlocks) +} + +function iterateKeywords(it: CompilationContext, group: RuleGroup) { + const { + gen, + schema, + opts: {allErrors, useDefaults}, + } = it + if (useDefaults) assignDefaults(it, group.type) + let closeBlocks = "" + // TODO remove Rule type cast + for (const rule of group.rules as Rule[]) { + if (shouldUseRule(schema, rule)) { + // TODO _outLen + const _outLen = gen._out.length + rule.code(it, rule.keyword, group.type) + if (_outLen < gen._out.length) { + if (!allErrors) closeBlocks += "}" } } } - if (!allErrors) gen.code(closingBraces2) + if (!allErrors) gen.code(closeBlocks) } diff --git a/lib/types.ts b/lib/types.ts index 27ca2ccf7b..ff12bf9912 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,6 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" +import {ValidationRules} from "./compile/rules" export interface Options { $data?: boolean @@ -124,7 +125,7 @@ export interface CompilationContext { useDefault: (value: any) => string util: object // TODO self: object // TODO - RULES: any // TODO replace? + RULES: ValidationRules logger: Logger // TODO ? isTop: boolean // TODO ? root: SchemaRoot // TODO ? From 25494563db75aba0a9ae1921fee419ce05cd44a5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 16 Aug 2020 21:01:37 +0100 Subject: [PATCH 052/322] refactor: $dataMetaSchema to typescript --- lib/ajv.js | 4 ++-- lib/data.js | 50 ------------------------------------------------- lib/data.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 52 deletions(-) delete mode 100644 lib/data.js create mode 100644 lib/data.ts diff --git a/lib/ajv.js b/lib/ajv.js index 1b3f3b2565..b522339318 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -2,11 +2,11 @@ import SchemaObject from "./compile/schema_obj" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules from "./compile/rules" +import $dataMetaSchema from "./data" var compileSchema = require("./compile"), resolve = require("./compile/resolve"), stableStringify = require("fast-json-stable-stringify"), - $dataMetaSchema = require("./data"), validationVocabulary = require("./vocabularies/validation") module.exports = Ajv @@ -39,7 +39,7 @@ Ajv.$dataMetaSchema = $dataMetaSchema var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes", "strictDefaults"] -var META_SUPPORT_DATA = ["/properties"] +const META_SUPPORT_DATA = ["/properties"] /** * Creates validator instance. diff --git a/lib/data.js b/lib/data.js deleted file mode 100644 index 76b6b92e96..0000000000 --- a/lib/data.js +++ /dev/null @@ -1,50 +0,0 @@ -// TODO use $data in keyword definitions -var KEYWORDS = [ - "multipleOf", - "maximum", - "exclusiveMaximum", - "minimum", - "exclusiveMinimum", - "maxLength", - "minLength", - "pattern", - "additionalItems", - "maxItems", - "minItems", - "uniqueItems", - "maxProperties", - "minProperties", - "required", - "additionalProperties", - "enum", - "format", - "const", -] - -module.exports = function (metaSchema, keywordsJsonPointers) { - for (var i = 0; i < keywordsJsonPointers.length; i++) { - metaSchema = JSON.parse(JSON.stringify(metaSchema)) - var segments = keywordsJsonPointers[i].split("/") - var keywords = metaSchema - var j - for (j = 1; j < segments.length; j++) keywords = keywords[segments[j]] - - for (j = 0; j < KEYWORDS.length; j++) { - var key = KEYWORDS[j] - var schema = keywords[key] - if (schema) { - keywords[key] = { - anyOf: [ - schema, - { - $ref: - "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", - }, - ], - } - } - } - } - - return metaSchema -} diff --git a/lib/data.ts b/lib/data.ts new file mode 100644 index 0000000000..8594c19341 --- /dev/null +++ b/lib/data.ts @@ -0,0 +1,54 @@ +// TODO use $data in keyword definitions +const KEYWORDS = [ + "multipleOf", + "maximum", + "exclusiveMaximum", + "minimum", + "exclusiveMinimum", + "maxLength", + "minLength", + "pattern", + "additionalItems", + "maxItems", + "minItems", + "uniqueItems", + "maxProperties", + "minProperties", + "required", + "additionalProperties", + "enum", + "format", + "const", +] + +export default function $dataMetaSchema( + metaSchema: object, + keywordsJsonPointers: string[] +): object { + for (const jsonPointer of keywordsJsonPointers) { + metaSchema = JSON.parse(JSON.stringify(metaSchema)) + const segments = jsonPointer.split("/").slice(1) // first segment is an empty string + let keywords = metaSchema + for (const seg of segments) keywords = keywords[seg] + + for (const key of KEYWORDS) { + const schema = keywords[key] + if (schema) keywords[key] = schemaOrData(schema) + } + } + + return metaSchema +} + +function schemaOrData(schema: object): object { + return { + anyOf: [ + schema, + { + $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", + }, + ], + } +} + +module.exports = $dataMetaSchema From 8e48c187b2f53856d8003c3e79f4b0ccdfc989e4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 16 Aug 2020 23:15:36 +0100 Subject: [PATCH 053/322] refactor: typescript --- .eslintrc.yml | 3 + lib/{ajv.d.ts => ajv.d._ts} | 0 lib/{ajv.js => ajv.ts} | 10 +-- lib/compile/{async.js => async.ts} | 16 ++--- lib/compile/{index.js => index.ts} | 65 ++++++++++++------- lib/compile/{resolve.js => resolve.ts} | 28 ++------ lib/compile/rules.ts | 2 +- lib/compile/schema_obj.ts | 3 + lib/compile/validate/index.ts | 2 - ...inition_schema.js => definition_schema.ts} | 7 +- lib/keyword.ts | 2 +- lib/types.ts | 18 +++-- package.json | 2 +- tsconfig.json | 1 - 14 files changed, 83 insertions(+), 76 deletions(-) rename lib/{ajv.d.ts => ajv.d._ts} (100%) rename lib/{ajv.js => ajv.ts} (98%) rename lib/compile/{async.js => async.ts} (86%) rename lib/compile/{index.js => index.ts} (89%) rename lib/compile/{resolve.js => resolve.ts} (92%) rename lib/{definition_schema.js => definition_schema.ts} (79%) diff --git a/.eslintrc.yml b/.eslintrc.yml index 565f1fc850..dec1f9f6a3 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -32,6 +32,9 @@ overrides: "@typescript-eslint/restrict-plus-operands": off "@typescript-eslint/no-unsafe-return": off "@typescript-eslint/no-var-requires": off + "@typescript-eslint/no-empty-function": off + "@typescript-eslint/no-this-alias": off + "@typescript-eslint/no-implied-eval": off rules: block-scoped-var: error callback-return: error diff --git a/lib/ajv.d.ts b/lib/ajv.d._ts similarity index 100% rename from lib/ajv.d.ts rename to lib/ajv.d._ts diff --git a/lib/ajv.js b/lib/ajv.ts similarity index 98% rename from lib/ajv.js rename to lib/ajv.ts index b522339318..0f37b1612e 100644 --- a/lib/ajv.js +++ b/lib/ajv.ts @@ -47,7 +47,7 @@ const META_SUPPORT_DATA = ["/properties"] * @param {Object} opts optional options * @return {Object} ajv instance */ -function Ajv(opts) { +function Ajv(opts): void { if (!(this instanceof Ajv)) return new Ajv(opts) opts = this._opts = {...(opts || {})} setLogger(this) @@ -280,7 +280,7 @@ function removeSchema(schemaKeyRef) { return this } -function _removeAllSchemas(self, schemas, regex) { +function _removeAllSchemas(self, schemas, regex?: RegExp) { for (var keyRef in schemas) { var schemaObj = schemas[keyRef] if (!schemaObj.meta && (!regex || regex.test(keyRef))) { @@ -365,10 +365,10 @@ function _compile(schemaObj, root) { return v /* @this {*} - custom context, see passContext option */ - function callValidate() { + function callValidate(...args) { /* jshint validthis: true */ var _validate = schemaObj.validate - var result = _validate.apply(this, arguments) + var result = _validate.apply(this, args) callValidate.errors = _validate.errors return result } @@ -438,7 +438,7 @@ function addInitialFormats(self) { } } -function addInitialKeywords(self, keywords, skipValidation) { +function addInitialKeywords(self, keywords, skipValidation?: boolean) { for (var name in keywords) { var keyword = keywords[name] self.addKeyword(name, keyword, skipValidation) diff --git a/lib/compile/async.js b/lib/compile/async.ts similarity index 86% rename from lib/compile/async.js rename to lib/compile/async.ts index 697eb2abbe..ba61bae7f8 100644 --- a/lib/compile/async.js +++ b/lib/compile/async.ts @@ -2,6 +2,8 @@ import {MissingRefError} from "./error_classes" module.exports = compileAsync +type Callback = (...args: any[]) => void + /** * Creates validating function for passed schema with asynchronous loading of missing schemas. * `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. @@ -11,7 +13,7 @@ module.exports = compileAsync * @param {Function} callback an optional node-style callback, it is called with 2 parameters: error (or null) and validating function. * @return {Promise} promise that resolves with a validating function. */ -function compileAsync(schema, meta, callback) { +function compileAsync(schema, meta?: boolean | Callback, callback?: Callback) { /* eslint no-shadow: 0 */ /* jshint validthis: true */ var self = this @@ -25,12 +27,12 @@ function compileAsync(schema, meta, callback) { } var p = loadMetaSchemaOf(schema).then(() => { - var schemaObj = self._addSchema(schema, undefined, meta) + var schemaObj = this._addSchema(schema, undefined, meta) return schemaObj.validate || _compileAsync(schemaObj) }) if (callback) { - p.then((v) => callback(null, v), callback) + p.then((v) => (callback)(null, v), callback) } return p @@ -53,13 +55,7 @@ function compileAsync(schema, meta, callback) { function loadMissingSchema(e) { var ref = e.missingSchema if (added(ref)) { - throw new Error( - "Schema " + - ref + - " is loaded but " + - e.missingRef + - " cannot be resolved" - ) + throw new Error("Schema " + ref + " is loaded but " + e.missingRef + " cannot be resolved") } var schemaPromise = self._loadingSchemas[ref] diff --git a/lib/compile/index.js b/lib/compile/index.ts similarity index 89% rename from lib/compile/index.js rename to lib/compile/index.ts index 351f208960..c3cd2f40d9 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.ts @@ -2,6 +2,8 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" import validateCode from "./validate" +import {Rule} from "./rules" +import {CompilationContext, KeywordDefinition, ErrorObject} from "../types" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -35,16 +37,31 @@ function compile(schema, root, localRefs, baseId) { opts = this._opts, refVal = [undefined], refs = {}, - patterns = [], + patterns: string[] = [], patternsHash = {}, - defaults = [], + defaults: any[] = [], defaultsHash = {}, - customRules = [] + customRules: any[] = [] root = root || {schema: schema, refVal: refVal, refs: refs} + interface CallValidate { + (): any + errors?: null | ErrorObject[] + } + var c = checkCompiling.call(this, schema, root, baseId) - var compilation = this._compilations[c.index] + const compilation = this._compilations[c.index] + + /* @this {*} - custom context, see passContext option */ + const callValidate: CallValidate = function (...args) { + var validate = compilation.validate + /* eslint-disable no-invalid-this */ + var result = validate.apply(this, args) + callValidate.errors = validate.errors + return result + } + if (c.compiling) return (compilation.callValidate = callValidate) var formats = this._formats @@ -68,15 +85,6 @@ function compile(schema, root, localRefs, baseId) { endCompiling.call(this, schema, root, baseId) } - /* @this {*} - custom context, see passContext option */ - function callValidate() { - /* jshint validthis: true */ - var validate = compilation.validate - var result = validate.apply(this, arguments) - callValidate.errors = validate.errors - return result - } - function localCompile(_schema, _root, localRefs, baseId) { var isRoot = !_root || (_root && _root.schema === _schema) if (_root.schema !== root.schema) { @@ -95,6 +103,7 @@ function compile(schema, root, localRefs, baseId) { schemaPath: "", errSchemaPath: "#", errorPath: '""', + dataPathArr: [""], level: 0, dataLevel: 0, data: "data", // TODO get unique name when passed from applicator keywords @@ -175,7 +184,7 @@ function compile(schema, root, localRefs, baseId) { return validate } - function resolveRef(baseId, ref, isRoot) { + function resolveRef(baseId: string, ref: string, isRoot: boolean) { ref = resolve.url(baseId, ref) var refIndex = refs[ref] var _refVal, refCode @@ -212,7 +221,7 @@ function compile(schema, root, localRefs, baseId) { } } - function addLocalRef(ref, v) { + function addLocalRef(ref, v?: any): string { var refId = refVal.length refVal[refId] = v refs[ref] = refId @@ -243,7 +252,7 @@ function compile(schema, root, localRefs, baseId) { return "pattern" + index } - function useDefault(value) { + function useDefault(value: any): string { switch (typeof value) { case "boolean": case "number": @@ -259,12 +268,20 @@ function compile(schema, root, localRefs, baseId) { defaults[index] = value } return "default" + index + default: + throw new Error(`unsupported default type "${typeof value}"`) } } - function useCustomRule(rule, schema, parentSchema, it) { + function useCustomRule( + rule: Rule, + schema: any, + parentSchema: object, + it: CompilationContext + ): any { + const ruleDef = rule.definition as KeywordDefinition if (self._opts.validateSchema !== false) { - var deps = rule.definition.dependencies + var deps = ruleDef.dependencies if ( deps && !deps.every((keyword) => Object.prototype.hasOwnProperty.call(parentSchema, keyword)) @@ -272,7 +289,7 @@ function compile(schema, root, localRefs, baseId) { throw new Error("parent schema must have all required keywords: " + deps.join(",")) } - var validateSchema = rule.definition.validateSchema + var validateSchema = ruleDef.validateSchema if (validateSchema) { var valid = validateSchema(schema) if (!valid) { @@ -283,9 +300,9 @@ function compile(schema, root, localRefs, baseId) { } } - var compile = rule.definition.compile, - inline = rule.definition.inline, - macro = rule.definition.macro + var compile = ruleDef.compile, + inline = ruleDef.inline, + macro = ruleDef.macro var validate if (compile) { @@ -296,7 +313,7 @@ function compile(schema, root, localRefs, baseId) { } else if (inline) { validate = inline.call(self, it, rule.keyword, schema, parentSchema) } else { - validate = rule.definition.validate + validate = ruleDef.validate if (!validate) return } @@ -309,7 +326,7 @@ function compile(schema, root, localRefs, baseId) { return { code: "customRule" + index, - validate: validate, + validate, } } } diff --git a/lib/compile/resolve.js b/lib/compile/resolve.ts similarity index 92% rename from lib/compile/resolve.js rename to lib/compile/resolve.ts index 9688507559..5e56153ee6 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.ts @@ -44,9 +44,7 @@ function resolve(compile, root, ref) { } if (schema instanceof SchemaObject) { - v = - schema.validate || - compile.call(this, schema.schema, root, undefined, baseId) + v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId) } else if (schema !== undefined) { v = inlineRef(schema, this._opts.inlineRefs) ? schema @@ -213,7 +211,7 @@ function countKeys(schema) { return count } -export function getFullPath(id, normalize) { +export function getFullPath(id: string, normalize?: boolean): string { if (normalize !== false) id = normalizeId(id) var p = URI.parse(id) return _getFullPath(p) @@ -224,11 +222,11 @@ function _getFullPath(p) { } var TRAILING_SLASH_HASH = /#\/?$/ -export function normalizeId(id) { +export function normalizeId(id: string): string { return id ? id.replace(TRAILING_SLASH_HASH, "") : "" } -export function resolveUrl(baseId, id) { +export function resolveUrl(baseId: string, id: string): string { id = normalizeId(id) return URI.resolve(baseId, id) } @@ -244,23 +242,13 @@ function resolveIds(schema) { traverse( schema, {allKeys: true}, - ( - sch, - jsonPtr, - rootSchema, - parentJsonPtr, - parentKeyword, - parentSchema, - keyIndex - ) => { + (sch, jsonPtr, _1, parentJsonPtr, parentKeyword, _2, keyIndex) => { if (jsonPtr === "") return var id = sch.$id var baseId = baseIds[parentJsonPtr] var fullPath = fullPaths[parentJsonPtr] + "/" + parentKeyword if (keyIndex !== undefined) { - fullPath += - "/" + - (typeof keyIndex == "number" ? keyIndex : escapeFragment(keyIndex)) + fullPath += "/" + (typeof keyIndex == "number" ? keyIndex : escapeFragment(keyIndex)) } if (typeof id == "string") { @@ -275,9 +263,7 @@ function resolveIds(schema) { } else if (id !== normalizeId(fullPath)) { if (id[0] === "#") { if (localRefs[id] && !equal(sch, localRefs[id])) { - throw new Error( - 'id "' + id + '" resolves to more than one schema' - ) + throw new Error('id "' + id + '" resolves to more than one schema') } localRefs[id] = sch } else { diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index dd4cb9293c..69302ee9b3 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -26,7 +26,7 @@ export interface Rule { custom?: true } -export default function rules() { +export default function rules(): ValidationRules { const ALL = ["type", "$comment"] const KEYWORDS = [ "$schema", diff --git a/lib/compile/schema_obj.ts b/lib/compile/schema_obj.ts index 8bfe31f219..7219362ce8 100644 --- a/lib/compile/schema_obj.ts +++ b/lib/compile/schema_obj.ts @@ -1,4 +1,7 @@ export default class SchemaObject { + schema?: any + validate?: () => any + constructor(obj: object) { Object.assign(this, obj) } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 675485df58..a585c7a1d8 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -98,8 +98,6 @@ function updateTopContext(it: CompilationContext): void { it.rootId = resolve.fullPath(it.root.schema.$id) it.baseId = it.baseId || it.rootId delete it.isTop - - it.dataPathArr = [""] } function checkNoDefault({ diff --git a/lib/definition_schema.js b/lib/definition_schema.ts similarity index 79% rename from lib/definition_schema.js rename to lib/definition_schema.ts index 18b0f2552e..2daf60e59a 100644 --- a/lib/definition_schema.js +++ b/lib/definition_schema.ts @@ -1,8 +1,7 @@ -var metaSchema = require("./refs/json-schema-draft-07.json") +const metaSchema = require("./refs/json-schema-draft-07.json") -module.exports = { - $id: - "https://github.com/ajv-validator/ajv/blob/master/lib/definition_schema.js", +export const definitionSchema: object = { + $id: "https://github.com/ajv-validator/ajv/blob/master/lib/definition_schema.js", definitions: { simpleTypes: metaSchema.definitions.simpleTypes, }, diff --git a/lib/keyword.ts b/lib/keyword.ts index a1a57aa086..932d650af8 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -12,10 +12,10 @@ import {ValidationRules, Rule} from "./compile/rules" import {reportError} from "./compile/errors" import {getData} from "./compile/util" import {schemaRefOrVal} from "./vocabularies/util" +import {definitionSchema} from "./definition_schema" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i const customRuleCode = require("./dotjs/custom") -const definitionSchema = require("./definition_schema") /** * Define vocabulary diff --git a/lib/types.ts b/lib/types.ts index ff12bf9912..9dfe9c2aae 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,7 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" -import {ValidationRules} from "./compile/rules" +import {ValidationRules, Rule} from "./compile/rules" +import {MissingRefError} from "./compile/error_classes" export interface Options { $data?: boolean @@ -105,6 +106,7 @@ export interface CompilationContext { data: string dataPathArr: string[] schema: any + isRoot: boolean schemaPath: string errorPath: string errSchemaPath: string @@ -116,13 +118,14 @@ export interface CompilationContext { formats: { [index: string]: Format | undefined } - keywords: { - [index: string]: KeywordDefinition | undefined - } - compositeRule: boolean - validate: (schema: object) => boolean + // keywords: { + // [index: string]: KeywordDefinition | undefined + // } + compositeRule?: boolean + validate: (it: CompilationContext) => string usePattern: (str: string) => string useDefault: (value: any) => string + useCustomRule: (rule: Rule, schema: any, parentSchema: object, it: CompilationContext) => any util: object // TODO self: object // TODO RULES: ValidationRules @@ -130,6 +133,9 @@ export interface CompilationContext { isTop: boolean // TODO ? root: SchemaRoot // TODO ? rootId?: string // TODO ? + MissingRefError: typeof MissingRefError + resolve: any + resolveRef: (...args: any[]) => any } interface SchemaRoot { diff --git a/package.json b/package.json index 158948aee5..7c5a01bb9b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint lib/{compile/,}*.{js,ts} spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint lib/{compile/,}*.ts spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "lint": "npm run eslint", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", diff --git a/tsconfig.json b/tsconfig.json index 7c5425b5b0..6361ceab7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,6 @@ "noImplicitThis": false, "noImplicitReturns": false, "allowJs": true, - "declaration": false, "target": "ES2018" } } From 43df4639d514f5e2da120dbc165899bbdff269aa Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 17 Aug 2020 11:12:54 +0100 Subject: [PATCH 054/322] refactor: validate, additional tests for $comment --- lib/compile/validate/index.ts | 70 +++++++++++++++++++---------------- spec/options/comment.spec.js | 15 +++++--- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index a585c7a1d8..224d57ae30 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -30,36 +30,44 @@ export default function validateCode(it: CompilationContext): string { let _out = gen._out gen._out = "" checkUnknownKeywords(it) + checkRefsAndKeywords(it) + if (isTop) startFunction(it) - if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { - booleanOrEmptySchema(it) - // TODO _out - ;[_out, gen._out] = [gen._out, _out] - return _out - } + if (booleanOrEmpty()) return _out + if ($comment && schema.$comment) commentKeyword(it) + if (isTop) { updateTopContext(it) checkNoDefault(it) initializeTop(it) + typeAndKeywords() + endFunction(it) } else { updateContext(it) checkAsync(it) gen.code(`var errs_${level} = errors;`) - } - checkRefsAndKeywords(it) - if ($comment && schema.$comment) commentKeyword(it) - const types = getSchemaTypes(it) - const checkedTypes = coerceAndCheckDataType(it, types) - schemaKeywords(it, types, !checkedTypes, isTop) - if (isTop) { - endFunction(it) - } else { + typeAndKeywords() gen.code(`var valid${level} = errors === errs_${level};`) } // TODO _out ;[_out, gen._out] = [gen._out, _out] return _out + + function booleanOrEmpty(): true | void { + if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { + booleanOrEmptySchema(it) + // TODO _out + ;[_out, gen._out] = [gen._out, _out] + return true + } + } + + function typeAndKeywords(): void { + const types = getSchemaTypes(it) + const checkedTypes = coerceAndCheckDataType(it, types) + schemaKeywords(it, types, !checkedTypes, isTop) + } } function checkUnknownKeywords({ @@ -78,6 +86,22 @@ function checkUnknownKeywords({ } } +function checkRefsAndKeywords({ + schema, + errSchemaPath, + RULES, + opts: {extendRefs}, + logger, +}: CompilationContext): void { + if (schema.$ref && schemaHasRulesExcept(schema, RULES.all, "$ref")) { + if (extendRefs === "fail") { + throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`) + } else if (extendRefs !== true) { + logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) + } + } +} + function startFunction({ gen, schema, @@ -129,22 +153,6 @@ function checkAsync(it: CompilationContext): void { if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") } -function checkRefsAndKeywords({ - schema, - errSchemaPath, - RULES, - opts: {extendRefs}, - logger, -}: CompilationContext): void { - if (schema.$ref && schemaHasRulesExcept(schema, RULES.all, "$ref")) { - if (extendRefs === "fail") { - throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`) - } else if (extendRefs !== true) { - logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) - } - } -} - function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: CompilationContext): void { const msg = quotedString(schema.$comment) if ($comment === true) { diff --git a/spec/options/comment.spec.js b/spec/options/comment.spec.js index 98cfc5cc7a..b1987bc699 100644 --- a/spec/options/comment.spec.js +++ b/spec/options/comment.spec.js @@ -22,6 +22,7 @@ describe("$comment option", () => { it("should log the text from $comment keyword", () => { var schema = { + $comment: "object root", properties: { foo: {$comment: "property foo"}, bar: {$comment: "property bar", type: "integer"}, @@ -34,10 +35,10 @@ describe("$comment option", () => { ;[ajv, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) - test({}, true, []) - test({foo: 1}, true, [["property foo"]]) - test({foo: 1, bar: 2}, true, [["property foo"], ["property bar"]]) - test({foo: 1, bar: "baz"}, false, [["property foo"], ["property bar"]]) + test({}, true, [["object root"]]) + test({foo: 1}, true, [["object root"], ["property foo"]]) + test({foo: 1, bar: 2}, true, [["object root"], ["property foo"], ["property bar"]]) + test({foo: 1, bar: "baz"}, false, [["object root"], ["property foo"], ["property bar"]]) function test(data, valid, expectedLogCalls) { logCalls = [] @@ -59,6 +60,7 @@ describe("$comment option", () => { it("should pass the text from $comment keyword to the hook", () => { var schema = { + $comment: "object root", properties: { foo: {$comment: "property foo"}, bar: {$comment: "property bar", type: "integer"}, @@ -71,15 +73,18 @@ describe("$comment option", () => { ;[ajv, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) - test({}, true, []) + test({}, true, [["object root", "#/$comment", schema]]) test({foo: 1}, true, [ + ["object root", "#/$comment", schema], ["property foo", "#/properties/foo/$comment", schema], ]) test({foo: 1, bar: 2}, true, [ + ["object root", "#/$comment", schema], ["property foo", "#/properties/foo/$comment", schema], ["property bar", "#/properties/bar/$comment", schema], ]) test({foo: 1, bar: "baz"}, false, [ + ["object root", "#/$comment", schema], ["property foo", "#/properties/foo/$comment", schema], ["property bar", "#/properties/bar/$comment", schema], ]) From db7574dcf6e3d1e8f60c2a2cadc5d543b8b18d45 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 17 Aug 2020 14:44:04 +0100 Subject: [PATCH 055/322] refactor: allOf to typescript --- lib/ajv.ts | 10 ++-- lib/compile/context._ts | 79 ++++++++++++++++++++++++++++ lib/compile/index.ts | 31 ++++++----- lib/compile/rules.ts | 2 +- lib/compile/subschema.ts | 30 +++++++++++ lib/compile/validate/boolSchema.ts | 8 +-- lib/compile/validate/index.ts | 41 +++++++++++---- lib/dot/allOf.jst | 34 ------------ lib/dot/custom.jst | 2 +- lib/dot/definitions.def | 4 +- lib/dot/not.jst | 2 +- lib/dot/ref.jst | 2 +- lib/dotjs/index.js | 1 - lib/types.ts | 6 ++- lib/vocabularies/applicator/allOf.ts | 44 ++++++++++++++++ lib/vocabularies/applicator/index.ts | 5 ++ lib/vocabularies/util.ts | 12 ++++- lib/vocabularies/validation/const.ts | 1 + lib/vocabularies/validation/enum.ts | 1 + 19 files changed, 240 insertions(+), 75 deletions(-) create mode 100644 lib/compile/context._ts create mode 100644 lib/compile/subschema.ts delete mode 100644 lib/dot/allOf.jst create mode 100644 lib/vocabularies/applicator/allOf.ts create mode 100644 lib/vocabularies/applicator/index.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 0f37b1612e..23959bb47d 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -3,11 +3,14 @@ import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules from "./compile/rules" import $dataMetaSchema from "./data" +import {Vocabulary, Options} from "./types" var compileSchema = require("./compile"), resolve = require("./compile/resolve"), - stableStringify = require("fast-json-stable-stringify"), - validationVocabulary = require("./vocabularies/validation") + stableStringify = require("fast-json-stable-stringify") + +const validationVocabulary: Vocabulary = require("./vocabularies/validation") +const applicatorVocabulary: Vocabulary = require("./vocabularies/applicator") module.exports = Ajv @@ -47,7 +50,7 @@ const META_SUPPORT_DATA = ["/properties"] * @param {Object} opts optional options * @return {Object} ajv instance */ -function Ajv(opts): void { +export default function Ajv(opts: Options): void { if (!(this instanceof Ajv)) return new Ajv(opts) opts = this._opts = {...(opts || {})} setLogger(this) @@ -74,6 +77,7 @@ function Ajv(opts): void { if (opts.formats) addInitialFormats(this) this.addVocabulary(validationVocabulary, true) + this.addVocabulary(applicatorVocabulary, true) if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) diff --git a/lib/compile/context._ts b/lib/compile/context._ts new file mode 100644 index 0000000000..79ce9d2621 --- /dev/null +++ b/lib/compile/context._ts @@ -0,0 +1,79 @@ +interface ContextConstructorArgument { + ajv: any + schema: any + isRoot: boolean + baseId: string +} + +export default class SchemaCompilationContext { + isTop?: boolean = true + async: boolean + schema: any + isRoot: boolean + baseId: string + + // TODO ajv type + constructor({ajv, schema, isRoot, baseId}: ContextConstructorArgument) { + this.async = schema.$async === true + this.schema = schema + this.isRoot = isRoot + this.baseId = baseId + } +} + + +// root: _root, +// schemaPath: "", +// errSchemaPath: "#", +// errorPath: '""', +// dataPathArr: [""], +// level: 0, +// dataLevel: 0, +// data: "data", // TODO get unique name when passed from applicator keywords +// gen: new CodeGen(), +// MissingRefError, +// RULES: RULES, +// validate: validateCode, +// util: util, +// resolve: resolve, +// resolveRef: resolveRef, +// usePattern: usePattern, +// useDefault: useDefault, +// useCustomRule: useCustomRule, +// opts: opts, +// formats: formats, +// logger: self.logger, +// self: self, + + +// level: number +// dataLevel: number +// data: string +// dataPathArr: string[] +// schemaPath: string +// errorPath: string +// errSchemaPath: string +// gen: CodeGen +// createErrors?: boolean // TODO maybe remove later +// opts: Options +// formats: { +// [index: string]: Format | undefined +// } +// // keywords: { +// // [index: string]: KeywordDefinition | undefined +// // } +// compositeRule?: boolean +// validate: (it: CompilationContext) => string +// usePattern: (str: string) => string +// useDefault: (value: any) => string +// useCustomRule: (rule: Rule, schema: any, parentSchema: object, it: CompilationContext) => any +// util: object // TODO +// self: object // TODO +// RULES: ValidationRules +// logger: Logger // TODO ? +// isTop: boolean // TODO ? +// root: SchemaRoot // TODO ? +// rootId?: string // TODO ? +// MissingRefError: typeof MissingRefError +// resolve: any +// resolveRef: (...args: any[]) => any \ No newline at end of file diff --git a/lib/compile/index.ts b/lib/compile/index.ts index c3cd2f40d9..8250428146 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -2,6 +2,7 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" import validateCode from "./validate" +import {applySchema} from "./subschema" import {Rule} from "./rules" import {CompilationContext, KeywordDefinition, ErrorObject} from "../types" @@ -93,12 +94,13 @@ function compile(schema, root, localRefs, baseId) { var $async = _schema.$async === true - var sourceCode = validateCode({ + // TODO refactor to extract code from gen + let sourceCode = validateCode({ isTop: true, async: _schema.$async === true, schema: _schema, - isRoot: isRoot, - baseId: baseId, + isRoot, + baseId, root: _root, schemaPath: "", errSchemaPath: "#", @@ -109,18 +111,19 @@ function compile(schema, root, localRefs, baseId) { data: "data", // TODO get unique name when passed from applicator keywords gen: new CodeGen(), MissingRefError, - RULES: RULES, - validate: validateCode, - util: util, - resolve: resolve, - resolveRef: resolveRef, - usePattern: usePattern, - useDefault: useDefault, - useCustomRule: useCustomRule, - opts: opts, - formats: formats, + RULES, + validateCode, + applySchema, // TODO remove to imports + util, // TODO remove to imports + resolve, // TODO remove to imports + resolveRef, // TODO remove to imports + usePattern, // TODO remove to imports + useDefault, // TODO remove to imports + useCustomRule, // TODO remove to imports + opts, + formats, logger: self.logger, - self: self, + self, }) sourceCode = diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 69302ee9b3..edb1a0b27a 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -66,7 +66,7 @@ export default function rules(): ValidationRules { {properties: ["additionalProperties", "patternProperties"]}, ], }, - {rules: ["$ref", "not", "anyOf", "oneOf", "allOf", "if"]}, + {rules: ["$ref", "not", "anyOf", "oneOf", "if"]}, ], all: toHash(ALL), keywords: {}, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts new file mode 100644 index 0000000000..2f4a860f08 --- /dev/null +++ b/lib/compile/subschema.ts @@ -0,0 +1,30 @@ +import {CompilationContext} from "../types" +import validateCode from "./validate" +import {getProperty, escapeFragment} from "./util" + +export interface Subschema { + schema: any + schemaPath: string + errSchemaPath: string +} + +export function applySchema(it: CompilationContext, subschema: Subschema): string { + const {gen, level} = it + const nextIt = {...it, ...subschema, level: level + 1} + const nextValid = gen.name("valid") + // TODO remove true once appendGen is removed + validateCode(nextIt, nextValid, true) + return nextValid +} + +export function applyKeywordSubschema( + it: CompilationContext, + keyword: string, + prop: string | number +): string { + return applySchema(it, { + schema: it.schema[keyword][prop], + schemaPath: it.schemaPath + getProperty(keyword) + getProperty(prop), + errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + prop)}`, + }) +} diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 0f93c75b80..f42d85c28a 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -6,8 +6,8 @@ const boolError: KeywordErrorDefinition = { params: () => "{}", } -export function booleanOrEmptySchema(it: CompilationContext): void { - const {gen, isTop, schema, level} = it +export function booleanOrEmptySchema(it: CompilationContext, valid: string): void { + const {gen, isTop, schema} = it if (isTop) { if (schema === false) { falseSchemaError(it, false) @@ -22,10 +22,10 @@ export function booleanOrEmptySchema(it: CompilationContext): void { ) } else { if (schema === false) { - gen.code(`var valid${level} = false;`) // TODO level, var + gen.code(`var ${valid} = false;`) // TODO var falseSchemaError(it) } else { - gen.code(`var valid${level} = true;`) // TODO level, var + gen.code(`var ${valid} = true;`) // TODO var } } } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 224d57ae30..5b2b9f312c 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -17,7 +17,11 @@ const resolve = require("../resolve") * "validate" is a variable name to which this function will be assigned * validateRef etc. are defined in the parent scope in index.js */ -export default function validateCode(it: CompilationContext): string { +export default function validateCode( + it: CompilationContext, + valid?: string, + appendGen?: true // TODO remove once all callers pass true +): string | void { const { isTop, schema, @@ -26,9 +30,17 @@ export default function validateCode(it: CompilationContext): string { gen, opts: {$comment}, } = it - // TODO _out - let _out = gen._out - gen._out = "" + + let _out + if (!appendGen) { + // TODO _out + _out = gen._out + gen._out = "" + } + + // TODO valid must be non-optional or maybe it must be returned + if (!valid) valid = `valid${level}` + checkUnknownKeywords(it) checkRefsAndKeywords(it) @@ -47,18 +59,25 @@ export default function validateCode(it: CompilationContext): string { checkAsync(it) gen.code(`var errs_${level} = errors;`) typeAndKeywords() - gen.code(`var valid${level} = errors === errs_${level};`) + // TODO level, var + gen.code(`var ${valid} = errors === errs_${level};`) } - // TODO _out - ;[_out, gen._out] = [gen._out, _out] - return _out + if (!appendGen) { + // TODO _out + ;[_out, gen._out] = [gen._out, _out] + return _out + } function booleanOrEmpty(): true | void { if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { - booleanOrEmptySchema(it) - // TODO _out - ;[_out, gen._out] = [gen._out, _out] + // TODO remove type cast once valid is non optional + booleanOrEmptySchema(it, valid) + + if (!appendGen) { + // TODO _out + ;[_out, gen._out] = [gen._out, _out] + } return true } } diff --git a/lib/dot/allOf.jst b/lib/dot/allOf.jst deleted file mode 100644 index 8045586d31..0000000000 --- a/lib/dot/allOf.jst +++ /dev/null @@ -1,34 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - -{{ - var $currentBaseId = $it.baseId - , $allSchemasEmpty = true; -}} - -{{~ $schema:$sch:$i }} - {{? {{# def.nonEmptySchema:$sch }} }} - {{ - $allSchemasEmpty = false; - $it.schema = $sch; - $it.schemaPath = $schemaPath + '[' + $i + ']'; - $it.errSchemaPath = $errSchemaPath + '/' + $i; - }} - - {{# def.insertSubschemaCode }} - - {{# def.ifResultValid }} - {{?}} -{{~}} - -{{? $breakOnError }} - {{? $allSchemasEmpty }} - if (true) { - {{??}} - {{= $closingBraces.slice(0,-1) }} - {{?}} -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst index 7ddb02f4e2..db3e1f6fdc 100644 --- a/lib/dot/custom.jst +++ b/lib/dot/custom.jst @@ -103,7 +103,7 @@ var {{=$valid}}; $it.schemaPath = ''; }} {{# def.setCompositeRule }} - {{ var $code = it.validate($it).replace(/validate\.schema/g, $validateCode); }} + {{ var $code = it.validateCode($it).replace(/validate\.schema/g, $validateCode); }} {{# def.resetCompositeRule }} {{= $code }} {{??}} diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index 6144eae46d..afd35b3969 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -76,14 +76,14 @@ {{## def.generateSubschemaCode: {{ - var $code = it.validate($it); + var $code = it.validateCode($it); $it.baseId = $currentBaseId; }} #}} {{## def.insertSubschemaCode: - {{= it.validate($it) }} + {{= it.validateCode($it) }} {{ $it.baseId = $currentBaseId; }} #}} diff --git a/lib/dot/not.jst b/lib/dot/not.jst index b49b85d05c..421e502a08 100644 --- a/lib/dot/not.jst +++ b/lib/dot/not.jst @@ -22,7 +22,7 @@ $it.opts.allErrors = false; } }} - {{= it.validate($it) }} + {{= it.validateCode($it) }} {{ $it.createErrors = true; if ($allErrorsOption) $it.opts.allErrors = $allErrorsOption; diff --git a/lib/dot/ref.jst b/lib/dot/ref.jst index 622bf93dc9..f5594fbe56 100644 --- a/lib/dot/ref.jst +++ b/lib/dot/ref.jst @@ -43,7 +43,7 @@ $it.schemaPath = ''; $it.errSchemaPath = $schema; }} - {{ var $code = it.validate($it).replace(/validate\.schema/g, $refVal.code); }} + {{ var $code = it.validateCode($it).replace(/validate\.schema/g, $refVal.code); }} {{= $code }} {{? $breakOnError}} if ({{=$nextValid}}) { diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index e434806668..0273692264 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -3,7 +3,6 @@ //all requires must be explicit because browserify won't work with dynamic requires module.exports = { $ref: require("./ref"), - allOf: require("./allOf"), anyOf: require("./anyOf"), contains: require("./contains"), dependencies: require("./dependencies"), diff --git a/lib/types.ts b/lib/types.ts index 9dfe9c2aae..b2f6b2cab6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -2,6 +2,7 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" import {ValidationRules, Rule} from "./compile/rules" import {MissingRefError} from "./compile/error_classes" +import {Subschema} from "./compile/subschema" export interface Options { $data?: boolean @@ -47,6 +48,8 @@ export interface Options { nullable?: boolean serialize?: false | ((schema: object | boolean) => any) $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) + schemaId?: string // not supported + _errorDataPathProperty?: boolean // private } interface Logger { @@ -122,7 +125,8 @@ export interface CompilationContext { // [index: string]: KeywordDefinition | undefined // } compositeRule?: boolean - validate: (it: CompilationContext) => string + validateCode: (it: CompilationContext) => string | void // TODO remove string + applySchema: (it: CompilationContext, subSchema: Subschema) => string usePattern: (str: string) => string useDefault: (value: any) => string useCustomRule: (rule: Rule, schema: any, parentSchema: object, it: CompilationContext) => any diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts new file mode 100644 index 0000000000..8cd24ff76d --- /dev/null +++ b/lib/vocabularies/applicator/allOf.ts @@ -0,0 +1,44 @@ +import {KeywordDefinition} from "../../types" +import {nonEmptySchema} from "../util" +import {applyKeywordSubschema} from "../../compile/subschema" + +const def: KeywordDefinition = { + keyword: "allOf", + schemaType: "array", + code({gen, ok, schema, it}) { + const {opts} = it + let emptySchemas = true + let closeBlocks = "" + schema.forEach((sch: object | boolean, i: number) => { + if (nonEmptySchema(it, sch)) { + emptySchemas = false + const valid = applyKeywordSubschema(it, "allOf", i) + if (!opts.allErrors) { + gen.code(`if (${valid}) {`) + closeBlocks += "}" + } + } + }) + + if (!opts.allErrors) { + if (emptySchemas) ok() + else gen.code(closeBlocks.slice(0, -1)) // TODO refactor + } + + // TODO possibly add allOf error + // const valid = gen.name("valid") + // gen.code(`let ${valid} = true;`) + // ... in the loop: + // gen.code(`${valid} = ${valid} && ${schValid};`) + // + // fail(`!${valid}`) + }, + error: { + // TODO allow message to be just a string if it is constant? + message: () => '"should match all schemas in allOf"', + // TODO make params optional if there are no params? + params: () => "{}", + }, +} + +module.exports = def diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts new file mode 100644 index 0000000000..cf200f3772 --- /dev/null +++ b/lib/vocabularies/applicator/index.ts @@ -0,0 +1,5 @@ +import {Vocabulary} from "../../types" + +const applicator: Vocabulary = [require("./allOf")] + +module.exports = applicator diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index f3af003b95..4509db5ae7 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,4 +1,5 @@ -import {getProperty} from "../compile/util" +import {getProperty, schemaHasRules} from "../compile/util" +import {CompilationContext} from "../types" export function appendSchema( schemaCode: string | number | boolean, @@ -41,3 +42,12 @@ export function schemaRefOrVal( } return `validate.schema${schemaPath + getProperty(keyword)}` } + +export function nonEmptySchema( + {RULES, opts: {strictKeywords}}: CompilationContext, + schema: boolean | object +): boolean | void { + return strictKeywords + ? (typeof schema == "object" && Object.keys(schema).length > 0) || schema === false + : schemaHasRules(schema, RULES.all) +} diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 829acdd7cf..6b967a9844 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -5,6 +5,7 @@ const def: KeywordDefinition = { $data: true, code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), error: { + // TODO allow message to be just a string if it is constant? message: () => '"should be equal to constant"', params: ({schemaCode}) => `{allowedValue: ${schemaCode}}`, }, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 921d4a5ca7..2c5c9848c9 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -58,6 +58,7 @@ const def: KeywordDefinition = { } }, error: { + // TODO allow message to be just a string if it is constant? message: () => '"should be equal to one of the allowed values"', params: ({schemaCode}) => `{allowedValues: ${schemaCode}}`, }, From ca44b8236efd428a0dec5b5b64db4afa2ed31596 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 17 Aug 2020 16:55:05 +0100 Subject: [PATCH 056/322] refactor: anyOf to typescript --- lib/compile/errors.ts | 54 +++++++++++++++++++++------- lib/compile/rules.ts | 2 +- lib/compile/subschema.ts | 18 ++++++---- lib/dot/anyOf.jst | 48 ------------------------- lib/dotjs/index.js | 1 - lib/types.ts | 4 +-- lib/vocabularies/applicator/anyOf.ts | 50 ++++++++++++++++++++++++++ lib/vocabularies/applicator/index.ts | 2 +- 8 files changed, 106 insertions(+), 73 deletions(-) delete mode 100644 lib/dot/anyOf.jst create mode 100644 lib/vocabularies/applicator/anyOf.ts diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index c405ad4552..5dab9c190f 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,5 +1,6 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {toQuotedString} from "./util" +import CodeGen from "./codegen" export function reportError( cxt: KeywordContext, @@ -9,23 +10,50 @@ export function reportError( const {gen, compositeRule, opts, async} = cxt.it const errObj = errorObjectCode(cxt, error) if (allErrors ?? (compositeRule || opts.allErrors)) { - const err = gen.name("err") - gen.code( - `const ${err} = ${errObj}; - if (vErrors === null) vErrors = [${err}]; - else vErrors.push(${err}); - errors++;` - ) + addError(gen, errObj) } else { - gen.code( - async - ? `throw new ValidationError([${errObj}]);` - : `validate.errors = [${errObj}]; - return false;` - ) + returnErrors(gen, async, `[${errObj}]`) } } +export function reportExtraError(cxt: KeywordContext, error: KeywordErrorDefinition): void { + const {gen, compositeRule, opts, async} = cxt.it + const errObj = errorObjectCode(cxt, error) + addError(gen, errObj) + if (!(compositeRule || opts.allErrors)) { + returnErrors(gen, async, "vErrors") + } +} + +export function resetErrorsCount(gen: CodeGen, errsCount: string): void { + gen.code( + `errors = ${errsCount}; + if (vErrors !== null) { + if (${errsCount}) vErrors.length = ${errsCount}; + else vErrors = null; + }` + ) +} + +function addError(gen: CodeGen, errObj: string): void { + const err = gen.name("err") + gen.code( + `const ${err} = ${errObj}; + if (vErrors === null) vErrors = [${err}]; + else vErrors.push(${err}); + errors++;` + ) +} + +function returnErrors(gen: CodeGen, async: boolean, errs: string): void { + gen.code( + async + ? `throw new ValidationError(${errs});` + : `validate.errors = ${errs}; + return false;` + ) +} + function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): string { const { keyword, diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index edb1a0b27a..218dbd65e5 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -66,7 +66,7 @@ export default function rules(): ValidationRules { {properties: ["additionalProperties", "patternProperties"]}, ], }, - {rules: ["$ref", "not", "anyOf", "oneOf", "if"]}, + {rules: ["$ref", "not", "oneOf", "if"]}, ], all: toHash(ALL), keywords: {}, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 2f4a860f08..d9c5112376 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -2,29 +2,33 @@ import {CompilationContext} from "../types" import validateCode from "./validate" import {getProperty, escapeFragment} from "./util" -export interface Subschema { +export interface SubschemaContext { schema: any schemaPath: string errSchemaPath: string + compositeRule?: true } -export function applySchema(it: CompilationContext, subschema: Subschema): string { +export function applySchema(it: CompilationContext, subschema: SubschemaContext): string { const {gen, level} = it - const nextIt = {...it, ...subschema, level: level + 1} + const nextContext = {...it, ...subschema, level: level + 1} const nextValid = gen.name("valid") // TODO remove true once appendGen is removed - validateCode(nextIt, nextValid, true) + validateCode(nextContext, nextValid, true) return nextValid } export function applyKeywordSubschema( it: CompilationContext, keyword: string, - prop: string | number + prop: string | number, + compositeRule?: true ): string { - return applySchema(it, { + const subschema: SubschemaContext = { schema: it.schema[keyword][prop], schemaPath: it.schemaPath + getProperty(keyword) + getProperty(prop), errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + prop)}`, - }) + } + if (compositeRule) subschema.compositeRule = compositeRule + return applySchema(it, subschema) } diff --git a/lib/dot/anyOf.jst b/lib/dot/anyOf.jst deleted file mode 100644 index 3ae85df199..0000000000 --- a/lib/dot/anyOf.jst +++ /dev/null @@ -1,48 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - -{{ - var $noEmptySchema = $schema.every(function($sch) { - return {{# def.nonEmptySchema:$sch }}; - }); -}} -{{? $noEmptySchema }} - {{ var $currentBaseId = $it.baseId; }} - var {{=$errs}} = errors; - var {{=$valid}} = false; - - {{# def.setCompositeRule }} - - {{~ $schema:$sch:$i }} - {{ - $it.schema = $sch; - $it.schemaPath = $schemaPath + '[' + $i + ']'; - $it.errSchemaPath = $errSchemaPath + '/' + $i; - }} - - {{# def.insertSubschemaCode }} - - {{=$valid}} = {{=$valid}} || {{=$nextValid}}; - - if (!{{=$valid}}) { - {{ $closingBraces += '}'; }} - {{~}} - - {{# def.resetCompositeRule }} - - {{= $closingBraces }} - - if (!{{=$valid}}) { - {{# def.extraError:'anyOf' }} - } else { - {{# def.resetErrors }} - {{? it.opts.allErrors }} } {{?}} -{{??}} - {{? $breakOnError }} - if (true) { - {{?}} -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 0273692264..09f64b5f4b 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -3,7 +3,6 @@ //all requires must be explicit because browserify won't work with dynamic requires module.exports = { $ref: require("./ref"), - anyOf: require("./anyOf"), contains: require("./contains"), dependencies: require("./dependencies"), format: require("./format"), diff --git a/lib/types.ts b/lib/types.ts index b2f6b2cab6..9d8e549b59 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -2,7 +2,7 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" import {ValidationRules, Rule} from "./compile/rules" import {MissingRefError} from "./compile/error_classes" -import {Subschema} from "./compile/subschema" +import {SubschemaContext} from "./compile/subschema" export interface Options { $data?: boolean @@ -126,7 +126,7 @@ export interface CompilationContext { // } compositeRule?: boolean validateCode: (it: CompilationContext) => string | void // TODO remove string - applySchema: (it: CompilationContext, subSchema: Subschema) => string + applySchema: (it: CompilationContext, subSchema: SubschemaContext) => string usePattern: (str: string) => string useDefault: (value: any) => string useCustomRule: (rule: Rule, schema: any, parentSchema: object, it: CompilationContext) => any diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts new file mode 100644 index 0000000000..52e5480425 --- /dev/null +++ b/lib/vocabularies/applicator/anyOf.ts @@ -0,0 +1,50 @@ +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {nonEmptySchema} from "../util" +import {applyKeywordSubschema} from "../../compile/subschema" +import {reportExtraError, resetErrorsCount} from "../../compile/errors" + +const def: KeywordDefinition = { + keyword: "anyOf", + schemaType: "array", + code(cxt) { + const {gen, ok, schema, it} = cxt + let hasEmptySchema = !schema.every((sch: object | boolean) => nonEmptySchema(it, sch)) + if (hasEmptySchema) { + ok() + return + } + const valid = gen.name("valid") + const errsCount = gen.name("_errs") + gen.code( + `let ${valid} = false; + const ${errsCount} = errors;` + ) + + let closeBlocks = "" + schema.forEach((_, i: number) => { + const schValid = applyKeywordSubschema(it, "anyOf", i, true) + gen.code( + `${valid} = ${valid} || ${schValid}; + if (!${valid}) {` + ) + closeBlocks += "}" + }) + + gen.code(closeBlocks) + + // TODO refactor failCompoundOrReset? + gen.code(`if (!${valid}) {`) + reportExtraError(cxt, def.error as KeywordErrorDefinition) + gen.code(`} else {`) + resetErrorsCount(gen, errsCount) + if (it.opts.allErrors) gen.code(`}`) + }, + error: { + // TODO allow message to be just a string if it is constant? + message: () => '"should match some schema in anyOf"', + // TODO make params optional if there are no params? + params: () => "{}", + }, +} + +module.exports = def diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index cf200f3772..46566bf403 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -1,5 +1,5 @@ import {Vocabulary} from "../../types" -const applicator: Vocabulary = [require("./allOf")] +const applicator: Vocabulary = [require("./allOf"), require("./anyOf")] module.exports = applicator From 45150901ab799f14b7c9458a975fe46c644203c5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 17 Aug 2020 18:36:20 +0100 Subject: [PATCH 057/322] refactor: items to typescript (one test fails) --- lib/ajv.ts | 2 +- lib/compile/rules.ts | 2 +- lib/compile/subschema.ts | 59 +++- lib/compile/util.ts | 18 +- lib/compile/validate/dataType.ts | 27 +- lib/dot/items.jst | 8 +- lib/keyword.ts | 47 ++- lib/types.ts | 6 +- lib/vocabularies/applicator/allOf.ts | 4 +- lib/vocabularies/applicator/anyOf.ts | 8 +- lib/vocabularies/applicator/index.ts | 2 +- lib/vocabularies/applicator/items.ts | 83 +++++ lib/vocabularies/util.ts | 2 +- lib/vocabularies/validation/limit.ts | 2 +- lib/vocabularies/validation/limitItems.ts | 2 +- lib/vocabularies/validation/limitLength.ts | 2 +- .../validation/limitProperties.ts | 2 +- lib/vocabularies/validation/multipleOf.ts | 2 +- lib/vocabularies/validation/pattern.ts | 2 +- spec/errors.spec.js | 320 ++++-------------- 20 files changed, 285 insertions(+), 315 deletions(-) create mode 100644 lib/vocabularies/applicator/items.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 23959bb47d..976013e718 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -76,8 +76,8 @@ export default function Ajv(opts: Options): void { this._metaOpts = getMetaSchemaOptions(this) if (opts.formats) addInitialFormats(this) - this.addVocabulary(validationVocabulary, true) this.addVocabulary(applicatorVocabulary, true) + this.addVocabulary(validationVocabulary, true) if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 218dbd65e5..06df8f68ab 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -55,7 +55,7 @@ export default function rules(): ValidationRules { {type: "string", rules: ["format"]}, { type: "array", - rules: ["items", "contains"], + rules: ["contains"], }, { type: "object", diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index d9c5112376..596a574bee 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,11 +1,14 @@ import {CompilationContext} from "../types" import validateCode from "./validate" -import {getProperty, escapeFragment} from "./util" +import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" export interface SubschemaContext { schema: any schemaPath: string errSchemaPath: string + errorPath?: string + dataPathArr?: (string | number)[] + dataLevel?: number compositeRule?: true } @@ -18,17 +21,55 @@ export function applySchema(it: CompilationContext, subschema: SubschemaContext) return nextValid } -export function applyKeywordSubschema( - it: CompilationContext, - keyword: string, - prop: string | number, +export enum Expr { + Const, + Num, + Str, +} + +interface SubschemaApplication { + keyword: string + schemaProp?: string | number compositeRule?: true + dataProp?: string | number + expr?: Expr +} + +export function applySubschema( + it: CompilationContext, + {keyword, schemaProp, compositeRule, dataProp, expr}: SubschemaApplication ): string { - const subschema: SubschemaContext = { - schema: it.schema[keyword][prop], - schemaPath: it.schemaPath + getProperty(keyword) + getProperty(prop), - errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + prop)}`, + const schema = it.schema[keyword] + const subschema: SubschemaContext = + schemaProp === undefined + ? { + schema: schema, + schemaPath: it.schemaPath + getProperty(keyword), + errSchemaPath: `${it.errSchemaPath}/${keyword}`, + } + : { + schema: schema[schemaProp], + schemaPath: it.schemaPath + getProperty(keyword) + getProperty(schemaProp), + errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + schemaProp)}`, + } + + if (dataProp !== undefined) { + const {gen, errorPath, dataPathArr, dataLevel, opts} = it + // TODO possibly refactor getPath and getPathExpr to one function using Expr enum + const nextLevel = dataLevel + 1 + Object.assign(subschema, { + errorPath: + expr === Expr.Const + ? getPath(errorPath, dataProp, opts.jsonPointers) + : getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num), + dataPathArr: [...dataPathArr, dataProp], + dataLevel: nextLevel, + }) + + const passDataProp = Expr.Const ? getProperty(dataProp) : `[${dataProp}]` + gen.code(`var data${nextLevel} = data${dataLevel || ""}${passDataProp};`) } + if (compositeRule) subschema.compositeRule = compositeRule return applySchema(it, subschema) } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index a716d9908e..798a4cd344 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -171,16 +171,22 @@ export function getPathExpr( return joinPaths(currentPath, path) } -export function getPath(currentPath: string, prop: string, jsonPointers?: boolean): string { - const path = jsonPointers // false by default - ? toQuotedString("/" + escapeJsonPointer(prop)) - : toQuotedString(getProperty(prop)) +export function getPath( + currentPath: string, + prop: string | number, + jsonPointers?: boolean +): string { + const path = toQuotedString( + jsonPointers // false by default + ? "/" + (typeof prop == "number" ? prop : escapeJsonPointer(prop)) + : getProperty(prop) + ) return joinPaths(currentPath, path) } const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ -export function getData($data: string, lvl: number, paths: string[]): string { +export function getData($data: string, lvl: number, paths: (number | string)[]): string { let jsonPointer, data if ($data === "") return "rootData" if ($data[0] === "/") { @@ -200,7 +206,7 @@ export function getData($data: string, lvl: number, paths: string[]): string { "Cannot access property/index " + up + " levels up, current level is " + lvl ) } - return paths[lvl - up] + return "" + paths[lvl - up] } if (up > lvl) { diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 6eb1228b6d..767cb52d1e 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,8 +1,8 @@ -import {CompilationContext, KeywordContext, KeywordErrorDefinition} from "../../types" +import {CompilationContext, KeywordErrorDefinition} from "../../types" import {toHash, checkDataType, checkDataTypes} from "../util" -import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" +import {getKeywordContext} from "../../keyword" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { const t: undefined | string | string[] = schema.type @@ -136,28 +136,7 @@ const typeError: KeywordErrorDefinition = { params: ({schema}) => `{type: "${Array.isArray(schema) ? schema.join(",") : schema}"}`, } -// TODO maybe combine with boolSchemaError -// TODO refactor type keyword context creation export function reportTypeError(it: CompilationContext) { - const {gen, schema, schemaPath, dataLevel} = it - const schemaCode = schemaRefOrVal(schema, schemaPath, "type") - const cxt: KeywordContext = { - gen, - fail: exception, - ok: exception, - errorParams: exception, - keyword: "type", - data: "data" + (dataLevel || ""), - schema: schema.type, - schemaCode, - schemaValue: schemaCode, - parentSchema: schema, - it, - } + const cxt = getKeywordContext(it, "type") reportError(cxt, typeError) } - -// TODO combine with exception from boolSchema -function exception() { - throw new Error("this function can only be used in keyword") -} diff --git a/lib/dot/items.jst b/lib/dot/items.jst index dcc2b696fe..c33a6f91cc 100644 --- a/lib/dot/items.jst +++ b/lib/dot/items.jst @@ -55,7 +55,7 @@ var {{=$valid}}; $it.schema = $sch; $it.schemaPath = $schemaPath + '[' + $i + ']'; $it.errSchemaPath = $errSchemaPath + '/' + $i; - $it.errorPath = it.util.getPathExpr(it.errorPath, $i, it.opts.jsonPointers, true); + $it.errorPath = it.util.getPath(it.errorPath, $i, it.opts.jsonPointers); $it.dataPathArr[$dataNxt] = $i; }} @@ -63,7 +63,11 @@ var {{=$valid}}; {{# def.optimizeValidate }} } - {{# def.ifResultValid }} + {{ /* def.ifResultValid */ }} + {{? $breakOnError }} + if ({{=$nextValid}}) { + {{ $closingBraces += '}'; }} + {{?}} {{?}} {{~}} diff --git a/lib/keyword.ts b/lib/keyword.ts index 932d650af8..260834f204 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -149,20 +149,15 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void } if ($data) { gen.code(`const ${cxt.schemaCode} = ${getData($data, dataLevel, dataPathArr)};`) - } else { - if ( - schemaType && - !(schemaType === "array" ? Array.isArray(schema) : typeof schema === schemaType) - ) { - throw new Error(`${keyword} must be ${schemaType}`) - } + } else if (schemaType && !validSchemaType(schema, schemaType)) { + throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) } // TODO check that code called "fail" or another valid way to return code code(cxt) - function fail(condition: string): void { + function fail(condition: string, context?: KeywordContext): void { gen.code(`if (${condition}) {`) - reportError(cxt, error as KeywordErrorDefinition) + reportError(context || cxt, error as KeywordErrorDefinition) gen.code(opts.allErrors ? "}" : "} else {") } @@ -176,6 +171,40 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void } } +function validSchemaType(schema: any, schemaType: string | string[]): boolean { + // TODO add tests + if (Array.isArray(schemaType)) { + return schemaType.some((st) => validSchemaType(schema, st)) + } + return schemaType === "array" + ? Array.isArray(schema) + : schemaType === "object" + ? schema && typeof schema == "object" && !Array.isArray(schema) + : typeof schema == schemaType +} + +export function getKeywordContext(it: CompilationContext, keyword: string): KeywordContext { + const {gen, schema, schemaPath, dataLevel} = it + const schemaCode = schemaRefOrVal(schema, schemaPath, keyword) + return { + gen, + fail: exception, + ok: exception, + errorParams: exception, + keyword, + data: "data" + (dataLevel || ""), + schema: schema[keyword], + schemaCode, + schemaValue: schemaCode, + parentSchema: schema, + it, + } +} + +function exception() { + throw new Error("this function can only be used in keyword") +} + /** * Get keyword * @this Ajv diff --git a/lib/types.ts b/lib/types.ts index 9d8e549b59..9629353f59 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -107,7 +107,7 @@ export interface CompilationContext { level: number dataLevel: number data: string - dataPathArr: string[] + dataPathArr: (string | number)[] schema: any isRoot: boolean schemaPath: string @@ -151,7 +151,7 @@ interface SchemaRoot { export interface KeywordDefinition { keyword?: string | string[] type?: string | string[] - schemaType?: string + schemaType?: string | string[] async?: boolean $data?: boolean errors?: boolean | "full" @@ -182,7 +182,7 @@ export type Vocabulary = KeywordDefinition[] export interface KeywordContext { gen: CodeGen - fail: (condition: string) => void + fail: (condition: string, context?: KeywordContext) => void ok: (condition?: string) => void errorParams: (obj: any) => void keyword: string diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 8cd24ff76d..1e3cb9d540 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,6 +1,6 @@ import {KeywordDefinition} from "../../types" import {nonEmptySchema} from "../util" -import {applyKeywordSubschema} from "../../compile/subschema" +import {applySubschema} from "../../compile/subschema" const def: KeywordDefinition = { keyword: "allOf", @@ -12,7 +12,7 @@ const def: KeywordDefinition = { schema.forEach((sch: object | boolean, i: number) => { if (nonEmptySchema(it, sch)) { emptySchemas = false - const valid = applyKeywordSubschema(it, "allOf", i) + const valid = applySubschema(it, {keyword: "allOf", schemaProp: i}) if (!opts.allErrors) { gen.code(`if (${valid}) {`) closeBlocks += "}" diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 52e5480425..e1bd034744 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,6 +1,6 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" import {nonEmptySchema} from "../util" -import {applyKeywordSubschema} from "../../compile/subschema" +import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" const def: KeywordDefinition = { @@ -22,7 +22,11 @@ const def: KeywordDefinition = { let closeBlocks = "" schema.forEach((_, i: number) => { - const schValid = applyKeywordSubschema(it, "anyOf", i, true) + const schValid = applySubschema(it, { + keyword: "anyOf", + schemaProp: i, + compositeRule: true, + }) gen.code( `${valid} = ${valid} || ${schValid}; if (!${valid}) {` diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 46566bf403..88647c1d0c 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -1,5 +1,5 @@ import {Vocabulary} from "../../types" -const applicator: Vocabulary = [require("./allOf"), require("./anyOf")] +const applicator: Vocabulary = [require("./allOf"), require("./anyOf"), require("./items")] module.exports = applicator diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts new file mode 100644 index 0000000000..ca9fd24190 --- /dev/null +++ b/lib/vocabularies/applicator/items.ts @@ -0,0 +1,83 @@ +import {KeywordDefinition} from "../../types" +import {nonEmptySchema} from "../util" +import {applySubschema, Expr} from "../../compile/subschema" + +const def: KeywordDefinition = { + keyword: "items", + type: "array", + schemaType: ["object", "array", "boolean"], + code(cxt) { + const {gen, fail, schema, parentSchema, data, it} = cxt + let closeBlocks = "" + const errsCount = gen.name("_errs") + const len = gen.name("len") + gen.code( + `const ${errsCount} = errors; + const ${len} = ${data}.length;` + ) + + if (Array.isArray(schema)) { + const addIts = parentSchema.additionalItems + if (addIts === false) validateDataLength() + validateDefinedItems() + if (typeof addIts == "object" && nonEmptySchema(it, addIts)) { + gen.code(`if (${len} > ${schema.length}) {`) + validateItems("additionalItems", schema.length) + gen.code("}") + } + } else if (nonEmptySchema(it, schema)) { + validateItems("items", 0) + } + + if (!it.opts.allErrors) { + gen.code(closeBlocks) + gen.code(`if (${errsCount} === errors) {`) + } + + function validateDataLength(): void { + fail(`${len} > ${schema.length}`, { + ...cxt, + keyword: "additionalItems", + schemaValue: false, + }) + closeBlocks += "}" + } + + function validateDefinedItems(): void { + schema.forEach((sch: any, i: number) => { + if (nonEmptySchema(it, sch)) { + gen.code(`if (${len} > ${i}) {`) + const schValid = applySubschema(it, { + keyword: "items", + schemaProp: i, + dataProp: i, + expr: Expr.Const, + }) + gen.code(`}`) + if (!it.opts.allErrors) { + gen.code(`if (${schValid}) {`) + closeBlocks += "}" + } + } + }) + } + + function validateItems(keyword: string, startFrom: number): void { + const i = gen.name("i") + gen.code(`for (let ${i}=${startFrom}; ${i}<${len}; ${i}++) {`) + const schValid = applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}) + if (!it.opts.allErrors) { + gen.code(`if (!${schValid}) { + break; + }`) + } + gen.code("}") + } + }, + error: { + message: ({schema}) => `"should NOT have more than ${schema.length} items"`, + params: ({schema}) => `{limit: ${schema.length}}`, + }, +} + +module.exports = def diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 4509db5ae7..51140ce4c8 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -23,7 +23,7 @@ export function quotedString(str: string): string { export function dataNotType( schemaCode: string | number | boolean, - schemaType?: string, + schemaType: string, $data?: string | false ): string { return $data ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` : "" diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 94fb4ca305..67bb7d41a8 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -14,7 +14,7 @@ const def: KeywordDefinition = { schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode}) { - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + data + OPS[keyword].fail + schemaCode + ` || isNaN(${data})`) }, error: { diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 2e2ae3901f..ab49caa5e9 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -8,7 +8,7 @@ const def: KeywordDefinition = { $data: true, code({fail, keyword, data, $data, schemaCode}) { const op = keyword == "maxItems" ? ">" : "<" - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + `${data}.length` + op + schemaCode) }, error: { diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 1670c8049f..a10130588b 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -8,7 +8,7 @@ const def: KeywordDefinition = { $data: true, code({fail, keyword, data, $data, schemaCode, it: {opts}}) { const op = keyword == "maxLength" ? ">" : "<" - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) const len = opts.unicode === false ? `${data}.length` : `ucs2length(${data})` fail(dnt + len + op + schemaCode) }, diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index d992da81bd..97b61aaa1e 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -8,7 +8,7 @@ const def: KeywordDefinition = { $data: true, code({fail, keyword, data, $data, schemaCode}) { const op = keyword == "maxProperties" ? ">" : "<" - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + `Object.keys(${data}).length` + op + schemaCode) }, error: { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 09200caa07..3b6f038e52 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -7,7 +7,7 @@ const def: KeywordDefinition = { schemaType: "number", $data: true, code({gen, fail, data, $data, schemaCode, it: {opts}}) { - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) const res = gen.name("res") const prec = opts.multipleOfPrecision const invalid = prec diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index f4f1a546e5..76c8897268 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -7,7 +7,7 @@ const def: KeywordDefinition = { schemaType: "string", $data: true, code({fail, data, $data, schema, schemaCode, it: {usePattern}}) { - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const dnt = dataNotType(schemaCode, def.schemaType, $data) const regExp = $data ? `(new RegExp(${schemaCode}))` : usePattern(schema) fail(dnt + `!${regExp}.test(${data})`) }, diff --git a/spec/errors.spec.js b/spec/errors.spec.js index b4e7be4517..b9d389a00c 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -122,9 +122,7 @@ describe("Validation errors", () => { fullValidate.errors .filter((err) => err.keyword === "additionalProperties") .map((err) => - fullAjv._opts.jsonPointers - ? err.dataPath.substr(1) - : err.dataPath.slice(2, -2) + fullAjv._opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2, -2) ) .forEach((p) => delete invalidData[p]) @@ -165,46 +163,26 @@ describe("Validation errors", () => { var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError( - validate.errors[0], - "type", - schPath, - "['baz'].quux", - "should be string", - {type: "string"} - ) + shouldBeError(validate.errors[0], "type", schPath, "['baz'].quux", "should be string", { + type: "string", + }) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData) - shouldBeError( - validateJP.errors[0], - "type", - schPath, - "/baz/quux", - "should be string", - {type: "string"} - ) + shouldBeError(validateJP.errors[0], "type", schPath, "/baz/quux", "should be string", { + type: "string", + }) var fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData, 2) - shouldBeError( - fullValidate.errors[0], - "type", - schPath, - "/baz/quux", - "should be string", - {type: "string"} - ) - shouldBeError( - fullValidate.errors[1], - "type", - schPath, - "/boo/quux", - "should be string", - {type: "string"} - ) + shouldBeError(fullValidate.errors[0], "type", schPath, "/baz/quux", "should be string", { + type: "string", + }) + shouldBeError(fullValidate.errors[1], "type", schPath, "/boo/quux", "should be string", { + type: "string", + }) } }) @@ -263,73 +241,38 @@ describe("Validation errors", () => { var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) - shouldBeError( - validate.errors[0], - "required", - schPath, - path("['1']"), - msg("1"), - {missingProperty: "1"} - ) + shouldBeError(validate.errors[0], "required", schPath, path("['1']"), msg("1"), { + missingProperty: "1", + }) shouldBeInvalid(validate, invalidData2, 1 + extraErrors) - shouldBeError( - validate.errors[0], - "required", - schPath, - path("['2']"), - msg("2"), - {missingProperty: "2"} - ) + shouldBeError(validate.errors[0], "required", schPath, path("['2']"), msg("2"), { + missingProperty: "2", + }) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) - shouldBeError( - validateJP.errors[0], - "required", - schPath, - path("/1"), - msg("1"), - {missingProperty: "1"} - ) + shouldBeError(validateJP.errors[0], "required", schPath, path("/1"), msg("1"), { + missingProperty: "1", + }) shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) - shouldBeError( - validateJP.errors[0], - "required", - schPath, - path("/2"), - msg("2"), - {missingProperty: "2"} - ) + shouldBeError(validateJP.errors[0], "required", schPath, path("/2"), msg("2"), { + missingProperty: "2", + }) var fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) - shouldBeError( - fullValidate.errors[0], - "required", - schPath, - path("/1"), - msg("1"), - {missingProperty: "1"} - ) + shouldBeError(fullValidate.errors[0], "required", schPath, path("/1"), msg("1"), { + missingProperty: "1", + }) shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) - shouldBeError( - fullValidate.errors[0], - "required", - schPath, - path("/2"), - msg("2"), - {missingProperty: "2"} - ) - shouldBeError( - fullValidate.errors[1], - "required", - schPath, - path("/98"), - msg("98"), - {missingProperty: "98"} - ) + shouldBeError(fullValidate.errors[0], "required", schPath, path("/2"), msg("2"), { + missingProperty: "2", + }) + shouldBeError(fullValidate.errors[1], "required", schPath, path("/98"), msg("98"), { + missingProperty: "98", + }) } } @@ -429,8 +372,7 @@ describe("Validation errors", () => { invalidData2 = {a: 0, bar: 2} var path = pathFunc(errorDataPath) - var msg = - "should have properties foo, bar, baz when property a is present" + var msg = "should have properties foo, bar, baz when property a is present" var validate = ajv.compile(schema) shouldBeValid(validate, data) @@ -515,13 +457,7 @@ describe("Validation errors", () => { } }) - function _testRequired( - errorDataPath, - schema, - schemaPathPrefix, - prefix, - extraErrors - ) { + function _testRequired(errorDataPath, schema, schemaPathPrefix, prefix, extraErrors) { var schPath = (schemaPathPrefix || "#") + "/required" prefix = prefix || "" extraErrors = extraErrors || 0 @@ -536,73 +472,38 @@ describe("Validation errors", () => { var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) - shouldBeError( - validate.errors[0], - "required", - schPath, - path(".bar"), - msg(prefix + "bar"), - {missingProperty: prefix + "bar"} - ) + shouldBeError(validate.errors[0], "required", schPath, path(".bar"), msg(prefix + "bar"), { + missingProperty: prefix + "bar", + }) shouldBeInvalid(validate, invalidData2, 1 + extraErrors) - shouldBeError( - validate.errors[0], - "required", - schPath, - path(".foo"), - msg(prefix + "foo"), - {missingProperty: prefix + "foo"} - ) + shouldBeError(validate.errors[0], "required", schPath, path(".foo"), msg(prefix + "foo"), { + missingProperty: prefix + "foo", + }) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) - shouldBeError( - validateJP.errors[0], - "required", - schPath, - path("/bar"), - msg("bar"), - {missingProperty: "bar"} - ) + shouldBeError(validateJP.errors[0], "required", schPath, path("/bar"), msg("bar"), { + missingProperty: "bar", + }) shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) - shouldBeError( - validateJP.errors[0], - "required", - schPath, - path("/foo"), - msg("foo"), - {missingProperty: "foo"} - ) + shouldBeError(validateJP.errors[0], "required", schPath, path("/foo"), msg("foo"), { + missingProperty: "foo", + }) var fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) - shouldBeError( - fullValidate.errors[0], - "required", - schPath, - path("/bar"), - msg("bar"), - {missingProperty: "bar"} - ) + shouldBeError(fullValidate.errors[0], "required", schPath, path("/bar"), msg("bar"), { + missingProperty: "bar", + }) shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) - shouldBeError( - fullValidate.errors[0], - "required", - schPath, - path("/foo"), - msg("foo"), - {missingProperty: "foo"} - ) - shouldBeError( - fullValidate.errors[1], - "required", - schPath, - path("/baz"), - msg("baz"), - {missingProperty: "baz"} - ) + shouldBeError(fullValidate.errors[0], "required", schPath, path("/foo"), msg("foo"), { + missingProperty: "foo", + }) + shouldBeError(fullValidate.errors[1], "required", schPath, path("/baz"), msg("baz"), { + missingProperty: "baz", + }) } function pathFunc(errorDataPath) { @@ -642,66 +543,24 @@ describe("Validation errors", () => { var validate = ajv.compile(schema1) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError( - validate.errors[0], - "minimum", - "#/items/minimum", - "[0]", - "should be >= 10" - ) + shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "[0]", "should be >= 10") shouldBeInvalid(validate, invalidData2) - shouldBeError( - validate.errors[0], - "minimum", - "#/items/minimum", - "[1]", - "should be >= 10" - ) + shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "[1]", "should be >= 10") var validateJP = ajvJP.compile(schema1) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1) - shouldBeError( - validateJP.errors[0], - "minimum", - "#/items/minimum", - "/0", - "should be >= 10" - ) + shouldBeError(validateJP.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") shouldBeInvalid(validateJP, invalidData2) - shouldBeError( - validateJP.errors[0], - "minimum", - "#/items/minimum", - "/1", - "should be >= 10" - ) + shouldBeError(validateJP.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") var fullValidate = fullAjv.compile(schema1) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1) - shouldBeError( - fullValidate.errors[0], - "minimum", - "#/items/minimum", - "/0", - "should be >= 10" - ) + shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") shouldBeInvalid(fullValidate, invalidData2, 2) - shouldBeError( - fullValidate.errors[0], - "minimum", - "#/items/minimum", - "/1", - "should be >= 10" - ) - shouldBeError( - fullValidate.errors[1], - "minimum", - "#/items/minimum", - "/3", - "should be >= 10" - ) + shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") + shouldBeError(fullValidate.errors[1], "minimum", "#/items/minimum", "/3", "should be >= 10") var schema2 = { $id: "schema2", @@ -712,21 +571,9 @@ describe("Validation errors", () => { validate = ajv.compile(schema2) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError( - validate.errors[0], - "minimum", - "#/items/0/minimum", - "[0]", - "should be >= 10" - ) + shouldBeError(validate.errors[0], "minimum", "#/items/0/minimum", "[0]", "should be >= 10") shouldBeInvalid(validate, invalidData2) - shouldBeError( - validate.errors[0], - "minimum", - "#/items/2/minimum", - "[2]", - "should be >= 12" - ) + shouldBeError(validate.errors[0], "minimum", "#/items/2/minimum", "[2]", "should be >= 12") }) it("should have correct schema path for additionalItems", () => { @@ -1114,14 +961,9 @@ describe("Validation errors", () => { if (numErrors === 2) { err = validate.errors[1] - shouldBeError( - err, - "if", - "#/if", - "", - 'should match "' + ifClause + '" schema', - {failingKeyword: ifClause} - ) + shouldBeError(err, "if", "#/if", "", 'should match "' + ifClause + '" schema', { + failingKeyword: ifClause, + }) } } }) @@ -1154,13 +996,7 @@ describe("Validation errors", () => { function testTypeError(i, dataPath) { var err = validate.errors[i] - shouldBeError( - err, - "type", - "#/items/type", - dataPath, - "should be number" - ) + shouldBeError(err, "type", "#/items/type", dataPath, "should be number") } }) }) @@ -1181,12 +1017,7 @@ describe("Validation errors", () => { var validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError( - validate.errors[0], - "type", - schPath, - _ajv._opts.jsonPointers ? "/foo" : ".foo" - ) + shouldBeError(validate.errors[0], "type", schPath, _ajv._opts.jsonPointers ? "/foo" : ".foo") } function shouldBeValid(validate, data) { @@ -1199,14 +1030,7 @@ describe("Validation errors", () => { should.equal(validate.errors.length, numErrors || 1) } - function shouldBeError( - error, - keyword, - schemaPath, - dataPath, - message, - params - ) { + function shouldBeError(error, keyword, schemaPath, dataPath, message, params) { error.keyword.should.equal(keyword) error.schemaPath.should.equal(schemaPath) error.dataPath.should.equal(dataPath) From 67d2102072b5506272c3c58d4a17dbe287651dcf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 19 Aug 2020 08:33:28 -0400 Subject: [PATCH 058/322] refactor: "contains" to typescript --- lib/compile/rules.ts | 4 - lib/dot/contains.jst | 57 ------------- lib/dot/errors.def | 9 -- lib/dot/items.jst | 104 ------------------------ lib/dotjs/index.js | 2 - lib/vocabularies/applicator/contains.ts | 45 ++++++++++ lib/vocabularies/applicator/index.ts | 7 +- 7 files changed, 51 insertions(+), 177 deletions(-) delete mode 100644 lib/dot/contains.jst delete mode 100644 lib/dot/items.jst create mode 100644 lib/vocabularies/applicator/contains.ts diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 06df8f68ab..708b5f89a5 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -53,10 +53,6 @@ export default function rules(): ValidationRules { rules: [ {type: "number", rules: ["format"]}, {type: "string", rules: ["format"]}, - { - type: "array", - rules: ["contains"], - }, { type: "object", rules: [ diff --git a/lib/dot/contains.jst b/lib/dot/contains.jst deleted file mode 100644 index 94da7a2ef5..0000000000 --- a/lib/dot/contains.jst +++ /dev/null @@ -1,57 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - - -{{ - var $idx = 'i' + $lvl - , $dataNxt = $it.dataLevel = it.dataLevel + 1 - , $nextData = 'data' + $dataNxt - , $currentBaseId = it.baseId - , $nonEmptySchema = {{# def.nonEmptySchema:$schema }}; -}} - -var {{=$errs}} = errors; -var {{=$valid}}; - -{{? $nonEmptySchema }} - {{# def.setCompositeRule }} - - {{ - $it.schema = $schema; - $it.schemaPath = $schemaPath; - $it.errSchemaPath = $errSchemaPath; - }} - - var {{=$nextValid}} = false; - - for (var {{=$idx}} = 0; {{=$idx}} < {{=$data}}.length; {{=$idx}}++) { - {{ - $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); - var $passData = $data + '[' + $idx + ']'; - $it.dataPathArr[$dataNxt] = $idx; - }} - - {{# def.generateSubschemaCode }} - {{# def.optimizeValidate }} - - if ({{=$nextValid}}) break; - } - - {{# def.resetCompositeRule }} - {{= $closingBraces }} - - if (!{{=$nextValid}}) { -{{??}} - if ({{=$data}}.length == 0) { -{{?}} - - {{# def.error:'contains' }} - } else { - {{? $nonEmptySchema }} - {{# def.resetErrors }} - {{?}} - {{? it.opts.allErrors }} } {{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index c0a462e2ee..787bceb876 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -90,10 +90,7 @@ {{## def._errorMessages = { $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", - additionalItems: "'should NOT have more than {{=$schema.length}} items'", additionalProperties: "'{{? it.opts._errorDataPathProperty }}is an invalid additional property{{??}}should NOT have additional properties{{?}}'", - anyOf: "'should match some schema in anyOf'", - contains: "'should contain a valid item'", dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", @@ -114,10 +111,7 @@ {{## def._errorSchemas = { $ref: "{{=it.util.toQuotedString($schema)}}", - additionalItems: "false", additionalProperties: "false", - anyOf: "validate.schema{{=$schemaPath}}", - contains: "validate.schema{{=$schemaPath}}", dependencies: "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", 'if': "validate.schema{{=$schemaPath}}", @@ -137,10 +131,7 @@ {{## def._errorParams = { $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", - additionalItems: "{ limit: {{=$schema.length}} }", additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", - anyOf: "{}", - contains: "{}", dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{= it.util.escapeQuotes($deps.length==1 ? $deps[0] : $deps.join(\", \")) }}' }", format: "{ format: {{#def.schemaValueQS}} }", 'if': "{ failingKeyword: {{=$ifClause}} }", diff --git a/lib/dot/items.jst b/lib/dot/items.jst deleted file mode 100644 index c33a6f91cc..0000000000 --- a/lib/dot/items.jst +++ /dev/null @@ -1,104 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - - -{{## def.validateItems:startFrom: - for (var {{=$idx}} = {{=startFrom}}; {{=$idx}} < {{=$data}}.length; {{=$idx}}++) { - {{ - $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); - var $passData = $data + '[' + $idx + ']'; - $it.dataPathArr[$dataNxt] = $idx; - }} - - {{# def.generateSubschemaCode }} - {{# def.optimizeValidate }} - - {{? $breakOnError }} - if (!{{=$nextValid}}) break; - {{?}} - } -#}} - -{{ - var $idx = 'i' + $lvl - , $dataNxt = $it.dataLevel = it.dataLevel + 1 - , $nextData = 'data' + $dataNxt - , $currentBaseId = it.baseId; -}} - -var {{=$errs}} = errors; -var {{=$valid}}; - -{{? Array.isArray($schema) }} - {{ /* 'items' is an array of schemas */}} - {{ var $additionalItems = it.schema.additionalItems; }} - {{? $additionalItems === false }} - {{=$valid}} = {{=$data}}.length <= {{= $schema.length }}; - {{ - var $currErrSchemaPath = $errSchemaPath; - $errSchemaPath = it.errSchemaPath + '/additionalItems'; - }} - {{# def.checkError:'additionalItems' }} - {{ $errSchemaPath = $currErrSchemaPath; }} - {{# def.elseIfValid}} - {{?}} - - {{~ $schema:$sch:$i }} - {{? {{# def.nonEmptySchema:$sch }} }} - {{=$nextValid}} = true; - - if ({{=$data}}.length > {{=$i}}) { - {{ - var $passData = $data + '[' + $i + ']'; - $it.schema = $sch; - $it.schemaPath = $schemaPath + '[' + $i + ']'; - $it.errSchemaPath = $errSchemaPath + '/' + $i; - $it.errorPath = it.util.getPath(it.errorPath, $i, it.opts.jsonPointers); - $it.dataPathArr[$dataNxt] = $i; - }} - - {{# def.generateSubschemaCode }} - {{# def.optimizeValidate }} - } - - {{ /* def.ifResultValid */ }} - {{? $breakOnError }} - if ({{=$nextValid}}) { - {{ $closingBraces += '}'; }} - {{?}} - {{?}} - {{~}} - - {{? typeof $additionalItems == 'object' && {{# def.nonEmptySchema:$additionalItems }} }} - {{ - $it.schema = $additionalItems; - $it.schemaPath = it.schemaPath + '.additionalItems'; - $it.errSchemaPath = it.errSchemaPath + '/additionalItems'; - }} - {{=$nextValid}} = true; - - if ({{=$data}}.length > {{= $schema.length }}) { - {{# def.validateItems: $schema.length }} - } - - {{# def.ifResultValid }} - {{?}} - -{{?? {{# def.nonEmptySchema:$schema }} }} - {{ /* 'items' is a single schema */}} - {{ - $it.schema = $schema; - $it.schemaPath = $schemaPath; - $it.errSchemaPath = $errSchemaPath; - }} - {{# def.validateItems: 0 }} -{{?}} - -{{? $breakOnError }} - {{= $closingBraces }} - if ({{=$errs}} == errors) { -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 09f64b5f4b..172f845a0f 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -3,11 +3,9 @@ //all requires must be explicit because browserify won't work with dynamic requires module.exports = { $ref: require("./ref"), - contains: require("./contains"), dependencies: require("./dependencies"), format: require("./format"), if: require("./if"), - items: require("./items"), not: require("./not"), oneOf: require("./oneOf"), properties: require("./properties"), diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts new file mode 100644 index 0000000000..99c0ca1e9b --- /dev/null +++ b/lib/vocabularies/applicator/contains.ts @@ -0,0 +1,45 @@ +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {nonEmptySchema} from "../util" +import {applySubschema, Expr} from "../../compile/subschema" +import {reportError, resetErrorsCount} from "../../compile/errors" + +const def: KeywordDefinition = { + keyword: "contains", + type: "array", + schemaType: ["object", "boolean"], + code(cxt) { + const {gen, fail, schema, data, it} = cxt + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + + if (nonEmptySchema(it, schema)) { + const i = gen.name("i") + gen.code(`for (let ${i}=0; ${i}<${data}.length; ${i}++) {`) + const schValid = applySubschema(it, { + keyword: "contains", + dataProp: i, + expr: Expr.Num, + compositeRule: true, + }) + gen.code( + `if (${schValid}) break; + }` + ) + + // TODO refactor failCompoundOrReset? It is different from anyOf though + gen.code(`if (!${schValid}) {`) + reportError(cxt, def.error as KeywordErrorDefinition) + gen.code(`} else {`) + resetErrorsCount(gen, errsCount) + if (it.opts.allErrors) gen.code(`}`) + } else { + fail(`${data}.length === 0`) + } + }, + error: { + message: () => '"should contain a valid item"', + params: () => "{}", + }, +} + +module.exports = def diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 88647c1d0c..861564dd15 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -1,5 +1,10 @@ import {Vocabulary} from "../../types" -const applicator: Vocabulary = [require("./allOf"), require("./anyOf"), require("./items")] +const applicator: Vocabulary = [ + require("./allOf"), + require("./anyOf"), + require("./items"), + require("./contains"), +] module.exports = applicator From f27dd0f430eed8ab9336417988767f8365275a69 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 19 Aug 2020 11:34:01 -0400 Subject: [PATCH 059/322] feat: keyword error definition: params is optional, message can be a string --- lib/compile/errors.ts | 8 ++++++-- lib/types.ts | 4 ++-- lib/vocabularies/applicator/allOf.ts | 6 ------ lib/vocabularies/applicator/anyOf.ts | 5 +---- lib/vocabularies/applicator/contains.ts | 3 +-- lib/vocabularies/validation/const.ts | 3 +-- lib/vocabularies/validation/enum.ts | 3 +-- package.json | 2 +- 8 files changed, 13 insertions(+), 21 deletions(-) diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 5dab9c190f..3468f2cf03 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,5 +1,6 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {toQuotedString} from "./util" +import {quotedString} from "../vocabularies/util" import CodeGen from "./codegen" export function reportError( @@ -63,13 +64,16 @@ function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): st } = cxt if (createErrors === false) return "{}" if (!error) throw new Error('keyword definition must have "error" property') + const {params, message} = error // TODO trim whitespace let out = `{ keyword: "${keyword}", dataPath: (dataPath || "") + ${errorPath}, schemaPath: ${toQuotedString(errSchemaPath + "/" + keyword)}, - params: ${error.params(cxt)},` - if (opts.messages !== false) out += `message: ${error.message(cxt)},` + params: ${params ? params(cxt) : "{}"},` + if (opts.messages !== false) { + out += `message: ${typeof message == "string" ? quotedString(message) : message(cxt)},` + } if (opts.verbose) { // TODO trim whitespace out += ` diff --git a/lib/types.ts b/lib/types.ts index 9629353f59..fec3e29302 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -174,8 +174,8 @@ export interface KeywordDefinition { } export interface KeywordErrorDefinition { - message: (cxt: KeywordContext, params?: any) => string - params: (cxt: KeywordContext, params?: any) => string + message: string | ((cxt: KeywordContext) => string) + params?: (cxt: KeywordContext) => string } export type Vocabulary = KeywordDefinition[] diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 1e3cb9d540..d895ba656f 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -33,12 +33,6 @@ const def: KeywordDefinition = { // // fail(`!${valid}`) }, - error: { - // TODO allow message to be just a string if it is constant? - message: () => '"should match all schemas in allOf"', - // TODO make params optional if there are no params? - params: () => "{}", - }, } module.exports = def diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index e1bd034744..e55beca563 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -44,10 +44,7 @@ const def: KeywordDefinition = { if (it.opts.allErrors) gen.code(`}`) }, error: { - // TODO allow message to be just a string if it is constant? - message: () => '"should match some schema in anyOf"', - // TODO make params optional if there are no params? - params: () => "{}", + message: "should match some schema in anyOf", }, } diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 99c0ca1e9b..bab93522ae 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -37,8 +37,7 @@ const def: KeywordDefinition = { } }, error: { - message: () => '"should contain a valid item"', - params: () => "{}", + message: "should contain a valid item", }, } diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 6b967a9844..07dab289fe 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -5,8 +5,7 @@ const def: KeywordDefinition = { $data: true, code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), error: { - // TODO allow message to be just a string if it is constant? - message: () => '"should be equal to constant"', + message: "should be equal to constant", params: ({schemaCode}) => `{allowedValue: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 2c5c9848c9..5199282af8 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -58,8 +58,7 @@ const def: KeywordDefinition = { } }, error: { - // TODO allow message to be just a string if it is constant? - message: () => '"should be equal to one of the allowed values"', + message: "should be equal to one of the allowed values", params: ({schemaCode}) => `{allowedValues: ${schemaCode}}`, }, } diff --git a/package.json b/package.json index 7c5a01bb9b..952d1aaf47 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "prettier": "@ajv-validator/config/prettierrc.json", "husky": { "hooks": { - "pre-commit": "lint-staged" + "pre-commit": "lint-staged && npm test" } }, "lint-staged": { From 77b6d00df41a842d34c000c668e6e7ea65ee4308 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 19 Aug 2020 12:57:19 -0400 Subject: [PATCH 060/322] refactor: pass valid var name to applySubschema --- lib/compile/index.ts | 2 -- lib/compile/subschema.ts | 22 +++++++++++---------- lib/types.ts | 2 -- lib/vocabularies/applicator/allOf.ts | 3 ++- lib/vocabularies/applicator/anyOf.ts | 15 +++++++++----- lib/vocabularies/applicator/contains.ts | 21 ++++++++++++-------- lib/vocabularies/applicator/items.ts | 26 ++++++++++++++----------- 7 files changed, 52 insertions(+), 39 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 8250428146..9b3e42af5b 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -2,7 +2,6 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {MissingRefError} from "./error_classes" import validateCode from "./validate" -import {applySchema} from "./subschema" import {Rule} from "./rules" import {CompilationContext, KeywordDefinition, ErrorObject} from "../types" @@ -113,7 +112,6 @@ function compile(schema, root, localRefs, baseId) { MissingRefError, RULES, validateCode, - applySchema, // TODO remove to imports util, // TODO remove to imports resolve, // TODO remove to imports resolveRef, // TODO remove to imports diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 596a574bee..dac5dde6e4 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -12,13 +12,14 @@ export interface SubschemaContext { compositeRule?: true } -export function applySchema(it: CompilationContext, subschema: SubschemaContext): string { - const {gen, level} = it - const nextContext = {...it, ...subschema, level: level + 1} - const nextValid = gen.name("valid") - // TODO remove true once appendGen is removed - validateCode(nextContext, nextValid, true) - return nextValid +export function applySchema( + it: CompilationContext, + subschema: SubschemaContext, + valid: string +): void { + const nextContext = {...it, ...subschema, level: it.level + 1} + // TODO remove "true" once appendGen is removed + validateCode(nextContext, valid, true) } export enum Expr { @@ -37,8 +38,9 @@ interface SubschemaApplication { export function applySubschema( it: CompilationContext, - {keyword, schemaProp, compositeRule, dataProp, expr}: SubschemaApplication -): string { + {keyword, schemaProp, compositeRule, dataProp, expr}: SubschemaApplication, + valid: string +): void { const schema = it.schema[keyword] const subschema: SubschemaContext = schemaProp === undefined @@ -71,5 +73,5 @@ export function applySubschema( } if (compositeRule) subschema.compositeRule = compositeRule - return applySchema(it, subschema) + applySchema(it, subschema, valid) } diff --git a/lib/types.ts b/lib/types.ts index fec3e29302..038c05c640 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -2,7 +2,6 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" import {ValidationRules, Rule} from "./compile/rules" import {MissingRefError} from "./compile/error_classes" -import {SubschemaContext} from "./compile/subschema" export interface Options { $data?: boolean @@ -126,7 +125,6 @@ export interface CompilationContext { // } compositeRule?: boolean validateCode: (it: CompilationContext) => string | void // TODO remove string - applySchema: (it: CompilationContext, subSchema: SubschemaContext) => string usePattern: (str: string) => string useDefault: (value: any) => string useCustomRule: (rule: Rule, schema: any, parentSchema: object, it: CompilationContext) => any diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index d895ba656f..8ff3f709d8 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -9,10 +9,11 @@ const def: KeywordDefinition = { const {opts} = it let emptySchemas = true let closeBlocks = "" + const valid = gen.name("valid") schema.forEach((sch: object | boolean, i: number) => { if (nonEmptySchema(it, sch)) { emptySchemas = false - const valid = applySubschema(it, {keyword: "allOf", schemaProp: i}) + applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) if (!opts.allErrors) { gen.code(`if (${valid}) {`) closeBlocks += "}" diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index e55beca563..e1b4496d21 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -21,12 +21,17 @@ const def: KeywordDefinition = { ) let closeBlocks = "" + const schValid = gen.name("valid") schema.forEach((_, i: number) => { - const schValid = applySubschema(it, { - keyword: "anyOf", - schemaProp: i, - compositeRule: true, - }) + applySubschema( + it, + { + keyword: "anyOf", + schemaProp: i, + compositeRule: true, + }, + schValid + ) gen.code( `${valid} = ${valid} || ${schValid}; if (!${valid}) {` diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index bab93522ae..dd56d2f472 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -13,21 +13,26 @@ const def: KeywordDefinition = { gen.code(`const ${errsCount} = errors;`) if (nonEmptySchema(it, schema)) { + const valid = gen.name("valid") const i = gen.name("i") gen.code(`for (let ${i}=0; ${i}<${data}.length; ${i}++) {`) - const schValid = applySubschema(it, { - keyword: "contains", - dataProp: i, - expr: Expr.Num, - compositeRule: true, - }) + applySubschema( + it, + { + keyword: "contains", + dataProp: i, + expr: Expr.Num, + compositeRule: true, + }, + valid + ) gen.code( - `if (${schValid}) break; + `if (${valid}) break; }` ) // TODO refactor failCompoundOrReset? It is different from anyOf though - gen.code(`if (!${schValid}) {`) + gen.code(`if (!${valid}) {`) reportError(cxt, def.error as KeywordErrorDefinition) gen.code(`} else {`) resetErrorsCount(gen, errsCount) diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index ca9fd24190..b08bc0ebad 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -44,18 +44,23 @@ const def: KeywordDefinition = { } function validateDefinedItems(): void { + const valid = gen.name("valid") schema.forEach((sch: any, i: number) => { if (nonEmptySchema(it, sch)) { gen.code(`if (${len} > ${i}) {`) - const schValid = applySubschema(it, { - keyword: "items", - schemaProp: i, - dataProp: i, - expr: Expr.Const, - }) + applySubschema( + it, + { + keyword: "items", + schemaProp: i, + dataProp: i, + expr: Expr.Const, + }, + valid + ) gen.code(`}`) if (!it.opts.allErrors) { - gen.code(`if (${schValid}) {`) + gen.code(`if (${valid}) {`) closeBlocks += "}" } } @@ -65,11 +70,10 @@ const def: KeywordDefinition = { function validateItems(keyword: string, startFrom: number): void { const i = gen.name("i") gen.code(`for (let ${i}=${startFrom}; ${i}<${len}; ${i}++) {`) - const schValid = applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}) + const valid = gen.name("valid") + applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) if (!it.opts.allErrors) { - gen.code(`if (!${schValid}) { - break; - }`) + gen.code(`if (!${valid}) break;`) } gen.code("}") } From dd763075be87a008392a2e07c0acb41d055c381e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 19 Aug 2020 16:27:36 -0400 Subject: [PATCH 061/322] refactor(codegen): track blocks of ifs --- lib/compile/codegen.ts | 78 ++++++++++++++++++++++ lib/keyword.ts | 10 +++ lib/vocabularies/applicator/allOf.ts | 15 ++--- lib/vocabularies/applicator/anyOf.ts | 14 ++-- lib/vocabularies/applicator/contains.ts | 9 ++- lib/vocabularies/applicator/items.ts | 74 ++++++++++---------- lib/vocabularies/validation/enum.ts | 17 ++--- lib/vocabularies/validation/uniqueItems.ts | 39 ++++++----- 8 files changed, 175 insertions(+), 81 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index bcd00f2573..45ef86174f 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -1,7 +1,15 @@ +enum Block { + If, + Else, + For, +} + export default class CodeGen { #names: {[key: string]: number} = {} // TODO make private. Possibly stack? _out = "" + #blocks: Block[] = [] + #blockStarts: number[] = [] name(prefix: string): string { if (!this.#names[prefix]) this.#names[prefix] = 0 @@ -14,4 +22,74 @@ export default class CodeGen { if (str) this._out += str + "\n" return this } + + if(condition: string): CodeGen { + this.#blocks.push(Block.If) + this.code(`if(${condition}){`) + return this + } + + elseIf(condition: string): CodeGen { + if (this._block !== Block.If) throw new Error('CodeGen: "else if" without "if"') + this.code(`}else if(${condition}){`) + return this + } + + else(): CodeGen { + if (this._block !== Block.If) throw new Error('CodeGen: "else" without "if"') + this._block = Block.Else + this.code(`}else{`) + return this + } + + endIf(): CodeGen { + const b = this._block + if (b !== Block.If && b !== Block.Else) throw new Error('CodeGen: "endIf" without "if"') + this.#blocks.pop() + this.code(`}`) + return this + } + + for(iteration: string): CodeGen { + this.#blocks.push(Block.For) + this.code(`for(${iteration}){`) + return this + } + + endFor(): CodeGen { + const b = this._block + if (b !== Block.For) throw new Error('CodeGen: "endFor" without "for"') + this.#blocks.pop() + this.code(`}`) + return this + } + + startBlock(): CodeGen { + this.#blockStarts.push(this.#blocks.length) + return this + } + + endBlock(expectedToClose?: number): CodeGen { + const len = this.#blockStarts.pop() + if (len === undefined) throw new Error("CodeGen: not in block sequence") + const toClose = this.#blocks.length - len + if (toClose < 0 || (expectedToClose !== undefined && toClose !== expectedToClose)) { + throw new Error("CodeGen: block sequence already ended or incorrect number of blocks") + } + this.#blocks.length = len + if (toClose > 0) this.code("}".repeat(toClose)) + return this + } + + get _block(): Block { + const len = this.#blocks.length + if (len === 0) throw new Error("CodeGen: not in block") + return this.#blocks[len - 1] + } + + set _block(b: Block) { + const len = this.#blocks.length + if (len === 0) throw new Error('CodeGen: not in "if" block') + this.#blocks[len - 1] = b + } } diff --git a/lib/keyword.ts b/lib/keyword.ts index 260834f204..8d63a5e248 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -155,6 +155,7 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void // TODO check that code called "fail" or another valid way to return code code(cxt) + // TODO replace with fail_ below function fail(condition: string, context?: KeywordContext): void { gen.code(`if (${condition}) {`) reportError(context || cxt, error as KeywordErrorDefinition) @@ -171,6 +172,15 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void } } +// TODO remove when "fail" replaced +export function fail_(condition: string, cxt: KeywordContext, error: KeywordErrorDefinition): void { + const {gen, opts} = cxt.it + gen.if(condition) + reportError(cxt, error) + if (opts.allErrors) gen.endIf() + else gen.else() +} + function validSchemaType(schema: any, schemaType: string | string[]): boolean { // TODO add tests if (Array.isArray(schemaType)) { diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 8ff3f709d8..9d0bc0ef2d 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -8,31 +8,26 @@ const def: KeywordDefinition = { code({gen, ok, schema, it}) { const {opts} = it let emptySchemas = true - let closeBlocks = "" const valid = gen.name("valid") + let count = 0 schema.forEach((sch: object | boolean, i: number) => { if (nonEmptySchema(it, sch)) { emptySchemas = false applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) if (!opts.allErrors) { - gen.code(`if (${valid}) {`) - closeBlocks += "}" + if (count === 1) gen.startBlock() + count++ + gen.if(`${valid}`) } } }) if (!opts.allErrors) { if (emptySchemas) ok() - else gen.code(closeBlocks.slice(0, -1)) // TODO refactor + else if (count > 1) gen.endBlock(count - 1) } // TODO possibly add allOf error - // const valid = gen.name("valid") - // gen.code(`let ${valid} = true;`) - // ... in the loop: - // gen.code(`${valid} = ${valid} && ${schValid};`) - // - // fail(`!${valid}`) }, } diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index e1b4496d21..02deda7b03 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -14,14 +14,14 @@ const def: KeywordDefinition = { return } const valid = gen.name("valid") + const schValid = gen.name("_valid") const errsCount = gen.name("_errs") gen.code( `let ${valid} = false; const ${errsCount} = errors;` ) - let closeBlocks = "" - const schValid = gen.name("valid") + gen.startBlock() schema.forEach((_, i: number) => { applySubschema( it, @@ -32,16 +32,14 @@ const def: KeywordDefinition = { }, schValid ) - gen.code( - `${valid} = ${valid} || ${schValid}; - if (!${valid}) {` - ) - closeBlocks += "}" + gen.code(`${valid} = ${valid} || ${schValid};`) + gen.if(`!${valid}`) }) - gen.code(closeBlocks) + gen.endBlock(schema.length) // TODO refactor failCompoundOrReset? + // TODO refactor ifs gen.code(`if (!${valid}) {`) reportExtraError(cxt, def.error as KeywordErrorDefinition) gen.code(`} else {`) diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index dd56d2f472..701b098a78 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -15,7 +15,7 @@ const def: KeywordDefinition = { if (nonEmptySchema(it, schema)) { const valid = gen.name("valid") const i = gen.name("i") - gen.code(`for (let ${i}=0; ${i}<${data}.length; ${i}++) {`) + gen.for(`let ${i}=0; ${i}<${data}.length; ${i}++`) applySubschema( it, { @@ -26,12 +26,11 @@ const def: KeywordDefinition = { }, valid ) - gen.code( - `if (${valid}) break; - }` - ) + gen.code(`if (${valid}) break;`) + gen.endFor() // TODO refactor failCompoundOrReset? It is different from anyOf though + // TODO refactor ifs gen.code(`if (!${valid}) {`) reportError(cxt, def.error as KeywordErrorDefinition) gen.code(`} else {`) diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index b08bc0ebad..8ed76c4e65 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,14 +1,14 @@ -import {KeywordDefinition} from "../../types" +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" import {nonEmptySchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" +import {fail_} from "../../keyword" const def: KeywordDefinition = { keyword: "items", type: "array", schemaType: ["object", "array", "boolean"], code(cxt) { - const {gen, fail, schema, parentSchema, data, it} = cxt - let closeBlocks = "" + const {gen, /* fail, */ schema, parentSchema, data, it} = cxt const errsCount = gen.name("_errs") const len = gen.name("len") gen.code( @@ -16,38 +16,49 @@ const def: KeywordDefinition = { const ${len} = ${data}.length;` ) - if (Array.isArray(schema)) { - const addIts = parentSchema.additionalItems - if (addIts === false) validateDataLength() - validateDefinedItems() - if (typeof addIts == "object" && nonEmptySchema(it, addIts)) { - gen.code(`if (${len} > ${schema.length}) {`) - validateItems("additionalItems", schema.length) - gen.code("}") - } - } else if (nonEmptySchema(it, schema)) { - validateItems("items", 0) + if (it.opts.allErrors) { + validateItemsKeyword() + } else { + gen.startBlock() + validateItemsKeyword() + gen.endBlock() + // TODO refactor ifs + gen.code(`if (${errsCount} === errors) {`) } - if (!it.opts.allErrors) { - gen.code(closeBlocks) - gen.code(`if (${errsCount} === errors) {`) + function validateItemsKeyword(): void { + if (Array.isArray(schema)) { + const addIts = parentSchema.additionalItems + if (addIts === false) validateDataLength() + validateDefinedItems() + if (typeof addIts == "object" && nonEmptySchema(it, addIts)) { + gen.if(`${len} > ${schema.length}`) + validateItems("additionalItems", schema.length) + gen.endIf() + } + } else if (nonEmptySchema(it, schema)) { + validateItems("items", 0) + } } function validateDataLength(): void { - fail(`${len} > ${schema.length}`, { - ...cxt, - keyword: "additionalItems", - schemaValue: false, - }) - closeBlocks += "}" + // TODO replace with "fail" + fail_( + `${len} > ${schema.length}`, + { + ...cxt, + keyword: "additionalItems", + schemaValue: false, + }, + def.error as KeywordErrorDefinition + ) } function validateDefinedItems(): void { const valid = gen.name("valid") schema.forEach((sch: any, i: number) => { if (nonEmptySchema(it, sch)) { - gen.code(`if (${len} > ${i}) {`) + gen.if(`${len} > ${i}`) applySubschema( it, { @@ -58,24 +69,19 @@ const def: KeywordDefinition = { }, valid ) - gen.code(`}`) - if (!it.opts.allErrors) { - gen.code(`if (${valid}) {`) - closeBlocks += "}" - } + gen.endIf() + if (!it.opts.allErrors) gen.if(valid) } }) } function validateItems(keyword: string, startFrom: number): void { const i = gen.name("i") - gen.code(`for (let ${i}=${startFrom}; ${i}<${len}; ${i}++) {`) const valid = gen.name("valid") + gen.for(`let ${i}=${startFrom}; ${i}<${len}; ${i}++`) applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) - if (!it.opts.allErrors) { - gen.code(`if (!${valid}) break;`) - } - gen.code("}") + if (!it.opts.allErrors) gen.code(`if(!${valid}){break}`) + gen.endFor() } }, error: { diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 5199282af8..5ea319ffa2 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -8,15 +8,16 @@ const def: KeywordDefinition = { code({gen, fail, data, $data, schema, schemaCode, it: {opts}}) { if ($data) { const valid = gen.name("valid") - gen.code( - `let ${valid}; - if (${schemaCode} === undefined) ${valid} = true; - else { - ${valid} = false; - if (Array.isArray(${schemaCode})) {` - ) + gen + .startBlock() + .code(`let ${valid};`) + .if(`${schemaCode} === undefined`) + .code(`${valid} = true;`) + .else() + .code(`${valid} = false;`) + .if(`Array.isArray(${schemaCode})`) loopEnum(schemaCode, valid) - gen.code("}}") + gen.endBlock(2) fail(`!${valid}`) } else { if (schema.length === 0) throw new Error("enum must have non-empty array") diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index c7ed32c814..86c675ac3b 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -10,28 +10,35 @@ const def: KeywordDefinition = { if (opts.uniqueItems === false || !($data || schema)) return ok() const i = gen.name("i") const j = gen.name("j") - errorParams({i, j}) const valid = gen.name("valid") + errorParams({i, j}) gen.code(`let ${valid}, ${i}, ${j};`) + const itemType = parentSchema.items?.type + if ($data) { + gen + .if(`${schemaCode} === false || ${schemaCode} === undefined`) + .code(`${valid} = true;`) + .elseIf(`typeof ${schemaCode} != "boolean"`) + .code(`${valid} = false;`) + .else() + validateUniqueItems() + gen.endIf() + } else { + validateUniqueItems() + } + + fail(`!${valid}`) + + function validateUniqueItems() { gen.code( - `if (${schemaCode} === false || ${schemaCode} === undefined) - ${valid} = true; - else if (typeof ${schemaCode} != "boolean") - ${valid} = false; - else {` + `${i} = ${data}.length; + ${valid} = true; + if (${i} > 1) { + ${canOptimize() ? loopN() : loopN2()} + }` ) } - const itemType = parentSchema.items?.type - gen.code( - `${i} = ${data}.length; - ${valid} = true; - if (${i} > 1) { - ${canOptimize() ? loopN() : loopN2()} - }` - ) - if ($data) gen.code("}") - fail(`!${valid}`) function canOptimize(): boolean { return Array.isArray(itemType) From fbe129cdb3d1c637e9165f84c96ce2570de75fa3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 19 Aug 2020 17:21:07 -0400 Subject: [PATCH 062/322] refactor: "not" to typescript, add allErrors to compilation context --- lib/compile/errors.ts | 10 +++--- lib/compile/index.ts | 1 + lib/compile/rules.ts | 2 +- lib/compile/subschema.ts | 23 +++++-------- lib/compile/validate/boolSchema.ts | 4 +-- lib/compile/validate/keywords.ts | 6 ++-- lib/dot/definitions.def | 2 +- lib/dot/errors.def | 3 -- lib/dot/not.jst | 45 ------------------------- lib/dot/oneOf.jst | 2 +- lib/dotjs/index.js | 1 - lib/keyword.ts | 25 +++++++++----- lib/types.ts | 3 +- lib/vocabularies/applicator/allOf.ts | 5 ++- lib/vocabularies/applicator/anyOf.ts | 2 +- lib/vocabularies/applicator/contains.ts | 2 +- lib/vocabularies/applicator/index.ts | 9 +++-- lib/vocabularies/applicator/items.ts | 6 ++-- lib/vocabularies/applicator/not.ts | 42 +++++++++++++++++++++++ 19 files changed, 99 insertions(+), 94 deletions(-) delete mode 100644 lib/dot/not.jst create mode 100644 lib/vocabularies/applicator/not.ts diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 3468f2cf03..023c27ca1c 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -6,11 +6,11 @@ import CodeGen from "./codegen" export function reportError( cxt: KeywordContext, error: KeywordErrorDefinition, - allErrors?: boolean + overrideAllErrors?: boolean ): void { - const {gen, compositeRule, opts, async} = cxt.it + const {gen, compositeRule, allErrors, async} = cxt.it const errObj = errorObjectCode(cxt, error) - if (allErrors ?? (compositeRule || opts.allErrors)) { + if (overrideAllErrors ?? (compositeRule || allErrors)) { addError(gen, errObj) } else { returnErrors(gen, async, `[${errObj}]`) @@ -18,10 +18,10 @@ export function reportError( } export function reportExtraError(cxt: KeywordContext, error: KeywordErrorDefinition): void { - const {gen, compositeRule, opts, async} = cxt.it + const {gen, compositeRule, allErrors, async} = cxt.it const errObj = errorObjectCode(cxt, error) addError(gen, errObj) - if (!(compositeRule || opts.allErrors)) { + if (!(compositeRule || allErrors)) { returnErrors(gen, async, "vErrors") } } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 9b3e42af5b..05c5c53a0e 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -95,6 +95,7 @@ function compile(schema, root, localRefs, baseId) { // TODO refactor to extract code from gen let sourceCode = validateCode({ + allErrors: !!opts.allErrors, isTop: true, async: _schema.$async === true, schema: _schema, diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 708b5f89a5..571ace5be6 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -62,7 +62,7 @@ export default function rules(): ValidationRules { {properties: ["additionalProperties", "patternProperties"]}, ], }, - {rules: ["$ref", "not", "oneOf", "if"]}, + {rules: ["$ref", "oneOf", "if"]}, ], all: toHash(ALL), keywords: {}, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index dac5dde6e4..489dd80c7a 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -12,16 +12,6 @@ export interface SubschemaContext { compositeRule?: true } -export function applySchema( - it: CompilationContext, - subschema: SubschemaContext, - valid: string -): void { - const nextContext = {...it, ...subschema, level: it.level + 1} - // TODO remove "true" once appendGen is removed - validateCode(nextContext, valid, true) -} - export enum Expr { Const, Num, @@ -31,14 +21,16 @@ export enum Expr { interface SubschemaApplication { keyword: string schemaProp?: string | number - compositeRule?: true dataProp?: string | number expr?: Expr + compositeRule?: true + createErrors?: boolean + allErrors?: boolean } export function applySubschema( it: CompilationContext, - {keyword, schemaProp, compositeRule, dataProp, expr}: SubschemaApplication, + {keyword, schemaProp, dataProp, expr, ...rest}: SubschemaApplication, valid: string ): void { const schema = it.schema[keyword] @@ -72,6 +64,9 @@ export function applySubschema( gen.code(`var data${nextLevel} = data${dataLevel || ""}${passDataProp};`) } - if (compositeRule) subschema.compositeRule = compositeRule - applySchema(it, subschema, valid) + Object.assign(subschema, rest) + + const nextContext = {...it, ...subschema, level: it.level + 1} + // TODO remove "true" once appendGen is removed + validateCode(nextContext, valid, true) } diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index f42d85c28a..5f76ca9bc4 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -30,7 +30,7 @@ export function booleanOrEmptySchema(it: CompilationContext, valid: string): voi } } -function falseSchemaError(it: CompilationContext, allErrors?: boolean) { +function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { const {gen, dataLevel} = it // TODO maybe some other interface should be used for non-keyword validation errors... const cxt: KeywordContext = { @@ -46,7 +46,7 @@ function falseSchemaError(it: CompilationContext, allErrors?: boolean) { parentSchema: false, it, } - reportError(cxt, boolError, allErrors) + reportError(cxt, boolError, overrideAllErrors) } // TODO combine with exception from dataType diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index eb519f7793..4c3859c43d 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -17,7 +17,8 @@ export function schemaKeywords( level, dataLevel, RULES, - opts: {allErrors, extendRefs, strictNumbers}, + allErrors, + opts: {extendRefs, strictNumbers}, } = it if (schema.$ref && !(extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref"))) { // TODO remove Rule type cast @@ -55,7 +56,8 @@ function iterateKeywords(it: CompilationContext, group: RuleGroup) { const { gen, schema, - opts: {allErrors, useDefaults}, + allErrors, + opts: {useDefaults}, } = it if (useDefaults) assignDefaults(it, group.type) let closeBlocks = "" diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index afd35b3969..3bbb994644 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -5,7 +5,7 @@ var $schema = it.schema[$keyword]; var $schemaPath = it.schemaPath + it.util.getProperty($keyword); var $errSchemaPath = it.errSchemaPath + '/' + $keyword; - var $breakOnError = !it.opts.allErrors; + var $breakOnError = !it.allErrors; var $errorKeyword; var $data = 'data' + ($dataLvl || ''); diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 787bceb876..57e36ea40d 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -94,7 +94,6 @@ dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", - not: "'should NOT be valid'", oneOf: "'should match exactly one schema in oneOf'", propertyNames: "'property name \\'{{=$invalidName}}\\' is invalid'", required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", @@ -115,7 +114,6 @@ dependencies: "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", 'if': "validate.schema{{=$schemaPath}}", - not: "validate.schema{{=$schemaPath}}", oneOf: "validate.schema{{=$schemaPath}}", propertyNames: "validate.schema{{=$schemaPath}}", required: "validate.schema{{=$schemaPath}}", @@ -135,7 +133,6 @@ dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{= it.util.escapeQuotes($deps.length==1 ? $deps[0] : $deps.join(\", \")) }}' }", format: "{ format: {{#def.schemaValueQS}} }", 'if': "{ failingKeyword: {{=$ifClause}} }", - not: "{}", oneOf: "{ passingSchemas: {{=$passingSchemas}} }", propertyNames: "{ propertyName: '{{=$invalidName}}' }", required: "{ missingProperty: '{{=$missingProperty}}' }", diff --git a/lib/dot/not.jst b/lib/dot/not.jst deleted file mode 100644 index 421e502a08..0000000000 --- a/lib/dot/not.jst +++ /dev/null @@ -1,45 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - -{{? {{# def.nonEmptySchema:$schema }} }} - {{ - $it.schema = $schema; - $it.schemaPath = $schemaPath; - $it.errSchemaPath = $errSchemaPath; - }} - - var {{=$errs}} = errors; - - {{# def.setCompositeRule }} - - {{ - $it.createErrors = false; - var $allErrorsOption; - if ($it.opts.allErrors) { - $allErrorsOption = $it.opts.allErrors; - $it.opts.allErrors = false; - } - }} - {{= it.validateCode($it) }} - {{ - $it.createErrors = true; - if ($allErrorsOption) $it.opts.allErrors = $allErrorsOption; - }} - - {{# def.resetCompositeRule }} - - if ({{=$nextValid}}) { - {{# def.error:'not' }} - } else { - {{# def.resetErrors }} - {{? it.opts.allErrors }} } {{?}} -{{??}} - {{# def.addError:'not' }} - {{? $breakOnError}} - if (false) { - {{?}} -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dot/oneOf.jst b/lib/dot/oneOf.jst index 86094a0005..1fa34ad1ef 100644 --- a/lib/dot/oneOf.jst +++ b/lib/dot/oneOf.jst @@ -51,6 +51,6 @@ if (!{{=$valid}}) { {{# def.extraError:'oneOf' }} } else { {{# def.resetErrors }} -{{? it.opts.allErrors }} } {{?}} +{{? it.allErrors }} } {{?}} {{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 172f845a0f..583a22db12 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -6,7 +6,6 @@ module.exports = { dependencies: require("./dependencies"), format: require("./format"), if: require("./if"), - not: require("./not"), oneOf: require("./oneOf"), properties: require("./properties"), propertyNames: require("./propertyNames"), diff --git a/lib/keyword.ts b/lib/keyword.ts index 8d63a5e248..54cba372f2 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -128,7 +128,7 @@ export function addKeyword( function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void { const schema = it.schema[keyword] const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition - const {gen, opts, dataLevel, schemaPath, dataPathArr} = it + const {gen, opts, dataLevel, schemaPath, dataPathArr, allErrors} = it if (!code) throw new Error('"code" and "error" must be defined') const $data = $defData && opts.$data && schema && schema.$data const data = "data" + (dataLevel || "") @@ -156,15 +156,24 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void code(cxt) // TODO replace with fail_ below - function fail(condition: string, context?: KeywordContext): void { - gen.code(`if (${condition}) {`) - reportError(context || cxt, error as KeywordErrorDefinition) - gen.code(opts.allErrors ? "}" : "} else {") + function fail(condition?: string, context?: KeywordContext): void { + if (condition) { + gen.code(`if (${condition}) {`) + _reportError() + gen.code(allErrors ? "}" : "} else {") + } else { + _reportError() + if (!allErrors) gen.code("if (false) {") + } + + function _reportError() { + reportError(context || cxt, error as KeywordErrorDefinition) + } } function ok(condition?: string): void { if (condition) fail(`!(${condition})`) - else if (!opts.allErrors) gen.code("if (true) {") + else if (!allErrors) gen.code("if (true) {") } function errorParams(obj: any) { @@ -174,10 +183,10 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void // TODO remove when "fail" replaced export function fail_(condition: string, cxt: KeywordContext, error: KeywordErrorDefinition): void { - const {gen, opts} = cxt.it + const {gen, allErrors} = cxt.it gen.if(condition) reportError(cxt, error) - if (opts.allErrors) gen.endIf() + if (allErrors) gen.endIf() else gen.else() } diff --git a/lib/types.ts b/lib/types.ts index 038c05c640..9f106f3f09 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -103,6 +103,7 @@ export interface ErrorObject { } export interface CompilationContext { + allErrors: boolean level: number dataLevel: number data: string @@ -180,7 +181,7 @@ export type Vocabulary = KeywordDefinition[] export interface KeywordContext { gen: CodeGen - fail: (condition: string, context?: KeywordContext) => void + fail: (condition?: string, context?: KeywordContext) => void ok: (condition?: string) => void errorParams: (obj: any) => void keyword: string diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 9d0bc0ef2d..64255037cd 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -6,7 +6,6 @@ const def: KeywordDefinition = { keyword: "allOf", schemaType: "array", code({gen, ok, schema, it}) { - const {opts} = it let emptySchemas = true const valid = gen.name("valid") let count = 0 @@ -14,7 +13,7 @@ const def: KeywordDefinition = { if (nonEmptySchema(it, sch)) { emptySchemas = false applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) - if (!opts.allErrors) { + if (!it.allErrors) { if (count === 1) gen.startBlock() count++ gen.if(`${valid}`) @@ -22,7 +21,7 @@ const def: KeywordDefinition = { } }) - if (!opts.allErrors) { + if (!it.allErrors) { if (emptySchemas) ok() else if (count > 1) gen.endBlock(count - 1) } diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 02deda7b03..432413a439 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -44,7 +44,7 @@ const def: KeywordDefinition = { reportExtraError(cxt, def.error as KeywordErrorDefinition) gen.code(`} else {`) resetErrorsCount(gen, errsCount) - if (it.opts.allErrors) gen.code(`}`) + if (it.allErrors) gen.code(`}`) }, error: { message: "should match some schema in anyOf", diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 701b098a78..1f088c71dc 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -35,7 +35,7 @@ const def: KeywordDefinition = { reportError(cxt, def.error as KeywordErrorDefinition) gen.code(`} else {`) resetErrorsCount(gen, errsCount) - if (it.opts.allErrors) gen.code(`}`) + if (it.allErrors) gen.code(`}`) } else { fail(`${data}.length === 0`) } diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 861564dd15..85e5a71ac4 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -1,10 +1,15 @@ import {Vocabulary} from "../../types" const applicator: Vocabulary = [ - require("./allOf"), - require("./anyOf"), + // array require("./items"), require("./contains"), + // any + require("./not"), + require("./anyOf"), + // TODO require("./oneOf"), + require("./allOf"), + // TODO require("./if"), ] module.exports = applicator diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 8ed76c4e65..f15b7ac165 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -16,7 +16,7 @@ const def: KeywordDefinition = { const ${len} = ${data}.length;` ) - if (it.opts.allErrors) { + if (it.allErrors) { validateItemsKeyword() } else { gen.startBlock() @@ -70,7 +70,7 @@ const def: KeywordDefinition = { valid ) gen.endIf() - if (!it.opts.allErrors) gen.if(valid) + if (!it.allErrors) gen.if(valid) } }) } @@ -80,7 +80,7 @@ const def: KeywordDefinition = { const valid = gen.name("valid") gen.for(`let ${i}=${startFrom}; ${i}<${len}; ${i}++`) applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) - if (!it.opts.allErrors) gen.code(`if(!${valid}){break}`) + if (!it.allErrors) gen.code(`if(!${valid}){break}`) gen.endFor() } }, diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts new file mode 100644 index 0000000000..557f5ed254 --- /dev/null +++ b/lib/vocabularies/applicator/not.ts @@ -0,0 +1,42 @@ +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {nonEmptySchema} from "../util" +import {applySubschema} from "../../compile/subschema" +import {reportError, resetErrorsCount} from "../../compile/errors" + +const def: KeywordDefinition = { + keyword: "not", + schemaType: ["object", "boolean"], + code(cxt) { + const {gen, fail, schema, it} = cxt + if (nonEmptySchema(it, schema)) { + const valid = gen.name("valid") + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + applySubschema( + it, + { + keyword: "not", + compositeRule: true, + createErrors: false, + allErrors: false, + }, + valid + ) + + // TODO refactor failCompoundOrReset? + // TODO refactor ifs + gen.code(`if (${valid}) {`) + reportError(cxt, def.error as KeywordErrorDefinition) + gen.code(`} else {`) + resetErrorsCount(gen, errsCount) + if (it.allErrors) gen.code(`}`) + } else { + fail() + } + }, + error: { + message: "should NOT be valid", + }, +} + +module.exports = def From c3b9fb9b8b304885f8dbc47d993f61992fb73426 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 04:34:04 -0400 Subject: [PATCH 063/322] refactor: "oneOf" to typescript --- lib/compile/rules.ts | 2 +- lib/dot/oneOf.jst | 56 --------------------- lib/dotjs/index.js | 1 - lib/vocabularies/applicator/index.ts | 2 +- lib/vocabularies/applicator/oneOf.ts | 75 ++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 59 deletions(-) delete mode 100644 lib/dot/oneOf.jst create mode 100644 lib/vocabularies/applicator/oneOf.ts diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 571ace5be6..b1013e4464 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -62,7 +62,7 @@ export default function rules(): ValidationRules { {properties: ["additionalProperties", "patternProperties"]}, ], }, - {rules: ["$ref", "oneOf", "if"]}, + {rules: ["$ref", "if"]}, ], all: toHash(ALL), keywords: {}, diff --git a/lib/dot/oneOf.jst b/lib/dot/oneOf.jst deleted file mode 100644 index 1fa34ad1ef..0000000000 --- a/lib/dot/oneOf.jst +++ /dev/null @@ -1,56 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - -{{ - var $currentBaseId = $it.baseId - , $prevValid = 'prevValid' + $lvl - , $passingSchemas = 'passingSchemas' + $lvl; -}} - -var {{=$errs}} = errors - , {{=$prevValid}} = false - , {{=$valid}} = false - , {{=$passingSchemas}} = null; - -{{# def.setCompositeRule }} - -{{~ $schema:$sch:$i }} - {{? {{# def.nonEmptySchema:$sch }} }} - {{ - $it.schema = $sch; - $it.schemaPath = $schemaPath + '[' + $i + ']'; - $it.errSchemaPath = $errSchemaPath + '/' + $i; - }} - - {{# def.insertSubschemaCode }} - {{??}} - var {{=$nextValid}} = true; - {{?}} - - {{? $i }} - if ({{=$nextValid}} && {{=$prevValid}}) { - {{=$valid}} = false; - {{=$passingSchemas}} = [{{=$passingSchemas}}, {{=$i}}]; - } else { - {{ $closingBraces += '}'; }} - {{?}} - - if ({{=$nextValid}}) { - {{=$valid}} = {{=$prevValid}} = true; - {{=$passingSchemas}} = {{=$i}}; - } -{{~}} - -{{# def.resetCompositeRule }} - -{{= $closingBraces }} - -if (!{{=$valid}}) { - {{# def.extraError:'oneOf' }} -} else { - {{# def.resetErrors }} -{{? it.allErrors }} } {{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 583a22db12..af67780f19 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -6,7 +6,6 @@ module.exports = { dependencies: require("./dependencies"), format: require("./format"), if: require("./if"), - oneOf: require("./oneOf"), properties: require("./properties"), propertyNames: require("./propertyNames"), required: require("./required"), diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 85e5a71ac4..31b8594cd4 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -7,7 +7,7 @@ const applicator: Vocabulary = [ // any require("./not"), require("./anyOf"), - // TODO require("./oneOf"), + require("./oneOf"), require("./allOf"), // TODO require("./if"), ] diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts new file mode 100644 index 0000000000..271c9d8ba7 --- /dev/null +++ b/lib/vocabularies/applicator/oneOf.ts @@ -0,0 +1,75 @@ +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {nonEmptySchema} from "../util" +import {applySubschema} from "../../compile/subschema" +import {reportExtraError, resetErrorsCount} from "../../compile/errors" + +const def: KeywordDefinition = { + keyword: "oneOf", + schemaType: "array", + code(cxt) { + const {gen, errorParams, schema, it} = cxt + const valid = gen.name("valid") + const schValid = gen.name("_valid") + const errsCount = gen.name("_errs") + const passing = gen.name("passing") + errorParams({passing}) + gen + .code( + `const ${errsCount} = errors; + let ${valid} = false; + let ${passing} = null;` + ) + .startBlock() + + // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas + + schema.forEach((sch, i: number) => { + if (nonEmptySchema(it, sch)) { + applySubschema( + it, + { + keyword: "oneOf", + schemaProp: i, + compositeRule: true, + }, + schValid + ) + } else { + gen.code(`var ${schValid} = true;`) + } + + if (i > 0) { + gen + .if(`${schValid} && ${valid}`) + .code( + `${valid} = false; + ${passing} = [${passing}, ${i}];` + ) + .else() + } + + gen.code( + `if (${schValid}) { + ${valid} = true; + ${passing} = ${i}; + }` + ) + }) + + gen.endBlock() + + // TODO refactor failCompoundOrReset? + // TODO refactor ifs + gen.code(`if (!${valid}) {`) + reportExtraError(cxt, def.error as KeywordErrorDefinition) + gen.code(`} else {`) + resetErrorsCount(gen, errsCount) + if (it.allErrors) gen.code(`}`) + }, + error: { + message: "should match exactly one schema in oneOf", + params: ({params}) => `{passingSchemas: ${params.passing}}`, + }, +} + +module.exports = def From 165aa98b3c92a065cd885be52008da2c690c503e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 04:54:12 -0400 Subject: [PATCH 064/322] refactor: replace "nonEmptySchema" with "alwaysValidSchema" --- lib/vocabularies/applicator/allOf.ts | 4 +- lib/vocabularies/applicator/anyOf.ts | 6 +-- lib/vocabularies/applicator/contains.ts | 53 +++++++++++++------------ lib/vocabularies/applicator/items.ts | 35 ++++++++-------- lib/vocabularies/applicator/not.ts | 49 ++++++++++++----------- lib/vocabularies/applicator/oneOf.ts | 8 ++-- lib/vocabularies/util.ts | 20 ++++++++-- 7 files changed, 94 insertions(+), 81 deletions(-) diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 64255037cd..f37f541741 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,5 +1,5 @@ import {KeywordDefinition} from "../../types" -import {nonEmptySchema} from "../util" +import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" const def: KeywordDefinition = { @@ -10,7 +10,7 @@ const def: KeywordDefinition = { const valid = gen.name("valid") let count = 0 schema.forEach((sch: object | boolean, i: number) => { - if (nonEmptySchema(it, sch)) { + if (!alwaysValidSchema(it, sch)) { emptySchemas = false applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) if (!it.allErrors) { diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 432413a439..5f934a7134 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,5 +1,5 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" -import {nonEmptySchema} from "../util" +import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" @@ -8,8 +8,8 @@ const def: KeywordDefinition = { schemaType: "array", code(cxt) { const {gen, ok, schema, it} = cxt - let hasEmptySchema = !schema.every((sch: object | boolean) => nonEmptySchema(it, sch)) - if (hasEmptySchema) { + let alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) + if (alwaysValid) { ok() return } diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 1f088c71dc..b1ba3d969a 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,5 +1,5 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" -import {nonEmptySchema} from "../util" +import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" @@ -12,33 +12,34 @@ const def: KeywordDefinition = { const errsCount = gen.name("_errs") gen.code(`const ${errsCount} = errors;`) - if (nonEmptySchema(it, schema)) { - const valid = gen.name("valid") - const i = gen.name("i") - gen.for(`let ${i}=0; ${i}<${data}.length; ${i}++`) - applySubschema( - it, - { - keyword: "contains", - dataProp: i, - expr: Expr.Num, - compositeRule: true, - }, - valid - ) - gen.code(`if (${valid}) break;`) - gen.endFor() - - // TODO refactor failCompoundOrReset? It is different from anyOf though - // TODO refactor ifs - gen.code(`if (!${valid}) {`) - reportError(cxt, def.error as KeywordErrorDefinition) - gen.code(`} else {`) - resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.code(`}`) - } else { + if (alwaysValidSchema(it, schema)) { fail(`${data}.length === 0`) + return } + + const valid = gen.name("valid") + const i = gen.name("i") + gen.for(`let ${i}=0; ${i}<${data}.length; ${i}++`) + applySubschema( + it, + { + keyword: "contains", + dataProp: i, + expr: Expr.Num, + compositeRule: true, + }, + valid + ) + gen.code(`if (${valid}) break;`) + gen.endFor() + + // TODO refactor failCompoundOrReset? It is different from anyOf though + // TODO refactor ifs + gen.code(`if (!${valid}) {`) + reportError(cxt, def.error as KeywordErrorDefinition) + gen.code(`} else {`) + resetErrorsCount(gen, errsCount) + if (it.allErrors) gen.code(`}`) }, error: { message: "should contain a valid item", diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index f15b7ac165..ecf64647fb 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,5 +1,5 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" -import {nonEmptySchema} from "../util" +import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {fail_} from "../../keyword" @@ -31,12 +31,12 @@ const def: KeywordDefinition = { const addIts = parentSchema.additionalItems if (addIts === false) validateDataLength() validateDefinedItems() - if (typeof addIts == "object" && nonEmptySchema(it, addIts)) { + if (typeof addIts == "object" && !alwaysValidSchema(it, addIts)) { gen.if(`${len} > ${schema.length}`) validateItems("additionalItems", schema.length) gen.endIf() } - } else if (nonEmptySchema(it, schema)) { + } else if (!alwaysValidSchema(it, schema)) { validateItems("items", 0) } } @@ -57,21 +57,20 @@ const def: KeywordDefinition = { function validateDefinedItems(): void { const valid = gen.name("valid") schema.forEach((sch: any, i: number) => { - if (nonEmptySchema(it, sch)) { - gen.if(`${len} > ${i}`) - applySubschema( - it, - { - keyword: "items", - schemaProp: i, - dataProp: i, - expr: Expr.Const, - }, - valid - ) - gen.endIf() - if (!it.allErrors) gen.if(valid) - } + if (alwaysValidSchema(it, sch)) return + gen.if(`${len} > ${i}`) + applySubschema( + it, + { + keyword: "items", + schemaProp: i, + dataProp: i, + expr: Expr.Const, + }, + valid + ) + gen.endIf() + if (!it.allErrors) gen.if(valid) }) } diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 557f5ed254..5bc4fa4001 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -1,5 +1,5 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" -import {nonEmptySchema} from "../util" +import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" @@ -8,31 +8,32 @@ const def: KeywordDefinition = { schemaType: ["object", "boolean"], code(cxt) { const {gen, fail, schema, it} = cxt - if (nonEmptySchema(it, schema)) { - const valid = gen.name("valid") - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) - applySubschema( - it, - { - keyword: "not", - compositeRule: true, - createErrors: false, - allErrors: false, - }, - valid - ) - - // TODO refactor failCompoundOrReset? - // TODO refactor ifs - gen.code(`if (${valid}) {`) - reportError(cxt, def.error as KeywordErrorDefinition) - gen.code(`} else {`) - resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.code(`}`) - } else { + if (alwaysValidSchema(it, schema)) { fail() + return } + + const valid = gen.name("valid") + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + applySubschema( + it, + { + keyword: "not", + compositeRule: true, + createErrors: false, + allErrors: false, + }, + valid + ) + + // TODO refactor failCompoundOrReset? + // TODO refactor ifs + gen.code(`if (${valid}) {`) + reportError(cxt, def.error as KeywordErrorDefinition) + gen.code(`} else {`) + resetErrorsCount(gen, errsCount) + if (it.allErrors) gen.code(`}`) }, error: { message: "should NOT be valid", diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 271c9d8ba7..8e3ba4c16a 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,5 +1,5 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" -import {nonEmptySchema} from "../util" +import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" @@ -24,7 +24,9 @@ const def: KeywordDefinition = { // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas schema.forEach((sch, i: number) => { - if (nonEmptySchema(it, sch)) { + if (alwaysValidSchema(it, sch)) { + gen.code(`var ${schValid} = true;`) + } else { applySubschema( it, { @@ -34,8 +36,6 @@ const def: KeywordDefinition = { }, schValid ) - } else { - gen.code(`var ${schValid} = true;`) } if (i > 0) { diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 51140ce4c8..85a6827193 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -43,11 +43,23 @@ export function schemaRefOrVal( return `validate.schema${schemaPath + getProperty(keyword)}` } -export function nonEmptySchema( +// TODO remove +// export function nonEmptySchema( +// {RULES, opts: {strictKeywords}}: CompilationContext, +// schema: boolean | object +// ): boolean | void { +// return strictKeywords +// ? (typeof schema == "object" && Object.keys(schema).length > 0) || schema === false +// : schemaHasRules(schema, RULES.all) +// } + +export function alwaysValidSchema( {RULES, opts: {strictKeywords}}: CompilationContext, schema: boolean | object ): boolean | void { - return strictKeywords - ? (typeof schema == "object" && Object.keys(schema).length > 0) || schema === false - : schemaHasRules(schema, RULES.all) + return typeof schema == "boolean" + ? schema === true + : strictKeywords + ? Object.keys(schema).length === 0 + : !schemaHasRules(schema, RULES.all) } From 6a7fe05b7ea0fc8d43f15f1a20929059ff85e00b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 06:27:18 -0400 Subject: [PATCH 065/322] refactor: "if" to typescript --- lib/compile/rules.ts | 2 +- lib/dot/if.jst | 74 ----------------------- lib/dotjs/index.js | 1 - lib/vocabularies/applicator/if.ts | 88 ++++++++++++++++++++++++++++ lib/vocabularies/applicator/index.ts | 2 +- 5 files changed, 90 insertions(+), 77 deletions(-) delete mode 100644 lib/dot/if.jst create mode 100644 lib/vocabularies/applicator/if.ts diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index b1013e4464..f8dfdf6eaf 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -62,7 +62,7 @@ export default function rules(): ValidationRules { {properties: ["additionalProperties", "patternProperties"]}, ], }, - {rules: ["$ref", "if"]}, + {rules: ["$ref"]}, ], all: toHash(ALL), keywords: {}, diff --git a/lib/dot/if.jst b/lib/dot/if.jst deleted file mode 100644 index 56148d28e7..0000000000 --- a/lib/dot/if.jst +++ /dev/null @@ -1,74 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - - -{{## def.validateIfClause:_clause: - {{ - $it.schema = it.schema['_clause']; - $it.schemaPath = it.schemaPath + '._clause'; - $it.errSchemaPath = it.errSchemaPath + '/_clause'; - }} - {{# def.insertSubschemaCode }} - {{=$valid}} = {{=$nextValid}}; - {{? $thenPresent && $elsePresent }} - {{ $ifClause = 'ifClause' + $lvl; }} - var {{=$ifClause}} = '_clause'; - {{??}} - {{ $ifClause = '\'_clause\''; }} - {{?}} -#}} - -{{ - var $thenSch = it.schema['then'] - , $elseSch = it.schema['else'] - , $thenPresent = $thenSch !== undefined && {{# def.nonEmptySchema:$thenSch }} - , $elsePresent = $elseSch !== undefined && {{# def.nonEmptySchema:$elseSch }} - , $currentBaseId = $it.baseId; -}} - -{{? $thenPresent || $elsePresent }} - {{ - var $ifClause; - $it.createErrors = false; - $it.schema = $schema; - $it.schemaPath = $schemaPath; - $it.errSchemaPath = $errSchemaPath; - }} - var {{=$errs}} = errors; - var {{=$valid}} = true; - - {{# def.setCompositeRule }} - {{# def.insertSubschemaCode }} - {{ $it.createErrors = true; }} - {{# def.resetErrors }} - {{# def.resetCompositeRule }} - - {{? $thenPresent }} - if ({{=$nextValid}}) { - {{# def.validateIfClause:then }} - } - {{? $elsePresent }} - else { - {{?}} - {{??}} - if (!{{=$nextValid}}) { - {{?}} - - {{? $elsePresent }} - {{# def.validateIfClause:else }} - } - {{?}} - - if (!{{=$valid}}) { - {{# def.extraError:'if' }} - } - {{? $breakOnError }} else { {{?}} -{{??}} - {{? $breakOnError }} - if (true) { - {{?}} -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index af67780f19..5a60ff81ff 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -5,7 +5,6 @@ module.exports = { $ref: require("./ref"), dependencies: require("./dependencies"), format: require("./format"), - if: require("./if"), properties: require("./properties"), propertyNames: require("./propertyNames"), required: require("./required"), diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts new file mode 100644 index 0000000000..1cabf27921 --- /dev/null +++ b/lib/vocabularies/applicator/if.ts @@ -0,0 +1,88 @@ +import {KeywordDefinition, KeywordErrorDefinition, CompilationContext} from "../../types" +import {alwaysValidSchema} from "../util" +import {applySubschema} from "../../compile/subschema" +import {reportExtraError, resetErrorsCount} from "../../compile/errors" + +const def: KeywordDefinition = { + keyword: "if", + schemaType: ["object", "boolean"], + // implements: ["then", "else"], + code(cxt) { + const {gen, ok, errorParams, it} = cxt + const hasThen = hasSchema(it, "then") + const hasElse = hasSchema(it, "else") + if (!hasThen && !hasElse) { + // TODO strict mode: fail or warning if both "then" and "else" are not present + ok() + return + } + + const valid = gen.name("valid") + const schValid = gen.name("_valid") + const errsCount = gen.name("_errs") + + gen.code( + `const ${errsCount} = errors; + let ${valid} = true;` + ) + + validateIf() + resetErrorsCount(gen, errsCount) + + if (hasThen && hasElse) { + const ifClause = gen.name("ifClause") + errorParams({ifClause}) + gen.code(`let ${ifClause};`) + gen.if(schValid) + validateClause("then", ifClause) + gen.else() + validateClause("else", ifClause) + gen.endIf() + } else if (hasThen) { + gen.if(schValid) + validateClause("then") + gen.endIf() + } else { + gen.if(`!${schValid}`) + validateClause("else") + gen.endIf() + } + + // // TODO refactor failCompoundOrReset? + // // TODO refactor ifs + gen.code(`if (!${valid}) {`) + reportExtraError(cxt, def.error as KeywordErrorDefinition) + gen.code(it.allErrors ? "}" : "} else {") + + function validateIf(): void { + applySubschema( + it, + { + keyword: "if", + compositeRule: true, + createErrors: false, + allErrors: false, + }, + schValid + ) + } + + function validateClause(keyword, ifClause?: string): void { + applySubschema(it, {keyword}, schValid) + gen.code(`${valid} = ${schValid};`) + if (ifClause) gen.code(`${ifClause} = "${keyword}";`) + else errorParams({ifClause: `"${keyword}"`}) + } + }, + error: { + message: ({params}) => `'should match "' + ${params.ifClause} + '" schema'`, + params: ({params}) => `{failingKeyword: ${params.ifClause}}`, + }, +} + +module.exports = def + +function hasSchema(it: CompilationContext, keyword: string): boolean { + const schema = it.schema[keyword] + return schema !== undefined && !alwaysValidSchema(it, schema) +} diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 31b8594cd4..a3dd3de749 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -9,7 +9,7 @@ const applicator: Vocabulary = [ require("./anyOf"), require("./oneOf"), require("./allOf"), - // TODO require("./if"), + require("./if"), ] module.exports = applicator From aeb67f2e4523c9138da04895858d8f6b28cf622b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 07:50:58 -0400 Subject: [PATCH 066/322] refactor(codegen): optionally pass body of if/for/block as function --- lib/compile/codegen.ts | 39 ++++++++--- lib/vocabularies/applicator/allOf.ts | 15 ++--- lib/vocabularies/applicator/anyOf.ts | 31 +++++---- lib/vocabularies/applicator/contains.ts | 26 ++++---- lib/vocabularies/applicator/if.ts | 26 +++----- lib/vocabularies/applicator/items.ts | 38 +++++------ lib/vocabularies/applicator/oneOf.ts | 76 +++++++++++----------- lib/vocabularies/validation/enum.ts | 19 +++--- lib/vocabularies/validation/uniqueItems.ts | 18 ++--- 9 files changed, 148 insertions(+), 140 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 45ef86174f..dfbd54f35e 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -23,49 +23,68 @@ export default class CodeGen { return this } - if(condition: string): CodeGen { + if(condition: string, thenBody?: () => void, elseBody?: () => void): CodeGen { this.#blocks.push(Block.If) this.code(`if(${condition}){`) + if (thenBody && elseBody) { + thenBody() + this.else() + elseBody() + this.endIf() + } else if (thenBody) { + thenBody() + this.endIf() + } else if (elseBody) { + throw new Error("CodeGen: else body without then body") + } return this } elseIf(condition: string): CodeGen { - if (this._block !== Block.If) throw new Error('CodeGen: "else if" without "if"') + if (this._lastBlock !== Block.If) throw new Error('CodeGen: "else if" without "if"') this.code(`}else if(${condition}){`) return this } else(): CodeGen { - if (this._block !== Block.If) throw new Error('CodeGen: "else" without "if"') - this._block = Block.Else + if (this._lastBlock !== Block.If) throw new Error('CodeGen: "else" without "if"') + this._lastBlock = Block.Else this.code(`}else{`) return this } endIf(): CodeGen { - const b = this._block + const b = this._lastBlock if (b !== Block.If && b !== Block.Else) throw new Error('CodeGen: "endIf" without "if"') this.#blocks.pop() this.code(`}`) return this } - for(iteration: string): CodeGen { + for(iteration: string, forBody?: () => void): CodeGen { this.#blocks.push(Block.For) this.code(`for(${iteration}){`) + if (forBody) { + forBody() + this.endFor() + } return this } endFor(): CodeGen { - const b = this._block + const b = this._lastBlock if (b !== Block.For) throw new Error('CodeGen: "endFor" without "for"') this.#blocks.pop() this.code(`}`) return this } - startBlock(): CodeGen { + block(body?: () => void, expectedToClose?: number): CodeGen { this.#blockStarts.push(this.#blocks.length) + if (body) { + body() + this.endBlock(expectedToClose) + } return this } @@ -81,13 +100,13 @@ export default class CodeGen { return this } - get _block(): Block { + get _lastBlock(): Block { const len = this.#blocks.length if (len === 0) throw new Error("CodeGen: not in block") return this.#blocks[len - 1] } - set _block(b: Block) { + set _lastBlock(b: Block) { const len = this.#blocks.length if (len === 0) throw new Error('CodeGen: not in "if" block') this.#blocks[len - 1] = b diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index f37f541741..4e8bd351d7 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -10,14 +10,13 @@ const def: KeywordDefinition = { const valid = gen.name("valid") let count = 0 schema.forEach((sch: object | boolean, i: number) => { - if (!alwaysValidSchema(it, sch)) { - emptySchemas = false - applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) - if (!it.allErrors) { - if (count === 1) gen.startBlock() - count++ - gen.if(`${valid}`) - } + if (alwaysValidSchema(it, sch)) return + emptySchemas = false + applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) + if (!it.allErrors) { + if (count === 1) gen.block() + count++ + gen.if(valid) } }) diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 5f934a7134..c73a3c7b57 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -21,22 +21,21 @@ const def: KeywordDefinition = { const ${errsCount} = errors;` ) - gen.startBlock() - schema.forEach((_, i: number) => { - applySubschema( - it, - { - keyword: "anyOf", - schemaProp: i, - compositeRule: true, - }, - schValid - ) - gen.code(`${valid} = ${valid} || ${schValid};`) - gen.if(`!${valid}`) - }) - - gen.endBlock(schema.length) + gen.block(() => { + schema.forEach((_, i: number) => { + applySubschema( + it, + { + keyword: "anyOf", + schemaProp: i, + compositeRule: true, + }, + schValid + ) + gen.code(`${valid} = ${valid} || ${schValid};`) + gen.if(`!${valid}`) + }) + }, schema.length) // TODO refactor failCompoundOrReset? // TODO refactor ifs diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index b1ba3d969a..c62eb9b63b 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -19,19 +19,19 @@ const def: KeywordDefinition = { const valid = gen.name("valid") const i = gen.name("i") - gen.for(`let ${i}=0; ${i}<${data}.length; ${i}++`) - applySubschema( - it, - { - keyword: "contains", - dataProp: i, - expr: Expr.Num, - compositeRule: true, - }, - valid - ) - gen.code(`if (${valid}) break;`) - gen.endFor() + gen.for(`let ${i}=0; ${i}<${data}.length; ${i}++`, () => { + applySubschema( + it, + { + keyword: "contains", + dataProp: i, + expr: Expr.Num, + compositeRule: true, + }, + valid + ) + gen.code(`if (${valid}) break;`) + }) // TODO refactor failCompoundOrReset? It is different from anyOf though // TODO refactor ifs diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 1cabf27921..67fe95d4ec 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -33,19 +33,11 @@ const def: KeywordDefinition = { const ifClause = gen.name("ifClause") errorParams({ifClause}) gen.code(`let ${ifClause};`) - gen.if(schValid) - validateClause("then", ifClause) - gen.else() - validateClause("else", ifClause) - gen.endIf() + gen.if(schValid, validateClause("then", ifClause), validateClause("else", ifClause)) } else if (hasThen) { - gen.if(schValid) - validateClause("then") - gen.endIf() + gen.if(schValid, validateClause("then")) } else { - gen.if(`!${schValid}`) - validateClause("else") - gen.endIf() + gen.if(`!${schValid}`, validateClause("else")) } // // TODO refactor failCompoundOrReset? @@ -67,11 +59,13 @@ const def: KeywordDefinition = { ) } - function validateClause(keyword, ifClause?: string): void { - applySubschema(it, {keyword}, schValid) - gen.code(`${valid} = ${schValid};`) - if (ifClause) gen.code(`${ifClause} = "${keyword}";`) - else errorParams({ifClause: `"${keyword}"`}) + function validateClause(keyword, ifClause?: string): () => void { + return () => { + applySubschema(it, {keyword}, schValid) + gen.code(`${valid} = ${schValid};`) + if (ifClause) gen.code(`${ifClause} = "${keyword}";`) + else errorParams({ifClause: `"${keyword}"`}) + } } }, error: { diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index ecf64647fb..8b436ece30 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -19,9 +19,7 @@ const def: KeywordDefinition = { if (it.allErrors) { validateItemsKeyword() } else { - gen.startBlock() - validateItemsKeyword() - gen.endBlock() + gen.block(validateItemsKeyword) // TODO refactor ifs gen.code(`if (${errsCount} === errors) {`) } @@ -32,9 +30,7 @@ const def: KeywordDefinition = { if (addIts === false) validateDataLength() validateDefinedItems() if (typeof addIts == "object" && !alwaysValidSchema(it, addIts)) { - gen.if(`${len} > ${schema.length}`) - validateItems("additionalItems", schema.length) - gen.endIf() + gen.if(`${len} > ${schema.length}`, () => validateItems("additionalItems", schema.length)) } } else if (!alwaysValidSchema(it, schema)) { validateItems("items", 0) @@ -58,18 +54,18 @@ const def: KeywordDefinition = { const valid = gen.name("valid") schema.forEach((sch: any, i: number) => { if (alwaysValidSchema(it, sch)) return - gen.if(`${len} > ${i}`) - applySubschema( - it, - { - keyword: "items", - schemaProp: i, - dataProp: i, - expr: Expr.Const, - }, - valid + gen.if(`${len} > ${i}`, () => + applySubschema( + it, + { + keyword: "items", + schemaProp: i, + dataProp: i, + expr: Expr.Const, + }, + valid + ) ) - gen.endIf() if (!it.allErrors) gen.if(valid) }) } @@ -77,10 +73,10 @@ const def: KeywordDefinition = { function validateItems(keyword: string, startFrom: number): void { const i = gen.name("i") const valid = gen.name("valid") - gen.for(`let ${i}=${startFrom}; ${i}<${len}; ${i}++`) - applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) - if (!it.allErrors) gen.code(`if(!${valid}){break}`) - gen.endFor() + gen.for(`let ${i}=${startFrom}; ${i}<${len}; ${i}++`, () => { + applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) + if (!it.allErrors) gen.code(`if(!${valid}){break}`) + }) } }, error: { diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 8e3ba4c16a..4f876d01fb 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -13,50 +13,15 @@ const def: KeywordDefinition = { const errsCount = gen.name("_errs") const passing = gen.name("passing") errorParams({passing}) + // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas + gen .code( `const ${errsCount} = errors; let ${valid} = false; let ${passing} = null;` ) - .startBlock() - - // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas - - schema.forEach((sch, i: number) => { - if (alwaysValidSchema(it, sch)) { - gen.code(`var ${schValid} = true;`) - } else { - applySubschema( - it, - { - keyword: "oneOf", - schemaProp: i, - compositeRule: true, - }, - schValid - ) - } - - if (i > 0) { - gen - .if(`${schValid} && ${valid}`) - .code( - `${valid} = false; - ${passing} = [${passing}, ${i}];` - ) - .else() - } - - gen.code( - `if (${schValid}) { - ${valid} = true; - ${passing} = ${i}; - }` - ) - }) - - gen.endBlock() + .block(validateOneOf) // TODO refactor failCompoundOrReset? // TODO refactor ifs @@ -65,6 +30,41 @@ const def: KeywordDefinition = { gen.code(`} else {`) resetErrorsCount(gen, errsCount) if (it.allErrors) gen.code(`}`) + + function validateOneOf() { + schema.forEach((sch, i: number) => { + if (alwaysValidSchema(it, sch)) { + gen.code(`var ${schValid} = true;`) + } else { + applySubschema( + it, + { + keyword: "oneOf", + schemaProp: i, + compositeRule: true, + }, + schValid + ) + } + + if (i > 0) { + gen + .if(`${schValid} && ${valid}`) + .code( + `${valid} = false; + ${passing} = [${passing}, ${i}];` + ) + .else() + } + + gen.code( + `if (${schValid}) { + ${valid} = true; + ${passing} = ${i}; + }` + ) + }) + } }, error: { message: "should match exactly one schema in oneOf", diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 5ea319ffa2..edf4d6c216 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -8,16 +8,15 @@ const def: KeywordDefinition = { code({gen, fail, data, $data, schema, schemaCode, it: {opts}}) { if ($data) { const valid = gen.name("valid") - gen - .startBlock() - .code(`let ${valid};`) - .if(`${schemaCode} === undefined`) - .code(`${valid} = true;`) - .else() - .code(`${valid} = false;`) - .if(`Array.isArray(${schemaCode})`) - loopEnum(schemaCode, valid) - gen.endBlock(2) + gen.code(`let ${valid};`) + gen.if( + `${schemaCode} === undefined`, + () => gen.code(`${valid} = true;`), + () => + gen + .code(`${valid} = false;`) + .if(`Array.isArray(${schemaCode})`, () => loopEnum(schemaCode, valid)) + ) fail(`!${valid}`) } else { if (schema.length === 0) throw new Error("enum must have non-empty array") diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 86c675ac3b..6dd5f0263b 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -33,11 +33,9 @@ const def: KeywordDefinition = { function validateUniqueItems() { gen.code( `${i} = ${data}.length; - ${valid} = true; - if (${i} > 1) { - ${canOptimize() ? loopN() : loopN2()} - }` + ${valid} = true;` ) + gen.if(`${i} > 1`, canOptimize() ? loopN : loopN2) } function canOptimize(): boolean { @@ -46,7 +44,7 @@ const def: KeywordDefinition = { : itemType && itemType !== "object" && itemType !== "array" } - function loopN(): string { + function loopN(): void { const wrongType = (Array.isArray(itemType) ? checkDataTypes : checkDataType)( itemType, "item", @@ -54,7 +52,8 @@ const def: KeywordDefinition = { true ) const indices = gen.name("indices") - return `const ${indices} = {}; + gen.code( + `const ${indices} = {}; for (;${i}--;) { let item = ${data}[${i}]; if (${wrongType}) continue; @@ -66,10 +65,12 @@ const def: KeywordDefinition = { } ${indices}[item] = ${i}; }` + ) } - function loopN2(): string { - return `outer: + function loopN2(): void { + gen.code( + `outer: for (;${i}--;) { for (${j} = ${i}; ${j}--;) { if (equal(${data}[${i}], ${data}[${j}])) { @@ -78,6 +79,7 @@ const def: KeywordDefinition = { } } }` + ) } }, error: { From 754e520e0bce08add0dac1f02bf46ef9f0cabcee Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 09:48:24 -0400 Subject: [PATCH 067/322] refactor: "propertyNames" to typescript --- lib/compile/errors.ts | 3 +- lib/compile/rules.ts | 2 +- lib/compile/subschema.ts | 13 ++++- lib/dot/errors.def | 9 ---- lib/dot/propertyNames.jst | 54 -------------------- lib/dotjs/index.js | 1 - lib/types.ts | 1 + lib/vocabularies/applicator/index.ts | 5 ++ lib/vocabularies/applicator/propertyNames.ts | 46 +++++++++++++++++ 9 files changed, 66 insertions(+), 68 deletions(-) delete mode 100644 lib/dot/propertyNames.jst create mode 100644 lib/vocabularies/applicator/propertyNames.ts diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 023c27ca1c..9a6f3cbf46 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -60,7 +60,7 @@ function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): st keyword, data, schemaValue, - it: {createErrors, schemaPath, errorPath, errSchemaPath, opts}, + it: {createErrors, schemaPath, errorPath, errSchemaPath, propertyName, opts}, } = cxt if (createErrors === false) return "{}" if (!error) throw new Error('keyword definition must have "error" property') @@ -71,6 +71,7 @@ function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): st dataPath: (dataPath || "") + ${errorPath}, schemaPath: ${toQuotedString(errSchemaPath + "/" + keyword)}, params: ${params ? params(cxt) : "{}"},` + if (propertyName) out += `propertyName: ${propertyName},` if (opts.messages !== false) { out += `message: ${typeof message == "string" ? quotedString(message) : message(cxt)},` } diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index f8dfdf6eaf..67c8691fad 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -53,12 +53,12 @@ export default function rules(): ValidationRules { rules: [ {type: "number", rules: ["format"]}, {type: "string", rules: ["format"]}, + {type: "array", rules: []}, { type: "object", rules: [ "required", "dependencies", - "propertyNames", {properties: ["additionalProperties", "patternProperties"]}, ], }, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 489dd80c7a..bfcc299a6f 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -21,7 +21,9 @@ export enum Expr { interface SubschemaApplication { keyword: string schemaProp?: string | number + data?: string dataProp?: string | number + propertyName?: string expr?: Expr compositeRule?: true createErrors?: boolean @@ -30,7 +32,7 @@ interface SubschemaApplication { export function applySubschema( it: CompilationContext, - {keyword, schemaProp, dataProp, expr, ...rest}: SubschemaApplication, + {keyword, schemaProp, data, dataProp, expr, ...rest}: SubschemaApplication, valid: string ): void { const schema = it.schema[keyword] @@ -47,7 +49,9 @@ export function applySubschema( errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + schemaProp)}`, } - if (dataProp !== undefined) { + if (data !== undefined && dataProp !== undefined) { + throw new Error('both "data" and "dataProp" are passed, only one allowed') + } else if (dataProp !== undefined) { const {gen, errorPath, dataPathArr, dataLevel, opts} = it // TODO possibly refactor getPath and getPathExpr to one function using Expr enum const nextLevel = dataLevel + 1 @@ -62,6 +66,11 @@ export function applySubschema( const passDataProp = Expr.Const ? getProperty(dataProp) : `[${dataProp}]` gen.code(`var data${nextLevel} = data${dataLevel || ""}${passDataProp};`) + } else if (data !== undefined) { + const {gen, dataLevel} = it + const nextLevel = dataLevel + 1 + subschema.dataLevel = nextLevel + gen.code(`var data${nextLevel} = ${data};`) } Object.assign(subschema, rest) diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 57e36ea40d..acffbcd8d7 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -93,9 +93,6 @@ additionalProperties: "'{{? it.opts._errorDataPathProperty }}is an invalid additional property{{??}}should NOT have additional properties{{?}}'", dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", - 'if': "'should match \"' + {{=$ifClause}} + '\" schema'", - oneOf: "'should match exactly one schema in oneOf'", - propertyNames: "'property name \\'{{=$invalidName}}\\' is invalid'", required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", @@ -113,9 +110,6 @@ additionalProperties: "false", dependencies: "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", - 'if': "validate.schema{{=$schemaPath}}", - oneOf: "validate.schema{{=$schemaPath}}", - propertyNames: "validate.schema{{=$schemaPath}}", required: "validate.schema{{=$schemaPath}}", custom: "validate.schema{{=$schemaPath}}", patternRequired: "validate.schema{{=$schemaPath}}", @@ -132,9 +126,6 @@ additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{= it.util.escapeQuotes($deps.length==1 ? $deps[0] : $deps.join(\", \")) }}' }", format: "{ format: {{#def.schemaValueQS}} }", - 'if': "{ failingKeyword: {{=$ifClause}} }", - oneOf: "{ passingSchemas: {{=$passingSchemas}} }", - propertyNames: "{ propertyName: '{{=$invalidName}}' }", required: "{ missingProperty: '{{=$missingProperty}}' }", custom: "{ keyword: '{{=$rule.keyword}}' }", patternRequired: "{ missingPattern: '{{=$missingPattern}}' }", diff --git a/lib/dot/propertyNames.jst b/lib/dot/propertyNames.jst deleted file mode 100644 index 4d44ff26db..0000000000 --- a/lib/dot/propertyNames.jst +++ /dev/null @@ -1,54 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - -var {{=$errs}} = errors; - -{{? {{# def.nonEmptySchema:$schema }} }} - {{ - $it.schema = $schema; - $it.schemaPath = $schemaPath; - $it.errSchemaPath = $errSchemaPath; - }} - - {{ - var $key = 'key' + $lvl - , $idx = 'idx' + $lvl - , $i = 'i' + $lvl - , $invalidName = '\' + ' + $key + ' + \'' - , $dataNxt = $it.dataLevel = it.dataLevel + 1 - , $nextData = 'data' + $dataNxt - , $dataProperties = 'dataProperties' + $lvl - , $ownProperties = it.opts.ownProperties - , $currentBaseId = it.baseId; - }} - - {{? $ownProperties }} - var {{=$dataProperties}} = undefined; - {{?}} - {{# def.iterateProperties }} - var startErrs{{=$lvl}} = errors; - - {{ var $passData = $key; }} - {{# def.setCompositeRule }} - {{# def.generateSubschemaCode }} - {{# def.optimizeValidate }} - {{# def.resetCompositeRule }} - - if (!{{=$nextValid}}) { - for (var {{=$i}}=startErrs{{=$lvl}}; {{=$i}} { + applySubschema( + it, + {keyword: "propertyNames", data: key, propertyName: key, compositeRule: true}, + valid + ) + gen.if(`!${valid}`, () => { + reportExtraError(cxt, def.error as KeywordErrorDefinition) + if (!it.allErrors) gen.code("break;") + }) + }) + + // TODO refactor ifs + if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + }, + error: { + message: ({params}) => `"property name '" + ${params.propertyName} + "' is invalid"`, + params: ({params}) => `{propertyName: ${params.propertyName}}`, + }, +} + +module.exports = def From f5e448dd3fc284a2a9b644dbf4ffa2b66376052f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 16:05:36 -0400 Subject: [PATCH 068/322] feat: remove support for option errorDataPath --- README.md | 2 - lib/ajv.ts | 1 - lib/dot/errors.def | 4 +- lib/dot/missing.def | 8 -- lib/dot/properties.jst | 10 +- lib/dot/required.jst | 3 - lib/types.ts | 2 - spec/errors.spec.js | 203 ++++++++++------------------------------- 8 files changed, 52 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 181d71c636..e6acbdb577 100644 --- a/README.md +++ b/README.md @@ -1131,7 +1131,6 @@ Defaults: loopEnum: Infinity, ownProperties: false, multipleOfPrecision: false, - errorDataPath: 'object', // deprecated messages: true, sourceCode: false, processCode: undefined, // function (str: string, schema: object): string {} @@ -1234,7 +1233,6 @@ Defaults: - _loopEnum_: by default `enum` keyword is compiled into a single expression. In case of a very large number of allowed values it may result in a large validation function. Pass integer to set the number of values above which `enum` keyword will be validated in a loop. - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). -- _errorDataPath_ (deprecated): set `dataPath` to point to 'object' (default) or to 'property' when validating keywords `required`, `additionalProperties` and `dependencies`. - _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when custom messages are used (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). - _sourceCode_: add `sourceCode` property to validating function (for debugging; this code can be different from the result of toString call). - _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. Starting from version 5.0.0 this option replaced options: diff --git a/lib/ajv.ts b/lib/ajv.ts index 976013e718..02841d01da 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -71,7 +71,6 @@ export default function Ajv(opts: Options): void { opts.loopRequired = opts.loopRequired || Infinity opts.loopEnum = opts.loopEnum || Infinity - if (opts.errorDataPath === "property") opts._errorDataPathProperty = true if (opts.serialize === undefined) opts.serialize = stableStringify this._metaOpts = getMetaSchemaOptions(this) diff --git a/lib/dot/errors.def b/lib/dot/errors.def index acffbcd8d7..9d458b3909 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -90,10 +90,10 @@ {{## def._errorMessages = { $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", - additionalProperties: "'{{? it.opts._errorDataPathProperty }}is an invalid additional property{{??}}should NOT have additional properties{{?}}'", + additionalProperties: "'should NOT have additional properties'", dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", - required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", + required: "'should have required property \\'{{=$missingProperty}}\\''", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", switch: "'should pass \"switch\" keyword validation'", diff --git a/lib/dot/missing.def b/lib/dot/missing.def index 40973df356..7d880a78f4 100644 --- a/lib/dot/missing.def +++ b/lib/dot/missing.def @@ -14,11 +14,6 @@ {{ var $propertyPath = 'missing' + $lvl , $missingProperty = '\' + ' + $propertyPath + ' + \''; - if (it.opts._errorDataPathProperty) { - it.errorPath = it.opts.jsonPointers - ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) - : $currentErrorPath + ' + ' + $propertyPath; - } }} {{# def.error:_error }} #}} @@ -29,9 +24,6 @@ var $prop = it.util.getProperty($propertyKey) , $missingProperty = it.util.escapeQuotes($propertyKey) , $useData = $data + $prop; - if (it.opts._errorDataPathProperty) { - it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); - } }} if ({{# def.noPropertyInData }}) { {{# def.addError:_error }} diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 3da5ecb141..1778be5ba0 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -9,9 +9,7 @@ $it.schema = $aProperties; $it.schemaPath = it.schemaPath + '.additionalProperties'; $it.errSchemaPath = it.errSchemaPath + '/additionalProperties'; - $it.errorPath = it.opts._errorDataPathProperty - ? it.errorPath - : it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + $it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); var $passData = $data + '[' + $key + ']'; $it.dataPathArr[$dataNxt] = $key; }} @@ -84,9 +82,6 @@ var {{=$nextValid}} = true; {{ var $currentErrorPath = it.errorPath; var $additionalProperty = '\' + ' + $key + ' + \''; - if (it.opts._errorDataPathProperty) { - it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); - } }} {{? $noAdditional }} {{? $removeAdditional }} @@ -173,9 +168,6 @@ var {{=$nextValid}} = true; var $currentErrorPath = it.errorPath , $currErrSchemaPath = $errSchemaPath , $missingProperty = it.util.escapeQuotes($propertyKey); - if (it.opts._errorDataPathProperty) { - it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); - } $errSchemaPath = it.errSchemaPath + '/required'; }} {{# def.error:'required' }} diff --git a/lib/dot/required.jst b/lib/dot/required.jst index 4fedc4eb83..ac5956f1af 100644 --- a/lib/dot/required.jst +++ b/lib/dot/required.jst @@ -15,9 +15,6 @@ var $i = 'i' + $lvl , $propertyPath = 'schema' + $lvl + '[' + $i + ']' , $missingProperty = '\' + ' + $propertyPath + ' + \''; - if (it.opts._errorDataPathProperty) { - it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); - } }} #}} diff --git a/lib/types.ts b/lib/types.ts index f48805b843..60bb11502d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -38,7 +38,6 @@ export interface Options { loopEnum?: number ownProperties?: boolean multipleOfPrecision?: boolean | number - errorDataPath?: string messages?: boolean sourceCode?: boolean processCode?: (code: string, schema: object) => string @@ -48,7 +47,6 @@ export interface Options { serialize?: false | ((schema: object | boolean) => any) $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) schemaId?: string // not supported - _errorDataPathProperty?: boolean // private } interface Logger { diff --git a/spec/errors.spec.js b/spec/errors.spec.js index b9d389a00c..a3617d73ff 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -10,15 +10,13 @@ describe("Validation errors", () => { createInstances() }) - function createInstances(errorDataPath) { - ajv = new Ajv({errorDataPath: errorDataPath, loopRequired: 21}) + function createInstances() { + ajv = new Ajv({loopRequired: 21}) ajvJP = new Ajv({ - errorDataPath: errorDataPath, jsonPointers: true, loopRequired: 21, }) fullAjv = new Ajv({ - errorDataPath: errorDataPath, allErrors: true, verbose: true, jsonPointers: true, @@ -50,16 +48,11 @@ describe("Validation errors", () => { }) describe('"additionalProperties" errors', () => { - it('should include property in dataPath with option errorDataPath="property"', () => { - createInstances("property") - testAdditional("property") - }) - - it("should NOT include property in dataPath WITHOUT option errorDataPath", () => { + it("should NOT include property in dataPath", () => { testAdditional() }) - function testAdditional(errorDataPath) { + function testAdditional() { var schema = { properties: { foo: {}, @@ -71,20 +64,14 @@ describe("Validation errors", () => { var data = {foo: 1, bar: 2}, invalidData = {foo: 1, bar: 2, baz: 3, quux: 4} - var path = pathFunc(errorDataPath) - var msg = additionalFunc(errorDataPath) + var msg = "should NOT have additional properties" var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError( - validate.errors[0], - "additionalProperties", - "#/additionalProperties", - path("['baz']"), - msg, - {additionalProperty: "baz"} - ) + shouldBeError(validate.errors[0], "additionalProperties", "#/additionalProperties", "", msg, { + additionalProperty: "baz", + }) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) @@ -93,7 +80,7 @@ describe("Validation errors", () => { validateJP.errors[0], "additionalProperties", "#/additionalProperties", - path("/baz"), + "", msg, {additionalProperty: "baz"} ) @@ -105,7 +92,7 @@ describe("Validation errors", () => { fullValidate.errors[0], "additionalProperties", "#/additionalProperties", - path("/baz"), + "", msg, {additionalProperty: "baz"} ) @@ -113,31 +100,15 @@ describe("Validation errors", () => { fullValidate.errors[1], "additionalProperties", "#/additionalProperties", - path("/quux"), + "", msg, {additionalProperty: "quux"} ) - - if (errorDataPath === "property") { - fullValidate.errors - .filter((err) => err.keyword === "additionalProperties") - .map((err) => - fullAjv._opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2, -2) - ) - .forEach((p) => delete invalidData[p]) - - invalidData.should.eql({foo: 1, bar: 2}) - } } }) describe('errors when "additionalProperties" is schema', () => { - it('should include property in dataPath with option errorDataPath="property"', () => { - createInstances("property") - testAdditionalIsSchema("property") - }) - - it("should NOT include property in dataPath WITHOUT option errorDataPath", () => { + it("should NOT include property in dataPath", () => { testAdditionalIsSchema() }) @@ -187,33 +158,23 @@ describe("Validation errors", () => { }) describe('"required" errors', () => { - it('should include missing property in dataPath with option errorDataPath="property"', () => { - createInstances("property") - testRequired("property") - }) - - it("should NOT include missing property in dataPath WITHOUT option errorDataPath", () => { + it("should NOT include missing property in dataPath", () => { testRequired() }) - function testRequired(errorDataPath) { + function testRequired() { var schema = { required: ["foo", "bar", "baz"], } - _testRequired(errorDataPath, schema, "#", ".") + _testRequired(schema, "#", ".") } - it('large data/schemas with option errorDataPath="property"', () => { - createInstances("property") - testRequiredLargeSchema("property") - }) - - it("large data/schemas WITHOUT option errorDataPath", () => { + it("large data/schemas", () => { testRequiredLargeSchema() }) - function testRequiredLargeSchema(errorDataPath) { + function testRequiredLargeSchema() { var schema = {required: []}, data = {}, invalidData1 = {}, @@ -227,9 +188,6 @@ describe("Validation errors", () => { delete invalidData2[2] // properties '2' and '198' will be missing delete invalidData2[98] - var path = pathFunc(errorDataPath) - var msg = requiredFunc(errorDataPath) - test() schema = {anyOf: [schema]} @@ -241,51 +199,46 @@ describe("Validation errors", () => { var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, path("['1']"), msg("1"), { + shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("1"), { missingProperty: "1", }) shouldBeInvalid(validate, invalidData2, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, path("['2']"), msg("2"), { + shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("2"), { missingProperty: "2", }) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, path("/1"), msg("1"), { + shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("1"), { missingProperty: "1", }) shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, path("/2"), msg("2"), { + shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("2"), { missingProperty: "2", }) var fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, path("/1"), msg("1"), { + shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("1"), { missingProperty: "1", }) shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, path("/2"), msg("2"), { + shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("2"), { missingProperty: "2", }) - shouldBeError(fullValidate.errors[1], "required", schPath, path("/98"), msg("98"), { + shouldBeError(fullValidate.errors[1], "required", schPath, "", requiredMsg("98"), { missingProperty: "98", }) } } - it('with "properties" with option errorDataPath="property"', () => { - createInstances("property") - testRequiredAndProperties("property") - }) - - it('with "properties" WITHOUT option errorDataPath', () => { + it('with "properties"', () => { testRequiredAndProperties() }) - function testRequiredAndProperties(errorDataPath) { + function testRequiredAndProperties() { var schema = { properties: { foo: {type: "number"}, @@ -295,24 +248,19 @@ describe("Validation errors", () => { required: ["foo", "bar", "baz"], } - _testRequired(errorDataPath, schema) + _testRequired(schema) } - it('in "anyOf" with option errorDataPath="property"', () => { - createInstances("property") - testRequiredInAnyOf("property") - }) - - it('in "anyOf" WITHOUT option errorDataPath', () => { + it('in "anyOf"', () => { testRequiredInAnyOf() }) - function testRequiredInAnyOf(errorDataPath) { + function testRequiredInAnyOf() { var schema = { anyOf: [{required: ["foo", "bar", "baz"]}], } - _testRequired(errorDataPath, schema, "#/anyOf/0", ".", 1) + _testRequired(schema, "#/anyOf/0", ".", 1) } it("should not validate required twice in large schemas with loopRequired option", () => { @@ -351,16 +299,11 @@ describe("Validation errors", () => { }) describe('"dependencies" errors', () => { - it('should include missing property in dataPath with option errorDataPath="property"', () => { - createInstances("property") - testDependencies("property") - }) - - it("should NOT include missing property in dataPath WITHOUT option errorDataPath", () => { + it("should NOT include missing property in dataPath", () => { testDependencies() }) - function testDependencies(errorDataPath) { + function testDependencies() { var schema = { dependencies: { a: ["foo", "bar", "baz"], @@ -371,50 +314,21 @@ describe("Validation errors", () => { invalidData1 = {a: 0, foo: 1, baz: 3}, invalidData2 = {a: 0, bar: 2} - var path = pathFunc(errorDataPath) var msg = "should have properties foo, bar, baz when property a is present" var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError( - validate.errors[0], - "dependencies", - "#/dependencies", - path(".bar"), - msg, - params(".bar") - ) + shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params(".bar")) shouldBeInvalid(validate, invalidData2) - shouldBeError( - validate.errors[0], - "dependencies", - "#/dependencies", - path(".foo"), - msg, - params(".foo") - ) + shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params(".foo")) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1) - shouldBeError( - validateJP.errors[0], - "dependencies", - "#/dependencies", - path("/bar"), - msg, - params("bar") - ) + shouldBeError(validateJP.errors[0], "dependencies", "#/dependencies", "", msg, params("bar")) shouldBeInvalid(validateJP, invalidData2) - shouldBeError( - validateJP.errors[0], - "dependencies", - "#/dependencies", - path("/foo"), - msg, - params("foo") - ) + shouldBeError(validateJP.errors[0], "dependencies", "#/dependencies", "", msg, params("foo")) var fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) @@ -423,7 +337,7 @@ describe("Validation errors", () => { fullValidate.errors[0], "dependencies", "#/dependencies", - path("/bar"), + "", msg, params("bar") ) @@ -432,7 +346,7 @@ describe("Validation errors", () => { fullValidate.errors[0], "dependencies", "#/dependencies", - path("/foo"), + "", msg, params("foo") ) @@ -440,7 +354,7 @@ describe("Validation errors", () => { fullValidate.errors[1], "dependencies", "#/dependencies", - path("/baz"), + "", msg, params("baz") ) @@ -457,7 +371,7 @@ describe("Validation errors", () => { } }) - function _testRequired(errorDataPath, schema, schemaPathPrefix, prefix, extraErrors) { + function _testRequired(schema, schemaPathPrefix, prefix, extraErrors) { var schPath = (schemaPathPrefix || "#") + "/required" prefix = prefix || "" extraErrors = extraErrors || 0 @@ -466,64 +380,45 @@ describe("Validation errors", () => { invalidData1 = {foo: 1, baz: 3}, invalidData2 = {bar: 2} - var path = pathFunc(errorDataPath) - var msg = requiredFunc(errorDataPath) - var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, path(".bar"), msg(prefix + "bar"), { + shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg(prefix + "bar"), { missingProperty: prefix + "bar", }) shouldBeInvalid(validate, invalidData2, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, path(".foo"), msg(prefix + "foo"), { + shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg(prefix + "foo"), { missingProperty: prefix + "foo", }) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, path("/bar"), msg("bar"), { + shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("bar"), { missingProperty: "bar", }) shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, path("/foo"), msg("foo"), { + shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("foo"), { missingProperty: "foo", }) var fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, path("/bar"), msg("bar"), { + shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("bar"), { missingProperty: "bar", }) shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, path("/foo"), msg("foo"), { + shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("foo"), { missingProperty: "foo", }) - shouldBeError(fullValidate.errors[1], "required", schPath, path("/baz"), msg("baz"), { + shouldBeError(fullValidate.errors[1], "required", schPath, "", requiredMsg("baz"), { missingProperty: "baz", }) } - function pathFunc(errorDataPath) { - return function (dataPath) { - return errorDataPath === "property" ? dataPath : "" - } - } - - function requiredFunc(errorDataPath) { - return function (prop) { - return errorDataPath === "property" - ? "is a required property" - : "should have required property '" + prop + "'" - } - } - - function additionalFunc(errorDataPath) { - return errorDataPath === "property" - ? "is an invalid additional property" - : "should NOT have additional properties" + function requiredMsg(prop) { + return `should have required property '${prop}'` } it('"items" errors should include item index without quotes in dataPath (#48)', () => { From 39dd71f37a96a39743c6efb776c9c1c26d5016cd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 17:03:14 -0400 Subject: [PATCH 069/322] eslint rules --- lib/compile/validate/applicability.ts | 14 +++++++----- lib/compile/validate/boolSchema.ts | 1 + lib/compile/validate/dataType.ts | 22 ++++++++++++------- lib/compile/validate/defaults.ts | 2 +- lib/compile/validate/index.ts | 20 ++++++++--------- lib/compile/validate/keywords.ts | 2 +- lib/keyword.ts | 2 ++ lib/types.ts | 2 +- lib/vocabularies/applicator/anyOf.ts | 2 +- lib/vocabularies/applicator/if.ts | 2 +- lib/vocabularies/applicator/items.ts | 10 ++++----- lib/vocabularies/util.ts | 7 +++--- lib/vocabularies/validation/enum.ts | 4 ++-- lib/vocabularies/validation/limit.ts | 2 +- lib/vocabularies/validation/limitItems.ts | 4 ++-- lib/vocabularies/validation/limitLength.ts | 4 ++-- .../validation/limitProperties.ts | 4 ++-- package.json | 2 +- 18 files changed, 57 insertions(+), 49 deletions(-) diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index a42c95627b..4ec20166b7 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,18 +1,20 @@ import {CompilationContext} from "../../types" +import {RuleGroup, Rule} from "../rules" -export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: string) { +export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: string): boolean { const group = RULES.types[ty] return group && group !== true && shouldUseGroup(schema, group) } -export function shouldUseGroup(schema, group): boolean { - return group.rules.some((rule) => shouldUseRule(schema, rule)) +export function shouldUseGroup(schema: object, group: RuleGroup): boolean { + // TODO remove type cast to Rule + return group.rules.some((rule) => shouldUseRule(schema, rule)) } -export function shouldUseRule(schema, rule): boolean { +export function shouldUseRule(schema: object, rule: Rule): boolean | undefined { return schema[rule.keyword] !== undefined || ruleImplementsSomeKeyword(schema, rule) } -function ruleImplementsSomeKeyword(schema, rule): boolean { - return rule.implements && rule.implements.some((kwd) => schema[kwd] !== undefined) +function ruleImplementsSomeKeyword(schema: object, rule: Rule): boolean | undefined { + return rule.implements?.some((kwd) => schema[kwd] !== undefined) } diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 5f76ca9bc4..eeff4eeaea 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -44,6 +44,7 @@ function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { schemaCode: false, schemaValue: false, parentSchema: false, + params: {}, it, } reportError(cxt, boolError, overrideAllErrors) diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 767cb52d1e..1644dc12d4 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -5,8 +5,8 @@ import {reportError} from "../errors" import {getKeywordContext} from "../../keyword" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { - const t: undefined | string | string[] = schema.type - const types: string[] = Array.isArray(t) ? t : t ? [t] : [] + const st: undefined | string | string[] = schema.type + const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach(checkType) if (opts.nullable) { const hasNull = types.includes("null") @@ -30,7 +30,7 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]): dataLevel, opts: {coerceTypes, strictNumbers}, } = it - let coerceTo = coerceToTypes(types, coerceTypes) + const coerceTo = coerceToTypes(types, coerceTypes) const checkTypes = types.length > 0 && (coerceTo.length > 0 || types.length > 1 || !schemaHasRulesForType(it, types[0])) @@ -52,7 +52,13 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string : [] } -const coerceCode = { +interface CoerceArgs { + dataType: string + data: string + coerced: string +} + +const coerceCode: {[x: string]: (arg: CoerceArgs) => string} = { string: ({dataType, data, coerced}) => `else if (${dataType} == "number" || ${dataType} == "boolean") ${coerced} = "" + ${data}; @@ -103,7 +109,7 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { ) } gen.code(`if (${coerced} !== undefined) ;`) - const args = {dataType, data, coerced} + const args: CoerceArgs = {dataType, data, coerced} for (const t of coerceTo) { if (t in coerceCode && (t !== "array" || coerceTypes === "array")) { gen.code(coerceCode[t](args)) @@ -131,12 +137,12 @@ function assignParentData({dataLevel, dataPathArr}: CompilationContext, expr: st } const typeError: KeywordErrorDefinition = { - message: ({schema}) => `"should be ${Array.isArray(schema) ? schema.join(",") : schema}"`, + message: ({schema}) => `"should be ${Array.isArray(schema) ? schema.join(",") : schema}"`, // TODO change: return type as array here - params: ({schema}) => `{type: "${Array.isArray(schema) ? schema.join(",") : schema}"}`, + params: ({schema}) => `{type: "${Array.isArray(schema) ? schema.join(",") : schema}"}`, } -export function reportTypeError(it: CompilationContext) { +export function reportTypeError(it: CompilationContext): void { const cxt = getKeywordContext(it, "type") reportError(cxt, typeError) } diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 6915c5500d..51112f189c 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -36,7 +36,7 @@ function assignDefault( return } - let condition = + const condition = `${data} === undefined` + (useDefaults === "empty" ? ` || ${data} === null || ${data} === ""` : "") // TODO remove option `useDefaults === "shared"` diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 5b2b9f312c..ef178056b4 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -7,16 +7,14 @@ import {schemaKeywords} from "./keywords" const resolve = require("../resolve") -/** - * schema compilation (render) time: - * it = { schema, RULES, _validate, opts } - * it.validate - this function (validateCode), - * it is used recursively to generate code for sub-schemas - * - * runtime: - * "validate" is a variable name to which this function will be assigned - * validateRef etc. are defined in the parent scope in index.js - */ +// schema compilation (render) time: +// it = { schema, RULES, _validate, opts } +// it.validate - this function (validateCode), +// it is used recursively to generate code for sub-schemas +// +// runtime: +// "validate" is a variable name to which this function will be assigned +// validateRef etc. are defined in the parent scope in index.js export default function validateCode( it: CompilationContext, valid?: string, @@ -129,7 +127,7 @@ function startFunction({ }: CompilationContext): void { const asyncFunc = async ? "async" : "" const sourceUrl = - schema.$id && (sourceCode || processCode) ? `/*# sourceURL=${schema.$id} */` : "" + schema.$id && (sourceCode || processCode) ? `/*# sourceURL=${schema.$id as string} */` : "" gen.code( `const validate = ${asyncFunc} function(data, dataPath, parentData, parentDataProperty, rootData) { 'use strict'; diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index 4c3859c43d..84cbbe094a 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -10,7 +10,7 @@ export function schemaKeywords( types: string[], typeErrors: boolean, top: boolean -) { +): void { const { gen, schema, diff --git a/lib/keyword.ts b/lib/keyword.ts index 54cba372f2..b7497841ea 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -145,6 +145,7 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void schemaCode: $data ? gen.name("schema") : schemaValue, schemaValue, parentSchema: it.schema, + params: {}, it, } if ($data) { @@ -216,6 +217,7 @@ export function getKeywordContext(it: CompilationContext, keyword: string): Keyw schemaCode, schemaValue: schemaCode, parentSchema: schema, + params: {}, it, } } diff --git a/lib/types.ts b/lib/types.ts index 60bb11502d..df7e50ab5a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -190,7 +190,7 @@ export interface KeywordContext { parentSchema: any schemaCode: string | number | boolean schemaValue: string | number | boolean - params?: any + params: {[x: string]: string} it: CompilationContext } diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index c73a3c7b57..93a6dc6d0a 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -8,7 +8,7 @@ const def: KeywordDefinition = { schemaType: "array", code(cxt) { const {gen, ok, schema, it} = cxt - let alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) + const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) if (alwaysValid) { ok() return diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 67fe95d4ec..d9f0c44c91 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -59,7 +59,7 @@ const def: KeywordDefinition = { ) } - function validateClause(keyword, ifClause?: string): () => void { + function validateClause(keyword: string, ifClause?: string): () => void { return () => { applySubschema(it, {keyword}, schValid) gen.code(`${valid} = ${schValid};`) diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 8b436ece30..9fdc215a89 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -27,7 +27,7 @@ const def: KeywordDefinition = { function validateItemsKeyword(): void { if (Array.isArray(schema)) { const addIts = parentSchema.additionalItems - if (addIts === false) validateDataLength() + if (addIts === false) validateDataLength(schema) validateDefinedItems() if (typeof addIts == "object" && !alwaysValidSchema(it, addIts)) { gen.if(`${len} > ${schema.length}`, () => validateItems("additionalItems", schema.length)) @@ -37,10 +37,10 @@ const def: KeywordDefinition = { } } - function validateDataLength(): void { + function validateDataLength(sch: any[]): void { // TODO replace with "fail" fail_( - `${len} > ${schema.length}`, + `${len} > ${sch.length}`, { ...cxt, keyword: "additionalItems", @@ -80,8 +80,8 @@ const def: KeywordDefinition = { } }, error: { - message: ({schema}) => `"should NOT have more than ${schema.length} items"`, - params: ({schema}) => `{limit: ${schema.length}}`, + message: ({schema}) => `"should NOT have more than ${schema.length as number} items"`, + params: ({schema}) => `{limit: ${schema.length as number}}`, }, } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 85a6827193..147fd6c0b4 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -30,15 +30,14 @@ export function dataNotType( } export function schemaRefOrVal( - schema, + schema: unknown, schemaPath: string, keyword: string, $data?: string | false ): string | number | boolean { - const t = typeof schema if (!$data) { - if (t === "number" || t === "boolean") return schema - if (t === "string") return quotedString(schema) + if (typeof schema == "number" || typeof schema == "boolean") return schema + if (typeof schema == "string") return quotedString(schema) } return `validate.schema${schemaPath + getProperty(keyword)}` } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index edf4d6c216..7a590d9950 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -28,7 +28,7 @@ const def: KeywordDefinition = { loopEnum(vSchema, valid) fail(`!${valid}`) } else { - let cond: string = schema.reduce( + const cond: string = schema.reduce( (c, _, i) => (c += (c && "||") + equalCode(vSchema, i)), "" ) @@ -49,7 +49,7 @@ const def: KeywordDefinition = { } function equalCode(vSchema: string, i: number): string { - let sch = schema[i] + let sch: string = schema[i] if (sch && typeof sch === "object") { return `equal(${data}, ${vSchema}[${i}])` } diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 67bb7d41a8..d583dac118 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,7 +1,7 @@ import {KeywordDefinition} from "../../types" import {appendSchema, dataNotType} from "../util" -const OPS = { +const OPS: {[index: string]: {fail: string; ok: string}} = { maximum: {fail: ">", ok: "<="}, minimum: {fail: "<", ok: ">="}, exclusiveMaximum: {fail: ">=", ok: "<"}, diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index ab49caa5e9..72fe760f61 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -7,13 +7,13 @@ const def: KeywordDefinition = { schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode}) { - const op = keyword == "maxItems" ? ">" : "<" + const op = keyword === "maxItems" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + `${data}.length` + op + schemaCode) }, error: { message({keyword, $data, schemaCode}) { - const comp = keyword == "maxItems" ? "more" : "fewer" + const comp = keyword === "maxItems" ? "more" : "fewer" const sch = concatSchema(schemaCode, $data) return `"should NOT have ${comp} than ${sch} items"` }, diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index a10130588b..2856438683 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -7,14 +7,14 @@ const def: KeywordDefinition = { schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode, it: {opts}}) { - const op = keyword == "maxLength" ? ">" : "<" + const op = keyword === "maxLength" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) const len = opts.unicode === false ? `${data}.length` : `ucs2length(${data})` fail(dnt + len + op + schemaCode) }, error: { message({keyword, $data, schemaCode}) { - const comp = keyword == "maxLength" ? "more" : "fewer" + const comp = keyword === "maxLength" ? "more" : "fewer" const sch = concatSchema(schemaCode, $data) return `"should NOT have ${comp} than ${sch} items"` }, diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index 97b61aaa1e..d58c057528 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -7,13 +7,13 @@ const def: KeywordDefinition = { schemaType: "number", $data: true, code({fail, keyword, data, $data, schemaCode}) { - const op = keyword == "maxProperties" ? ">" : "<" + const op = keyword === "maxProperties" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) fail(dnt + `Object.keys(${data}).length` + op + schemaCode) }, error: { message({keyword, $data, schemaCode}) { - const comp = keyword == "maxProperties" ? "more" : "fewer" + const comp = keyword === "maxProperties" ? "more" : "fewer" const sch = concatSchema(schemaCode, $data) return `"should NOT have ${comp} than ${sch} items"` }, diff --git a/package.json b/package.json index 952d1aaf47..8c80673f0d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint lib/{compile/,}*.ts spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint lib/{**/,}*.ts spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "lint": "npm run eslint", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", From 3ac717f1840f35aca39f6ddafeb6ffd07686c2c1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 20 Aug 2020 17:05:43 -0400 Subject: [PATCH 070/322] refactor: "dependencies" keyword to typesript --- lib/compile/codegen.ts | 2 + lib/compile/rules.ts | 6 +- lib/compile/validate/index.ts | 1 + lib/dot/definitions.def | 2 - lib/dot/dependencies.jst | 81 ----------------- lib/dot/errors.def | 4 - lib/dot/missing.def | 2 - lib/dotjs/index.js | 1 - lib/keyword.ts | 6 +- lib/types.ts | 6 +- lib/vocabularies/applicator/dependencies.ts | 99 +++++++++++++++++++++ lib/vocabularies/applicator/index.ts | 2 +- lib/vocabularies/applicator/missing.ts | 44 +++++++++ lib/vocabularies/util.ts | 16 ++++ 14 files changed, 172 insertions(+), 100 deletions(-) delete mode 100644 lib/dot/dependencies.jst create mode 100644 lib/vocabularies/applicator/dependencies.ts create mode 100644 lib/vocabularies/applicator/missing.ts diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index dfbd54f35e..df07fac100 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -54,6 +54,7 @@ export default class CodeGen { } endIf(): CodeGen { + // TODO possibly remove empty branches here const b = this._lastBlock if (b !== Block.If && b !== Block.Else) throw new Error('CodeGen: "endIf" without "if"') this.#blocks.pop() @@ -89,6 +90,7 @@ export default class CodeGen { } endBlock(expectedToClose?: number): CodeGen { + // TODO maybe close blocks one by one, eliminating empty branches const len = this.#blockStarts.pop() if (len === undefined) throw new Error("CodeGen: not in block sequence") const toClose = this.#blocks.length - len diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 67c8691fad..a93d35bd7c 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -56,11 +56,7 @@ export default function rules(): ValidationRules { {type: "array", rules: []}, { type: "object", - rules: [ - "required", - "dependencies", - {properties: ["additionalProperties", "patternProperties"]}, - ], + rules: ["required", {properties: ["additionalProperties", "patternProperties"]}], }, {rules: ["$ref"]}, ], diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index ef178056b4..7e50171fd5 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -55,6 +55,7 @@ export default function validateCode( } else { updateContext(it) checkAsync(it) + // TODO level, var - it is coupled with errs count in keyword.ts gen.code(`var errs_${level} = errors;`) typeAndKeywords() // TODO level, var diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index 3bbb994644..c02efb960d 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -191,5 +191,3 @@ {{## def.isOwnProperty: Object.prototype.hasOwnProperty.call({{=$data}}, '{{=it.util.escapeQuotes($propertyKey)}}') #}} - -{{ it.gen.code(out); }} diff --git a/lib/dot/dependencies.jst b/lib/dot/dependencies.jst deleted file mode 100644 index 00a5e67a32..0000000000 --- a/lib/dot/dependencies.jst +++ /dev/null @@ -1,81 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.missing }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - - -{{## def.propertyInData: - {{=$data}}{{= it.util.getProperty($property) }} !== undefined - {{? $ownProperties }} - && Object.prototype.hasOwnProperty.call({{=$data}}, '{{=it.util.escapeQuotes($property)}}') - {{?}} -#}} - - -{{ - var $schemaDeps = {} - , $propertyDeps = {} - , $ownProperties = it.opts.ownProperties; - - for ($property in $schema) { - if ($property == '__proto__') continue; - var $sch = $schema[$property]; - var $deps = Array.isArray($sch) ? $propertyDeps : $schemaDeps; - $deps[$property] = $sch; - } -}} - -var {{=$errs}} = errors; - -{{ var $currentErrorPath = it.errorPath; }} - -var missing{{=$lvl}}; -{{ for (var $property in $propertyDeps) { }} - {{ $deps = $propertyDeps[$property]; }} - {{? $deps.length }} - if ({{# def.propertyInData }} - {{? $breakOnError }} - && ({{# def.checkMissingProperty:$deps }})) { - {{# def.errorMissingProperty:'dependencies' }} - {{??}} - ) { - {{~ $deps:$propertyKey }} - {{# def.allErrorsMissingProperty:'dependencies' }} - {{~}} - {{?}} - } {{# def.elseIfValid }} - {{?}} -{{ } }} - -{{ - it.errorPath = $currentErrorPath; - var $currentBaseId = $it.baseId; -}} - - -{{ for (var $property in $schemaDeps) { }} - {{ var $sch = $schemaDeps[$property]; }} - {{? {{# def.nonEmptySchema:$sch }} }} - {{=$nextValid}} = true; - - if ({{# def.propertyInData }}) { - {{ - $it.schema = $sch; - $it.schemaPath = $schemaPath + it.util.getProperty($property); - $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($property); - }} - - {{# def.insertSubschemaCode }} - } - - {{# def.ifResultValid }} - {{?}} -{{ } }} - -{{? $breakOnError }} - {{= $closingBraces }} - if ({{=$errs}} == errors) { -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 9d458b3909..b2fde26457 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -91,7 +91,6 @@ {{## def._errorMessages = { $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", additionalProperties: "'should NOT have additional properties'", - dependencies: "'should have {{? $deps.length == 1 }}property {{= it.util.escapeQuotes($deps[0]) }}{{??}}properties {{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}} when property {{= it.util.escapeQuotes($property) }} is present'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", required: "'should have required property \\'{{=$missingProperty}}\\''", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", @@ -102,13 +101,11 @@ } #}} -{{## def.schemaRefOrVal: {{?$isData}}validate.schema{{=$schemaPath}}{{??}}{{=$schema}}{{?}} #}} {{## def.schemaRefOrQS: {{?$isData}}validate.schema{{=$schemaPath}}{{??}}{{=it.util.toQuotedString($schema)}}{{?}} #}} {{## def._errorSchemas = { $ref: "{{=it.util.toQuotedString($schema)}}", additionalProperties: "false", - dependencies: "validate.schema{{=$schemaPath}}", format: "{{#def.schemaRefOrQS}}", required: "validate.schema{{=$schemaPath}}", custom: "validate.schema{{=$schemaPath}}", @@ -124,7 +121,6 @@ {{## def._errorParams = { $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", - dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{= it.util.escapeQuotes($deps.length==1 ? $deps[0] : $deps.join(\", \")) }}' }", format: "{ format: {{#def.schemaValueQS}} }", required: "{ missingProperty: '{{=$missingProperty}}' }", custom: "{ keyword: '{{=$rule.keyword}}' }", diff --git a/lib/dot/missing.def b/lib/dot/missing.def index 7d880a78f4..3382beb543 100644 --- a/lib/dot/missing.def +++ b/lib/dot/missing.def @@ -29,5 +29,3 @@ {{# def.addError:_error }} } #}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 347fbdc915..da09003451 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -3,7 +3,6 @@ //all requires must be explicit because browserify won't work with dynamic requires module.exports = { $ref: require("./ref"), - dependencies: require("./dependencies"), format: require("./format"), properties: require("./properties"), required: require("./required"), diff --git a/lib/keyword.ts b/lib/keyword.ts index b7497841ea..6047fc385f 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -6,6 +6,7 @@ import { ValidateFunction, CompilationContext, KeywordContext, + KeywordContextParams, } from "./types" import {ValidationRules, Rule} from "./compile/rules" @@ -177,8 +178,9 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void else if (!allErrors) gen.code("if (true) {") } - function errorParams(obj: any) { - cxt.params = obj + function errorParams(obj: KeywordContextParams, assign?: true) { + if (assign) Object.assign(cxt.params, obj) + else cxt.params = obj } } diff --git a/lib/types.ts b/lib/types.ts index df7e50ab5a..f3c586e66d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -182,7 +182,7 @@ export interface KeywordContext { gen: CodeGen fail: (condition?: string, context?: KeywordContext) => void ok: (condition?: string) => void - errorParams: (obj: any) => void + errorParams: (obj: KeywordContextParams, assing?: true) => void keyword: string data: string $data?: string | false @@ -190,10 +190,12 @@ export interface KeywordContext { parentSchema: any schemaCode: string | number | boolean schemaValue: string | number | boolean - params: {[x: string]: string} + params: KeywordContextParams it: CompilationContext } +export type KeywordContextParams = {[x: string]: string} + export type FormatMode = "fast" | "full" type SN = string | number diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts new file mode 100644 index 0000000000..522aab344c --- /dev/null +++ b/lib/vocabularies/applicator/dependencies.ts @@ -0,0 +1,99 @@ +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {alwaysValidSchema, quotedString, propertyInData} from "../util" +import {applySubschema} from "../../compile/subschema" +import {escapeQuotes} from "../../compile/util" +import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "./missing" + +interface PropertyDependencies { + [x: string]: string[] +} +interface SchemaDependencies { + [x: string]: object | boolean +} + +const def: KeywordDefinition = { + keyword: "dependencies", + type: "object", + schemaType: ["object"], + code(cxt) { + const {gen, errorParams, schema, data, it} = cxt + + const [propDeps, schDeps] = splitDependencies() + + const valid = gen.name("valid") + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + + gen.block(() => { + validatePropertyDeps(propDeps) + validateSchemaDeps(schDeps) + }) + + // TODO refactor ifs + if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + + function splitDependencies(): [PropertyDependencies, SchemaDependencies] { + const propertyDeps: PropertyDependencies = {} + const schemaDeps: SchemaDependencies = {} + for (const key in schema) { + if (key === "__proto__") continue + const deps = Array.isArray(schema[key]) ? propertyDeps : schemaDeps + deps[key] = schema[key] + } + return [propertyDeps, schemaDeps] + } + + function validatePropertyDeps(propertyDeps: {[x: string]: string[]}): void { + for (const prop in propertyDeps) { + const deps = propertyDeps[prop] + if (deps.length === 0) continue + const hasProperty = propertyInData(data, prop, it.opts.ownProperties) + errorParams({ + property: prop, + depsCount: "" + deps.length, + deps: deps.join(", "), + }) + if (it.allErrors) { + gen.if(hasProperty, () => { + for (const depProp of deps) { + checkReportMissingProp(cxt, depProp, def.error as KeywordErrorDefinition) + } + }) + } else { + // TODO refactor: maybe use one variable for all dependencies + // or not use this variable at all? + const missing = gen.name("missing") + gen.code(`let ${missing};`) + gen.if(`${hasProperty} && (${checkMissingProp(cxt, deps, missing)})`) + reportMissingProp(cxt, missing, def.error as KeywordErrorDefinition) + gen.else() + } + } + } + + function validateSchemaDeps(schemaDeps: {[x: string]: object | boolean}): void { + for (const prop in schemaDeps) { + if (alwaysValidSchema(it, schemaDeps[prop])) continue + gen.if( + propertyInData(data, prop, it.opts.ownProperties), + () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), + () => gen.code(`var ${valid} = true;`) // TODO refactor var + ) + if (!it.allErrors) gen.if(valid) + } + } + }, + error: { + message: ({params: {property, depsCount, deps}}) => { + const requiredProps = (depsCount === "1" ? "property " : "properties ") + escapeQuotes(deps) + return `'should have ${requiredProps} when property ${escapeQuotes(property)} is present'` + }, + params: ({params: {property, depsCount, deps, missingProperty}}) => + `{property: ${quotedString(property)}, + missingProperty: ${missingProperty}, + depsCount: ${depsCount}, + deps: ${quotedString(deps)}}`, // TODO change to reference? + }, +} + +module.exports = def diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 27b87f2b10..64b03ab357 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -6,7 +6,7 @@ const applicator: Vocabulary = [ require("./contains"), // object // require("./required"), - // require("./dependencies"), + require("./dependencies"), require("./propertyNames"), // require("./properties"), // any diff --git a/lib/vocabularies/applicator/missing.ts b/lib/vocabularies/applicator/missing.ts new file mode 100644 index 0000000000..dd288f9d03 --- /dev/null +++ b/lib/vocabularies/applicator/missing.ts @@ -0,0 +1,44 @@ +import {KeywordContext, KeywordErrorDefinition} from "../../types" +import {noPropertyInData, quotedString} from "../util" +import {getProperty} from "../../compile/util" +import {reportError} from "../../compile/errors" + +export function checkReportMissingProp( + cxt: KeywordContext, + prop: string, + error: KeywordErrorDefinition +): void { + const { + gen, + errorParams, + data, + it: {opts}, + } = cxt + gen.if(noPropertyInData(data, prop, opts.ownProperties), () => { + errorParams({missingProperty: quotedString(prop)}, true) + reportError(cxt, error) + }) +} + +export function checkMissingProp( + {data, it: {opts}}: KeywordContext, + properties: string[], + missing: string +): string { + return properties + .map((prop) => { + const hasNoProp = noPropertyInData(data, prop, opts.ownProperties) + const missingProp = quotedString(opts.jsonPointers ? prop : getProperty(prop)) + return `(${hasNoProp} && (${missing} = ${missingProp}))` + }) + .reduce((cond, part) => `${cond} || ${part}`) +} + +export function reportMissingProp( + cxt: KeywordContext, + missing: string, + error: KeywordErrorDefinition +): void { + cxt.errorParams({missingProperty: missing}, true) + reportError(cxt, error) +} diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 147fd6c0b4..d4bf709374 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -62,3 +62,19 @@ export function alwaysValidSchema( ? Object.keys(schema).length === 0 : !schemaHasRules(schema, RULES.all) } + +export function isOwnProperty(data: string, property: string): string { + return `Object.prototype.hasOwnProperty.call(${data}, ${quotedString(property)})` +} + +export function propertyInData(data: string, propertry: string, ownProperties?: boolean): string { + let cond = `${data}${getProperty(propertry)} !== undefined` + if (ownProperties) cond += ` && ${isOwnProperty(data, propertry)}` + return cond +} + +export function noPropertyInData(data: string, propertry: string, ownProperties?: boolean): string { + let cond = `${data}${getProperty(propertry)} === undefined` + if (ownProperties) cond += ` || !${isOwnProperty(data, propertry)}` + return cond +} From 353ce0f608b4884feeda12ba12528a319b9103a3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 21 Aug 2020 08:28:55 -0400 Subject: [PATCH 071/322] feat: applicator keywords should be run after validation keywords, but "items" and "contains" should be run before uniqueItems --- lib/ajv.ts | 2 +- lib/keyword.ts | 20 ++++++++++++++++++-- lib/types.ts | 1 + lib/vocabularies/applicator/contains.ts | 1 + lib/vocabularies/applicator/items.ts | 1 + 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 02841d01da..ba143ae77d 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -75,8 +75,8 @@ export default function Ajv(opts: Options): void { this._metaOpts = getMetaSchemaOptions(this) if (opts.formats) addInitialFormats(this) - this.addVocabulary(applicatorVocabulary, true) this.addVocabulary(validationVocabulary, true) + this.addVocabulary(applicatorVocabulary, true) if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) diff --git a/lib/keyword.ts b/lib/keyword.ts index 6047fc385f..2bc2021694 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -67,9 +67,11 @@ export function addKeyword( throw new Error("Keyword " + keyword + " is not a valid identifier") } + // TODO any + const self = this + if (definition) { if (!_skipValidation) this.validateKeyword(definition, true) - const dataType = definition.type if (Array.isArray(dataType)) { for (const t of dataType) { @@ -113,7 +115,21 @@ export function addKeyword( code: definition.code ? ruleCode : customRuleCode, implements: definition.implements, } - ruleGroup.rules.push(rule) + + if (definition.before) { + // TODO remove type case when RuleDef is removed + const i = ruleGroup.rules.findIndex((rule) => (rule as Rule).keyword === definition.before) + if (i >= 0) { + ruleGroup.rules.splice(i, 0, rule) + } else { + ruleGroup.rules.push(rule) + // TODO replace with Ajv this.logger + self.logger.log(`rule ${definition.before} is not defined`) + } + } else { + ruleGroup.rules.push(rule) + } + RULES.custom[keyword] = rule } diff --git a/lib/types.ts b/lib/types.ts index f3c586e66d..99725237f6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -154,6 +154,7 @@ export interface KeywordDefinition { $data?: boolean errors?: boolean | "full" metaSchema?: object + before?: string // schema: false makes validate not to expect schema (ValidateFunction) schema?: boolean statements?: boolean diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index c62eb9b63b..df095cec8d 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -7,6 +7,7 @@ const def: KeywordDefinition = { keyword: "contains", type: "array", schemaType: ["object", "boolean"], + before: "uniqueItems", code(cxt) { const {gen, fail, schema, data, it} = cxt const errsCount = gen.name("_errs") diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 9fdc215a89..48f68ea6f5 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -7,6 +7,7 @@ const def: KeywordDefinition = { keyword: "items", type: "array", schemaType: ["object", "array", "boolean"], + before: "uniqueItems", code(cxt) { const {gen, /* fail, */ schema, parentSchema, data, it} = cxt const errsCount = gen.name("_errs") From a7ec60447ad55e91a8f19e0902f700177bc42987 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 21 Aug 2020 09:14:05 -0400 Subject: [PATCH 072/322] feat: normalize missingProperty in error messages and params (now always without ".") --- lib/dot/missing.def | 2 +- lib/vocabularies/applicator/dependencies.ts | 2 +- lib/vocabularies/{applicator => }/missing.ts | 10 ++++------ lib/vocabularies/validation/index.ts | 1 + spec/errors.spec.js | 19 +++++++++---------- 5 files changed, 16 insertions(+), 18 deletions(-) rename lib/vocabularies/{applicator => }/missing.ts (70%) diff --git a/lib/dot/missing.def b/lib/dot/missing.def index 3382beb543..1476e3bf78 100644 --- a/lib/dot/missing.def +++ b/lib/dot/missing.def @@ -5,7 +5,7 @@ var $prop = it.util.getProperty($propertyKey) , $useData = $data + $prop; }} - ( ({{# def.noPropertyInData }}) && (missing{{=$lvl}} = {{= it.util.toQuotedString(it.opts.jsonPointers ? $propertyKey : $prop) }}) ) + ( ({{# def.noPropertyInData }}) && (missing{{=$lvl}} = {{= it.util.toQuotedString($propertyKey) }}) ) {{~}} #}} diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 522aab344c..c2ade101d4 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -2,7 +2,7 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema, quotedString, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" import {escapeQuotes} from "../../compile/util" -import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "./missing" +import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" interface PropertyDependencies { [x: string]: string[] diff --git a/lib/vocabularies/applicator/missing.ts b/lib/vocabularies/missing.ts similarity index 70% rename from lib/vocabularies/applicator/missing.ts rename to lib/vocabularies/missing.ts index dd288f9d03..8a281a1771 100644 --- a/lib/vocabularies/applicator/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,7 +1,6 @@ -import {KeywordContext, KeywordErrorDefinition} from "../../types" -import {noPropertyInData, quotedString} from "../util" -import {getProperty} from "../../compile/util" -import {reportError} from "../../compile/errors" +import {KeywordContext, KeywordErrorDefinition} from "../types" +import {noPropertyInData, quotedString} from "./util" +import {reportError} from "../compile/errors" export function checkReportMissingProp( cxt: KeywordContext, @@ -28,8 +27,7 @@ export function checkMissingProp( return properties .map((prop) => { const hasNoProp = noPropertyInData(data, prop, opts.ownProperties) - const missingProp = quotedString(opts.jsonPointers ? prop : getProperty(prop)) - return `(${hasNoProp} && (${missing} = ${missingProp}))` + return `(${hasNoProp} && (${missing} = ${quotedString(prop)}))` }) .reduce((cond, part) => `${cond} || ${part}`) } diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index a5901e57f5..8ddf97242e 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -9,6 +9,7 @@ const validation: Vocabulary = [ require("./pattern"), // object require("./limitProperties"), + // require("./required"), // array require("./limitItems"), require("./uniqueItems"), diff --git a/spec/errors.spec.js b/spec/errors.spec.js index a3617d73ff..5385f0e355 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -167,7 +167,7 @@ describe("Validation errors", () => { required: ["foo", "bar", "baz"], } - _testRequired(schema, "#", ".") + _testRequired(schema, "#") } it("large data/schemas", () => { @@ -260,7 +260,7 @@ describe("Validation errors", () => { anyOf: [{required: ["foo", "bar", "baz"]}], } - _testRequired(schema, "#/anyOf/0", ".", 1) + _testRequired(schema, "#/anyOf/0", 1) } it("should not validate required twice in large schemas with loopRequired option", () => { @@ -319,9 +319,9 @@ describe("Validation errors", () => { var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params(".bar")) + shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params("bar")) shouldBeInvalid(validate, invalidData2) - shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params(".foo")) + shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params("foo")) var validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) @@ -371,9 +371,8 @@ describe("Validation errors", () => { } }) - function _testRequired(schema, schemaPathPrefix, prefix, extraErrors) { + function _testRequired(schema, schemaPathPrefix, extraErrors) { var schPath = (schemaPathPrefix || "#") + "/required" - prefix = prefix || "" extraErrors = extraErrors || 0 var data = {foo: 1, bar: 2, baz: 3}, @@ -383,12 +382,12 @@ describe("Validation errors", () => { var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg(prefix + "bar"), { - missingProperty: prefix + "bar", + shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("bar"), { + missingProperty: "bar", }) shouldBeInvalid(validate, invalidData2, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg(prefix + "foo"), { - missingProperty: prefix + "foo", + shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("foo"), { + missingProperty: "foo", }) var validateJP = ajvJP.compile(schema) From 385d53b57b9380a454095685a4c552ed1b3c88cb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 21 Aug 2020 09:20:17 -0400 Subject: [PATCH 073/322] decouple "required" and "properties" keywords (marginal performance change: faster validation in case of missing required properties, slower validation in case of "required" properties also used in "properties" keyword because of a duplicate check) --- lib/dot/properties.jst | 34 +++++++--------------------------- lib/dot/required.jst | 25 ++++--------------------- 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 1778be5ba0..836a7ad631 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -39,11 +39,6 @@ , $ownProperties = it.opts.ownProperties , $currentBaseId = it.baseId; - var $required = it.schema.required; - if ($required && !(it.opts.$data && $required.$data) && $required.length < it.opts.loopRequired) { - var $requiredHash = it.util.toHash($required); - } - function notProto(p) { return p !== '__proto__'; } }} @@ -161,31 +156,16 @@ var {{=$nextValid}} = true; {{? $hasDefault }} {{= $code }} {{??}} - {{? $requiredHash && $requiredHash[$propertyKey] }} + {{? $breakOnError }} if ({{# def.noPropertyInData }}) { - {{=$nextValid}} = false; - {{ - var $currentErrorPath = it.errorPath - , $currErrSchemaPath = $errSchemaPath - , $missingProperty = it.util.escapeQuotes($propertyKey); - $errSchemaPath = it.errSchemaPath + '/required'; - }} - {{# def.error:'required' }} - {{ $errSchemaPath = $currErrSchemaPath; }} - {{ it.errorPath = $currentErrorPath; }} + {{=$nextValid}} = true; } else { {{??}} - {{? $breakOnError }} - if ({{# def.noPropertyInData }}) { - {{=$nextValid}} = true; - } else { - {{??}} - if ({{=$useData}} !== undefined - {{? $ownProperties }} - && {{# def.isOwnProperty }} - {{?}} - ) { - {{?}} + if ({{=$useData}} !== undefined + {{? $ownProperties }} + && {{# def.isOwnProperty }} + {{?}} + ) { {{?}} {{= $code }} diff --git a/lib/dot/required.jst b/lib/dot/required.jst index ac5956f1af..e2814f71ac 100644 --- a/lib/dot/required.jst +++ b/lib/dot/required.jst @@ -23,27 +23,10 @@ Object.prototype.hasOwnProperty.call({{=$data}}, {{=$vSchema}}[{{=$i}}]) #}} - -{{? !$isData }} - {{? $schema.length < it.opts.loopRequired && - it.schema.properties && Object.keys(it.schema.properties).length }} - {{ var $required = []; }} - {{~ $schema:$property }} - {{ var $propertySch = it.schema.properties[$property]; }} - {{? !($propertySch && {{# def.nonEmptySchema:$propertySch}}) }} - {{ $required[$required.length] = $property; }} - {{?}} - {{~}} - {{??}} - {{ var $required = $schema; }} - {{?}} -{{?}} - - -{{? $isData || $required.length }} +{{? $isData || $schema.length }} {{ var $currentErrorPath = it.errorPath - , $loopRequired = $isData || $required.length >= it.opts.loopRequired + , $loopRequired = $isData || $schema.length >= it.opts.loopRequired , $ownProperties = it.opts.ownProperties; }} @@ -68,7 +51,7 @@ {{# def.checkError:'required' }} else { {{??}} - if ({{# def.checkMissingProperty:$required }}) { + if ({{# def.checkMissingProperty:$schema }}) { {{# def.errorMissingProperty:'required' }} } else { {{?}} @@ -92,7 +75,7 @@ {{? $isData }} } {{?}} {{??}} - {{~ $required:$propertyKey }} + {{~ $schema:$propertyKey }} {{# def.allErrorsMissingProperty:'required' }} {{~}} {{?}} From 98443ce1ef3c34fe19163c947a23c682a4e2c936 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 21 Aug 2020 10:25:25 -0400 Subject: [PATCH 074/322] refactor: "required" keyword to typescript --- lib/compile/rules.ts | 2 +- lib/dot/errors.def | 3 - lib/dot/required.jst | 90 ---------------- lib/dotjs/index.js | 1 - lib/keyword.ts | 4 +- lib/vocabularies/applicator/dependencies.ts | 6 +- lib/vocabularies/applicator/index.ts | 1 - lib/vocabularies/missing.ts | 5 +- lib/vocabularies/util.ts | 32 ++++-- lib/vocabularies/validation/index.ts | 2 +- lib/vocabularies/validation/required.ts | 107 ++++++++++++++++++++ spec/errors.spec.js | 39 +++++++ 12 files changed, 180 insertions(+), 112 deletions(-) delete mode 100644 lib/dot/required.jst create mode 100644 lib/vocabularies/validation/required.ts diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index a93d35bd7c..f795c6d578 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -56,7 +56,7 @@ export default function rules(): ValidationRules { {type: "array", rules: []}, { type: "object", - rules: ["required", {properties: ["additionalProperties", "patternProperties"]}], + rules: [{properties: ["additionalProperties", "patternProperties"]}], }, {rules: ["$ref"]}, ], diff --git a/lib/dot/errors.def b/lib/dot/errors.def index b2fde26457..43a3022e00 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -92,7 +92,6 @@ $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", additionalProperties: "'should NOT have additional properties'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", - required: "'should have required property \\'{{=$missingProperty}}\\''", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", switch: "'should pass \"switch\" keyword validation'", @@ -107,7 +106,6 @@ $ref: "{{=it.util.toQuotedString($schema)}}", additionalProperties: "false", format: "{{#def.schemaRefOrQS}}", - required: "validate.schema{{=$schemaPath}}", custom: "validate.schema{{=$schemaPath}}", patternRequired: "validate.schema{{=$schemaPath}}", switch: "validate.schema{{=$schemaPath}}", @@ -122,7 +120,6 @@ $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", format: "{ format: {{#def.schemaValueQS}} }", - required: "{ missingProperty: '{{=$missingProperty}}' }", custom: "{ keyword: '{{=$rule.keyword}}' }", patternRequired: "{ missingPattern: '{{=$missingPattern}}' }", switch: "{ caseIndex: {{=$caseIndex}} }", diff --git a/lib/dot/required.jst b/lib/dot/required.jst deleted file mode 100644 index e2814f71ac..0000000000 --- a/lib/dot/required.jst +++ /dev/null @@ -1,90 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.missing }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{ var $vSchema = 'schema' + $lvl; }} - -{{## def.setupLoop: - {{? !$isData }} - var {{=$vSchema}} = validate.schema{{=$schemaPath}}; - {{?}} - - {{ - var $i = 'i' + $lvl - , $propertyPath = 'schema' + $lvl + '[' + $i + ']' - , $missingProperty = '\' + ' + $propertyPath + ' + \''; - }} -#}} - - -{{## def.isRequiredOwnProperty: - Object.prototype.hasOwnProperty.call({{=$data}}, {{=$vSchema}}[{{=$i}}]) -#}} - -{{? $isData || $schema.length }} - {{ - var $currentErrorPath = it.errorPath - , $loopRequired = $isData || $schema.length >= it.opts.loopRequired - , $ownProperties = it.opts.ownProperties; - }} - - {{? $breakOnError }} - var missing{{=$lvl}}; - {{? $loopRequired }} - {{# def.setupLoop }} - var {{=$valid}} = true; - - {{?$isData}}{{# def.check$dataIsArray }}{{?}} - - for (var {{=$i}} = 0; {{=$i}} < {{=$vSchema}}.length; {{=$i}}++) { - {{=$valid}} = {{=$data}}[{{=$vSchema}}[{{=$i}}]] !== undefined - {{? $ownProperties }} - && {{# def.isRequiredOwnProperty }} - {{?}}; - if (!{{=$valid}}) break; - } - - {{? $isData }} } {{?}} - - {{# def.checkError:'required' }} - else { - {{??}} - if ({{# def.checkMissingProperty:$schema }}) { - {{# def.errorMissingProperty:'required' }} - } else { - {{?}} - {{??}} - {{? $loopRequired }} - {{# def.setupLoop }} - {{? $isData }} - if ({{=$vSchema}} && !Array.isArray({{=$vSchema}})) { - {{# def.addError:'required' }} - } else if ({{=$vSchema}} !== undefined) { - {{?}} - - for (var {{=$i}} = 0; {{=$i}} < {{=$vSchema}}.length; {{=$i}}++) { - if ({{=$data}}[{{=$vSchema}}[{{=$i}}]] === undefined - {{? $ownProperties }} - || !{{# def.isRequiredOwnProperty }} - {{?}}) { - {{# def.addError:'required' }} - } - } - - {{? $isData }} } {{?}} - {{??}} - {{~ $schema:$propertyKey }} - {{# def.allErrorsMissingProperty:'required' }} - {{~}} - {{?}} - {{?}} - - {{ it.errorPath = $currentErrorPath; }} - -{{?? $breakOnError }} - if (true) { -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index da09003451..b9d5655edc 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -5,5 +5,4 @@ module.exports = { $ref: require("./ref"), format: require("./format"), properties: require("./properties"), - required: require("./required"), } diff --git a/lib/keyword.ts b/lib/keyword.ts index 2bc2021694..356624abf3 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -60,11 +60,11 @@ export function addKeyword( /* eslint no-shadow: 0 */ const RULES: ValidationRules = this.RULES if (RULES.keywords[keyword]) { - throw new Error("Keyword " + keyword + " is already defined") + throw new Error(`Keyword ${keyword} is already defined`) } if (!IDENTIFIER.test(keyword)) { - throw new Error("Keyword " + keyword + " is not a valid identifier") + throw new Error(`Keyword ${keyword} is not a valid identifier`) } // TODO any diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index c2ade101d4..7d9dd4a7c5 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,6 +1,6 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema, quotedString, propertyInData} from "../util" -import {applySubschema} from "../../compile/subschema" +import {applySubschema, Expr} from "../../compile/subschema" import {escapeQuotes} from "../../compile/util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" @@ -47,7 +47,7 @@ const def: KeywordDefinition = { for (const prop in propertyDeps) { const deps = propertyDeps[prop] if (deps.length === 0) continue - const hasProperty = propertyInData(data, prop, it.opts.ownProperties) + const hasProperty = propertyInData(data, prop, Expr.Const, it.opts.ownProperties) errorParams({ property: prop, depsCount: "" + deps.length, @@ -75,7 +75,7 @@ const def: KeywordDefinition = { for (const prop in schemaDeps) { if (alwaysValidSchema(it, schemaDeps[prop])) continue gen.if( - propertyInData(data, prop, it.opts.ownProperties), + propertyInData(data, prop, Expr.Const, it.opts.ownProperties), () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), () => gen.code(`var ${valid} = true;`) // TODO refactor var ) diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 64b03ab357..4e75a9ed05 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -5,7 +5,6 @@ const applicator: Vocabulary = [ require("./items"), require("./contains"), // object - // require("./required"), require("./dependencies"), require("./propertyNames"), // require("./properties"), diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 8a281a1771..ff2f92716e 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,6 +1,7 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {noPropertyInData, quotedString} from "./util" import {reportError} from "../compile/errors" +import {Expr} from "../compile/subschema" export function checkReportMissingProp( cxt: KeywordContext, @@ -13,7 +14,7 @@ export function checkReportMissingProp( data, it: {opts}, } = cxt - gen.if(noPropertyInData(data, prop, opts.ownProperties), () => { + gen.if(noPropertyInData(data, prop, Expr.Const, opts.ownProperties), () => { errorParams({missingProperty: quotedString(prop)}, true) reportError(cxt, error) }) @@ -26,7 +27,7 @@ export function checkMissingProp( ): string { return properties .map((prop) => { - const hasNoProp = noPropertyInData(data, prop, opts.ownProperties) + const hasNoProp = noPropertyInData(data, prop, Expr.Const, opts.ownProperties) return `(${hasNoProp} && (${missing} = ${quotedString(prop)}))` }) .reduce((cond, part) => `${cond} || ${part}`) diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index d4bf709374..8b63ad0b32 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,5 +1,6 @@ import {getProperty, schemaHasRules} from "../compile/util" import {CompilationContext} from "../types" +import {Expr} from "../compile/subschema" export function appendSchema( schemaCode: string | number | boolean, @@ -63,18 +64,33 @@ export function alwaysValidSchema( : !schemaHasRules(schema, RULES.all) } -export function isOwnProperty(data: string, property: string): string { - return `Object.prototype.hasOwnProperty.call(${data}, ${quotedString(property)})` +export function isOwnProperty(data: string, property: string, expr: Expr): string { + const prop = expr === Expr.Const ? quotedString(property) : property + return `Object.prototype.hasOwnProperty.call(${data}, ${prop})` } -export function propertyInData(data: string, propertry: string, ownProperties?: boolean): string { - let cond = `${data}${getProperty(propertry)} !== undefined` - if (ownProperties) cond += ` && ${isOwnProperty(data, propertry)}` +export function propertyInData( + data: string, + property: string, + expr: Expr, + ownProperties?: boolean +): string { + let cond = `${data}${accessProperty(property, expr)} !== undefined` + if (ownProperties) cond += ` && ${isOwnProperty(data, property, expr)}` return cond } -export function noPropertyInData(data: string, propertry: string, ownProperties?: boolean): string { - let cond = `${data}${getProperty(propertry)} === undefined` - if (ownProperties) cond += ` || !${isOwnProperty(data, propertry)}` +export function noPropertyInData( + data: string, + property: string, + expr: Expr, + ownProperties?: boolean +): string { + let cond = `${data}${accessProperty(property, expr)} === undefined` + if (ownProperties) cond += ` || !${isOwnProperty(data, property, expr)}` return cond } + +function accessProperty(property: string | number, expr: Expr): string { + return expr === Expr.Const ? getProperty(property) : `[${property}]` +} diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 8ddf97242e..36ccfc2ce1 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -9,7 +9,7 @@ const validation: Vocabulary = [ require("./pattern"), // object require("./limitProperties"), - // require("./required"), + require("./required"), // array require("./limitItems"), require("./uniqueItems"), diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts new file mode 100644 index 0000000000..1bed5aafb1 --- /dev/null +++ b/lib/vocabularies/validation/required.ts @@ -0,0 +1,107 @@ +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {propertyInData, noPropertyInData} from "../util" +import {Expr} from "../../compile/subschema" +import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" +import {reportError} from "../../compile/errors" + +const error: KeywordErrorDefinition = { + message: ({params: {missingProperty}}) => { + return missingProperty + ? `"should have required property '" + ${missingProperty} + "'"` // TODO missingProperty can be string constant + : `'"required" keyword value must be array'` + }, + params: ({params: {missingProperty}}) => + missingProperty ? `{missingProperty: ${missingProperty}}` : "{}", +} + +const def: KeywordDefinition = { + keyword: "required", + type: "object", + schemaType: ["array"], + $data: true, + code(cxt) { + const {gen, ok, fail, errorParams, schema, schemaCode, data, $data, it} = cxt + if (!$data && schema.length === 0) { + ok() + return + } + + const loopRequired = $data || schema.length >= it.opts.loopRequired + + if (it.allErrors) allErrorsMode() + else exitOnErrorMode() + + function allErrorsMode(): void { + if (loopRequired) { + if ($data) { + gen.if(`${schemaCode} && !Array.isArray(${schemaCode})`) + reportError(cxt, error) + gen.elseIf(`${schemaCode} !== undefined`) + loopAllRequired() + gen.endIf() + } else { + loopAllRequired() + } + } else { + for (const prop of schema) { + checkReportMissingProp(cxt, prop, error) + } + } + } + + function exitOnErrorMode(): void { + const missing = gen.name("missing") + gen.code(`let ${missing};`) + errorParams({missingProperty: missing}) + + if (loopRequired) { + const valid = gen.name("valid") + gen.code(`let ${valid} = true;`) + + // TODO refactor and enable/fix test in errors.spec.js line 301 + // it can be simpler once blocks are globally supported - endIf can be removed, so there will be 2 open blocks + if ($data) { + gen + .if(`${schemaCode} === undefined`) + .code(`${valid} = true;`) + .elseIf(`!Array.isArray(${schemaCode})`) + .code(`${valid} = false;`) + .else() + loopUntilMissing(missing, valid) + gen.endIf() + } else { + loopUntilMissing(missing, valid) + } + + fail(`!${valid}`) + } else { + // TODO refactor ifs + gen.code(`if (${checkMissingProp(cxt, schema, missing)}) {`) + reportMissingProp(cxt, missing, error) + gen.code(`} else {`) + } + } + + function loopAllRequired(): void { + const prop = gen.name("prop") + errorParams({missingProperty: prop}) + gen.for(`const ${prop} of ${schemaCode}`, () => + gen.if(noPropertyInData(data, prop, Expr.Str, it.opts.ownProperties), () => + reportError(cxt, error) + ) + ) + } + + function loopUntilMissing(missing: string, valid: string): void { + gen.for(`${missing} of ${schemaCode}`, () => + gen.code( + `${valid} = ${propertyInData(data, missing, Expr.Str, it.opts.ownProperties)}; + if (!${valid}) break;` + ) + ) + } + }, + error, +} + +module.exports = def diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 5385f0e355..edce57e602 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -296,6 +296,45 @@ describe("Validation errors", () => { validate({requiredProperties: ["foo", "bar"]}).should.equal(false) validate.errors.should.have.length(2) }) + + it("should show different error when required is $data of incorrect type", () => { + // test(new Ajv({$data: true})) + test(new Ajv({$data: true, allErrors: true})) + + function test(_ajv) { + const schema = { + required: {$data: "0/req"}, + properties: { + req: {}, + foo: {}, + bar: {}, + }, + } + + const validate = _ajv.compile(schema) + + shouldBeValid(validate, {req: ["foo", "bar"], foo: 1, bar: 2}) + shouldBeInvalid(validate, {req: ["foo", "bar"], foo: 1}) + shouldBeError( + validate.errors[0], + "required", + "#/required", + "", + "should have required property 'bar'", + {missingProperty: "bar"} + ) + + shouldBeInvalid(validate, {req: "invalid"}) + shouldBeError( + validate.errors[0], + "required", + "#/required", + "", + '"required" keyword value must be array', + {} + ) + } + }) }) describe('"dependencies" errors', () => { From da748716d4a0f0810fc288f9ef929b45e18a73f1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 21 Aug 2020 15:34:03 -0400 Subject: [PATCH 075/322] refactor: "patternProperties" to typescript --- lib/compile/rules.ts | 2 +- lib/dot/properties.jst | 33 ----------- lib/vocabularies/applicator/dependencies.ts | 2 +- lib/vocabularies/applicator/index.ts | 1 + .../applicator/patternProperties.ts | 58 +++++++++++++++++++ lib/vocabularies/applicator/propertyNames.ts | 11 ++-- lib/vocabularies/util.ts | 26 +++++---- 7 files changed, 80 insertions(+), 53 deletions(-) create mode 100644 lib/vocabularies/applicator/patternProperties.ts diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index f795c6d578..7674ed0aac 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -56,7 +56,7 @@ export default function rules(): ValidationRules { {type: "array", rules: []}, { type: "object", - rules: [{properties: ["additionalProperties", "patternProperties"]}], + rules: [{properties: ["additionalProperties"]}], }, {rules: ["$ref"]}, ], diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 836a7ad631..fc3630edbf 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -177,39 +177,6 @@ var {{=$nextValid}} = true; {{~}} {{?}} -{{? $pPropertyKeys.length }} - {{~ $pPropertyKeys:$pProperty }} - {{ var $sch = $pProperties[$pProperty]; }} - - {{? {{# def.nonEmptySchema:$sch}} }} - {{ - $it.schema = $sch; - $it.schemaPath = it.schemaPath + '.patternProperties' + it.util.getProperty($pProperty); - $it.errSchemaPath = it.errSchemaPath + '/patternProperties/' - + it.util.escapeFragment($pProperty); - }} - - {{# def.iterateProperties }} - if ({{= it.usePattern($pProperty) }}.test({{=$key}})) { - {{ - $it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); - var $passData = $data + '[' + $key + ']'; - $it.dataPathArr[$dataNxt] = $key; - }} - - {{# def.generateSubschemaCode }} - {{# def.optimizeValidate }} - - {{? $breakOnError }} if (!{{=$nextValid}}) break; {{?}} - } - {{? $breakOnError }} else {{=$nextValid}} = true; {{?}} - } - - {{# def.ifResultValid }} - {{?}} {{ /* def.nonEmptySchema */ }} - {{~}} -{{?}} - {{? $breakOnError }} {{= $closingBraces }} diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 7d9dd4a7c5..aa12bcc162 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -14,7 +14,7 @@ interface SchemaDependencies { const def: KeywordDefinition = { keyword: "dependencies", type: "object", - schemaType: ["object"], + schemaType: "object", code(cxt) { const {gen, errorParams, schema, data, it} = cxt diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 4e75a9ed05..691cfbed43 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -8,6 +8,7 @@ const applicator: Vocabulary = [ require("./dependencies"), require("./propertyNames"), // require("./properties"), + require("./patternProperties"), // any require("./not"), require("./anyOf"), diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts new file mode 100644 index 0000000000..1d7f412b59 --- /dev/null +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -0,0 +1,58 @@ +import {KeywordDefinition} from "../../types" +import {schemaProperties, loopPropertiesCode} from "../util" +import {applySubschema, Expr} from "../../compile/subschema" + +const def: KeywordDefinition = { + keyword: "patternProperties", + type: "object", + schemaType: "object", + code(cxt) { + const {gen, ok, schema, it} = cxt + const patterns = schemaProperties(it, schema) + if (patterns.length === 0) { + ok() + return + } + + const valid = gen.name("valid") + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + + gen.block(validatePatternProperties) + + // TODO refactor ifs + if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + + function validatePatternProperties() { + for (const pat of patterns) { + if (it.allErrors) { + validateProperties(pat) + } else { + gen.code(`var ${valid} = true`) // TODO var + validateProperties(pat) + gen.if(valid) + } + } + } + + function validateProperties(pat: string) { + loopPropertiesCode(cxt, (key) => { + gen.if(`${it.usePattern(pat)}.test(${key})`, () => { + applySubschema( + it, + { + keyword: "patternProperties", + schemaProp: pat, + dataProp: key, + expr: Expr.Str, + }, + valid + ) + if (!it.allErrors) gen.if(`!${valid}`, () => gen.code("break;")) + }) + }) + } + }, +} + +module.exports = def diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index b914e84a2a..e33ff80dab 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,5 +1,5 @@ import {KeywordDefinition, KeywordErrorDefinition} from "../../types" -import {alwaysValidSchema} from "../util" +import {alwaysValidSchema, loopPropertiesCode} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError} from "../../compile/errors" @@ -8,21 +8,18 @@ const def: KeywordDefinition = { type: "object", schemaType: ["object", "boolean"], code(cxt) { - const {gen, ok, errorParams, schema, data, it} = cxt + const {gen, ok, errorParams, schema, it} = cxt if (alwaysValidSchema(it, schema)) { ok() return } const valid = gen.name("valid") - const key = gen.name("key") const errsCount = gen.name("_errs") - errorParams({propertyName: key}) gen.code(`const ${errsCount} = errors;`) - // TODO maybe always iterate own properties in v7? - const iteration = it.opts.ownProperties ? `of Object.keys(${data})` : `in ${data}` - gen.for(`const ${key} ${iteration}`, () => { + loopPropertiesCode(cxt, (key) => { + errorParams({propertyName: key}) applySubschema( it, {keyword: "propertyNames", data: key, propertyName: key, compositeRule: true}, diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 8b63ad0b32..e70b83da54 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,5 +1,5 @@ import {getProperty, schemaHasRules} from "../compile/util" -import {CompilationContext} from "../types" +import {CompilationContext, KeywordContext} from "../types" import {Expr} from "../compile/subschema" export function appendSchema( @@ -43,16 +43,6 @@ export function schemaRefOrVal( return `validate.schema${schemaPath + getProperty(keyword)}` } -// TODO remove -// export function nonEmptySchema( -// {RULES, opts: {strictKeywords}}: CompilationContext, -// schema: boolean | object -// ): boolean | void { -// return strictKeywords -// ? (typeof schema == "object" && Object.keys(schema).length > 0) || schema === false -// : schemaHasRules(schema, RULES.all) -// } - export function alwaysValidSchema( {RULES, opts: {strictKeywords}}: CompilationContext, schema: boolean | object @@ -64,6 +54,10 @@ export function alwaysValidSchema( : !schemaHasRules(schema, RULES.all) } +export function schemaProperties(it: CompilationContext, schema: object): string[] { + return Object.keys(schema).filter((p) => p !== "__proto__" && !alwaysValidSchema(it, schema[p])) +} + export function isOwnProperty(data: string, property: string, expr: Expr): string { const prop = expr === Expr.Const ? quotedString(property) : property return `Object.prototype.hasOwnProperty.call(${data}, ${prop})` @@ -94,3 +88,13 @@ export function noPropertyInData( function accessProperty(property: string | number, expr: Expr): string { return expr === Expr.Const ? getProperty(property) : `[${property}]` } + +export function loopPropertiesCode( + {gen, data, it}: KeywordContext, + loopBody: (key: string) => void +) { + // TODO maybe always iterate own properties in v7? + const key = gen.name("key") + const iteration = it.opts.ownProperties ? `of Object.keys(${data})` : `in ${data}` + gen.for(`const ${key} ${iteration}`, () => loopBody(key)) +} From b1da85fd796da5132904d8d71419f0808fd5f503 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 21 Aug 2020 15:59:26 -0400 Subject: [PATCH 076/322] refactor(codegen): allow passing string as block body --- lib/compile/codegen.ts | 43 ++++++++----------- lib/vocabularies/applicator/contains.ts | 2 +- lib/vocabularies/applicator/dependencies.ts | 2 +- .../applicator/patternProperties.ts | 2 +- lib/vocabularies/validation/enum.ts | 11 ++--- lib/vocabularies/validation/required.ts | 7 ++- 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index df07fac100..6ef7fbfe22 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -4,6 +4,8 @@ enum Block { For, } +type Code = string | (() => void) + export default class CodeGen { #names: {[key: string]: number} = {} // TODO make private. Possibly stack? @@ -17,23 +19,20 @@ export default class CodeGen { return `${prefix}_${num}` } - code(str?: string): CodeGen { + code(c?: Code): CodeGen { // TODO optionally strip whitespace - if (str) this._out += str + "\n" + if (typeof c == "function") c() + else if (c) this._out += c + "\n" return this } - if(condition: string, thenBody?: () => void, elseBody?: () => void): CodeGen { + if(condition: string, thenBody?: Code, elseBody?: Code): CodeGen { this.#blocks.push(Block.If) this.code(`if(${condition}){`) if (thenBody && elseBody) { - thenBody() - this.else() - elseBody() - this.endIf() + this.code(thenBody).else().code(elseBody).endIf() } else if (thenBody) { - thenBody() - this.endIf() + this.code(thenBody).endIf() } else if (elseBody) { throw new Error("CodeGen: else body without then body") } @@ -62,13 +61,10 @@ export default class CodeGen { return this } - for(iteration: string, forBody?: () => void): CodeGen { + for(iteration: string, forBody?: Code): CodeGen { this.#blocks.push(Block.For) this.code(`for(${iteration}){`) - if (forBody) { - forBody() - this.endFor() - } + if (forBody) this.code(forBody).endFor() return this } @@ -80,12 +76,9 @@ export default class CodeGen { return this } - block(body?: () => void, expectedToClose?: number): CodeGen { + block(body?: Code, expectedToClose?: number): CodeGen { this.#blockStarts.push(this.#blocks.length) - if (body) { - body() - this.endBlock(expectedToClose) - } + if (body) this.code(body).endBlock(expectedToClose) return this } @@ -103,14 +96,16 @@ export default class CodeGen { } get _lastBlock(): Block { - const len = this.#blocks.length - if (len === 0) throw new Error("CodeGen: not in block") - return this.#blocks[len - 1] + return this.#blocks[this._last()] } set _lastBlock(b: Block) { + this.#blocks[this._last()] = b + } + + _last() { const len = this.#blocks.length - if (len === 0) throw new Error('CodeGen: not in "if" block') - this.#blocks[len - 1] = b + if (len === 0) throw new Error("CodeGen: not in block") + return len - 1 } } diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index df095cec8d..235175ce4b 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -31,7 +31,7 @@ const def: KeywordDefinition = { }, valid ) - gen.code(`if (${valid}) break;`) + gen.if(valid, "break") }) // TODO refactor failCompoundOrReset? It is different from anyOf though diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index aa12bcc162..957f91769d 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -77,7 +77,7 @@ const def: KeywordDefinition = { gen.if( propertyInData(data, prop, Expr.Const, it.opts.ownProperties), () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), - () => gen.code(`var ${valid} = true;`) // TODO refactor var + `var ${valid} = true;` // TODO refactor var ) if (!it.allErrors) gen.if(valid) } diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 1d7f412b59..2c100ed4f6 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -48,7 +48,7 @@ const def: KeywordDefinition = { }, valid ) - if (!it.allErrors) gen.if(`!${valid}`, () => gen.code("break;")) + if (!it.allErrors) gen.if(`!${valid}`, "break") }) }) } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 7a590d9950..ae64c7105d 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -9,13 +9,10 @@ const def: KeywordDefinition = { if ($data) { const valid = gen.name("valid") gen.code(`let ${valid};`) - gen.if( - `${schemaCode} === undefined`, - () => gen.code(`${valid} = true;`), - () => - gen - .code(`${valid} = false;`) - .if(`Array.isArray(${schemaCode})`, () => loopEnum(schemaCode, valid)) + gen.if(`${schemaCode} === undefined`, `${valid} = true;`, () => + gen + .code(`${valid} = false;`) + .if(`Array.isArray(${schemaCode})`, () => loopEnum(schemaCode, valid)) ) fail(`!${valid}`) } else { diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 1bed5aafb1..505984862a 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -94,10 +94,9 @@ const def: KeywordDefinition = { function loopUntilMissing(missing: string, valid: string): void { gen.for(`${missing} of ${schemaCode}`, () => - gen.code( - `${valid} = ${propertyInData(data, missing, Expr.Str, it.opts.ownProperties)}; - if (!${valid}) break;` - ) + gen + .code(`${valid} = ${propertyInData(data, missing, Expr.Str, it.opts.ownProperties)};`) + .if(`!${valid}`, "break") ) } }, From 448c9b84af823c84f2791c5b96a185a013fdf8fe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 21 Aug 2020 16:14:38 -0400 Subject: [PATCH 077/322] refactor: remove unused function --- lib/compile/util.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 798a4cd344..0abef12492 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -2,8 +2,6 @@ module.exports = { checkDataType, checkDataTypes, - // TODO remove when validate is refactored - coerceToTypes, toHash, escapeQuotes, varOccurrences, @@ -73,26 +71,6 @@ export function checkDataTypes( return code } -// TODO remove when validate is refactored -const COERCE_TYPES = toHash(["string", "number", "integer", "boolean", "null"]) -export function coerceToTypes( - optionCoerceTypes: undefined | boolean | "array", - dataTypes: string[] -): string[] | void { - if (Array.isArray(dataTypes)) { - const types: string[] = [] - for (const t of dataTypes) { - if (COERCE_TYPES[t] || (optionCoerceTypes === "array" && t === "array")) { - types.push(t) - } - } - if (types.length) return types - return - } - if (COERCE_TYPES[dataTypes]) return [dataTypes] - if (optionCoerceTypes === "array" && dataTypes === "array") return ["array"] -} - export function toHash(arr: string[]): {[key: string]: true} { const hash = {} for (const item of arr) hash[item] = true From aa4be0b1d52efcb9e6fcb6877840e5eb0585b11b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 22 Aug 2020 05:59:33 -0400 Subject: [PATCH 078/322] refactor: codegen in enum, required, uniqueItems --- lib/compile/codegen.ts | 2 +- lib/vocabularies/util.ts | 2 +- lib/vocabularies/validation/enum.ts | 29 ++++-------- lib/vocabularies/validation/required.ts | 23 ++++----- lib/vocabularies/validation/uniqueItems.ts | 55 +++++++++------------- 5 files changed, 44 insertions(+), 67 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 6ef7fbfe22..ca2f6020a4 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -103,7 +103,7 @@ export default class CodeGen { this.#blocks[this._last()] = b } - _last() { + _last(): number { const len = this.#blocks.length if (len === 0) throw new Error("CodeGen: not in block") return len - 1 diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index e70b83da54..7b86bef6d8 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -92,7 +92,7 @@ function accessProperty(property: string | number, expr: Expr): string { export function loopPropertiesCode( {gen, data, it}: KeywordContext, loopBody: (key: string) => void -) { +): void { // TODO maybe always iterate own properties in v7? const key = gen.name("key") const iteration = it.opts.ownProperties ? `of Object.keys(${data})` : `in ${data}` diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index ae64c7105d..f38e20a706 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -10,38 +10,29 @@ const def: KeywordDefinition = { const valid = gen.name("valid") gen.code(`let ${valid};`) gen.if(`${schemaCode} === undefined`, `${valid} = true;`, () => - gen - .code(`${valid} = false;`) - .if(`Array.isArray(${schemaCode})`, () => loopEnum(schemaCode, valid)) + gen.code(`${valid} = false;`).if(`Array.isArray(${schemaCode})`, () => loopEnum(valid)) ) fail(`!${valid}`) } else { if (schema.length === 0) throw new Error("enum must have non-empty array") - const vSchema = gen.name("schema") - gen.code(`const ${vSchema} = ${schemaCode};`) if (schema.length > (opts.loopEnum as number)) { const valid = gen.name("valid") gen.code(`let ${valid} = false;`) - loopEnum(vSchema, valid) + loopEnum(valid) fail(`!${valid}`) } else { - const cond: string = schema.reduce( - (c, _, i) => (c += (c && "||") + equalCode(vSchema, i)), - "" - ) + const vSchema = gen.name("schema") + gen.code(`const ${vSchema} = ${schemaCode};`) + const cond: string = schema + .map((_, i: number) => equalCode(vSchema, i)) + .reduce((acc: string, eq: string) => `${acc} || ${eq}`) fail(`!(${cond})`) } } - function loopEnum(sch: string, valid: string): void { - // TODO trim whitespace - gen.code( - `for (const v of ${sch}) { - if (equal(${data}, v)) { - ${valid} = true; - break; - } - }` + function loopEnum(valid: string): void { + gen.for(`const v of ${schemaCode}`, () => + gen.if(`equal(${data}, v)`, `${valid} = true; break;`) ) } diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 505984862a..eab35544ed 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -34,11 +34,11 @@ const def: KeywordDefinition = { function allErrorsMode(): void { if (loopRequired) { if ($data) { - gen.if(`${schemaCode} && !Array.isArray(${schemaCode})`) - reportError(cxt, error) - gen.elseIf(`${schemaCode} !== undefined`) - loopAllRequired() - gen.endIf() + gen.if( + `${schemaCode} && !Array.isArray(${schemaCode})`, + () => reportError(cxt, error), + () => gen.if(`${schemaCode} !== undefined`, loopAllRequired) + ) } else { loopAllRequired() } @@ -61,14 +61,11 @@ const def: KeywordDefinition = { // TODO refactor and enable/fix test in errors.spec.js line 301 // it can be simpler once blocks are globally supported - endIf can be removed, so there will be 2 open blocks if ($data) { - gen - .if(`${schemaCode} === undefined`) - .code(`${valid} = true;`) - .elseIf(`!Array.isArray(${schemaCode})`) - .code(`${valid} = false;`) - .else() - loopUntilMissing(missing, valid) - gen.endIf() + gen.if(`${schemaCode} === undefined`, `${valid} = true`, () => + gen.if(`!Array.isArray(${schemaCode})`, `${valid} = false`, () => + loopUntilMissing(missing, valid) + ) + ) } else { loopUntilMissing(missing, valid) } diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 6dd5f0263b..4d02488a78 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -16,14 +16,9 @@ const def: KeywordDefinition = { const itemType = parentSchema.items?.type if ($data) { - gen - .if(`${schemaCode} === false || ${schemaCode} === undefined`) - .code(`${valid} = true;`) - .elseIf(`typeof ${schemaCode} != "boolean"`) - .code(`${valid} = false;`) - .else() - validateUniqueItems() - gen.endIf() + gen.if(`${schemaCode} === false || ${schemaCode} === undefined`, `${valid} = true`, () => + gen.if(`typeof ${schemaCode} != "boolean"`, `${valid} = false`, validateUniqueItems) + ) } else { validateUniqueItems() } @@ -52,34 +47,28 @@ const def: KeywordDefinition = { true ) const indices = gen.name("indices") - gen.code( - `const ${indices} = {}; - for (;${i}--;) { - let item = ${data}[${i}]; - if (${wrongType}) continue; - ${Array.isArray(itemType) ? 'if (typeof item == "string") item += "_";' : ""} - if (typeof ${indices}[item] == "number") { - ${valid} = false; - ${j} = ${indices}[item]; - break; - } - ${indices}[item] = ${i}; - }` - ) + gen.code(`const ${indices} = {};`) + gen.for(`;${i}--;`, () => { + gen.code(`let item = ${data}[${i}];`) + gen.if(wrongType, "continue") + if (Array.isArray(itemType)) gen.if('typeof item == "string"', 'item += "_"') + gen + .if( + `typeof ${indices}[item] == "number"`, + `${valid} = false; ${j} = ${indices}[item]; break;` + ) + .code(`${indices}[item] = ${i};`) + }) } function loopN2(): void { - gen.code( - `outer: - for (;${i}--;) { - for (${j} = ${i}; ${j}--;) { - if (equal(${data}[${i}], ${data}[${j}])) { - ${valid} = false; - break outer; - } - } - }` - ) + gen + .code(`outer:`) + .for(`;${i}--;`, () => + gen.for(`${j} = ${i}; ${j}--;`, () => + gen.if(`equal(${data}[${i}], ${data}[${j}])`, `${valid} = false; break outer;`) + ) + ) } }, error: { From 1ad8b6a8e2ce1c0c7f79eb563e0bf8042533afec Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 22 Aug 2020 09:11:44 -0400 Subject: [PATCH 079/322] refactor: "properties" keyword to typescript (WIP - tests fail) --- lib/compile/rules.ts | 2 +- lib/compile/subschema.ts | 9 +++- lib/dot/properties.jst | 59 +---------------------- lib/dotjs/index.js | 2 +- lib/vocabularies/applicator/if.ts | 1 + lib/vocabularies/applicator/index.ts | 2 +- lib/vocabularies/applicator/items.ts | 4 ++ lib/vocabularies/applicator/properties.ts | 59 +++++++++++++++++++++++ spec/options/options_refs.spec.js | 2 +- 9 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 lib/vocabularies/applicator/properties.ts diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 7674ed0aac..6ecfba4167 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -56,7 +56,7 @@ export default function rules(): ValidationRules { {type: "array", rules: []}, { type: "object", - rules: [{properties: ["additionalProperties"]}], + rules: ["additionalProperties"], }, {rules: ["$ref"]}, ], diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index bfcc299a6f..3c390431a1 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,6 +1,7 @@ import {CompilationContext} from "../types" import validateCode from "./validate" import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" +import {quotedString} from "../vocabularies/util" export interface SubschemaContext { schema: any @@ -60,11 +61,15 @@ export function applySubschema( expr === Expr.Const ? getPath(errorPath, dataProp, opts.jsonPointers) : getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num), - dataPathArr: [...dataPathArr, dataProp], + dataPathArr: [ + ...dataPathArr, + expr === Expr.Const && typeof dataProp == "string" ? quotedString(dataProp) : dataProp, + ], dataLevel: nextLevel, }) - const passDataProp = Expr.Const ? getProperty(dataProp) : `[${dataProp}]` + // TODO refactor - use accessProperty + const passDataProp = expr === Expr.Const ? getProperty(dataProp) : `[${dataProp}]` gen.code(`var data${nextLevel} = data${dataLevel || ""}${passDataProp};`) } else if (data !== undefined) { const {gen, dataLevel} = it diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index fc3630edbf..686719bcc8 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -26,10 +26,10 @@ , $nextData = 'data' + $dataNxt , $dataProperties = 'dataProperties' + $lvl; - var $schemaKeys = Object.keys($schema || {}).filter(notProto) + var $schemaKeys = Object.keys(it.schema.properties || {}).filter(notProto) , $pProperties = it.schema.patternProperties || {} , $pPropertyKeys = Object.keys($pProperties).filter(notProto) - , $aProperties = it.schema.additionalProperties + , $aProperties = $schema , $someProperties = $schemaKeys.length || $pPropertyKeys.length , $noAdditional = $aProperties === false , $additionalIsSchema = typeof $aProperties == 'object' @@ -123,61 +123,6 @@ var {{=$nextValid}} = true; {{# def.ifResultValid }} {{?}} -{{ var $useDefaults = it.opts.useDefaults && !it.compositeRule; }} - -{{? $schemaKeys.length }} - {{~ $schemaKeys:$propertyKey }} - {{ var $sch = $schema[$propertyKey]; }} - - {{? {{# def.nonEmptySchema:$sch}} }} - {{ - var $prop = it.util.getProperty($propertyKey) - , $passData = $data + $prop - , $hasDefault = $useDefaults && $sch.default !== undefined; - $it.schema = $sch; - $it.schemaPath = $schemaPath + $prop; - $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($propertyKey); - $it.errorPath = it.util.getPath(it.errorPath, $propertyKey, it.opts.jsonPointers); - $it.dataPathArr[$dataNxt] = it.util.toQuotedString($propertyKey); - }} - - {{# def.generateSubschemaCode }} - - {{? {{# def.willOptimize }} }} - {{ - $code = {{# def._optimizeValidate }}; - var $useData = $passData; - }} - {{??}} - {{ var $useData = $nextData; }} - var {{=$nextData}} = {{=$passData}}; - {{?}} - - {{? $hasDefault }} - {{= $code }} - {{??}} - {{? $breakOnError }} - if ({{# def.noPropertyInData }}) { - {{=$nextValid}} = true; - } else { - {{??}} - if ({{=$useData}} !== undefined - {{? $ownProperties }} - && {{# def.isOwnProperty }} - {{?}} - ) { - {{?}} - - {{= $code }} - } - {{?}} {{ /* $hasDefault */ }} - {{?}} {{ /* def.nonEmptySchema */ }} - - {{# def.ifResultValid }} - {{~}} -{{?}} - - {{? $breakOnError }} {{= $closingBraces }} if ({{=$errs}} == errors) { diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index b9d5655edc..9122c74ce5 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -4,5 +4,5 @@ module.exports = { $ref: require("./ref"), format: require("./format"), - properties: require("./properties"), + additionalProperties: require("./properties"), } diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index d9f0c44c91..96d0e11422 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -6,6 +6,7 @@ import {reportExtraError, resetErrorsCount} from "../../compile/errors" const def: KeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], + // TODO // implements: ["then", "else"], code(cxt) { const {gen, ok, errorParams, it} = cxt diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 691cfbed43..96b36f1d7a 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -7,7 +7,7 @@ const applicator: Vocabulary = [ // object require("./dependencies"), require("./propertyNames"), - // require("./properties"), + require("./properties"), require("./patternProperties"), // any require("./not"), diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 48f68ea6f5..cca0a07f9a 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -8,7 +8,11 @@ const def: KeywordDefinition = { type: "array", schemaType: ["object", "array", "boolean"], before: "uniqueItems", + // TODO + // implements: ["additionalItems"], code(cxt) { + // TODO strict mode: fail or warning if "additionalItems" is present without "items" + const {gen, /* fail, */ schema, parentSchema, data, it} = cxt const errsCount = gen.name("_errs") const len = gen.name("len") diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts new file mode 100644 index 0000000000..7748ce3847 --- /dev/null +++ b/lib/vocabularies/applicator/properties.ts @@ -0,0 +1,59 @@ +import {KeywordDefinition} from "../../types" +import {schemaProperties, propertyInData} from "../util" +import {applySubschema, Expr} from "../../compile/subschema" + +const def: KeywordDefinition = { + keyword: "properties", + type: "object", + schemaType: "object", + code(cxt) { + const {gen, ok, schema, data, it} = cxt + const properties = schemaProperties(it, schema) + if (properties.length === 0) { + ok() + return + } + + const valid = gen.name("valid") + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + + gen.block(validateProperties) + + // TODO refactor ifs + if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + + function validateProperties() { + for (const prop of properties) { + if (hasDefault(prop)) { + applyPropertySchema(prop) + } else { + gen.if(propertyInData(data, prop, Expr.Const, it.opts.ownProperties)) + applyPropertySchema(prop) + if (!it.allErrors) gen.else().code(`${valid} = true;`) + gen.endIf() + } + if (!it.allErrors) gen.if(valid) + } + } + + function hasDefault(prop: string): boolean | undefined { + return it.opts.useDefaults && !it.compositeRule && schema[prop].default !== undefined + } + + function applyPropertySchema(prop: string) { + applySubschema( + it, + { + keyword: "properties", + schemaProp: prop, + dataProp: prop, + expr: Expr.Const, + }, + valid + ) + } + }, +} + +module.exports = def diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js index 48f7a16b46..cddd3733f3 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.js @@ -16,7 +16,7 @@ describe("referenced schema options", () => { }) describe('= "ignore" and default', () => { - it("should ignore other keywords when $ref is used", () => { + it.only("should ignore other keywords when $ref is used", () => { test(new Ajv()) test(new Ajv({extendRefs: "ignore"}), false) }) From 5f1b66569ddae002f1f348175fac10ab25e57a03 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 22 Aug 2020 14:24:33 -0400 Subject: [PATCH 080/322] refactor: "if"s to methods, new "properties" disabled, tests pass --- lib/compile/rules.ts | 2 +- lib/compile/validate/dataType.ts | 131 ++++++++++++---------- lib/compile/validate/defaults.ts | 2 +- lib/compile/validate/keywords.ts | 7 +- lib/dot/properties.jst | 56 ++++++++- lib/dotjs/index.js | 2 +- lib/vocabularies/applicator/index.ts | 2 +- lib/vocabularies/applicator/properties.ts | 2 +- spec/options/options_refs.spec.js | 2 +- 9 files changed, 135 insertions(+), 71 deletions(-) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 6ecfba4167..7674ed0aac 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -56,7 +56,7 @@ export default function rules(): ValidationRules { {type: "array", rules: []}, { type: "object", - rules: ["additionalProperties"], + rules: [{properties: ["additionalProperties"]}], }, {rules: ["$ref"]}, ], diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 1644dc12d4..35713f65df 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -33,14 +33,14 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]): const coerceTo = coerceToTypes(types, coerceTypes) const checkTypes = types.length > 0 && - (coerceTo.length > 0 || types.length > 1 || !schemaHasRulesForType(it, types[0])) + !(coerceTo.length === 0 && types.length === 1 && schemaHasRulesForType(it, types[0])) if (checkTypes) { // TODO refactor `data${dataLevel || ""}` const wrongType = checkDataTypes(types, `data${dataLevel || ""}`, strictNumbers, true) - gen.code(`if (${wrongType}) {`) - if (coerceTo.length) coerceData(it, coerceTo) - else reportTypeError(it) - gen.code("}") + gen.if(wrongType, () => { + if (coerceTo.length) coerceData(it, coerceTo) + else reportTypeError(it) + }) } return checkTypes } @@ -52,39 +52,6 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string : [] } -interface CoerceArgs { - dataType: string - data: string - coerced: string -} - -const coerceCode: {[x: string]: (arg: CoerceArgs) => string} = { - string: ({dataType, data, coerced}) => - `else if (${dataType} == "number" || ${dataType} == "boolean") - ${coerced} = "" + ${data}; - else if (${data} === null) - ${coerced} = "";`, - number: ({dataType, data, coerced}) => - `else if (${dataType} == "boolean" || ${data} === null - || (${dataType} == "string" && ${data} && ${data} == +${data})) - ${coerced} = +${data};`, - integer: ({dataType, data, coerced}) => - `else if (${dataType} === "boolean" || ${data} === null - || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))) - ${coerced} = +${data};`, - boolean: ({data, coerced}) => - `else if (${data} === "false" || ${data} === 0 || ${data} === null) - ${coerced} = false; - else if (${data} === "true" || ${data} === 1) - ${coerced} = true;`, - null: ({data, coerced}) => - `else if (${data} === "" || ${data} === 0 || ${data} === false) - ${coerced} = null;`, - array: ({dataType, data, coerced}) => - `else if (${dataType} === "string" || ${dataType} === "number" || ${dataType} === "boolean" || ${data} === null) - ${coerced} = [${data}];`, -} - export function coerceData(it: CompilationContext, coerceTo: string[]): void { const { gen, @@ -92,48 +59,90 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { dataLevel, opts: {coerceTypes, strictNumbers}, } = it - // TODO use "data" to CompilationContext + // TODO move "data" to CompilationContext const data = `data${dataLevel || ""}` const dataType = gen.name("dataType") const coerced = gen.name("coerced") gen.code(`let ${coerced};`) gen.code(`let ${dataType} = typeof ${data};`) if (coerceTypes === "array") { - gen.code( - `if (${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1) { - ${data} = ${data}[0]; - ${dataType} = typeof ${data}; - if (${checkDataType(schema.type, data, strictNumbers)}) - ${coerced} = ${data}; - }` + gen.if(`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => + gen + .code(`${data} = ${data}[0]; ${dataType} = typeof ${data};`) + .if(`${checkDataType(schema.type, data, strictNumbers)}`, `${coerced} = ${data}`) ) } - gen.code(`if (${coerced} !== undefined) ;`) - const args: CoerceArgs = {dataType, data, coerced} + gen.if(`${coerced} !== undefined`) for (const t of coerceTo) { - if (t in coerceCode && (t !== "array" || coerceTypes === "array")) { - gen.code(coerceCode[t](args)) + if (t in COERCIBLE || (t === "array" && coerceTypes === "array")) { + coerceSpecificType(t) } } - gen.code(`else {`) + gen.else() reportTypeError(it) - gen.code(`}`) + gen.endIf() - gen.code( - `if (${coerced} !== undefined) { - ${data} = ${coerced}; - ${assignParentData(it, coerced)} - }` - ) + gen.if(`${coerced} !== undefined`, () => { + gen.code(`${data} = ${coerced};`) + assignParentData(it, coerced) + }) + + function coerceSpecificType(t) { + switch (t) { + case "string": + gen + .elseIf(`${dataType} == "number" || ${dataType} == "boolean"`) + .code(`${coerced} = "" + ${data}`) + .elseIf(`${data} === null`) + .code(`${coerced} = ""`) + return + case "number": + gen + .elseIf( + `${dataType} == "boolean" || ${data} === null + || (${dataType} == "string" && ${data} && ${data} == +${data})` + ) + .code(`${coerced} = +${data}`) + return + case "integer": + gen + .elseIf( + `${dataType} === "boolean" || ${data} === null + || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))` + ) + .code(`${coerced} = +${data}`) + return + case "boolean": + gen + .elseIf(`${data} === "false" || ${data} === 0 || ${data} === null`) + .code(`${coerced} = false`) + .elseIf(`${data} === "true" || ${data} === 1`) + .code(`${coerced} = true`) + return + case "null": + gen.elseIf(`${data} === "" || ${data} === 0 || ${data} === false`) + gen.code(`${coerced} = null`) + return + + case "array": + gen + .elseIf( + `${dataType} === "string" || ${dataType} === "number" + || ${dataType} === "boolean" || ${data} === null` + ) + .code(`${coerced} = [${data}]`) + } + } } -function assignParentData({dataLevel, dataPathArr}: CompilationContext, expr: string): string { +function assignParentData({gen, dataLevel, dataPathArr}: CompilationContext, expr: string): void { // TODO replace dataLevel if (dataLevel) { const parentData = "data" + (dataLevel - 1 || "") - return `${parentData}[${dataPathArr[dataLevel]}] = ${expr};` + gen.code(`${parentData}[${dataPathArr[dataLevel]}] = ${expr};`) + } else { + gen.if("parentData !== undefined", `parentData[parentDataProperty] = ${expr};`) } - return `if (parentData !== undefined) parentData[parentDataProperty] = ${expr};` } const typeError: KeywordErrorDefinition = { diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 51112f189c..04c1130d70 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -41,5 +41,5 @@ function assignDefault( (useDefaults === "empty" ? ` || ${data} === null || ${data} === ""` : "") // TODO remove option `useDefaults === "shared"` const defaultExpr = useDefaults === "shared" ? useDefault : JSON.stringify - gen.code(`if (${condition}) ${data} = ${defaultExpr(defaultValue)};`) + gen.if(condition, `${data} = ${defaultExpr(defaultValue)}`) } diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index 84cbbe094a..d213d536d3 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -33,13 +33,14 @@ export function schemaKeywords( if (group.type) { // TODO refactor `data${dataLevel || ""}` const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) - gen.code(`if (${checkType}) {`) + // TODO refactor ifs + gen.if(checkType) iterateKeywords(it, group) if (types.length === 1 && types[0] === group.type && typeErrors) { - gen.code(`} else {`) + gen.else() reportTypeError(it) } - gen.code("}") + gen.endIf() } else { iterateKeywords(it, group) } diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 686719bcc8..81bde8e91b 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -29,7 +29,7 @@ var $schemaKeys = Object.keys(it.schema.properties || {}).filter(notProto) , $pProperties = it.schema.patternProperties || {} , $pPropertyKeys = Object.keys($pProperties).filter(notProto) - , $aProperties = $schema + , $aProperties = it.schema.additionalProperties , $someProperties = $schemaKeys.length || $pPropertyKeys.length , $noAdditional = $aProperties === false , $additionalIsSchema = typeof $aProperties == 'object' @@ -123,6 +123,60 @@ var {{=$nextValid}} = true; {{# def.ifResultValid }} {{?}} +{{ var $useDefaults = it.opts.useDefaults && !it.compositeRule; }} + +{{? $schemaKeys.length }} + {{~ $schemaKeys:$propertyKey }} + {{ var $sch = $schema[$propertyKey]; }} + + {{? {{# def.nonEmptySchema:$sch}} }} + {{ + var $prop = it.util.getProperty($propertyKey) + , $passData = $data + $prop + , $hasDefault = $useDefaults && $sch.default !== undefined; + $it.schema = $sch; + $it.schemaPath = $schemaPath + $prop; + $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($propertyKey); + $it.errorPath = it.util.getPath(it.errorPath, $propertyKey, it.opts.jsonPointers); + $it.dataPathArr[$dataNxt] = it.util.toQuotedString($propertyKey); + }} + + {{# def.generateSubschemaCode }} + + {{? {{# def.willOptimize }} }} + {{ + $code = {{# def._optimizeValidate }}; + var $useData = $passData; + }} + {{??}} + {{ var $useData = $nextData; }} + var {{=$nextData}} = {{=$passData}}; + {{?}} + + {{? $hasDefault }} + {{= $code }} + {{??}} + {{? $breakOnError }} + if ({{# def.noPropertyInData }}) { + {{=$nextValid}} = true; + } else { + {{??}} + if ({{=$useData}} !== undefined + {{? $ownProperties }} + && {{# def.isOwnProperty }} + {{?}} + ) { + {{?}} + + {{= $code }} + } + {{?}} {{ /* $hasDefault */ }} + {{?}} {{ /* def.nonEmptySchema */ }} + + {{# def.ifResultValid }} + {{~}} +{{?}} + {{? $breakOnError }} {{= $closingBraces }} if ({{=$errs}} == errors) { diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 9122c74ce5..b9d5655edc 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -4,5 +4,5 @@ module.exports = { $ref: require("./ref"), format: require("./format"), - additionalProperties: require("./properties"), + properties: require("./properties"), } diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 96b36f1d7a..691cfbed43 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -7,7 +7,7 @@ const applicator: Vocabulary = [ // object require("./dependencies"), require("./propertyNames"), - require("./properties"), + // require("./properties"), require("./patternProperties"), // any require("./not"), diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 7748ce3847..8d2f8ddecc 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -30,7 +30,7 @@ const def: KeywordDefinition = { } else { gen.if(propertyInData(data, prop, Expr.Const, it.opts.ownProperties)) applyPropertySchema(prop) - if (!it.allErrors) gen.else().code(`${valid} = true;`) + if (!it.allErrors) gen.else().code(`var ${valid} = true;`) gen.endIf() } if (!it.allErrors) gen.if(valid) diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js index cddd3733f3..48f7a16b46 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.js @@ -16,7 +16,7 @@ describe("referenced schema options", () => { }) describe('= "ignore" and default', () => { - it.only("should ignore other keywords when $ref is used", () => { + it("should ignore other keywords when $ref is used", () => { test(new Ajv()) test(new Ajv({extendRefs: "ignore"}), false) }) From 2483969e4963b0c4974d013d2d24720fa4adb587 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 22 Aug 2020 16:21:31 -0400 Subject: [PATCH 081/322] refactor: "properties" keyword to typescript (2 tests to be re-enabled once additionalProperties refactored) --- lib/compile/rules.ts | 2 +- lib/compile/validate/keywords.ts | 42 +++++----- lib/dot/properties.jst | 56 +------------- lib/dotjs/index.js | 2 +- lib/keyword.ts | 6 +- lib/vocabularies/applicator/allOf.ts | 25 +++--- lib/vocabularies/applicator/anyOf.ts | 6 +- lib/vocabularies/applicator/contains.ts | 5 +- lib/vocabularies/applicator/dependencies.ts | 5 +- lib/vocabularies/applicator/index.ts | 2 +- lib/vocabularies/applicator/items.ts | 12 +-- lib/vocabularies/applicator/not.ts | 5 +- .../applicator/patternProperties.ts | 9 +-- lib/vocabularies/applicator/properties.ts | 13 ++-- lib/vocabularies/applicator/propertyNames.ts | 5 +- lib/vocabularies/validation/required.ts | 5 +- spec/custom.spec.js | 76 ++++--------------- ...5_removeAdditional_custom_keywords.spec.js | 2 +- spec/options/removeAdditional.spec.js | 2 +- 19 files changed, 75 insertions(+), 205 deletions(-) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 7674ed0aac..6ecfba4167 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -56,7 +56,7 @@ export default function rules(): ValidationRules { {type: "array", rules: []}, { type: "object", - rules: [{properties: ["additionalProperties"]}], + rules: ["additionalProperties"], }, {rules: ["$ref"]}, ], diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index d213d536d3..01f014d665 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -26,31 +26,29 @@ export function schemaKeywords( if (!allErrors) gen.code("}") return } - let closeBlocks = "" const ruleGroups = RULES.rules.filter((group) => shouldUseGroup(schema, group)) const last = ruleGroups.length - 1 - ruleGroups.forEach((group, i) => { - if (group.type) { - // TODO refactor `data${dataLevel || ""}` - const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) - // TODO refactor ifs - gen.if(checkType) - iterateKeywords(it, group) - if (types.length === 1 && types[0] === group.type && typeErrors) { - gen.else() - reportTypeError(it) + gen.block(() => + ruleGroups.forEach((group, i) => { + if (group.type) { + // TODO refactor `data${dataLevel || ""}` + const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) + gen.if(checkType) + iterateKeywords(it, group) + if (types.length === 1 && types[0] === group.type && typeErrors) { + gen.else() + reportTypeError(it) + } + gen.endIf() + } else { + iterateKeywords(it, group) } - gen.endIf() - } else { - iterateKeywords(it, group) - } - if (!allErrors && i < last) { - const errCount = top ? "0" : `errs_${level}` - gen.code(`if (errors === ${errCount}) {`) - closeBlocks += "}" - } - }) - if (!allErrors) gen.code(closeBlocks) + if (!allErrors && i < last) { + const errCount = top ? "0" : `errs_${level}` + gen.if(`errors === ${errCount}`) + } + }) + ) } function iterateKeywords(it: CompilationContext, group: RuleGroup) { diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 81bde8e91b..724e5bc26e 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -1,3 +1,5 @@ +{{ $keyword = "properties"; }} + {{# def.definitions }} {{# def.errors }} {{# def.setupKeyword }} @@ -123,60 +125,6 @@ var {{=$nextValid}} = true; {{# def.ifResultValid }} {{?}} -{{ var $useDefaults = it.opts.useDefaults && !it.compositeRule; }} - -{{? $schemaKeys.length }} - {{~ $schemaKeys:$propertyKey }} - {{ var $sch = $schema[$propertyKey]; }} - - {{? {{# def.nonEmptySchema:$sch}} }} - {{ - var $prop = it.util.getProperty($propertyKey) - , $passData = $data + $prop - , $hasDefault = $useDefaults && $sch.default !== undefined; - $it.schema = $sch; - $it.schemaPath = $schemaPath + $prop; - $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($propertyKey); - $it.errorPath = it.util.getPath(it.errorPath, $propertyKey, it.opts.jsonPointers); - $it.dataPathArr[$dataNxt] = it.util.toQuotedString($propertyKey); - }} - - {{# def.generateSubschemaCode }} - - {{? {{# def.willOptimize }} }} - {{ - $code = {{# def._optimizeValidate }}; - var $useData = $passData; - }} - {{??}} - {{ var $useData = $nextData; }} - var {{=$nextData}} = {{=$passData}}; - {{?}} - - {{? $hasDefault }} - {{= $code }} - {{??}} - {{? $breakOnError }} - if ({{# def.noPropertyInData }}) { - {{=$nextValid}} = true; - } else { - {{??}} - if ({{=$useData}} !== undefined - {{? $ownProperties }} - && {{# def.isOwnProperty }} - {{?}} - ) { - {{?}} - - {{= $code }} - } - {{?}} {{ /* $hasDefault */ }} - {{?}} {{ /* def.nonEmptySchema */ }} - - {{# def.ifResultValid }} - {{~}} -{{?}} - {{? $breakOnError }} {{= $closingBraces }} if ({{=$errs}} == errors) { diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index b9d5655edc..9122c74ce5 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -4,5 +4,5 @@ module.exports = { $ref: require("./ref"), format: require("./format"), - properties: require("./properties"), + additionalProperties: require("./properties"), } diff --git a/lib/keyword.ts b/lib/keyword.ts index 356624abf3..9dfad4e021 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -190,8 +190,10 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void } function ok(condition?: string): void { - if (condition) fail(`!(${condition})`) - else if (!allErrors) gen.code("if (true) {") + if (!allErrors) { + if (condition) gen.code(`if (${condition}) {`) + else gen.code("if (true) {") + } } function errorParams(obj: KeywordContextParams, assign?: true) { diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 4e8bd351d7..eb81ffe2a0 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -8,22 +8,17 @@ const def: KeywordDefinition = { code({gen, ok, schema, it}) { let emptySchemas = true const valid = gen.name("valid") - let count = 0 - schema.forEach((sch: object | boolean, i: number) => { - if (alwaysValidSchema(it, sch)) return - emptySchemas = false - applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) - if (!it.allErrors) { - if (count === 1) gen.block() - count++ - gen.if(valid) - } - }) + gen.block(() => + schema.forEach((sch: object | boolean, i: number) => { + if (alwaysValidSchema(it, sch)) return + emptySchemas = false + applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) + if (!it.allErrors) gen.if(valid) + }) + ) - if (!it.allErrors) { - if (emptySchemas) ok() - else if (count > 1) gen.endBlock(count - 1) - } + if (emptySchemas) ok() + else ok(valid) // TODO possibly add allOf error }, diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 93a6dc6d0a..9041b52ef4 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -9,10 +9,8 @@ const def: KeywordDefinition = { code(cxt) { const {gen, ok, schema, it} = cxt const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) - if (alwaysValid) { - ok() - return - } + if (alwaysValid) return ok() + const valid = gen.name("valid") const schValid = gen.name("_valid") const errsCount = gen.name("_errs") diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 235175ce4b..232c8726fe 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -13,10 +13,7 @@ const def: KeywordDefinition = { const errsCount = gen.name("_errs") gen.code(`const ${errsCount} = errors;`) - if (alwaysValidSchema(it, schema)) { - fail(`${data}.length === 0`) - return - } + if (alwaysValidSchema(it, schema)) return fail(`${data}.length === 0`) const valid = gen.name("valid") const i = gen.name("i") diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 957f91769d..04aea9b59b 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -16,7 +16,7 @@ const def: KeywordDefinition = { type: "object", schemaType: "object", code(cxt) { - const {gen, errorParams, schema, data, it} = cxt + const {gen, ok, errorParams, schema, data, it} = cxt const [propDeps, schDeps] = splitDependencies() @@ -29,8 +29,7 @@ const def: KeywordDefinition = { validateSchemaDeps(schDeps) }) - // TODO refactor ifs - if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + ok(`${errsCount} === errors`) function splitDependencies(): [PropertyDependencies, SchemaDependencies] { const propertyDeps: PropertyDependencies = {} diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 691cfbed43..96b36f1d7a 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -7,7 +7,7 @@ const applicator: Vocabulary = [ // object require("./dependencies"), require("./propertyNames"), - // require("./properties"), + require("./properties"), require("./patternProperties"), // any require("./not"), diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index cca0a07f9a..4b6e2fe22d 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -13,21 +13,15 @@ const def: KeywordDefinition = { code(cxt) { // TODO strict mode: fail or warning if "additionalItems" is present without "items" - const {gen, /* fail, */ schema, parentSchema, data, it} = cxt + const {gen, ok, schema, parentSchema, data, it} = cxt const errsCount = gen.name("_errs") const len = gen.name("len") gen.code( `const ${errsCount} = errors; const ${len} = ${data}.length;` ) - - if (it.allErrors) { - validateItemsKeyword() - } else { - gen.block(validateItemsKeyword) - // TODO refactor ifs - gen.code(`if (${errsCount} === errors) {`) - } + gen.block(validateItemsKeyword) + ok(`${errsCount} === errors`) function validateItemsKeyword(): void { if (Array.isArray(schema)) { diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 5bc4fa4001..32e344e29c 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -8,10 +8,7 @@ const def: KeywordDefinition = { schemaType: ["object", "boolean"], code(cxt) { const {gen, fail, schema, it} = cxt - if (alwaysValidSchema(it, schema)) { - fail() - return - } + if (alwaysValidSchema(it, schema)) return fail() const valid = gen.name("valid") const errsCount = gen.name("_errs") diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 2c100ed4f6..4dcaa7da0b 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -9,19 +9,14 @@ const def: KeywordDefinition = { code(cxt) { const {gen, ok, schema, it} = cxt const patterns = schemaProperties(it, schema) - if (patterns.length === 0) { - ok() - return - } + if (patterns.length === 0) return ok() const valid = gen.name("valid") const errsCount = gen.name("_errs") gen.code(`const ${errsCount} = errors;`) gen.block(validatePatternProperties) - - // TODO refactor ifs - if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + ok(`${errsCount} === errors`) function validatePatternProperties() { for (const pat of patterns) { diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 8d2f8ddecc..10c73fb776 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -8,20 +8,19 @@ const def: KeywordDefinition = { schemaType: "object", code(cxt) { const {gen, ok, schema, data, it} = cxt + // TODO + // if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { + // remove all additional properties - it will fix skipped tests + // } const properties = schemaProperties(it, schema) - if (properties.length === 0) { - ok() - return - } + if (properties.length === 0) return ok() const valid = gen.name("valid") const errsCount = gen.name("_errs") gen.code(`const ${errsCount} = errors;`) gen.block(validateProperties) - - // TODO refactor ifs - if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + ok(`${errsCount} === errors`) function validateProperties() { for (const prop of properties) { diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index e33ff80dab..0f2600ec93 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -9,10 +9,7 @@ const def: KeywordDefinition = { schemaType: ["object", "boolean"], code(cxt) { const {gen, ok, errorParams, schema, it} = cxt - if (alwaysValidSchema(it, schema)) { - ok() - return - } + if (alwaysValidSchema(it, schema)) return ok() const valid = gen.name("valid") const errsCount = gen.name("_errs") diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index eab35544ed..046eaada8e 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -21,10 +21,7 @@ const def: KeywordDefinition = { $data: true, code(cxt) { const {gen, ok, fail, errorParams, schema, schemaCode, data, $data, it} = cxt - if (!$data && schema.length === 0) { - ok() - return - } + if (!$data && schema.length === 0) return ok() const loopRequired = $data || schema.length >= it.opts.loopRequired diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 192c761e2b..054455f93b 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -175,9 +175,7 @@ describe("Custom keywords", () => { }) function compileConstant(schema) { - return typeof schema == "object" && schema !== null - ? isDeepEqual - : isStrictEqual + return typeof schema == "object" && schema !== null ? isDeepEqual : isStrictEqual function isDeepEqual(data) { return equal(data, schema) @@ -568,19 +566,7 @@ describe("Custom keywords", () => { data = "data" + (it.dataLevel || ""), gt = parentSchema.exclusiveRange ? " > " : " >= ", lt = parentSchema.exclusiveRange ? " < " : " <= " - return ( - "var valid" + - it.level + - " = " + - data + - gt + - min + - " && " + - data + - lt + - max + - ";" - ) + return "var valid" + it.level + " = " + data + gt + min + " && " + data + lt + max + ";" } }) @@ -908,25 +894,11 @@ describe("Custom keywords", () => { shouldBeInvalid(validate, {foo: 2}, numErrors) if (customErrors) { - shouldBeRangeError( - validate.errors[0], - ".foo", - "#/properties/foo/x-range", - ">", - 2, - true - ) + shouldBeRangeError(validate.errors[0], ".foo", "#/properties/foo/x-range", ">", 2, true) } shouldBeInvalid(validate, {foo: 4}, numErrors) if (customErrors) { - shouldBeRangeError( - validate.errors[0], - ".foo", - "#/properties/foo/x-range", - "<", - 4, - true - ) + shouldBeRangeError(validate.errors[0], ".foo", "#/properties/foo/x-range", "<", 4, true) } }) } @@ -957,14 +929,7 @@ describe("Custom keywords", () => { }) } - function shouldBeRangeError( - error, - dataPath, - schemaPath, - comparison, - limit, - exclusive - ) { + function shouldBeRangeError(error, dataPath, schemaPath, comparison, limit, exclusive) { delete error.schema delete error.data error.should.eql({ @@ -987,18 +952,13 @@ describe("Custom keywords", () => { typeof schema[0] == "number" && typeof schema[1] == "number" if (!schemaValid) { - throw new Error( - "Invalid schema for range keyword, should be array of 2 numbers" - ) + throw new Error("Invalid schema for range keyword, should be array of 2 numbers") } var exclusiveRangeSchemaValid = - parentSchema.exclusiveRange === undefined || - typeof parentSchema.exclusiveRange == "boolean" + parentSchema.exclusiveRange === undefined || typeof parentSchema.exclusiveRange == "boolean" if (!exclusiveRangeSchemaValid) { - throw new Error( - "Invalid schema for exclusiveRange keyword, should be boolean" - ) + throw new Error("Invalid schema for exclusiveRange keyword, should be boolean") } } @@ -1021,13 +981,7 @@ describe("Custom keywords", () => { } describe("addKeyword method", () => { - var TEST_TYPES = [ - undefined, - "number", - "string", - "boolean", - ["number", "string"], - ] + var TEST_TYPES = [undefined, "number", "string", "boolean", ["number", "string"]] it("should throw if defined keyword is passed", () => { testThrow(["minimum", "maximum", "multipleOf", "minLength", "maxLength"]) @@ -1129,13 +1083,15 @@ describe("Custom keywords", () => { }) describe("getKeyword", () => { - it("should return boolean for pre-defined and unknown keywords", () => { + // TODO update this test + it("should return boolean for reserved and unknown keywords", () => { ajv.getKeyword("type").should.equal(true) - ajv.getKeyword("properties").should.equal(true) - ajv.getKeyword("additionalProperties").should.equal(true) + // ajv.getKeyword("properties").should.equal(true) + // ajv.getKeyword("additionalProperties").should.equal(true) ajv.getKeyword("unknown").should.equal(false) }) + // TODO change to account for pre-defined keywords with definitions it("should return keyword definition for custom keywords", () => { var definition = { validate: () => { @@ -1327,9 +1283,7 @@ describe("Custom keywords", () => { it("should require properties in the parent schema", () => { ajv.addKeyword("allRequired", { macro: function (schema, parentSchema) { - return schema - ? {required: Object.keys(parentSchema.properties)} - : true + return schema ? {required: Object.keys(parentSchema.properties)} : true }, metaSchema: {type: "boolean"}, dependencies: ["properties"], diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js index 949918a7d9..47823ae402 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -4,7 +4,7 @@ var Ajv = require("../ajv") require("../chai").should() describe("issue #955: option removeAdditional breaks custom keywords", () => { - it("should support custom keywords with option removeAdditional", () => { + it.skip("should support custom keywords with option removeAdditional", () => { var ajv = new Ajv({removeAdditional: "all"}) ajv.addKeyword("minTrimmedLength", { diff --git a/spec/options/removeAdditional.spec.js b/spec/options/removeAdditional.spec.js index 6e488b578b..64a6b55d4f 100644 --- a/spec/options/removeAdditional.spec.js +++ b/spec/options/removeAdditional.spec.js @@ -4,7 +4,7 @@ var Ajv = require("../ajv") require("../chai").should() describe("removeAdditional option", () => { - it("should remove all additional properties", () => { + it.skip("should remove all additional properties", () => { var ajv = new Ajv({removeAdditional: "all"}) ajv.addSchema({ From 839955c3d6563280963a112464b597d12a546517 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 23 Aug 2020 07:32:33 -0400 Subject: [PATCH 082/322] refactor: "additionalProperties" to typescript --- lib/compile/rules.ts | 5 +- lib/compile/subschema.ts | 2 +- lib/dot/errors.def | 3 - lib/dot/missing.def | 31 ---- lib/dot/properties.jst | 133 ------------------ lib/dotjs/index.js | 1 - .../applicator/additionalProperties.ts | 116 +++++++++++++++ lib/vocabularies/applicator/index.ts | 1 + lib/vocabularies/missing.ts | 12 +- lib/vocabularies/util.ts | 10 +- lib/vocabularies/validation/enum.ts | 6 +- spec/tests/issues/70_swagger_schema.json | 9 +- 12 files changed, 136 insertions(+), 193 deletions(-) delete mode 100644 lib/dot/missing.def delete mode 100644 lib/dot/properties.jst create mode 100644 lib/vocabularies/applicator/additionalProperties.ts diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 6ecfba4167..57cc24d25b 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -54,10 +54,7 @@ export default function rules(): ValidationRules { {type: "number", rules: ["format"]}, {type: "string", rules: ["format"]}, {type: "array", rules: []}, - { - type: "object", - rules: ["additionalProperties"], - }, + {type: "object", rules: []}, {rules: ["$ref"]}, ], all: toHash(ALL), diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 3c390431a1..d7835e7938 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -19,7 +19,7 @@ export enum Expr { Str, } -interface SubschemaApplication { +export interface SubschemaApplication { keyword: string schemaProp?: string | number data?: string diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 43a3022e00..b6b112074f 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -90,7 +90,6 @@ {{## def._errorMessages = { $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", - additionalProperties: "'should NOT have additional properties'", format: "'should match format \"{{#def.concatSchemaEQ}}\"'", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", @@ -104,7 +103,6 @@ {{## def._errorSchemas = { $ref: "{{=it.util.toQuotedString($schema)}}", - additionalProperties: "false", format: "{{#def.schemaRefOrQS}}", custom: "validate.schema{{=$schemaPath}}", patternRequired: "validate.schema{{=$schemaPath}}", @@ -118,7 +116,6 @@ {{## def._errorParams = { $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", - additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", format: "{ format: {{#def.schemaValueQS}} }", custom: "{ keyword: '{{=$rule.keyword}}' }", patternRequired: "{ missingPattern: '{{=$missingPattern}}' }", diff --git a/lib/dot/missing.def b/lib/dot/missing.def deleted file mode 100644 index 1476e3bf78..0000000000 --- a/lib/dot/missing.def +++ /dev/null @@ -1,31 +0,0 @@ -{{## def.checkMissingProperty:_properties: - {{~ _properties:$propertyKey:$i }} - {{?$i}} || {{?}} - {{ - var $prop = it.util.getProperty($propertyKey) - , $useData = $data + $prop; - }} - ( ({{# def.noPropertyInData }}) && (missing{{=$lvl}} = {{= it.util.toQuotedString($propertyKey) }}) ) - {{~}} -#}} - - -{{## def.errorMissingProperty:_error: - {{ - var $propertyPath = 'missing' + $lvl - , $missingProperty = '\' + ' + $propertyPath + ' + \''; - }} - {{# def.error:_error }} -#}} - - -{{## def.allErrorsMissingProperty:_error: - {{ - var $prop = it.util.getProperty($propertyKey) - , $missingProperty = it.util.escapeQuotes($propertyKey) - , $useData = $data + $prop; - }} - if ({{# def.noPropertyInData }}) { - {{# def.addError:_error }} - } -#}} diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst deleted file mode 100644 index 724e5bc26e..0000000000 --- a/lib/dot/properties.jst +++ /dev/null @@ -1,133 +0,0 @@ -{{ $keyword = "properties"; }} - -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.setupNextLevel }} - - -{{## def.validateAdditional: - {{ /* additionalProperties is schema */ - $it.schema = $aProperties; - $it.schemaPath = it.schemaPath + '.additionalProperties'; - $it.errSchemaPath = it.errSchemaPath + '/additionalProperties'; - $it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); - var $passData = $data + '[' + $key + ']'; - $it.dataPathArr[$dataNxt] = $key; - }} - - {{# def.generateSubschemaCode }} - {{# def.optimizeValidate }} -#}} - - -{{ - var $key = 'key' + $lvl - , $idx = 'idx' + $lvl - , $dataNxt = $it.dataLevel = it.dataLevel + 1 - , $nextData = 'data' + $dataNxt - , $dataProperties = 'dataProperties' + $lvl; - - var $schemaKeys = Object.keys(it.schema.properties || {}).filter(notProto) - , $pProperties = it.schema.patternProperties || {} - , $pPropertyKeys = Object.keys($pProperties).filter(notProto) - , $aProperties = it.schema.additionalProperties - , $someProperties = $schemaKeys.length || $pPropertyKeys.length - , $noAdditional = $aProperties === false - , $additionalIsSchema = typeof $aProperties == 'object' - && Object.keys($aProperties).length - , $removeAdditional = it.opts.removeAdditional - , $checkAdditional = $noAdditional || $additionalIsSchema || $removeAdditional - , $ownProperties = it.opts.ownProperties - , $currentBaseId = it.baseId; - - function notProto(p) { return p !== '__proto__'; } -}} - - -var {{=$errs}} = errors; -var {{=$nextValid}} = true; -{{? $ownProperties }} - var {{=$dataProperties}} = undefined; -{{?}} - -{{? $checkAdditional }} - {{# def.iterateProperties }} - {{? $someProperties }} - var isAdditional{{=$lvl}} = !(false - {{? $schemaKeys.length }} - {{? $schemaKeys.length > 8 }} - || validate.schema{{=$schemaPath}}.hasOwnProperty({{=$key}}) - {{??}} - {{~ $schemaKeys:$propertyKey }} - || {{=$key}} == {{= it.util.toQuotedString($propertyKey) }} - {{~}} - {{?}} - {{?}} - {{? $pPropertyKeys.length }} - {{~ $pPropertyKeys:$pProperty:$i }} - || {{= it.usePattern($pProperty) }}.test({{=$key}}) - {{~}} - {{?}} - ); - - if (isAdditional{{=$lvl}}) { - {{?}} - {{? $removeAdditional == 'all' }} - delete {{=$data}}[{{=$key}}]; - {{??}} - {{ - var $currentErrorPath = it.errorPath; - var $additionalProperty = '\' + ' + $key + ' + \''; - }} - {{? $noAdditional }} - {{? $removeAdditional }} - delete {{=$data}}[{{=$key}}]; - {{??}} - {{=$nextValid}} = false; - {{ - var $currErrSchemaPath = $errSchemaPath; - $errSchemaPath = it.errSchemaPath + '/additionalProperties'; - }} - {{# def.error:'additionalProperties' }} - {{ $errSchemaPath = $currErrSchemaPath; }} - {{? $breakOnError }} break; {{?}} - {{?}} - {{?? $additionalIsSchema }} - {{? $removeAdditional == 'failing' }} - var {{=$errs}} = errors; - {{# def.setCompositeRule }} - - {{# def.validateAdditional }} - - if (!{{=$nextValid}}) { - errors = {{=$errs}}; - if (validate.errors !== null) { - if (errors) validate.errors.length = errors; - else validate.errors = null; - } - delete {{=$data}}[{{=$key}}]; - } - - {{# def.resetCompositeRule }} - {{??}} - {{# def.validateAdditional }} - {{? $breakOnError }} if (!{{=$nextValid}}) break; {{?}} - {{?}} - {{?}} - {{ it.errorPath = $currentErrorPath; }} - {{?}} - {{? $someProperties }} - } - {{?}} - } - - {{# def.ifResultValid }} -{{?}} - -{{? $breakOnError }} - {{= $closingBraces }} - if ({{=$errs}} == errors) { -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 9122c74ce5..79adb9ceb2 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -4,5 +4,4 @@ module.exports = { $ref: require("./ref"), format: require("./format"), - additionalProperties: require("./properties"), } diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts new file mode 100644 index 0000000000..5a00700e0b --- /dev/null +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -0,0 +1,116 @@ +import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import { + allSchemaProperties, + schemaRefOrVal, + quotedString, + alwaysValidSchema, + loopPropertiesCode, + orExpr, +} from "../util" +import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" +import {reportError, resetErrorsCount} from "../../compile/errors" + +const error: KeywordErrorDefinition = { + message: "should NOT have additional properties", + params: ({params}) => `{additionalProperty: ${params.additionalProperty}}`, +} + +const def: KeywordDefinition = { + keyword: "additionalProperties", + type: "object", + schemaType: ["object", "boolean"], + error, + code(cxt) { + const {gen, ok, errorParams, schema, parentSchema, data, it} = cxt + const { + allErrors, + usePattern, + opts: {removeAdditional}, + } = it + + if ((schema === undefined || alwaysValidSchema(it, schema)) && removeAdditional !== "all") { + return ok() + } + + const props = allSchemaProperties(parentSchema.properties) + const patProps = allSchemaProperties(parentSchema.patternProperties) + + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + checkAdditionalProperties() + if (!allErrors) gen.code(`if (${errsCount} === errors) {`) + + function checkAdditionalProperties(): void { + loopPropertiesCode(cxt, (key: string) => { + if (!props.length && !patProps.length) additionalPropertyCode(key) + else gen.if(isAdditional(key), () => additionalPropertyCode(key)) + }) + } + + function isAdditional(key: string): string { + let definedProp = "" + if (props.length > 8) { + // TODO maybe an option instead of hard-coded 8? + const propsSchema = schemaRefOrVal(parentSchema.properties, it.schemaPath, "properties") + definedProp = `${propsSchema}.hasOwnProperty(${key})` + } else if (props.length) { + definedProp = orExpr(props, (p) => `${key} === ${quotedString(p)}`) + } + if (patProps.length) { + definedProp += + (definedProp ? " || " : "") + orExpr(patProps, (p) => `${usePattern(p)}.test(${key})`) + } + return `!(${definedProp})` + } + + function deleteAdditional(key: string): void { + gen.code(`delete ${data}[${key}];`) + } + + function additionalPropertyCode(key: string): void { + if (removeAdditional === "all" || (removeAdditional && schema === false)) { + deleteAdditional(key) + return + } + + if (schema === false) { + errorParams({additionalProperty: key}) + reportError(cxt, error) + if (!allErrors) gen.code("break;") + return + } + + if (typeof schema == "object" && !alwaysValidSchema(it, schema)) { + const valid = gen.name("valid") + if (removeAdditional === "failing") { + applyAdditionalSchema(key, valid, false) + gen.if(`!${valid}`, () => { + resetErrorsCount(gen, errsCount) + deleteAdditional(key) + }) + } else { + applyAdditionalSchema(key, valid) + if (!allErrors) gen.if(`!${valid}`, "break") + } + } + } + + function applyAdditionalSchema(key: string, valid: string, errors?: false): void { + const subschema: SubschemaApplication = { + keyword: "additionalProperties", + dataProp: key, + expr: Expr.Str, + } + if (errors === false) { + Object.assign(subschema, { + compositeRule: true, + createErrors: false, + allErrors: false, + }) + } + applySubschema(it, subschema, valid) + } + }, +} + +module.exports = def diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 96b36f1d7a..782570da8f 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -7,6 +7,7 @@ const applicator: Vocabulary = [ // object require("./dependencies"), require("./propertyNames"), + require("./additionalProperties"), require("./properties"), require("./patternProperties"), // any diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index ff2f92716e..809676600d 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,5 +1,5 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" -import {noPropertyInData, quotedString} from "./util" +import {noPropertyInData, quotedString, orExpr} from "./util" import {reportError} from "../compile/errors" import {Expr} from "../compile/subschema" @@ -25,12 +25,10 @@ export function checkMissingProp( properties: string[], missing: string ): string { - return properties - .map((prop) => { - const hasNoProp = noPropertyInData(data, prop, Expr.Const, opts.ownProperties) - return `(${hasNoProp} && (${missing} = ${quotedString(prop)}))` - }) - .reduce((cond, part) => `${cond} || ${part}`) + return orExpr(properties, (prop) => { + const hasNoProp = noPropertyInData(data, prop, Expr.Const, opts.ownProperties) + return `(${hasNoProp} && (${missing} = ${quotedString(prop)}))` + }) } export function reportMissingProp( diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 7b86bef6d8..822a871525 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -54,8 +54,12 @@ export function alwaysValidSchema( : !schemaHasRules(schema, RULES.all) } +export function allSchemaProperties(schema?: object): string[] { + return schema ? Object.keys(schema).filter((p) => p !== "__proto__") : [] +} + export function schemaProperties(it: CompilationContext, schema: object): string[] { - return Object.keys(schema).filter((p) => p !== "__proto__" && !alwaysValidSchema(it, schema[p])) + return allSchemaProperties(schema).filter((p) => !alwaysValidSchema(it, schema[p])) } export function isOwnProperty(data: string, property: string, expr: Expr): string { @@ -98,3 +102,7 @@ export function loopPropertiesCode( const iteration = it.opts.ownProperties ? `of Object.keys(${data})` : `in ${data}` gen.for(`const ${key} ${iteration}`, () => loopBody(key)) } + +export function orExpr(items: string[], mapCondition: (s: string, i: number) => string): string { + return items.map(mapCondition).reduce((expr, cond) => `${expr} || ${cond}`) +} diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index f38e20a706..63b1c992e7 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,5 +1,5 @@ import {KeywordDefinition} from "../../types" -import {quotedString} from "../util" +import {quotedString, orExpr} from "../util" const def: KeywordDefinition = { keyword: "enum", @@ -23,9 +23,7 @@ const def: KeywordDefinition = { } else { const vSchema = gen.name("schema") gen.code(`const ${vSchema} = ${schemaCode};`) - const cond: string = schema - .map((_, i: number) => equalCode(vSchema, i)) - .reduce((acc: string, eq: string) => `${acc} || ${eq}`) + const cond: string = orExpr(schema, (_, i) => equalCode(vSchema, i)) fail(`!(${cond})`) } } diff --git a/spec/tests/issues/70_swagger_schema.json b/spec/tests/issues/70_swagger_schema.json index 0b3da339a0..0bfd6eade3 100644 --- a/spec/tests/issues/70_swagger_schema.json +++ b/spec/tests/issues/70_swagger_schema.json @@ -686,14 +686,7 @@ }, "type": { "type": "string", - "enum": [ - "string", - "number", - "boolean", - "integer", - "array", - "file" - ] + "enum": ["string", "number", "boolean", "integer", "array", "file"] }, "format": { "type": "string" From 0273cab5bf830aad5d367420074f70de0a53a322 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 23 Aug 2020 09:11:25 -0400 Subject: [PATCH 083/322] refactor: "format" keyword to typescript --- lib/ajv.ts | 2 + lib/compile/rules.ts | 4 +- lib/dot/errors.def | 3 - lib/dot/format.jst | 108 ------------------------------ lib/dotjs/index.js | 1 - lib/keyword.ts | 5 +- lib/types.ts | 11 +-- lib/vocabularies/format/format.ts | 104 ++++++++++++++++++++++++++++ lib/vocabularies/format/index.ts | 5 ++ spec/ajv.spec.js | 16 ++--- 10 files changed, 127 insertions(+), 132 deletions(-) delete mode 100644 lib/dot/format.jst create mode 100644 lib/vocabularies/format/format.ts create mode 100644 lib/vocabularies/format/index.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index ba143ae77d..24059ba6d9 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -11,6 +11,7 @@ var compileSchema = require("./compile"), const validationVocabulary: Vocabulary = require("./vocabularies/validation") const applicatorVocabulary: Vocabulary = require("./vocabularies/applicator") +const formatVocabulary: Vocabulary = require("./vocabularies/format") module.exports = Ajv @@ -77,6 +78,7 @@ export default function Ajv(opts: Options): void { if (opts.formats) addInitialFormats(this) this.addVocabulary(validationVocabulary, true) this.addVocabulary(applicatorVocabulary, true) + this.addVocabulary(formatVocabulary, true) if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 57cc24d25b..0fc722175f 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -51,8 +51,8 @@ export default function rules(): ValidationRules { const RULES: ValidationRules = { rules: [ - {type: "number", rules: ["format"]}, - {type: "string", rules: ["format"]}, + {type: "number", rules: []}, + {type: "string", rules: []}, {type: "array", rules: []}, {type: "object", rules: []}, {rules: ["$ref"]}, diff --git a/lib/dot/errors.def b/lib/dot/errors.def index b6b112074f..59895a5b60 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -90,7 +90,6 @@ {{## def._errorMessages = { $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", - format: "'should match format \"{{#def.concatSchemaEQ}}\"'", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", switch: "'should pass \"switch\" keyword validation'", @@ -103,7 +102,6 @@ {{## def._errorSchemas = { $ref: "{{=it.util.toQuotedString($schema)}}", - format: "{{#def.schemaRefOrQS}}", custom: "validate.schema{{=$schemaPath}}", patternRequired: "validate.schema{{=$schemaPath}}", switch: "validate.schema{{=$schemaPath}}", @@ -116,7 +114,6 @@ {{## def._errorParams = { $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", - format: "{ format: {{#def.schemaValueQS}} }", custom: "{ keyword: '{{=$rule.keyword}}' }", patternRequired: "{ missingPattern: '{{=$missingPattern}}' }", switch: "{ caseIndex: {{=$caseIndex}} }", diff --git a/lib/dot/format.jst b/lib/dot/format.jst deleted file mode 100644 index 24c05e5229..0000000000 --- a/lib/dot/format.jst +++ /dev/null @@ -1,108 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} - -{{## def.skipFormat: - {{? $breakOnError }} if (true) { {{?}} - {{ return out; }} -#}} - -{{? it.opts.format === false }}{{# def.skipFormat }}{{?}} - - -{{# def.$data }} - - -{{## def.$dataCheckFormat: - {{# def.$dataNotType:'string' }} - ({{? $unknownFormats != 'ignore' }} - ({{=$schemaValue}} && !{{=$format}} - {{? $allowUnknown }} - && self._opts.unknownFormats.indexOf({{=$schemaValue}}) == -1 - {{?}}) || - {{?}} - ({{=$format}} && {{=$formatType}} == '{{=$ruleType}}' - && !(typeof {{=$format}} == 'function' - ? {{? it.async}} - (async{{=$lvl}} ? await {{=$format}}({{=$data}}) : {{=$format}}({{=$data}})) - {{??}} - {{=$format}}({{=$data}}) - {{?}} - : {{=$format}}.test({{=$data}})))) -#}} - -{{## def.checkFormat: - {{ - var $formatRef = 'formats' + it.util.getProperty($schema); - if ($isObject) $formatRef += '.validate'; - }} - {{? typeof $format == 'function' }} - {{=$formatRef}}({{=$data}}) - {{??}} - {{=$formatRef}}.test({{=$data}}) - {{?}} -#}} - - -{{ - var $unknownFormats = it.opts.unknownFormats - , $allowUnknown = Array.isArray($unknownFormats); -}} - -{{? $isData }} - {{ - var $format = 'format' + $lvl - , $isObject = 'isObject' + $lvl - , $formatType = 'formatType' + $lvl; - }} - var {{=$format}} = formats[{{=$schemaValue}}]; - var {{=$isObject}} = typeof {{=$format}} == 'object' - && !({{=$format}} instanceof RegExp) - && {{=$format}}.validate; - var {{=$formatType}} = {{=$isObject}} && {{=$format}}.type || 'string'; - if ({{=$isObject}}) { - {{? it.async}} - var async{{=$lvl}} = {{=$format}}.async; - {{?}} - {{=$format}} = {{=$format}}.validate; - } - if ({{# def.$dataCheckFormat }}) { -{{??}} - {{ var $format = it.formats[$schema]; }} - {{? !$format }} - {{? $unknownFormats == 'ignore' }} - {{ it.logger.warn('unknown format "' + $schema + '" ignored in schema at path "' + it.errSchemaPath + '"'); }} - {{# def.skipFormat }} - {{?? $allowUnknown && $unknownFormats.indexOf($schema) >= 0 }} - {{# def.skipFormat }} - {{??}} - {{ throw new Error('unknown format "' + $schema + '" is used in schema at path "' + it.errSchemaPath + '"'); }} - {{?}} - {{?}} - {{ - var $isObject = typeof $format == 'object' - && !($format instanceof RegExp) - && $format.validate; - var $formatType = $isObject && $format.type || 'string'; - if ($isObject) { - var $async = $format.async === true; - $format = $format.validate; - } - }} - {{? $formatType != $ruleType }} - {{# def.skipFormat }} - {{?}} - {{? $async }} - {{ - if (!it.async) throw new Error('async format in sync schema'); - var $formatRef = 'formats' + it.util.getProperty($schema) + '.validate'; - }} - if (!(await {{=$formatRef}}({{=$data}}))) { - {{??}} - if (!{{# def.checkFormat }}) { - {{?}} -{{?}} - {{# def.error:'format' }} - } {{? $breakOnError }} else { {{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js index 79adb9ceb2..7ef164599b 100644 --- a/lib/dotjs/index.js +++ b/lib/dotjs/index.js @@ -3,5 +3,4 @@ //all requires must be explicit because browserify won't work with dynamic requires module.exports = { $ref: require("./ref"), - format: require("./format"), } diff --git a/lib/keyword.ts b/lib/keyword.ts index 9dfad4e021..7759c77d2d 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -141,8 +141,9 @@ export function addKeyword( * @this rule * @param {Object} it schema compilation context. * @param {String} keyword pre-defined or custom keyword. + * @param {String} ruleType current data type the rule is applied to (for rules supporting multiple types). */ -function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void { +function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): void { const schema = it.schema[keyword] const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition const {gen, opts, dataLevel, schemaPath, dataPathArr, allErrors} = it @@ -171,7 +172,7 @@ function ruleCode(it: CompilationContext, keyword: string /*, ruleType */): void throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) } // TODO check that code called "fail" or another valid way to return code - code(cxt) + code(cxt, ruleType) // TODO replace with fail_ below function fail(condition?: string, context?: KeywordContext): void { diff --git a/lib/types.ts b/lib/types.ts index 99725237f6..09aec9c6c6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -118,7 +118,7 @@ export interface CompilationContext { async: boolean opts: Options formats: { - [index: string]: Format | undefined + [index: string]: AddedFormat } // keywords: { // [index: string]: KeywordDefinition | undefined @@ -166,7 +166,7 @@ export interface KeywordDefinition { compile?: (schema: any, parentSchema: object, it: CompilationContext) => ValidateFunction macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean inline?: (it: CompilationContext, keyword: string, schema: any, parentSchema: object) => string - code?: (cxt: KeywordContext) => string | void + code?: (cxt: KeywordContext, ruleType?: string) => string | void error?: KeywordErrorDefinition validateSchema?: ValidateFunction implements?: string[] @@ -221,9 +221,12 @@ export interface AsyncFormatDefinition { compare?: FormatCompare } -export type Format = - | string +export type FormatValidate = FormatValidator | AsyncFormatValidator | RegExp + +export type AddedFormat = | RegExp | FormatValidator | FormatDefinition | AsyncFormatDefinition + +export type Format = AddedFormat | string diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts new file mode 100644 index 0000000000..8fa5e4223b --- /dev/null +++ b/lib/vocabularies/format/format.ts @@ -0,0 +1,104 @@ +import {KeywordDefinition, AddedFormat, FormatValidate} from "../../types" +import {dataNotType} from "../util" +import {getProperty} from "../../compile/util" + +const def: KeywordDefinition = { + keyword: "format", + type: ["number", "string"], + schemaType: "string", + $data: true, + code({gen, ok, fail, data, $data, schema, schemaCode, it}, ruleType) { + const {formats, opts, logger, errSchemaPath} = it + if (opts.format === false) return ok() + + if ($data) validate$DataFormat() + else validateFormat() + + function validate$DataFormat() { + const fmtDef = gen.name("fmtDef") + const fmtType = gen.name("fmtType") + const format = gen.name("format") + prepare() + fail(invalidCondition()) + + function prepare() { + gen.code(`const ${fmtDef} = formats[${schemaCode}]; let ${fmtType}, ${format};`) + gen.if( + `typeof ${fmtDef} == "object" && !(${fmtDef} instanceof RegExp)`, + `${fmtType} = ${fmtDef}.type || "string"; ${format} = ${fmtDef}.validate;`, + `${fmtType} = "string"; ${format} = ${fmtDef}` + ) + } + + function invalidCondition(): string { + const dnt = dataNotType(schemaCode, def.schemaType, $data) + return dnt + unknownFmt() + invalidFmt() + } + + function unknownFmt(): string { + if (opts.unknownFormats === "ignore") return "" + let unknown = `(${schemaCode} && !${format}` + if (Array.isArray(opts.unknownFormats)) { + unknown += ` && !self._opts.unknownFormats.includes(${schemaCode})` + } + return unknown + ") || " + } + + function invalidFmt(): string { + const fmt = `${format}(${data})` + const callFormat = it.async ? `${fmtDef}.async ? await ${fmt} : ${fmt}` : fmt + const validData = `typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` + return `(${format} && ${fmtType} === "${ruleType}" && !(${validData}))` + } + } + + function validateFormat() { + const formatDef: AddedFormat = formats[schema] + if (!formatDef) { + unknownFormat() + ok() + return + } + const [fmtType, format, fmtRef] = getFormat(formatDef) + if (fmtType !== ruleType) ok() + else fail(`!(${validCondition()})`) + + function unknownFormat() { + if (opts.unknownFormats === "ignore") return logger.warn(unknownMsg()) + if (Array.isArray(opts.unknownFormats) && opts.unknownFormats.includes(schema)) return + throw new Error(unknownMsg()) + + function unknownMsg(): string { + return `unknown format "${schema}" ignored in schema at path "${errSchemaPath}"` + } + } + + function getFormat(fmtDef: AddedFormat): [string, FormatValidate, string] { + const fmt = `formats${getProperty(schema)}` + if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) { + return [fmtDef.type || "string", fmtDef.validate as FormatValidate, `${fmt}.validate`] + } + + return ["string", fmtDef, fmt] + } + + function validCondition(): string { + if (typeof formatDef == "object" && !(formatDef instanceof RegExp) && formatDef.async) { + if (!it.async) throw new Error("async format in sync schema") + return `await ${fmtRef}(${data})` + } + + return fmtRef + (typeof format == "function" ? "" : ".test") + `(${data})` + } + } + }, + error: { + message: ({$data, schemaCode}) => + $data + ? `'should match format "' + ${schemaCode} + '"'` + : `"should match format \\"${(schemaCode).slice(1, -1)}\\""`, + params: ({schemaCode}) => `{format: ${schemaCode}}`, + }, +} + +module.exports = def diff --git a/lib/vocabularies/format/index.ts b/lib/vocabularies/format/index.ts new file mode 100644 index 0000000000..e40be9fcb0 --- /dev/null +++ b/lib/vocabularies/format/index.ts @@ -0,0 +1,5 @@ +import {Vocabulary} from "../../types" + +const format: Vocabulary = [require("./format")] + +module.exports = format diff --git a/spec/ajv.spec.js b/spec/ajv.spec.js index 07ca56c74f..9810f21ee7 100644 --- a/spec/ajv.spec.js +++ b/spec/ajv.spec.js @@ -79,15 +79,11 @@ describe("Ajv", () => { }) it("should validate against previously compiled schema by id (also see addSchema)", () => { - ajv - .validate({$id: "//e.com/int.json", type: "integer"}, 1) - .should.equal(true) + ajv.validate({$id: "//e.com/int.json", type: "integer"}, 1).should.equal(true) ajv.validate("//e.com/int.json", 1).should.equal(true) ajv.validate("//e.com/int.json", "1").should.equal(false) - ajv - .compile({$id: "//e.com/str.json", type: "string"}) - .should.be.a("function") + ajv.compile({$id: "//e.com/str.json", type: "string"}).should.be.a("function") ajv.validate("//e.com/str.json", "a").should.equal(true) ajv.validate("//e.com/str.json", 1).should.equal(false) }) @@ -109,12 +105,8 @@ describe("Ajv", () => { }, }) - ajv - .validate("http://e.com/types.json#/definitions/int", 1) - .should.equal(true) - ajv - .validate("http://e.com/types.json#/definitions/int", "1") - .should.equal(false) + ajv.validate("http://e.com/types.json#/definitions/int", 1).should.equal(true) + ajv.validate("http://e.com/types.json#/definitions/int", "1").should.equal(false) }) it("should return schema fragment by id", () => { From eeb3a52c6103c71683e5313d64e11ef43f6eb758 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 23 Aug 2020 17:52:23 -0400 Subject: [PATCH 084/322] refactor: "$ref" to typescript --- lib/ajv.ts | 16 +-- lib/compile/codegen.ts | 9 ++ lib/compile/errors.ts | 23 ++-- lib/compile/index.ts | 47 +++++--- lib/compile/rules.ts | 28 +---- lib/compile/subschema.ts | 100 ++++++++++++---- lib/compile/util.ts | 1 + lib/compile/validate/index.ts | 4 +- lib/dot/custom.jst | 4 +- lib/dot/errors.def | 12 +- lib/dot/ref.jst | 87 -------------- lib/dotjs/index.js | 6 - lib/keyword.ts | 13 +- lib/types.ts | 8 +- .../applicator/additionalProperties.ts | 2 +- lib/vocabularies/applicator/index.ts | 2 + lib/vocabularies/core/index.ts | 7 ++ lib/vocabularies/core/ref.ts | 113 ++++++++++++++++++ lib/vocabularies/format/index.ts | 2 + lib/vocabularies/util.ts | 4 +- lib/vocabularies/validation/index.ts | 2 + 21 files changed, 286 insertions(+), 204 deletions(-) delete mode 100644 lib/dot/ref.jst delete mode 100644 lib/dotjs/index.js create mode 100644 lib/vocabularies/core/index.ts create mode 100644 lib/vocabularies/core/ref.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 24059ba6d9..9e65fc256d 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -3,15 +3,16 @@ import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules from "./compile/rules" import $dataMetaSchema from "./data" -import {Vocabulary, Options} from "./types" +import {Options} from "./types" var compileSchema = require("./compile"), resolve = require("./compile/resolve"), stableStringify = require("fast-json-stable-stringify") -const validationVocabulary: Vocabulary = require("./vocabularies/validation") -const applicatorVocabulary: Vocabulary = require("./vocabularies/applicator") -const formatVocabulary: Vocabulary = require("./vocabularies/format") +import coreVocabulary from "./vocabularies/core" +import validationVocabulary from "./vocabularies/validation" +import applicatorVocabulary from "./vocabularies/applicator" +import formatVocabulary from "./vocabularies/format" module.exports = Ajv @@ -76,6 +77,7 @@ export default function Ajv(opts: Options): void { this._metaOpts = getMetaSchemaOptions(this) if (opts.formats) addInitialFormats(this) + this.addVocabulary(coreVocabulary, true) this.addVocabulary(validationVocabulary, true) this.addVocabulary(applicatorVocabulary, true) this.addVocabulary(formatVocabulary, true) @@ -296,7 +298,7 @@ function _removeAllSchemas(self, schemas, regex?: RegExp) { } /* @this Ajv */ -function _addSchema(schema, skipValidation, meta, shouldAddSchema) { +function _addSchema(schema: object | boolean, skipValidation, meta, shouldAddSchema) { if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema should be object or boolean") } @@ -307,12 +309,12 @@ function _addSchema(schema, skipValidation, meta, shouldAddSchema) { shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false - var id = resolve.normalizeId(schema.$id) + var id = resolve.normalizeId(schema["$id"]) if (id && shouldAddSchema) checkUnique(this, id) var willValidate = this._opts.validateSchema !== false && !skipValidation var recursiveMeta - if (willValidate && !(recursiveMeta = id && id === resolve.normalizeId(schema.$schema))) { + if (willValidate && !(recursiveMeta = id && id === resolve.normalizeId(schema["$schema"]))) { this.validateSchema(schema, true) } diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index ca2f6020a4..84076b4ec2 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -76,6 +76,15 @@ export default class CodeGen { return this } + try(tryBody: Code, catchBody?: Code, err?: string, finallyBody?: Code): CodeGen { + if (!catchBody && !finallyBody) throw new Error('"try" without "catch" and "finally"') + this.code("try{").code(tryBody) + if (catchBody) this.code(err ? `}catch(${err}){` : "}catch{").code(catchBody) + if (finallyBody) this.code("}finally{").code(finallyBody) + this.code("}") + return this + } + block(body?: Code, expectedToClose?: number): CodeGen { this.#blockStarts.push(this.#blocks.length) if (body) this.code(body).endBlock(expectedToClose) diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 9a6f3cbf46..d5d11fcd9f 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -27,23 +27,18 @@ export function reportExtraError(cxt: KeywordContext, error: KeywordErrorDefinit } export function resetErrorsCount(gen: CodeGen, errsCount: string): void { - gen.code( - `errors = ${errsCount}; - if (vErrors !== null) { - if (${errsCount}) vErrors.length = ${errsCount}; - else vErrors = null; - }` + gen.code(`errors = ${errsCount};`) + gen.if(`vErrors !== null`, () => + gen.if(errsCount, `vErrors.length = ${errsCount}`, "vErrors = null") ) } function addError(gen: CodeGen, errObj: string): void { const err = gen.name("err") - gen.code( - `const ${err} = ${errObj}; - if (vErrors === null) vErrors = [${err}]; - else vErrors.push(${err}); - errors++;` - ) + gen + .code(`const ${err} = ${errObj};`) + .if("vErrors === null", `vErrors = [${err}]`, `vErrors.push(${err})`) + .code(`errors++;`) } function returnErrors(gen: CodeGen, async: boolean, errs: string): void { @@ -60,7 +55,7 @@ function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): st keyword, data, schemaValue, - it: {createErrors, schemaPath, errorPath, errSchemaPath, propertyName, opts}, + it: {createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, } = cxt if (createErrors === false) return "{}" if (!error) throw new Error('keyword definition must have "error" property') @@ -79,7 +74,7 @@ function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): st // TODO trim whitespace out += ` schema: ${schemaValue}, - parentSchema: validate.schema${schemaPath}, + parentSchema: ${topSchemaRef}${schemaPath}, data: ${data},` } return out + "}" diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 05c5c53a0e..a4acdd9607 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,5 +1,6 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" +import {quotedString} from "../vocabularies/util" import {MissingRefError} from "./error_classes" import validateCode from "./validate" import {Rule} from "./rules" @@ -21,6 +22,20 @@ const ValidationError = require("./error_classes").ValidationError module.exports = compile +export type ResolvedRef = InlineResolvedRef | FuncResolvedRef + +export interface InlineResolvedRef { + code: string + schema: object | boolean + inline: true +} + +export interface FuncResolvedRef { + code: string + $async?: boolean + inline?: false +} + /** * Compiles schema to validation function * @this Ajv @@ -92,16 +107,19 @@ function compile(schema, root, localRefs, baseId) { } var $async = _schema.$async === true + const rootId = resolve.fullPath(_root.schema.$id) // TODO refactor to extract code from gen let sourceCode = validateCode({ allErrors: !!opts.allErrors, isTop: true, + topSchemaRef: "validate.schema", async: _schema.$async === true, schema: _schema, isRoot, - baseId, root: _root, + rootId, + baseId: baseId || rootId, schemaPath: "", errSchemaPath: "#", errorPath: '""', @@ -186,7 +204,7 @@ function compile(schema, root, localRefs, baseId) { return validate } - function resolveRef(baseId: string, ref: string, isRoot: boolean) { + function resolveRef(baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { ref = resolve.url(baseId, ref) var refIndex = refs[ref] var _refVal, refCode @@ -239,7 +257,7 @@ function compile(schema, root, localRefs, baseId) { refVal[refId] = v } - function resolvedRef(refVal, code) { + function resolvedRef(refVal, code): ResolvedRef { return typeof refVal == "object" || typeof refVal == "boolean" ? {code: code, schema: refVal, inline: true} : {code: code, $async: refVal && !!refVal.$async} @@ -384,25 +402,22 @@ function compIndex(schema, root, baseId) { return -1 } -function patternCode(i, patterns) { - return "var pattern" + i + " = new RegExp(" + toQuotedString(patterns[i]) + ");" +function patternCode(i: number, patterns: string[]): string { + return `const pattern${i} = new RegExp(${quotedString(patterns[i])});` } -function defaultCode(i) { - return "var default" + i + " = defaults[" + i + "];" +function defaultCode(i: number): string { + return `const default${i} = defaults[${i}];` } -function refValCode(i, refVal) { - return refVal[i] === undefined ? "" : "var refVal" + i + " = refVal[" + i + "];" +function refValCode(i: number, refVal): string { + return refVal[i] === undefined ? "" : `const refVal${i} = refVal[${i}];` } -function customRuleCode(i) { - return "var customRule" + i + " = customRules[" + i + "];" +function customRuleCode(i: number): string { + return `const customRule${i} = customRules[${i}];` } -function vars(arr, statement) { - if (!arr.length) return "" - var code = "" - for (var i = 0; i < arr.length; i++) code += statement(i, arr) - return code +function vars(arr: unknown[], statement: (i: number, arr: any[]) => string) { + return arr.reduce((code: string, _, i: number) => code + statement(i, arr), "") } diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 0fc722175f..2d2a578b32 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -1,8 +1,6 @@ import {toHash} from "./util" import {CompilationContext, KeywordDefinition} from "../types" -const ruleModules = require("../dotjs") - export interface ValidationRules { rules: RuleGroup[] all: {[key: string]: boolean | Rule} @@ -13,11 +11,9 @@ export interface ValidationRules { export interface RuleGroup { type?: string - rules: RuleDef[] + rules: Rule[] } -type RuleDef = string | {[key: string]: string[]} | Rule - export interface Rule { keyword: string code: (it: CompilationContext, keyword?: string, ruleType?: string) => void @@ -55,7 +51,7 @@ export default function rules(): ValidationRules { {type: "string", rules: []}, {type: "array", rules: []}, {type: "object", rules: []}, - {rules: ["$ref"]}, + {rules: []}, ], all: toHash(ALL), keywords: {}, @@ -64,26 +60,6 @@ export default function rules(): ValidationRules { } RULES.rules.forEach((group) => { - group.rules = group.rules.map((keyword: string | object) => { - let implKeywords: string[] | undefined - if (typeof keyword == "object") { - const key: string = Object.keys(keyword)[0] - implKeywords = keyword[key] - keyword = key - ;(implKeywords).forEach((k) => { - ALL.push(k) - RULES.all[k] = true - }) - } - ALL.push(keyword) - const rule: Rule = (RULES.all[keyword] = { - keyword: keyword, - code: ruleModules[keyword], - implements: implKeywords, - }) - return rule - }) - if (group.type) RULES.types[group.type] = group }) diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index d7835e7938..e91e3245fd 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -4,13 +4,17 @@ import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" import {quotedString} from "../vocabularies/util" export interface SubschemaContext { - schema: any + schema: object | boolean schemaPath: string errSchemaPath: string + topSchemaRef?: string errorPath?: string dataPathArr?: (string | number)[] dataLevel?: number + propertyName?: string compositeRule?: true + createErrors?: boolean + allErrors?: boolean } export enum Expr { @@ -20,8 +24,12 @@ export enum Expr { } export interface SubschemaApplication { - keyword: string + keyword?: string schemaProp?: string | number + schema?: object | boolean + schemaPath?: string + errSchemaPath?: string + topSchemaRef?: string data?: string dataProp?: string | number propertyName?: string @@ -33,12 +41,28 @@ export interface SubschemaApplication { export function applySubschema( it: CompilationContext, - {keyword, schemaProp, data, dataProp, expr, ...rest}: SubschemaApplication, + appl: SubschemaApplication, valid: string ): void { - const schema = it.schema[keyword] - const subschema: SubschemaContext = - schemaProp === undefined + const subschema = getSubschema(it, appl) + extendSubschemaData(subschema, it, appl) + extendSubschemaMode(subschema, appl) + const nextContext = {...it, ...subschema, level: it.level + 1} + // TODO remove "true" once appendGen is removed + validateCode(nextContext, valid, true) +} + +function getSubschema( + it: CompilationContext, + {keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaApplication +): SubschemaContext { + if (keyword !== undefined && schema !== undefined) { + throw new Error('both "keyword" and "schema" passed, only one allowed') + } + + if (keyword !== undefined) { + const schema = it.schema[keyword] + return schemaProp === undefined ? { schema: schema, schemaPath: it.schemaPath + getProperty(keyword), @@ -49,38 +73,64 @@ export function applySubschema( schemaPath: it.schemaPath + getProperty(keyword) + getProperty(schemaProp), errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + schemaProp)}`, } + } + + if (schema !== undefined) { + if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) + throw new Error('"schemaPath", "errSchemaPath" and "topSchemaRef" are required with "schema"') + return { + schema, + schemaPath, + topSchemaRef, + errSchemaPath, + } + } + throw new Error('either "keyword" or "schema" must be passed') +} + +function extendSubschemaData( + subschema: SubschemaContext, + it: CompilationContext, + {dataProp, expr, data, propertyName}: SubschemaApplication +) { if (data !== undefined && dataProp !== undefined) { - throw new Error('both "data" and "dataProp" are passed, only one allowed') - } else if (dataProp !== undefined) { + throw new Error('both "data" and "dataProp" passed, only one allowed') + } + + if (dataProp !== undefined) { const {gen, errorPath, dataPathArr, dataLevel, opts} = it // TODO possibly refactor getPath and getPathExpr to one function using Expr enum const nextLevel = dataLevel + 1 - Object.assign(subschema, { - errorPath: - expr === Expr.Const - ? getPath(errorPath, dataProp, opts.jsonPointers) - : getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num), - dataPathArr: [ - ...dataPathArr, - expr === Expr.Const && typeof dataProp == "string" ? quotedString(dataProp) : dataProp, - ], - dataLevel: nextLevel, - }) + subschema.errorPath = + expr === Expr.Const + ? getPath(errorPath, dataProp, opts.jsonPointers) + : getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num) + subschema.dataPathArr = [ + ...dataPathArr, + expr === Expr.Const && typeof dataProp == "string" ? quotedString(dataProp) : dataProp, + ] + subschema.dataLevel = nextLevel // TODO refactor - use accessProperty const passDataProp = expr === Expr.Const ? getProperty(dataProp) : `[${dataProp}]` gen.code(`var data${nextLevel} = data${dataLevel || ""}${passDataProp};`) - } else if (data !== undefined) { + } + + if (data !== undefined) { const {gen, dataLevel} = it const nextLevel = dataLevel + 1 subschema.dataLevel = nextLevel + if (propertyName !== undefined) subschema.propertyName = propertyName gen.code(`var data${nextLevel} = ${data};`) } +} - Object.assign(subschema, rest) - - const nextContext = {...it, ...subschema, level: it.level + 1} - // TODO remove "true" once appendGen is removed - validateCode(nextContext, valid, true) +function extendSubschemaMode( + subschema: SubschemaContext, + {compositeRule, createErrors, allErrors}: SubschemaApplication +) { + if (compositeRule !== undefined) subschema.compositeRule = compositeRule + if (createErrors !== undefined) subschema.createErrors = createErrors + if (allErrors !== undefined) subschema.allErrors = allErrors } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 0abef12492..4b007620db 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -207,6 +207,7 @@ export function getData($data: string, lvl: number, paths: (number | string)[]): export function joinPaths(a: string, b: string): string { if (a === '""' || a === "''") return b + if (b === '""' || b === "''") return a return `${a} + ${b}`.replace(/([^\\])' \+ '/g, "$1") } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 7e50171fd5..6ee325f829 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -137,8 +137,8 @@ function startFunction({ } function updateTopContext(it: CompilationContext): void { - it.rootId = resolve.fullPath(it.root.schema.$id) - it.baseId = it.baseId || it.rootId + // it.rootId = resolve.fullPath(it.root.schema.$id) + // it.baseId = it.baseId || it.rootId delete it.isTop } diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst index db3e1f6fdc..8b60fcda44 100644 --- a/lib/dot/custom.jst +++ b/lib/dot/custom.jst @@ -23,7 +23,7 @@ {{ $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it); if (!$ruleValidate) return; - $schemaValue = 'validate.schema' + $schemaPath; + $schemaValue = it.topSchemaRef + $schemaPath; $validateCode = $ruleValidate.code; $compile = $rDef.compile; $inline = $rDef.inline; @@ -54,7 +54,7 @@ var {{=$valid}}; {{??}} , {{=$schemaValue}} , {{=$data}} - , validate.schema{{=it.schemaPath}} + , {{=it.topSchemaRef}}{{=it.schemaPath}} {{?}} , {{# def.dataPath }} {{# def.passParentData }} diff --git a/lib/dot/errors.def b/lib/dot/errors.def index 59895a5b60..b65e02521f 100644 --- a/lib/dot/errors.def +++ b/lib/dot/errors.def @@ -13,7 +13,7 @@ {{?}} {{? it.opts.verbose }} , schema: {{# def._errorSchemas[_rule] }} - , parentSchema: validate.schema{{=it.schemaPath}} + , parentSchema: {{=it.topSchemaRef}}{{=it.schemaPath}} , data: {{=$data}} {{?}} } @@ -98,15 +98,15 @@ } #}} -{{## def.schemaRefOrQS: {{?$isData}}validate.schema{{=$schemaPath}}{{??}}{{=it.util.toQuotedString($schema)}}{{?}} #}} +{{## def.schemaRefOrQS: {{?$isData}}{{=it.topSchemaRef}}{{=$schemaPath}}{{??}}{{=it.util.toQuotedString($schema)}}{{?}} #}} {{## def._errorSchemas = { $ref: "{{=it.util.toQuotedString($schema)}}", - custom: "validate.schema{{=$schemaPath}}", - patternRequired: "validate.schema{{=$schemaPath}}", - switch: "validate.schema{{=$schemaPath}}", + custom: "{{=it.topSchemaRef}}{{=$schemaPath}}", + patternRequired: "{{=it.topSchemaRef}}{{=$schemaPath}}", + switch: "{{=it.topSchemaRef}}{{=$schemaPath}}", _formatLimit: "{{#def.schemaRefOrQS}}", - _formatExclusiveLimit: "validate.schema{{=$schemaPath}}" + _formatExclusiveLimit: "{{=it.topSchemaRef}}{{=$schemaPath}}" } #}} diff --git a/lib/dot/ref.jst b/lib/dot/ref.jst deleted file mode 100644 index f5594fbe56..0000000000 --- a/lib/dot/ref.jst +++ /dev/null @@ -1,87 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} - -{{## def._validateRef:_v: - {{? it.opts.passContext }} - {{=_v}}.call(this, - {{??}} - {{=_v}}( - {{?}} - {{=$data}}, {{# def.dataPath }}{{# def.passParentData }}, rootData) -#}} - -{{ var $async, $refCode; }} -{{? $schema == '#' || $schema == '#/' }} - {{ - if (it.isRoot) { - $async = it.async; - $refCode = 'validate'; - } else { - $async = it.root.schema.$async === true; - $refCode = 'root.refVal[0]'; - } - }} -{{??}} - {{ var $refVal = it.resolveRef(it.baseId, $schema, it.isRoot); }} - {{? $refVal === undefined }} - {{ var $message = it.MissingRefError.message(it.baseId, $schema); }} - {{? it.opts.missingRefs == 'fail' }} - {{ it.logger.error($message); }} - {{# def.error:'$ref' }} - {{? $breakOnError }} if (false) { {{?}} - {{?? it.opts.missingRefs == 'ignore' }} - {{ it.logger.warn($message); }} - {{? $breakOnError }} if (true) { {{?}} - {{??}} - {{ throw new it.MissingRefError(it.baseId, $schema, $message); }} - {{?}} - {{?? $refVal.inline }} - {{# def.setupNextLevel }} - {{ - $it.schema = $refVal.schema; - $it.schemaPath = ''; - $it.errSchemaPath = $schema; - }} - {{ var $code = it.validateCode($it).replace(/validate\.schema/g, $refVal.code); }} - {{= $code }} - {{? $breakOnError}} - if ({{=$nextValid}}) { - {{?}} - {{??}} - {{ - $async = $refVal.$async === true || (it.async && $refVal.$async !== false); - $refCode = $refVal.code; - }} - {{?}} -{{?}} - -{{? $refCode }} - {{# def.beginDefOut}} - {{# def._validateRef:$refCode }} - {{# def.storeDefOut:__callValidate }} - - {{? $async }} - {{ if (!it.async) throw new Error('async schema referenced by sync schema'); }} - {{? $breakOnError }} var {{=$valid}}; {{?}} - try { - await {{=__callValidate}}; - {{? $breakOnError }} {{=$valid}} = true; {{?}} - } catch (e) { - if (!(e instanceof ValidationError)) throw e; - if (vErrors === null) vErrors = e.errors; - else vErrors = vErrors.concat(e.errors); - errors = vErrors.length; - {{? $breakOnError }} {{=$valid}} = false; {{?}} - } - {{? $breakOnError }} if ({{=$valid}}) { {{?}} - {{??}} - if (!{{=__callValidate}}) { - if (vErrors === null) vErrors = {{=$refCode}}.errors; - else vErrors = vErrors.concat({{=$refCode}}.errors); - errors = vErrors.length; - } {{? $breakOnError }} else { {{?}} - {{?}} -{{?}} - -{{ it.gen.code(out); }} diff --git a/lib/dotjs/index.js b/lib/dotjs/index.js deleted file mode 100644 index 7ef164599b..0000000000 --- a/lib/dotjs/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict" - -//all requires must be explicit because browserify won't work with dynamic requires -module.exports = { - $ref: require("./ref"), -} diff --git a/lib/keyword.ts b/lib/keyword.ts index 7759c77d2d..5d104f61d5 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -98,8 +98,6 @@ export function addKeyword( } } - RULES.keywords[keyword] = RULES.all[keyword] = true - function _addRule(keyword: string, dataType: string | undefined, definition: KeywordDefinition) { let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) @@ -130,7 +128,8 @@ export function addKeyword( ruleGroup.rules.push(rule) } - RULES.custom[keyword] = rule + RULES.custom[keyword] = RULES.all[keyword] = rule + RULES.keywords[keyword] = true } return this @@ -146,11 +145,11 @@ export function addKeyword( function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): void { const schema = it.schema[keyword] const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition - const {gen, opts, dataLevel, schemaPath, dataPathArr, allErrors} = it + const {gen, opts, dataLevel, dataPathArr, allErrors} = it if (!code) throw new Error('"code" and "error" must be defined') const $data = $defData && opts.$data && schema && schema.$data const data = "data" + (dataLevel || "") - const schemaValue = schemaRefOrVal(schema, schemaPath, keyword, $data) + const schemaValue = schemaRefOrVal(it, schema, keyword, $data) const cxt: KeywordContext = { gen, fail, @@ -225,8 +224,8 @@ function validSchemaType(schema: any, schemaType: string | string[]): boolean { } export function getKeywordContext(it: CompilationContext, keyword: string): KeywordContext { - const {gen, schema, schemaPath, dataLevel} = it - const schemaCode = schemaRefOrVal(schema, schemaPath, keyword) + const {gen, schema, dataLevel} = it + const schemaCode = schemaRefOrVal(it, schema, keyword) return { gen, fail: exception, diff --git a/lib/types.ts b/lib/types.ts index 09aec9c6c6..1b3cd8aa9b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -2,6 +2,7 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" import {ValidationRules, Rule} from "./compile/rules" import {MissingRefError} from "./compile/error_classes" +import {ResolvedRef} from "./compile" export interface Options { $data?: boolean @@ -114,7 +115,7 @@ export interface CompilationContext { propertyName?: string gen: CodeGen createErrors?: boolean // TODO maybe remove later - baseId?: string // TODO probably not optional + baseId: string async: boolean opts: Options formats: { @@ -134,10 +135,11 @@ export interface CompilationContext { logger: Logger // TODO ? isTop: boolean // TODO ? root: SchemaRoot // TODO ? - rootId?: string // TODO ? + rootId: string // TODO ? + topSchemaRef: string MissingRefError: typeof MissingRefError resolve: any - resolveRef: (...args: any[]) => any + resolveRef: (...args: any[]) => ResolvedRef | void } interface SchemaRoot { diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 5a00700e0b..3155a23b37 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -51,7 +51,7 @@ const def: KeywordDefinition = { let definedProp = "" if (props.length > 8) { // TODO maybe an option instead of hard-coded 8? - const propsSchema = schemaRefOrVal(parentSchema.properties, it.schemaPath, "properties") + const propsSchema = schemaRefOrVal(it, parentSchema.properties, "properties") definedProp = `${propsSchema}.hasOwnProperty(${key})` } else if (props.length) { definedProp = orExpr(props, (p) => `${key} === ${quotedString(p)}`) diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 782570da8f..205f12ac62 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -18,4 +18,6 @@ const applicator: Vocabulary = [ require("./if"), ] +export default applicator + module.exports = applicator diff --git a/lib/vocabularies/core/index.ts b/lib/vocabularies/core/index.ts new file mode 100644 index 0000000000..a6602c0bc6 --- /dev/null +++ b/lib/vocabularies/core/index.ts @@ -0,0 +1,7 @@ +import {Vocabulary} from "../../types" + +const core: Vocabulary = [require("./ref")] + +export default core + +module.exports = core diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts new file mode 100644 index 0000000000..c3e655fdd1 --- /dev/null +++ b/lib/vocabularies/core/ref.ts @@ -0,0 +1,113 @@ +import {KeywordDefinition} from "../../types" +import {MissingRefError} from "../../compile/error_classes" +import {applySubschema} from "../../compile/subschema" +import {ResolvedRef, InlineResolvedRef} from "../../compile" + +const def: KeywordDefinition = { + keyword: "$ref", + schemaType: "string", + code({gen, ok, fail, data, schema, it}) { + const {resolveRef, allErrors, baseId, isRoot, root, opts, logger} = it + const ref = getRef() + if (ref === undefined) missingRef() + else if (ref.inline) applyRefSchema(ref) + else if (ref.$async) validateAsyncRef(ref.code) + else validateRef(ref.code) + + function getRef(): ResolvedRef | void { + if (schema === "#" || schema === "#/") { + return isRoot + ? {code: "validate", $async: it.async} + : {code: "root.refVal[0]", $async: root.schema.$async === true} + } + + const ref = resolveRef(baseId, schema, isRoot) + if (ref && !ref.inline && it.async) ref.$async = true + return ref + } + + function missingRef(): void { + const msg = MissingRefError.message(baseId, schema) + switch (opts.missingRefs) { + case "fail": + logger.error(msg) + return fail() + case "ignore": + logger.warn(msg) + return ok() + default: + throw new MissingRefError(baseId, schema, msg) + } + } + + function applyRefSchema(ref: InlineResolvedRef): void { + const valid = gen.name("valid") + applySubschema( + it, + { + schema: ref.schema, + schemaPath: "", + topSchemaRef: ref.code, + errSchemaPath: schema, + }, + valid + ) + ok(valid) + } + + function validateAsyncRef(v: string): void { + const valid = gen.name("valid") + if (!it.async) throw new Error("async schema referenced by sync schema") + if (!allErrors) gen.code(`let ${valid};`) + gen.try( + () => { + gen.code(`await ${callValidate(v)};`) + if (!allErrors) gen.code(`${valid} = true;`) + }, + () => { + gen.if("!(e instanceof ValidationError)", "throw e") + addErrorsFrom("e") + if (!allErrors) gen.code(`${valid} = false;`) + }, + "e" + ) + ok(valid) + } + + function validateRef(v: string): void { + // TODO refactor ifs + gen.code(`if (!${callValidate(v)}) {`) + addErrorsFrom(v) + // refactor ifs + gen.code(allErrors ? "}" : "} else {") + } + + function callValidate(v: string): string { + const {errorPath, dataLevel, dataPathArr} = it + const dataPath = `(dataPath || '')${errorPath === '""' ? "" : ` + ${errorPath}`}` // TODO joinPaths? + const parentArgs = dataLevel + ? `data${dataLevel - 1 || ""}, ${dataPathArr[dataLevel]}` + : "parentData, parentDataProperty" + const args = `${data}, ${dataPath}, ${parentArgs}, rootData` + return opts.passContext ? `${v}.call(this, ${args})` : `${v}(${args})` + } + + function addErrorsFrom(source: string): void { + gen.if( + "vErrors === null", + `vErrors = ${source}.errors`, + `vErrors = vErrors.concat(${source}.errors)` + ) + gen.code(`errors = vErrors.length;`) + } + }, + error: { + message: ({$data, schemaCode}) => + $data + ? `'should match format "' + ${schemaCode} + '"'` + : `"should match format \\"${(schemaCode).slice(1, -1)}\\""`, + params: ({schemaCode}) => `{format: ${schemaCode}}`, + }, +} + +module.exports = def diff --git a/lib/vocabularies/format/index.ts b/lib/vocabularies/format/index.ts index e40be9fcb0..4a7ccc4c35 100644 --- a/lib/vocabularies/format/index.ts +++ b/lib/vocabularies/format/index.ts @@ -2,4 +2,6 @@ import {Vocabulary} from "../../types" const format: Vocabulary = [require("./format")] +export default format + module.exports = format diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 822a871525..d3837282ff 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -31,8 +31,8 @@ export function dataNotType( } export function schemaRefOrVal( + {topSchemaRef, schemaPath}: CompilationContext, schema: unknown, - schemaPath: string, keyword: string, $data?: string | false ): string | number | boolean { @@ -40,7 +40,7 @@ export function schemaRefOrVal( if (typeof schema == "number" || typeof schema == "boolean") return schema if (typeof schema == "string") return quotedString(schema) } - return `validate.schema${schemaPath + getProperty(keyword)}` + return `${topSchemaRef}${schemaPath + getProperty(keyword)}` } export function alwaysValidSchema( diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 36ccfc2ce1..35238ea938 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -18,4 +18,6 @@ const validation: Vocabulary = [ require("./enum"), ] +export default validation + module.exports = validation From a1eb1fb863f210d59f37509d725d507adbabcb75 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 24 Aug 2020 09:10:33 -0400 Subject: [PATCH 085/322] refactor, fix eslint rules --- .eslintrc.yml | 5 +-- lib/ajv.ts | 53 ++++++++++++++------------- lib/compile/subschema.ts | 9 +++-- lib/compile/validate/applicability.ts | 3 +- lib/compile/validate/keywords.ts | 3 +- lib/keyword.ts | 6 +-- lib/vocabularies/core/ref.ts | 13 +++---- 7 files changed, 44 insertions(+), 48 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index dec1f9f6a3..82985087ef 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -21,8 +21,7 @@ overrides: plugins: ["@typescript-eslint"] rules: no-var: 0 - "@typescript-eslint/restrict-template-expressions": - [error, allowBoolean: true] + "@typescript-eslint/restrict-template-expressions": [error, allowBoolean: true] "@typescript-eslint/ban-types": off "@typescript-eslint/no-empty-interface": off "@typescript-eslint/no-explicit-any": off @@ -38,7 +37,7 @@ overrides: rules: block-scoped-var: error callback-return: error - complexity: [error, 16] + complexity: [error, 17] curly: [error, multi-line, consistent] dot-location: [error, property] dot-notation: error diff --git a/lib/ajv.ts b/lib/ajv.ts index 9e65fc256d..8abf5a9970 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -298,7 +298,7 @@ function _removeAllSchemas(self, schemas, regex?: RegExp) { } /* @this Ajv */ -function _addSchema(schema: object | boolean, skipValidation, meta, shouldAddSchema) { +function _addSchema(schema: {[x: string]: any} | boolean, skipValidation, meta, shouldAddSchema) { if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema should be object or boolean") } @@ -309,24 +309,23 @@ function _addSchema(schema: object | boolean, skipValidation, meta, shouldAddSch shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false - var id = resolve.normalizeId(schema["$id"]) + let $id, $schema + if (typeof schema == "object") { + $id = schema.$id + $schema = schema.$schema + } + var id = resolve.normalizeId($id) if (id && shouldAddSchema) checkUnique(this, id) var willValidate = this._opts.validateSchema !== false && !skipValidation var recursiveMeta - if (willValidate && !(recursiveMeta = id && id === resolve.normalizeId(schema["$schema"]))) { + if (willValidate && !(recursiveMeta = id && id === resolve.normalizeId($schema))) { this.validateSchema(schema, true) } var localRefs = resolve.ids.call(this, schema) - var schemaObj = new SchemaObject({ - id: id, - schema: schema, - localRefs: localRefs, - cacheKey: cacheKey, - meta: meta, - }) + var schemaObj = new SchemaObject({id, schema, localRefs, cacheKey, meta}) if (id[0] !== "#" && shouldAddSchema) this._refs[id] = schemaObj this._cache.put(cacheKey, schemaObj) @@ -338,25 +337,26 @@ function _addSchema(schema: object | boolean, skipValidation, meta, shouldAddSch /* @this Ajv */ function _compile(schemaObj, root) { - if (schemaObj.compiling) { - schemaObj.validate = callValidate - callValidate.schema = schemaObj.schema - callValidate.errors = null - callValidate.root = root ? root : callValidate - if (schemaObj.schema.$async === true) callValidate.$async = true - return callValidate - } + if (schemaObj.compiling) return _makeValidate(schemaObj, root) schemaObj.compiling = true + const v = _tryCompile.call(this, schemaObj, root) + schemaObj.validate = v + schemaObj.refs = v.refs + schemaObj.refVal = v.refVal + schemaObj.root = v.root + return v +} +/* @this Ajv */ +function _tryCompile(schemaObj, root) { var currentOpts if (schemaObj.meta) { currentOpts = this._opts this._opts = this._metaOpts } - var v try { - v = compileSchema.call(this, schemaObj.schema, root, schemaObj.localRefs) + return compileSchema.call(this, schemaObj.schema, root, schemaObj.localRefs) } catch (e) { delete schemaObj.validate throw e @@ -364,12 +364,15 @@ function _compile(schemaObj, root) { schemaObj.compiling = false if (schemaObj.meta) this._opts = currentOpts } +} - schemaObj.validate = v - schemaObj.refs = v.refs - schemaObj.refVal = v.refVal - schemaObj.root = v.root - return v +function _makeValidate(schemaObj, root) { + schemaObj.validate = callValidate + callValidate.schema = schemaObj.schema + callValidate.errors = null + callValidate.root = root ? root : callValidate + if (schemaObj.schema.$async === true) callValidate.$async = true + return callValidate /* @this {*} - custom context, see passContext option */ function callValidate(...args) { diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index e91e3245fd..0f782051f8 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -61,23 +61,24 @@ function getSubschema( } if (keyword !== undefined) { - const schema = it.schema[keyword] + const sch = it.schema[keyword] return schemaProp === undefined ? { - schema: schema, + schema: sch, schemaPath: it.schemaPath + getProperty(keyword), errSchemaPath: `${it.errSchemaPath}/${keyword}`, } : { - schema: schema[schemaProp], + schema: sch[schemaProp], schemaPath: it.schemaPath + getProperty(keyword) + getProperty(schemaProp), errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + schemaProp)}`, } } if (schema !== undefined) { - if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) + if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) { throw new Error('"schemaPath", "errSchemaPath" and "topSchemaRef" are required with "schema"') + } return { schema, schemaPath, diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 4ec20166b7..010de7e57f 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -7,8 +7,7 @@ export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: s } export function shouldUseGroup(schema: object, group: RuleGroup): boolean { - // TODO remove type cast to Rule - return group.rules.some((rule) => shouldUseRule(schema, rule)) + return group.rules.some((rule) => shouldUseRule(schema, rule)) } export function shouldUseRule(schema: object, rule: Rule): boolean | undefined { diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/keywords.ts index 01f014d665..bb070a562c 100644 --- a/lib/compile/validate/keywords.ts +++ b/lib/compile/validate/keywords.ts @@ -60,8 +60,7 @@ function iterateKeywords(it: CompilationContext, group: RuleGroup) { } = it if (useDefaults) assignDefaults(it, group.type) let closeBlocks = "" - // TODO remove Rule type cast - for (const rule of group.rules as Rule[]) { + for (const rule of group.rules) { if (shouldUseRule(schema, rule)) { // TODO _outLen const _outLen = gen._out.length diff --git a/lib/keyword.ts b/lib/keyword.ts index 5d104f61d5..151f85d99d 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -115,8 +115,7 @@ export function addKeyword( } if (definition.before) { - // TODO remove type case when RuleDef is removed - const i = ruleGroup.rules.findIndex((rule) => (rule as Rule).keyword === definition.before) + const i = ruleGroup.rules.findIndex((rule) => rule.keyword === definition.before) if (i >= 0) { ruleGroup.rules.splice(i, 0, rule) } else { @@ -272,8 +271,7 @@ export function removeKeyword(keyword: string): object { delete RULES.all[keyword] delete RULES.custom[keyword] for (const group of RULES.rules) { - // TODO remove type cast once all rules migrated - const i = group.rules.findIndex((rule) => (rule).keyword === keyword) + const i = group.rules.findIndex((rule) => rule.keyword === keyword) if (i >= 0) group.rules.splice(i, 1) } return this diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index c3e655fdd1..193d6af875 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -11,7 +11,7 @@ const def: KeywordDefinition = { const ref = getRef() if (ref === undefined) missingRef() else if (ref.inline) applyRefSchema(ref) - else if (ref.$async) validateAsyncRef(ref.code) + else if (ref.$async || it.async) validateAsyncRef(ref.code) else validateRef(ref.code) function getRef(): ResolvedRef | void { @@ -20,10 +20,7 @@ const def: KeywordDefinition = { ? {code: "validate", $async: it.async} : {code: "root.refVal[0]", $async: root.schema.$async === true} } - - const ref = resolveRef(baseId, schema, isRoot) - if (ref && !ref.inline && it.async) ref.$async = true - return ref + return resolveRef(baseId, schema, isRoot) } function missingRef(): void { @@ -40,14 +37,14 @@ const def: KeywordDefinition = { } } - function applyRefSchema(ref: InlineResolvedRef): void { + function applyRefSchema(inlineRef: InlineResolvedRef): void { const valid = gen.name("valid") applySubschema( it, { - schema: ref.schema, + schema: inlineRef.schema, schemaPath: "", - topSchemaRef: ref.code, + topSchemaRef: inlineRef.code, errSchemaPath: schema, }, valid From 84fa223efe0d7fb7857696d113ed28ab5f37eb85 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 24 Aug 2020 09:50:43 -0400 Subject: [PATCH 086/322] remove support for "inline" keyword (9 tests skipped, to replace with "code" keywords) --- lib/compile/index.ts | 3 --- lib/definition_schema.ts | 9 +++++++-- lib/dot/custom.jst | 31 ++++--------------------------- lib/types.ts | 2 -- spec/ajv.spec.js | 3 ++- spec/custom.spec.js | 9 ++++++--- 6 files changed, 19 insertions(+), 38 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index a4acdd9607..5a3523a017 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -321,7 +321,6 @@ function compile(schema, root, localRefs, baseId) { } var compile = ruleDef.compile, - inline = ruleDef.inline, macro = ruleDef.macro var validate @@ -330,8 +329,6 @@ function compile(schema, root, localRefs, baseId) { } else if (macro) { validate = macro.call(self, schema, parentSchema, it) if (opts.validateSchema !== false) self.validateSchema(validate, true) - } else if (inline) { - validate = inline.call(self, it, rule.keyword, schema, parentSchema) } else { validate = ruleDef.validate if (!validate) return diff --git a/lib/definition_schema.ts b/lib/definition_schema.ts index 2daf60e59a..09562dce59 100644 --- a/lib/definition_schema.ts +++ b/lib/definition_schema.ts @@ -2,20 +2,25 @@ const metaSchema = require("./refs/json-schema-draft-07.json") export const definitionSchema: object = { $id: "https://github.com/ajv-validator/ajv/blob/master/lib/definition_schema.js", + description: 'Keyword definition schema. "inline" keywords are replaced with "code" in Ajv v7', definitions: { simpleTypes: metaSchema.definitions.simpleTypes, }, type: "object", dependencies: { schema: ["validate"], - statements: ["inline"], valid: {not: {required: ["macro"]}}, $data: {anyOf: [{required: ["code"]}, {required: ["validate"]}]}, }, properties: { + inline: false, + validate: true, + compile: true, + macro: true, + code: true, type: metaSchema.properties.type, + schemaType: metaSchema.properties.type, schema: {type: "boolean"}, - statements: {type: "boolean"}, dependencies: { type: "array", items: {type: "string"}, diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst index 8b60fcda44..1c47e1767e 100644 --- a/lib/dot/custom.jst +++ b/lib/dot/custom.jst @@ -9,7 +9,7 @@ , $rDef = $rule.definition , $closingBraces = ''; var $validate = $rDef.validate; - var $compile, $inline, $macro, $ruleValidate, $validateCode; + var $compile, $macro, $ruleValidate, $validateCode; }} {{? $isData && $rDef.$data }} @@ -26,7 +26,6 @@ $schemaValue = it.topSchemaRef + $schemaPath; $validateCode = $ruleValidate.code; $compile = $rDef.compile; - $inline = $rDef.inline; $macro = $rDef.macro; }} {{?}} @@ -42,7 +41,7 @@ }} -{{? !($inline || $macro) }}{{=$ruleErrs}} = null;{{?}} +{{? !$macro }}{{=$ruleErrs}} = null;{{?}} var {{=$errs}} = errors; var {{=$valid}}; @@ -90,13 +89,7 @@ var {{=$valid}}; {{?}} {{?}} -{{? $inline }} - {{? $rDef.statements }} - {{= $ruleValidate.validate }} - {{??}} - {{=$valid}} = {{= $ruleValidate.validate }}; - {{?}} -{{?? $macro }} +{{? $macro }} {{# def.setupNextLevel }} {{ $it.schema = $ruleValidate.validate; @@ -154,23 +147,7 @@ var {{=$valid}}; {{# def.error:'custom' }} {{# def.storeDefOut:def_customError }} - {{? $inline }} - {{? $rDef.errors }} - {{? $rDef.errors != 'full' }} - {{# def.extendErrors:true }} - {{?}} - {{??}} - {{? $rDef.errors === false}} - {{= def_customError }} - {{??}} - if ({{=$errs}} == errors) { - {{= def_customError }} - } else { - {{# def.extendErrors:true }} - } - {{?}} - {{?}} - {{?? $macro }} + {{? $macro }} {{# def.extraError:'custom' }} {{??}} {{? $rDef.errors === false}} diff --git a/lib/types.ts b/lib/types.ts index 1b3cd8aa9b..4295b78909 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -159,7 +159,6 @@ export interface KeywordDefinition { before?: string // schema: false makes validate not to expect schema (ValidateFunction) schema?: boolean - statements?: boolean dependencies?: string[] modifying?: boolean valid?: boolean @@ -167,7 +166,6 @@ export interface KeywordDefinition { validate?: SchemaValidateFunction | ValidateFunction compile?: (schema: any, parentSchema: object, it: CompilationContext) => ValidateFunction macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean - inline?: (it: CompilationContext, keyword: string, schema: any, parentSchema: object) => string code?: (cxt: KeywordContext, ruleType?: string) => string | void error?: KeywordErrorDefinition validateSchema?: ValidateFunction diff --git a/spec/ajv.spec.js b/spec/ajv.spec.js index 9810f21ee7..efed36b43a 100644 --- a/spec/ajv.spec.js +++ b/spec/ajv.spec.js @@ -51,7 +51,8 @@ describe("Ajv", () => { }) }) - it("should throw if compiled schema has an invalid JavaScript code", () => { + // TODO replace with custom "code" keyword + it.skip("should throw if compiled schema has an invalid JavaScript code", () => { ajv.addKeyword("even", {inline: badEvenCode}) var schema = {even: true} var validate = ajv.compile(schema) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 054455f93b..85926109a8 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -498,7 +498,8 @@ describe("Custom keywords", () => { } }) - describe("inline rules", () => { + // TODO replace with custom "code" keyword + describe.skip("inline rules", () => { it('should add and validate rule with "inline" code keyword', () => { testEvenKeyword({type: "number", inline: inlineEven}) }) @@ -708,7 +709,8 @@ describe("Custom keywords", () => { } }) - it('should validate rule with "inline" and "validate" funcs', () => { + // TODO replace with custom "code" keyword + it.skip('should validate rule with "inline" and "validate" funcs', () => { var inlineCalled testEvenKeyword$data({ type: "number", @@ -730,7 +732,8 @@ describe("Custom keywords", () => { } }) - it('should validate with "inline" and "validate" funcs with meta-schema', () => { + // TODO replace with custom "code" keyword + it.skip('should validate with "inline" and "validate" funcs with meta-schema', () => { var inlineCalled testEvenKeyword$data({ type: "number", From 1dd63053c38789b2c414173022388cf2e7b31cd3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 24 Aug 2020 14:54:31 -0400 Subject: [PATCH 087/322] refactor: macro keywords code gen to typescript --- lib/compile/index.ts | 63 +------------ lib/compile/subschema.ts | 6 +- lib/compile/validate/index.ts | 2 +- .../validate/{keywords.ts => iterate.ts} | 0 lib/compile/validate/keyword.ts | 94 +++++++++++++++++++ lib/dot/custom.jst | 47 +++++++++- lib/keyword.ts | 16 +++- lib/types.ts | 77 +++++++++++---- .../applicator/additionalProperties.ts | 4 +- lib/vocabularies/applicator/allOf.ts | 4 +- lib/vocabularies/applicator/anyOf.ts | 4 +- lib/vocabularies/applicator/contains.ts | 4 +- lib/vocabularies/applicator/dependencies.ts | 4 +- lib/vocabularies/applicator/if.ts | 4 +- lib/vocabularies/applicator/items.ts | 4 +- lib/vocabularies/applicator/not.ts | 4 +- lib/vocabularies/applicator/oneOf.ts | 4 +- .../applicator/patternProperties.ts | 4 +- lib/vocabularies/applicator/properties.ts | 4 +- lib/vocabularies/applicator/propertyNames.ts | 4 +- lib/vocabularies/core/ref.ts | 4 +- lib/vocabularies/format/format.ts | 4 +- lib/vocabularies/validation/const.ts | 4 +- lib/vocabularies/validation/enum.ts | 4 +- lib/vocabularies/validation/limit.ts | 4 +- lib/vocabularies/validation/limitItems.ts | 4 +- lib/vocabularies/validation/limitLength.ts | 4 +- .../validation/limitProperties.ts | 4 +- lib/vocabularies/validation/multipleOf.ts | 4 +- lib/vocabularies/validation/pattern.ts | 4 +- lib/vocabularies/validation/required.ts | 4 +- lib/vocabularies/validation/uniqueItems.ts | 4 +- 32 files changed, 266 insertions(+), 135 deletions(-) rename lib/compile/validate/{keywords.ts => iterate.ts} (100%) create mode 100644 lib/compile/validate/keyword.ts diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 5a3523a017..f6509cf605 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -3,8 +3,8 @@ import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" import {MissingRefError} from "./error_classes" import validateCode from "./validate" -import {Rule} from "./rules" -import {CompilationContext, KeywordDefinition, ErrorObject} from "../types" +import {validateKeywordSchema} from "./validate/keyword" +import {ErrorObject, KeywordCompilationResult} from "../types" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -56,7 +56,7 @@ function compile(schema, root, localRefs, baseId) { patternsHash = {}, defaults: any[] = [], defaultsHash = {}, - customRules: any[] = [] + customRules: KeywordCompilationResult[] = [] root = root || {schema: schema, refVal: refVal, refs: refs} @@ -136,7 +136,8 @@ function compile(schema, root, localRefs, baseId) { resolveRef, // TODO remove to imports usePattern, // TODO remove to imports useDefault, // TODO remove to imports - useCustomRule, // TODO remove to imports + validateKeywordSchema, // TODO remove + customRules, // TODO add to types opts, formats, logger: self.logger, @@ -292,60 +293,6 @@ function compile(schema, root, localRefs, baseId) { throw new Error(`unsupported default type "${typeof value}"`) } } - - function useCustomRule( - rule: Rule, - schema: any, - parentSchema: object, - it: CompilationContext - ): any { - const ruleDef = rule.definition as KeywordDefinition - if (self._opts.validateSchema !== false) { - var deps = ruleDef.dependencies - if ( - deps && - !deps.every((keyword) => Object.prototype.hasOwnProperty.call(parentSchema, keyword)) - ) { - throw new Error("parent schema must have all required keywords: " + deps.join(",")) - } - - var validateSchema = ruleDef.validateSchema - if (validateSchema) { - var valid = validateSchema(schema) - if (!valid) { - var message = "keyword schema is invalid: " + self.errorsText(validateSchema.errors) - if (self._opts.validateSchema === "log") self.logger.error(message) - else throw new Error(message) - } - } - } - - var compile = ruleDef.compile, - macro = ruleDef.macro - - var validate - if (compile) { - validate = compile.call(self, schema, parentSchema, it) - } else if (macro) { - validate = macro.call(self, schema, parentSchema, it) - if (opts.validateSchema !== false) self.validateSchema(validate, true) - } else { - validate = ruleDef.validate - if (!validate) return - } - - if (validate === undefined) { - throw new Error('custom keyword "' + rule.keyword + '"failed to compile') - } - - var index = customRules.length - customRules[index] = validate - - return { - code: "customRule" + index, - validate, - } - } } /** diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 0f782051f8..18e5808fac 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -76,14 +76,14 @@ function getSubschema( } if (schema !== undefined) { - if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) { - throw new Error('"schemaPath", "errSchemaPath" and "topSchemaRef" are required with "schema"') + if (schemaPath === undefined || topSchemaRef === undefined) { + throw new Error('"schemaPath" and "topSchemaRef" are required with "schema"') } return { schema, schemaPath, topSchemaRef, - errSchemaPath, + errSchemaPath: errSchemaPath || it.errSchemaPath, } } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 6ee325f829..ac6ac3f28c 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -3,7 +3,7 @@ import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util" import {quotedString} from "../../vocabularies/util" import {booleanOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" -import {schemaKeywords} from "./keywords" +import {schemaKeywords} from "./iterate" const resolve = require("../resolve") diff --git a/lib/compile/validate/keywords.ts b/lib/compile/validate/iterate.ts similarity index 100% rename from lib/compile/validate/keywords.ts rename to lib/compile/validate/iterate.ts diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts new file mode 100644 index 0000000000..336d6cfed5 --- /dev/null +++ b/lib/compile/validate/keyword.ts @@ -0,0 +1,94 @@ +import { + KeywordDefinition, + KeywordErrorDefinition, + KeywordContext, + MacroKeywordDefinition, + CompiledKeywordDefinition, + ValidatedKeywordDefinition, + CompilationContext, + KeywordCompilationResult, +} from "../../types" +import {applySubschema} from "../subschema" +import {reportExtraError} from "../errors" + +const keywordError: KeywordErrorDefinition = { + message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, + params: ({keyword}) => `{keyword: "${keyword}"}`, // TODO possibly remove it as keyword is reported in the object +} + +export default function keywordCode( + cxt: KeywordContext, + ruleType: string, + def: KeywordDefinition +): void { + if ("macro" in def) macroKeywordCode(cxt, def) + else if ("compile" in def) compiledKeywordCode(cxt, ruleType, def) + else if ("validate" in def) validatedKeywordCode(cxt, ruleType, def) +} + +function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { + const {gen, keyword, schema, parentSchema, it} = cxt + const macroSchema = def.macro.call(it.self, schema, parentSchema, it) + const schemaRef = addCustomRule(it, keyword, macroSchema) + if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true) + + const valid = gen.name("valid") + applySubschema( + it, + { + schema: macroSchema, + schemaPath: "", + topSchemaRef: schemaRef, + compositeRule: true, + }, + valid + ) + + // TODO refactor ifs + gen.code(`if (!${valid}) {`) + reportExtraError(cxt, keywordError) + gen.code(it.allErrors ? "}" : "} else {") +} + +function compiledKeywordCode( + _cxt: KeywordContext, + _ruleType: string, + _def: CompiledKeywordDefinition +) {} + +function validatedKeywordCode( + _cxt: KeywordContext, + _ruleType: string, + _def: ValidatedKeywordDefinition +) {} + +export function validateKeywordSchema( + it: CompilationContext, + keyword: string, + def: KeywordDefinition +): void { + const deps = def.dependencies + if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(it.schema, kwd))) { + throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) + } + + if (def.validateSchema) { + const valid = def.validateSchema(it.schema[keyword]) + if (!valid) { + const msg = "keyword schema is invalid: " + it.self.errorsText(def.validateSchema.errors) + if (it.opts.validateSchema === "log") it.logger.error(msg) + else throw new Error(msg) + } + } +} + +function addCustomRule( + it: CompilationContext, + keyword: string, + res: KeywordCompilationResult +): string { + if (res === undefined) throw new Error(`custom keyword "${keyword}" failed to compile`) + const idx = it.customRules.length + it.customRules[idx] = res + return `customRule${idx}` +} diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst index 1c47e1767e..d33eaa02c8 100644 --- a/lib/dot/custom.jst +++ b/lib/dot/custom.jst @@ -21,7 +21,7 @@ var {{=$validateCode}} = {{=$definition}}.validate; {{??}} {{ - $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it); + $ruleValidate = useCustomRule.call(it.self, $rule, $schema, it.schema, it); if (!$ruleValidate) return; $schemaValue = it.topSchemaRef + $schemaPath; $validateCode = $ruleValidate.code; @@ -167,4 +167,47 @@ var {{=$valid}}; } {{? $breakOnError }} else { {{?}} {{?}} -{{ it.gen.code(out); }} +{{ + function useCustomRule( + rule, + schema, + parentSchema, + it + ) { + const ruleDef = rule.definition; + + if (this._opts.validateSchema !== false) { + it.validateKeywordSchema(it, $keyword, ruleDef); + } + + var compile = ruleDef.compile, + macro = ruleDef.macro; + + var validate; + if (compile) { + validate = compile.call(this, schema, parentSchema, it); + } else if (macro) { + validate = macro.call(this, schema, parentSchema, it); + if (it.opts.validateSchema !== false) this.validateSchema(validate, true); + } else { + validate = ruleDef.validate; + if (!validate) return; + } + + /* addCustomRule */ + if (validate === undefined) { + throw new Error('custom keyword "' + rule.keyword + '"failed to compile'); + } + + var index = it.customRules.length; + it.customRules[index] = validate; + + return { + code: "customRule" + index, + validate, + }; + } + +it.gen.code(out); + +}} \ No newline at end of file diff --git a/lib/keyword.ts b/lib/keyword.ts index 151f85d99d..75415a6bf6 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,5 +1,6 @@ import { KeywordDefinition, + CodeKeywordDefinition, KeywordErrorDefinition, Vocabulary, ErrorObject, @@ -14,6 +15,7 @@ import {reportError} from "./compile/errors" import {getData} from "./compile/util" import {schemaRefOrVal} from "./vocabularies/util" import {definitionSchema} from "./definition_schema" +import keywordCode, {validateKeywordSchema} from "./compile/validate/keyword" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i const customRuleCode = require("./dotjs/custom") @@ -110,7 +112,10 @@ export function addKeyword( keyword, definition, custom: true, - code: definition.code ? ruleCode : customRuleCode, + code: + "code" in definition || ("macro" in definition && !definition.$data) + ? ruleCode + : customRuleCode, implements: definition.implements, } @@ -143,9 +148,12 @@ export function addKeyword( */ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): void { const schema = it.schema[keyword] - const {schemaType, code, error, $data: $defData}: KeywordDefinition = this.definition + const def: CodeKeywordDefinition = this.definition + const {schemaType, code, error, $data: $defData} = def + validateKeywordSchema(it, keyword, def) const {gen, opts, dataLevel, dataPathArr, allErrors} = it - if (!code) throw new Error('"code" and "error" must be defined') + // TODO + // if (!code) throw new Error('"code" and "error" must be defined') const $data = $defData && opts.$data && schema && schema.$data const data = "data" + (dataLevel || "") const schemaValue = schemaRefOrVal(it, schema, keyword, $data) @@ -170,7 +178,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) } // TODO check that code called "fail" or another valid way to return code - code(cxt, ruleType) + ;(code || keywordCode)(cxt, ruleType, this.definition) // TODO replace with fail_ below function fail(condition?: string, context?: KeywordContext): void { diff --git a/lib/types.ts b/lib/types.ts index 4295b78909..2f033af1e4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,6 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" -import {ValidationRules, Rule} from "./compile/rules" +import {ValidationRules} from "./compile/rules" import {MissingRefError} from "./compile/error_classes" import {ResolvedRef} from "./compile" @@ -101,6 +101,8 @@ export interface ErrorObject { data?: any } +export type KeywordCompilationResult = object | boolean | SchemaValidateFunction | ValidateFunction + export interface CompilationContext { allErrors: boolean level: number @@ -128,9 +130,10 @@ export interface CompilationContext { validateCode: (it: CompilationContext) => string | void // TODO remove string usePattern: (str: string) => string useDefault: (value: any) => string - useCustomRule: (rule: Rule, schema: any, parentSchema: object, it: CompilationContext) => any - util: object // TODO - self: object // TODO + customRules: KeywordCompilationResult[] + validateKeywordSchema: (it: CompilationContext, keyword: string, def: KeywordDefinition) => void // TODO remove + util: any // TODO + self: any // TODO RULES: ValidationRules logger: Logger // TODO ? isTop: boolean // TODO ? @@ -148,30 +151,66 @@ interface SchemaRoot { refs: {[key: string]: any} // TODO } -export interface KeywordDefinition { +interface _KeywordDef { keyword?: string | string[] type?: string | string[] schemaType?: string | string[] - async?: boolean - $data?: boolean - errors?: boolean | "full" - metaSchema?: object + $data?: boolean // requires "validate" or "code" + implements?: string[] before?: string + metaSchema?: object + validateSchema?: ValidateFunction // compiled keyword metaSchema - should not be passed + dependencies?: string[] // keywords that must be present in the same schema +} + +interface FuncKeywordDef extends _KeywordDef { + validate?: SchemaValidateFunction | ValidateFunction // schema: false makes validate not to expect schema (ValidateFunction) - schema?: boolean - dependencies?: string[] + schema?: boolean // requires "validate" + errors?: boolean | "full" +} + +export interface CodeKeywordDefinition extends _KeywordDef { + code: (cxt: KeywordContext, ruleType?: string, def?: KeywordDefinition) => string | void + error?: KeywordErrorDefinition +} + +export type MacroKeywordFunc = ( + schema: any, + parentSchema: object, + it: CompilationContext +) => object | boolean + +export interface MacroKeywordDefinition extends FuncKeywordDef { + macro: MacroKeywordFunc +} + +export type CompileKeywordFunc = ( + schema: any, + parentSchema: object, + it: CompilationContext +) => ValidateFunction + +export interface CompiledKeywordDefinition extends FuncKeywordDef { + compile: CompileKeywordFunc modifying?: boolean + async?: boolean + valid?: boolean +} + +export interface ValidatedKeywordDefinition extends FuncKeywordDef { + validate: SchemaValidateFunction | ValidateFunction + modifying?: boolean + async?: boolean valid?: boolean - // at least one of the following properties should be present - validate?: SchemaValidateFunction | ValidateFunction - compile?: (schema: any, parentSchema: object, it: CompilationContext) => ValidateFunction - macro?: (schema: any, parentSchema: object, it: CompilationContext) => object | boolean - code?: (cxt: KeywordContext, ruleType?: string) => string | void - error?: KeywordErrorDefinition - validateSchema?: ValidateFunction - implements?: string[] } +export type KeywordDefinition = + | MacroKeywordDefinition + | CompiledKeywordDefinition + | ValidatedKeywordDefinition + | CodeKeywordDefinition + export interface KeywordErrorDefinition { message: string | ((cxt: KeywordContext) => string) params?: (cxt: KeywordContext) => string diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 3155a23b37..f62848bc3a 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,4 +1,4 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import { allSchemaProperties, schemaRefOrVal, @@ -15,7 +15,7 @@ const error: KeywordErrorDefinition = { params: ({params}) => `{additionalProperty: ${params.additionalProperty}}`, } -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "additionalProperties", type: "object", schemaType: ["object", "boolean"], diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index eb81ffe2a0..e06641418d 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,8 +1,8 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "allOf", schemaType: "array", code({gen, ok, schema, it}) { diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 9041b52ef4..536cd18d7d 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", code(cxt) { diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 232c8726fe..32f94770cd 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "contains", type: "array", schemaType: ["object", "boolean"], diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 04aea9b59b..186bb32fa4 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,4 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema, quotedString, propertyInData} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {escapeQuotes} from "../../compile/util" @@ -11,7 +11,7 @@ interface SchemaDependencies { [x: string]: object | boolean } -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "dependencies", type: "object", schemaType: "object", diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 96d0e11422..d70e67b488 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition, KeywordErrorDefinition, CompilationContext} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition, CompilationContext} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], // TODO diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 4b6e2fe22d..65500bcf60 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {fail_} from "../../keyword" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "items", type: "array", schemaType: ["object", "array", "boolean"], diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 32e344e29c..5edac49b48 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "not", schemaType: ["object", "boolean"], code(cxt) { diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 4f876d01fb..17b9c4ed7d 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", code(cxt) { diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 4dcaa7da0b..30d475e154 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,8 +1,8 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {schemaProperties, loopPropertiesCode} from "../util" import {applySubschema, Expr} from "../../compile/subschema" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "patternProperties", type: "object", schemaType: "object", diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 10c73fb776..4ccc90e310 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -1,8 +1,8 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {schemaProperties, propertyInData} from "../util" import {applySubschema, Expr} from "../../compile/subschema" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "properties", type: "object", schemaType: "object", diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 0f2600ec93..92b032dc1a 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema, loopPropertiesCode} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError} from "../../compile/errors" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "propertyNames", type: "object", schemaType: ["object", "boolean"], diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 193d6af875..47aa27a2bc 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,9 +1,9 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", code({gen, ok, fail, data, schema, it}) { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 8fa5e4223b..ea573e0a5d 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,8 +1,8 @@ -import {KeywordDefinition, AddedFormat, FormatValidate} from "../../types" +import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import {dataNotType} from "../util" import {getProperty} from "../../compile/util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "format", type: ["number", "string"], schemaType: "string", diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 07dab289fe..f1966e2408 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,6 +1,6 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "const", $data: true, code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 63b1c992e7..5bbf076501 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,7 +1,7 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {quotedString, orExpr} from "../util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "enum", schemaType: "array", $data: true, diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index d583dac118..f69a00e49f 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,4 +1,4 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {appendSchema, dataNotType} from "../util" const OPS: {[index: string]: {fail: string; ok: string}} = { @@ -8,7 +8,7 @@ const OPS: {[index: string]: {fail: string; ok: string}} = { exclusiveMinimum: {fail: "<=", ok: ">"}, } -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], type: "number", schemaType: "number", diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 72fe760f61..42eefe6999 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,7 +1,7 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: ["maxItems", "minItems"], type: "array", schemaType: "number", diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 2856438683..591f7f3c74 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,7 +1,7 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: ["maxLength", "minLength"], type: "string", schemaType: "number", diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index d58c057528..a05d7f6faf 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,7 +1,7 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {concatSchema, dataNotType} from "../util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: ["maxProperties", "minProperties"], type: "object", schemaType: "number", diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 3b6f038e52..338d446160 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,7 +1,7 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {appendSchema, dataNotType} from "../util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "multipleOf", type: "number", schemaType: "number", diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 76c8897268..5f05dec4d7 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,7 +1,7 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {dataNotType} from "../util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "pattern", type: "string", schemaType: "string", diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 046eaada8e..70e4662ec1 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,4 +1,4 @@ -import {KeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {propertyInData, noPropertyInData} from "../util" import {Expr} from "../../compile/subschema" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" @@ -14,7 +14,7 @@ const error: KeywordErrorDefinition = { missingProperty ? `{missingProperty: ${missingProperty}}` : "{}", } -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "required", type: "object", schemaType: ["array"], diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 4d02488a78..4a132484f2 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,7 +1,7 @@ -import {KeywordDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {checkDataType, checkDataTypes} from "../../compile/util" -const def: KeywordDefinition = { +const def: CodeKeywordDefinition = { keyword: "uniqueItems", type: "array", schemaType: "boolean", From 51ce38cea22d5bf4ec56864b5f02da4ead243ed2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 24 Aug 2020 16:59:00 -0400 Subject: [PATCH 088/322] refactor: "validate" and "compile" keywords to typescript (except $data) --- lib/compile/codegen.ts | 12 ++- lib/compile/errors.ts | 20 ++++- lib/compile/subschema.ts | 6 +- lib/compile/validate/keyword.ts | 132 +++++++++++++++++++++++++++----- lib/keyword.ts | 13 ++-- lib/types.ts | 2 + lib/vocabularies/core/ref.ts | 18 ++--- lib/vocabularies/util.ts | 11 +++ package.json | 2 +- 9 files changed, 170 insertions(+), 46 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 84076b4ec2..ca343870f4 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -76,11 +76,15 @@ export default class CodeGen { return this } - try(tryBody: Code, catchBody?: Code, err?: string, finallyBody?: Code): CodeGen { - if (!catchBody && !finallyBody) throw new Error('"try" without "catch" and "finally"') + try(tryBody: Code, catchCode?: (e: string) => void, finallyCode?: Code): CodeGen { + if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') this.code("try{").code(tryBody) - if (catchBody) this.code(err ? `}catch(${err}){` : "}catch{").code(catchBody) - if (finallyBody) this.code("}finally{").code(finallyBody) + if (catchCode) { + const err = this.name("e") + this.code(`}catch(${err}){`) + catchCode(err) + } + if (finallyCode) this.code("}finally{").code(finallyCode) this.code("}") return this } diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index d5d11fcd9f..238ca35d61 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,5 +1,4 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" -import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" import CodeGen from "./codegen" @@ -33,6 +32,23 @@ export function resetErrorsCount(gen: CodeGen, errsCount: string): void { ) } +export function extendErrors( + {gen, keyword, schemaValue, data, it}: KeywordContext, + errsCount: string +): void { + gen.for(`let i=${errsCount}; i { + gen.code(`const err = vErrors[i];`) + gen.if("err.dataPath === undefined", `err.dataPath = (dataPath || '') + ${it.errorPath}`) + gen.code(`err.schemaPath = ${quotedString(it.errSchemaPath + "/" + keyword)};`) + if (it.opts.verbose) { + gen.code( + `err.schema = ${schemaValue}; + err.data = ${data};` + ) + } + }) +} + function addError(gen: CodeGen, errObj: string): void { const err = gen.name("err") gen @@ -64,7 +80,7 @@ function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): st let out = `{ keyword: "${keyword}", dataPath: (dataPath || "") + ${errorPath}, - schemaPath: ${toQuotedString(errSchemaPath + "/" + keyword)}, + schemaPath: ${quotedString(errSchemaPath + "/" + keyword)}, params: ${params ? params(cxt) : "{}"},` if (propertyName) out += `propertyName: ${propertyName},` if (opts.messages !== false) { diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 18e5808fac..0f782051f8 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -76,14 +76,14 @@ function getSubschema( } if (schema !== undefined) { - if (schemaPath === undefined || topSchemaRef === undefined) { - throw new Error('"schemaPath" and "topSchemaRef" are required with "schema"') + if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) { + throw new Error('"schemaPath", "errSchemaPath" and "topSchemaRef" are required with "schema"') } return { schema, schemaPath, topSchemaRef, - errSchemaPath: errSchemaPath || it.errSchemaPath, + errSchemaPath, } } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 336d6cfed5..1918ad8ae1 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -3,15 +3,17 @@ import { KeywordErrorDefinition, KeywordContext, MacroKeywordDefinition, - CompiledKeywordDefinition, - ValidatedKeywordDefinition, + FuncKeywordDefinition, + // CompiledKeywordDefinition, + // ValidatedKeywordDefinition, CompilationContext, KeywordCompilationResult, } from "../../types" import {applySubschema} from "../subschema" -import {reportExtraError} from "../errors" +import {reportError, reportExtraError, extendErrors} from "../errors" +import {getParentData} from "../../vocabularies/util" -const keywordError: KeywordErrorDefinition = { +export const keywordError: KeywordErrorDefinition = { message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, params: ({keyword}) => `{keyword: "${keyword}"}`, // TODO possibly remove it as keyword is reported in the object } @@ -21,9 +23,14 @@ export default function keywordCode( ruleType: string, def: KeywordDefinition ): void { - if ("macro" in def) macroKeywordCode(cxt, def) - else if ("compile" in def) compiledKeywordCode(cxt, ruleType, def) - else if ("validate" in def) validatedKeywordCode(cxt, ruleType, def) + const {it} = cxt + if ("macro" in def) { + macroKeywordCode(cxt, def) + } else if ("compile" in def || "validate" in def) { + // TODO "code" keyword + checkAsync(it, def) + funcKeywordCode(cxt, ruleType, def) + } } function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { @@ -38,6 +45,7 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { { schema: macroSchema, schemaPath: "", + errSchemaPath: `${it.errSchemaPath}/${keyword}`, topSchemaRef: schemaRef, compositeRule: true, }, @@ -50,17 +58,105 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { gen.code(it.allErrors ? "}" : "} else {") } -function compiledKeywordCode( - _cxt: KeywordContext, - _ruleType: string, - _def: CompiledKeywordDefinition -) {} - -function validatedKeywordCode( - _cxt: KeywordContext, - _ruleType: string, - _def: ValidatedKeywordDefinition -) {} +function checkAsync(it: CompilationContext, def: FuncKeywordDefinition) { + if (def.async && !it.async) throw new Error("async keyword in sync schema") +} + +function funcKeywordCode(cxt: KeywordContext, _ruleType: string, def: FuncKeywordDefinition) { + const {gen, ok, fail, keyword, schema, schemaValue, parentSchema, data, it} = cxt + const validate = + "compile" in def ? def.compile.call(it.self, schema, parentSchema, it) : def.validate + const validateRef = addCustomRule(it, keyword, validate) + const errsCount = gen.name("_errs") + const valid = gen.name("valid") + gen.code(`const ${errsCount} = errors;`) + if (def.errors === false) { + validateNoErrorsRule() + } else { + if (def.async) validateAsyncRule() + else validateRule() + } + + function validateNoErrorsRule() { + gen.code(`let ${valid} = ${def.async ? "await " : ""}${callValidate(validateRef)};`) + if (def.modifying) modifyData() + if (def.valid) return ok() + fail(`!${valid}`) + } + + function validateAsyncRule() { + const ruleErrs = gen.name("ruleErrs") + gen + .code( + `let ${ruleErrs} = null + let ${valid};` + ) + .try(`${valid} = await ${callValidate(validateRef)};`, (e) => + gen + .code(`${valid} = false;`) + .if(`${e} instanceof ValidationError`, `${ruleErrs} = ${e}.errors;`, `throw ${e};`) + ) + if (def.modifying) modifyData() + reportKeywordErrors(ruleErrs) + } + + function validateRule() { + const validateErrs = `${validateRef}.errors` + gen.code( + `${validateErrs} = null; + let ${valid} = ${callValidate(validateRef)};` + ) + if (def.modifying) modifyData() + reportKeywordErrors(validateErrs) + } + + // TODO refactor with ref? + function callValidate(v: string): string { + const {errorPath} = it + const context = it.opts.passContext ? "this" : "self" + const dataAndSchema = + "compile" in def || def.schema === false // TODO $data? + ? `${data}` + : `${schemaValue}, ${data}, ${it.topSchemaRef}${it.schemaPath}` + const dataPath = `(dataPath || '')${errorPath === '""' ? "" : ` + ${errorPath}`}` // TODO joinPaths? + const parent = getParentData(it) + return `${v}.call(${context}, ${dataAndSchema}, ${dataPath}, ${parent.data}, ${parent.property}, rootData)` + } + + function modifyData() { + const parent = getParentData(it) + gen.if(parent.data, `${data} = ${parent.data}[${parent.property}];`) + } + + function reportKeywordErrors(ruleErrs: string): void { + switch (def.valid) { + case true: + return ok() + case false: + addKeywordErrors(ruleErrs) + return ok("false") + default: + // TODO refactor ifs + gen.code(`if (!${valid}) {`) + addKeywordErrors(ruleErrs) + gen.code(`}`) + if (!it.allErrors) gen.code(" else {") + } + } + + function addKeywordErrors(ruleErrs: string): void { + gen.if( + `Array.isArray(${ruleErrs})`, + () => { + gen + .if("vErrors === null", `vErrors = ${ruleErrs}`, `vErrors = vErrors.concat(${ruleErrs})`) + .code("errors = vErrors.length;") + extendErrors(cxt, errsCount) + }, + () => reportError(cxt, keywordError) + ) + } +} export function validateKeywordSchema( it: CompilationContext, diff --git a/lib/keyword.ts b/lib/keyword.ts index 75415a6bf6..32c99cb379 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -15,7 +15,7 @@ import {reportError} from "./compile/errors" import {getData} from "./compile/util" import {schemaRefOrVal} from "./vocabularies/util" import {definitionSchema} from "./definition_schema" -import keywordCode, {validateKeywordSchema} from "./compile/validate/keyword" +import keywordCode, {validateKeywordSchema, keywordError} from "./compile/validate/keyword" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i const customRuleCode = require("./dotjs/custom") @@ -112,10 +112,7 @@ export function addKeyword( keyword, definition, custom: true, - code: - "code" in definition || ("macro" in definition && !definition.$data) - ? ruleCode - : customRuleCode, + code: "code" in definition || !definition.$data ? ruleCode : customRuleCode, implements: definition.implements, } @@ -149,7 +146,7 @@ export function addKeyword( function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): void { const schema = it.schema[keyword] const def: CodeKeywordDefinition = this.definition - const {schemaType, code, error, $data: $defData} = def + const {schemaType, $data: $defData} = def validateKeywordSchema(it, keyword, def) const {gen, opts, dataLevel, dataPathArr, allErrors} = it // TODO @@ -178,7 +175,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) } // TODO check that code called "fail" or another valid way to return code - ;(code || keywordCode)(cxt, ruleType, this.definition) + ;(def.code || keywordCode)(cxt, ruleType, this.definition) // TODO replace with fail_ below function fail(condition?: string, context?: KeywordContext): void { @@ -192,7 +189,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } function _reportError() { - reportError(context || cxt, error as KeywordErrorDefinition) + reportError(context || cxt, def.error || keywordError) } } diff --git a/lib/types.ts b/lib/types.ts index 2f033af1e4..4a15dfe855 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -191,6 +191,8 @@ export type CompileKeywordFunc = ( it: CompilationContext ) => ValidateFunction +export type FuncKeywordDefinition = CompiledKeywordDefinition | ValidatedKeywordDefinition + export interface CompiledKeywordDefinition extends FuncKeywordDef { compile: CompileKeywordFunc modifying?: boolean diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 47aa27a2bc..f58c681289 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -2,6 +2,7 @@ import {CodeKeywordDefinition} from "../../types" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" +import {getParentData} from "../util" const def: CodeKeywordDefinition = { keyword: "$ref", @@ -61,12 +62,11 @@ const def: CodeKeywordDefinition = { gen.code(`await ${callValidate(v)};`) if (!allErrors) gen.code(`${valid} = true;`) }, - () => { - gen.if("!(e instanceof ValidationError)", "throw e") - addErrorsFrom("e") + (e) => { + gen.if(`!(${e} instanceof ValidationError)`, `throw ${e}`) + addErrorsFrom(e) if (!allErrors) gen.code(`${valid} = false;`) - }, - "e" + } ) ok(valid) } @@ -80,12 +80,10 @@ const def: CodeKeywordDefinition = { } function callValidate(v: string): string { - const {errorPath, dataLevel, dataPathArr} = it + const {errorPath} = it const dataPath = `(dataPath || '')${errorPath === '""' ? "" : ` + ${errorPath}`}` // TODO joinPaths? - const parentArgs = dataLevel - ? `data${dataLevel - 1 || ""}, ${dataPathArr[dataLevel]}` - : "parentData, parentDataProperty" - const args = `${data}, ${dataPath}, ${parentArgs}, rootData` + const parent = getParentData(it) + const args = `${data}, ${dataPath}, ${parent.data}, ${parent.property}, rootData` return opts.passContext ? `${v}.call(this, ${args})` : `${v}(${args})` } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index d3837282ff..4c32000bc4 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -106,3 +106,14 @@ export function loopPropertiesCode( export function orExpr(items: string[], mapCondition: (s: string, i: number) => string): string { return items.map(mapCondition).reduce((expr, cond) => `${expr} || ${cond}`) } + +export interface ParentData { + data: string + property: string +} + +export function getParentData({dataLevel, dataPathArr}: CompilationContext): ParentData { + return dataLevel + ? {data: `data${dataLevel - 1 || ""}`, property: `${dataPathArr[dataLevel]}`} + : {data: "parentData", property: "parentDataProperty"} +} diff --git a/package.json b/package.json index 8c80673f0d..c4367ecac7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", "test-spec": "mocha spec/{**/,}*.spec.js -R spec", "test-fast": "AJV_FAST_TEST=true npm run test-spec", - "test-debug": "npm run test-spec -- --inspect-brk", + "test-debug": "npm run test-spec -- --inspect", "test-cov": "nyc npm run test-spec", "test-ts": "tsc --target ES5 --noImplicitAny --noEmit spec/typescript/index.ts", "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", From f3f353adbb0121da659c74fda7cb978945d2a726 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 25 Aug 2020 09:03:37 -0400 Subject: [PATCH 089/322] refactor: $data support in "compile" and "validate" keywords --- lib/compile/validate/keyword.ts | 158 +++++++++++++++++++------------- lib/keyword.ts | 6 +- lib/types.ts | 38 ++++---- package.json | 2 +- 4 files changed, 118 insertions(+), 86 deletions(-) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 1918ad8ae1..32d6f43daa 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -4,14 +4,12 @@ import { KeywordContext, MacroKeywordDefinition, FuncKeywordDefinition, - // CompiledKeywordDefinition, - // ValidatedKeywordDefinition, CompilationContext, KeywordCompilationResult, } from "../../types" import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" -import {getParentData} from "../../vocabularies/util" +import {getParentData, dataNotType} from "../../vocabularies/util" export const keywordError: KeywordErrorDefinition = { message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, @@ -20,16 +18,16 @@ export const keywordError: KeywordErrorDefinition = { export default function keywordCode( cxt: KeywordContext, - ruleType: string, + _ruleType: string, def: KeywordDefinition ): void { - const {it} = cxt - if ("macro" in def) { + // TODO "code" keyword + if (cxt.$data && "validate" in def) { + funcKeywordCode(cxt, def as FuncKeywordDefinition) + } else if ("macro" in def) { macroKeywordCode(cxt, def) } else if ("compile" in def || "validate" in def) { - // TODO "code" keyword - checkAsync(it, def) - funcKeywordCode(cxt, ruleType, def) + funcKeywordCode(cxt, def) } } @@ -58,18 +56,19 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { gen.code(it.allErrors ? "}" : "} else {") } -function checkAsync(it: CompilationContext, def: FuncKeywordDefinition) { - if (def.async && !it.async) throw new Error("async keyword in sync schema") -} - -function funcKeywordCode(cxt: KeywordContext, _ruleType: string, def: FuncKeywordDefinition) { - const {gen, ok, fail, keyword, schema, schemaValue, parentSchema, data, it} = cxt +function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { + const {gen, ok, fail, keyword, schema, schemaCode, parentSchema, data, $data, it} = cxt + checkAsync(it, def) const validate = - "compile" in def ? def.compile.call(it.self, schema, parentSchema, it) : def.validate + "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate const validateRef = addCustomRule(it, keyword, validate) - const errsCount = gen.name("_errs") const valid = gen.name("valid") - gen.code(`const ${errsCount} = errors;`) + gen.code(`let ${valid};`) + + gen.block() + + if ($data) check$data() + if (def.errors === false) { validateNoErrorsRule() } else { @@ -77,37 +76,59 @@ function funcKeywordCode(cxt: KeywordContext, _ruleType: string, def: FuncKeywor else validateRule() } + function check$data() { + gen + // TODO add support for schemaType in keyword definition + // .if(`${dataNotType(schemaCode, def.schemaType, $data)} false`) // TODO refactor + .if(`${schemaCode} === undefined`) + .code(`${valid} = true;`) + .else() + if (def.validateSchema) { + const validateSchemaRef = addCustomRule(it, keyword, def.validateSchema) + gen.code(`${valid} = ${validateSchemaRef}(${schemaCode});`) + // TODO fail if schema fails validation + // gen.if(`!${valid}`) + // reportError(cxt, keywordError) + // gen.else() + gen.if(valid) + } + } + function validateNoErrorsRule() { - gen.code(`let ${valid} = ${def.async ? "await " : ""}${callValidate(validateRef)};`) - if (def.modifying) modifyData() + gen.code(`${valid} = ${def.async ? "await " : ""}${callValidate(validateRef)};`) + if (def.modifying) modifyData(cxt) + gen.endBlock() if (def.valid) return ok() fail(`!${valid}`) } function validateAsyncRule() { + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) const ruleErrs = gen.name("ruleErrs") gen - .code( - `let ${ruleErrs} = null - let ${valid};` - ) + .code(`let ${ruleErrs} = null;`) .try(`${valid} = await ${callValidate(validateRef)};`, (e) => gen .code(`${valid} = false;`) .if(`${e} instanceof ValidationError`, `${ruleErrs} = ${e}.errors;`, `throw ${e};`) ) - if (def.modifying) modifyData() - reportKeywordErrors(ruleErrs) + if (def.modifying) modifyData(cxt) + gen.endBlock() + reportKeywordErrors(cxt, def, valid, ruleErrs, errsCount) } function validateRule() { const validateErrs = `${validateRef}.errors` + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) gen.code( `${validateErrs} = null; - let ${valid} = ${callValidate(validateRef)};` + ${valid} = ${callValidate(validateRef)};` ) - if (def.modifying) modifyData() - reportKeywordErrors(validateErrs) + if (def.modifying) modifyData(cxt) + gen.endBlock() + reportKeywordErrors(cxt, def, valid, validateErrs, errsCount) } // TODO refactor with ref? @@ -115,47 +136,60 @@ function funcKeywordCode(cxt: KeywordContext, _ruleType: string, def: FuncKeywor const {errorPath} = it const context = it.opts.passContext ? "this" : "self" const dataAndSchema = - "compile" in def || def.schema === false // TODO $data? + ("compile" in def && !$data) || def.schema === false ? `${data}` - : `${schemaValue}, ${data}, ${it.topSchemaRef}${it.schemaPath}` + : `${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` const dataPath = `(dataPath || '')${errorPath === '""' ? "" : ` + ${errorPath}`}` // TODO joinPaths? const parent = getParentData(it) return `${v}.call(${context}, ${dataAndSchema}, ${dataPath}, ${parent.data}, ${parent.property}, rootData)` } +} - function modifyData() { - const parent = getParentData(it) - gen.if(parent.data, `${data} = ${parent.data}[${parent.property}];`) - } +function modifyData(cxt: KeywordContext) { + const {gen, data, it} = cxt + const parent = getParentData(it) + gen.if(parent.data, `${data} = ${parent.data}[${parent.property}];`) +} - function reportKeywordErrors(ruleErrs: string): void { - switch (def.valid) { - case true: - return ok() - case false: - addKeywordErrors(ruleErrs) - return ok("false") - default: - // TODO refactor ifs - gen.code(`if (!${valid}) {`) - addKeywordErrors(ruleErrs) - gen.code(`}`) - if (!it.allErrors) gen.code(" else {") - } +function reportKeywordErrors( + cxt: KeywordContext, + def: FuncKeywordDefinition, + valid: string, + ruleErrs: string, + errsCount: string +): void { + const {gen, ok, it} = cxt + switch (def.valid) { + case true: + return ok() + case false: + addKeywordErrors(cxt, ruleErrs, errsCount) + return ok("false") + default: + // TODO refactor ifs + gen.code(`if (!${valid}) {`) + addKeywordErrors(cxt, ruleErrs, errsCount) + gen.code(`}`) + if (!it.allErrors) gen.code(" else {") } +} - function addKeywordErrors(ruleErrs: string): void { - gen.if( - `Array.isArray(${ruleErrs})`, - () => { - gen - .if("vErrors === null", `vErrors = ${ruleErrs}`, `vErrors = vErrors.concat(${ruleErrs})`) - .code("errors = vErrors.length;") - extendErrors(cxt, errsCount) - }, - () => reportError(cxt, keywordError) - ) - } +function addKeywordErrors(cxt: KeywordContext, ruleErrs: string, errsCount: string): void { + const {gen} = cxt + gen.if( + `Array.isArray(${ruleErrs})`, + () => { + gen + .if("vErrors === null", `vErrors = ${ruleErrs}`, `vErrors = vErrors.concat(${ruleErrs})`) + .code("errors = vErrors.length;") + extendErrors(cxt, errsCount) + }, + () => reportError(cxt, keywordError) + ) +} + +function checkAsync(it: CompilationContext, def: FuncKeywordDefinition) { + if (def.async && !it.async) throw new Error("async keyword in sync schema") } export function validateKeywordSchema( @@ -181,7 +215,7 @@ export function validateKeywordSchema( function addCustomRule( it: CompilationContext, keyword: string, - res: KeywordCompilationResult + res?: KeywordCompilationResult ): string { if (res === undefined) throw new Error(`custom keyword "${keyword}" failed to compile`) const idx = it.customRules.length diff --git a/lib/keyword.ts b/lib/keyword.ts index 32c99cb379..dd5d73f549 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -112,7 +112,7 @@ export function addKeyword( keyword, definition, custom: true, - code: "code" in definition || !definition.$data ? ruleCode : customRuleCode, + code: ruleCode, // "code" in definition || !definition.$data ? ruleCode : customRuleCode, implements: definition.implements, } @@ -163,8 +163,8 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v data, $data, schema, - schemaCode: $data ? gen.name("schema") : schemaValue, - schemaValue, + schemaCode: $data ? gen.name("schema") : schemaValue, // reference to resolved schema value + schemaValue, // actual schema reference or value for primitive values parentSchema: it.schema, params: {}, it, diff --git a/lib/types.ts b/lib/types.ts index 4a15dfe855..51b5e4552d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -155,7 +155,6 @@ interface _KeywordDef { keyword?: string | string[] type?: string | string[] schemaType?: string | string[] - $data?: boolean // requires "validate" or "code" implements?: string[] before?: string metaSchema?: object @@ -163,16 +162,10 @@ interface _KeywordDef { dependencies?: string[] // keywords that must be present in the same schema } -interface FuncKeywordDef extends _KeywordDef { - validate?: SchemaValidateFunction | ValidateFunction - // schema: false makes validate not to expect schema (ValidateFunction) - schema?: boolean // requires "validate" - errors?: boolean | "full" -} - export interface CodeKeywordDefinition extends _KeywordDef { code: (cxt: KeywordContext, ruleType?: string, def?: KeywordDefinition) => string | void - error?: KeywordErrorDefinition + error?: KeywordErrorDefinition // TODO all keyword types should support error + $data?: boolean } export type MacroKeywordFunc = ( @@ -181,9 +174,7 @@ export type MacroKeywordFunc = ( it: CompilationContext ) => object | boolean -export interface MacroKeywordDefinition extends FuncKeywordDef { - macro: MacroKeywordFunc -} +export type FuncKeywordDefinition = CompiledKeywordDefinition | ValidatedKeywordDefinition export type CompileKeywordFunc = ( schema: any, @@ -191,20 +182,27 @@ export type CompileKeywordFunc = ( it: CompilationContext ) => ValidateFunction -export type FuncKeywordDefinition = CompiledKeywordDefinition | ValidatedKeywordDefinition - -export interface CompiledKeywordDefinition extends FuncKeywordDef { - compile: CompileKeywordFunc +interface $DataKeywordDef extends _KeywordDef { + validate?: SchemaValidateFunction | ValidateFunction + $data?: boolean // requires "validate" + // schema: false makes validate not to expect schema (ValidateFunction) + schema?: boolean // requires "validate" modifying?: boolean async?: boolean valid?: boolean + errors?: boolean | "full" +} + +export interface MacroKeywordDefinition extends $DataKeywordDef { + macro: MacroKeywordFunc } -export interface ValidatedKeywordDefinition extends FuncKeywordDef { +export interface CompiledKeywordDefinition extends $DataKeywordDef { + compile: CompileKeywordFunc +} + +export interface ValidatedKeywordDefinition extends $DataKeywordDef { validate: SchemaValidateFunction | ValidateFunction - modifying?: boolean - async?: boolean - valid?: boolean } export type KeywordDefinition = diff --git a/package.json b/package.json index c4367ecac7..7a3d4113c5 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "pre-commit": "^1.1.1", "prettier": "^2.0.5", "require-globify": "^1.3.0", - "typescript": "^3.9.7", + "typescript": "^4.0.0", "uglify-js": "^3.6.9", "watch": "^1.0.0" }, From 188eadf806662fe8b30bdf305707e3bf26b76e5d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 25 Aug 2020 09:30:11 -0400 Subject: [PATCH 090/322] refactor: remove doT --- .codeclimate.yml | 2 - .gitignore | 3 - .prettierignore | 1 - CONTRIBUTING.md | 6 +- CUSTOM.md | 8 +- README.md | 8 +- lib/compile/rules.ts | 2 +- lib/compile/validate/index.ts | 8 +- lib/compile/validate/iterate.ts | 2 +- lib/compile/validate/keyword.ts | 2 +- lib/dot/custom.jst | 213 ------------------------ lib/dot/definitions.def | 194 --------------------- lib/dot/errors.def | 122 -------------- lib/dotjs/README.md | 3 - lib/keyword.ts | 3 +- lib/types.ts | 2 +- package.json | 9 +- scripts/compile-dots.js | 85 ---------- spec/custom.spec.js | 73 ++++---- spec/custom_rules/index.js | 14 -- spec/custom_rules/range.jst | 8 - spec/custom_rules/range_with_errors.jst | 42 ----- 22 files changed, 51 insertions(+), 759 deletions(-) delete mode 100644 .codeclimate.yml delete mode 100644 lib/dot/custom.jst delete mode 100644 lib/dot/definitions.def delete mode 100644 lib/dot/errors.def delete mode 100644 lib/dotjs/README.md delete mode 100644 scripts/compile-dots.js delete mode 100644 spec/custom_rules/index.js delete mode 100644 spec/custom_rules/range.jst delete mode 100644 spec/custom_rules/range_with_errors.jst diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 84849f0085..0000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,2 +0,0 @@ -exclude_paths: -- lib/dotjs/** diff --git a/.gitignore b/.gitignore index b7f774a0d1..9951605f1c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,9 +29,6 @@ node_modules .DS_Store -# Compiled templates -lib/dotjs/*.js - # Browserified tests .browser diff --git a/.prettierignore b/.prettierignore index e2fcb36476..8d0387cf3e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,4 +3,3 @@ spec/JSON-Schema-Test-Suite coverage dist .nyc_output -lib/dotjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00a6afcd1d..e83384d008 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,11 +133,9 @@ npm run test-fast git commit -nm 'type: message' ``` -All validation functions are generated using doT templates in [dot](https://github.com/ajv-validator/ajv/tree/master/lib/dot) folder. Templates are precompiled so doT is not a run-time dependency. +`npm run build` - compiles typescript to dist folder. -`npm run build` - compiles templates to [dotjs](https://github.com/ajv-validator/ajv/tree/master/lib/dotjs) folder. - -`npm run watch` - automatically compiles templates when files in dot folder change +`npm run watch` - automatically compiles typescript when files in lib folder change #### Pull requests diff --git a/CUSTOM.md b/CUSTOM.md index 49d5b7a907..41fa6f2ea7 100644 --- a/CUSTOM.md +++ b/CUSTOM.md @@ -43,9 +43,7 @@ Example. `constant` keyword (a synonym for draft-06 keyword `const`, it is equiv ```javascript ajv.addKeyword("constant", { validate: function (schema, data) { - return typeof schema == "object" && schema !== null - ? deepEqual(schema, data) - : schema === data + return typeof schema == "object" && schema !== null ? deepEqual(schema, data) : schema === data }, errors: false, }) @@ -388,7 +386,9 @@ All custom keywords but macro keywords can optionally create custom error messag Synchronous validating and compiled keywords should define errors by assigning them to `.errors` property of the validation function. Asynchronous keywords can return promise that rejects with `new Ajv.ValidationError(errors)`, where `errors` is an array of custom validation errors (if you don't want to define custom errors in asynchronous keyword, its validation function can return the promise that resolves with `false`). -Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). This can be done for both synchronous and asynchronous keywords. See [example range keyword](https://github.com/ajv-validator/ajv/blob/master/spec/custom_rules/range_with_errors.jst). +TODO replace "inline" keywords with "code" keywords + +Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). This can be done for both synchronous and asynchronous keywords. When inline keyword performs validation Ajv checks whether it created errors by comparing errors count before and after validation. To skip this check add option `errors` (can be `"full"`, `true` or `false`) to keyword definition: diff --git a/README.md b/README.md index e6acbdb577..5526d4c637 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")) ## Performance -Ajv generates code using [doT templates](https://github.com/olado/doT) to turn JSON Schemas into super-fast validation functions that are efficient for v8 optimization. +Ajv generates code to turn JSON Schemas into super-fast validation functions that are efficient for v8 optimization. Currently Ajv is the fastest and the most standard compliant validator according to these benchmarks: @@ -1370,11 +1370,9 @@ npm test ## Contributing -All validation functions are generated using doT templates in [dot](https://github.com/ajv-validator/ajv/tree/master/lib/dot) folder. Templates are precompiled so doT is not a run-time dependency. +`npm run build` - compiles typescript to dist folder. -`npm run build` - compiles templates to [dotjs](https://github.com/ajv-validator/ajv/tree/master/lib/dotjs) folder. - -`npm run watch` - automatically compiles templates when files in dot folder change +`npm run watch` - automatically compiles typescript when files in lib folder change Please see [Contributing guidelines](https://github.com/ajv-validator/ajv/blob/master/CONTRIBUTING.md) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 2d2a578b32..169f8db3fc 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -16,7 +16,7 @@ export interface RuleGroup { export interface Rule { keyword: string - code: (it: CompilationContext, keyword?: string, ruleType?: string) => void + code: (it: CompilationContext, keyword: string, ruleType?: string) => void implements?: string[] definition?: KeywordDefinition custom?: true diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index ac6ac3f28c..76f444b3ba 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -47,7 +47,7 @@ export default function validateCode( if ($comment && schema.$comment) commentKeyword(it) if (isTop) { - updateTopContext(it) + delete it.isTop checkNoDefault(it) initializeTop(it) typeAndKeywords() @@ -136,12 +136,6 @@ function startFunction({ ) } -function updateTopContext(it: CompilationContext): void { - // it.rootId = resolve.fullPath(it.root.schema.$id) - // it.baseId = it.baseId || it.rootId - delete it.isTop -} - function checkNoDefault({ schema, opts: {useDefaults, strictDefaults}, diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index bb070a562c..d56c74574a 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -9,7 +9,7 @@ export function schemaKeywords( it: CompilationContext, types: string[], typeErrors: boolean, - top: boolean + top?: boolean ): void { const { gen, diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 32d6f43daa..0d7d76d696 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -9,7 +9,7 @@ import { } from "../../types" import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" -import {getParentData, dataNotType} from "../../vocabularies/util" +import {getParentData} from "../../vocabularies/util" export const keywordError: KeywordErrorDefinition = { message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst deleted file mode 100644 index d33eaa02c8..0000000000 --- a/lib/dot/custom.jst +++ /dev/null @@ -1,213 +0,0 @@ -{{# def.definitions }} -{{# def.errors }} -{{# def.setupKeyword }} -{{# def.$data }} - -{{ - var $rule = this - , $definition = 'definition' + $lvl - , $rDef = $rule.definition - , $closingBraces = ''; - var $validate = $rDef.validate; - var $compile, $macro, $ruleValidate, $validateCode; -}} - -{{? $isData && $rDef.$data }} - {{ - $validateCode = 'keywordValidate' + $lvl; - var $validateSchema = $rDef.validateSchema; - }} - var {{=$definition}} = RULES.custom['{{=$keyword}}'].definition; - var {{=$validateCode}} = {{=$definition}}.validate; -{{??}} - {{ - $ruleValidate = useCustomRule.call(it.self, $rule, $schema, it.schema, it); - if (!$ruleValidate) return; - $schemaValue = it.topSchemaRef + $schemaPath; - $validateCode = $ruleValidate.code; - $compile = $rDef.compile; - $macro = $rDef.macro; - }} -{{?}} - -{{ - var $ruleErrs = $validateCode + '.errors' - , $i = 'i' + $lvl - , $ruleErr = 'ruleErr' + $lvl - , $asyncKeyword = $rDef.async; - - if ($asyncKeyword && !it.async) - throw new Error('async keyword in sync schema'); -}} - - -{{? !$macro }}{{=$ruleErrs}} = null;{{?}} -var {{=$errs}} = errors; -var {{=$valid}}; - -{{## def.callRuleValidate: - {{=$validateCode}}.call( - {{? it.opts.passContext }}this{{??}}self{{?}} - {{? $compile || $rDef.schema === false }} - , {{=$data}} - {{??}} - , {{=$schemaValue}} - , {{=$data}} - , {{=it.topSchemaRef}}{{=it.schemaPath}} - {{?}} - , {{# def.dataPath }} - {{# def.passParentData }} - , rootData - ) -#}} - -{{## def.extendErrors:_inline: - for (var {{=$i}}={{=$errs}}; {{=$i}} 0) - || _schema === false - : it.util.schemaHasRules(_schema, it.RULES.all)) -#}} - - -{{## def.willOptimize: - it.util.varOccurrences($code, $nextData) < 2 -#}} - - -{{## def.generateSubschemaCode: - {{ - var $code = it.validateCode($it); - $it.baseId = $currentBaseId; - }} -#}} - - -{{## def.insertSubschemaCode: - {{= it.validateCode($it) }} - {{ $it.baseId = $currentBaseId; }} -#}} - - -{{## def._optimizeValidate: - it.util.varReplace($code, $nextData, $passData) -#}} - - -{{## def.optimizeValidate: - {{? {{# def.willOptimize}} }} - {{= {{# def._optimizeValidate }} }} - {{??}} - var {{=$nextData}} = {{=$passData}}; - {{= $code }} - {{?}} -#}} - - -{{## def.$data: - {{ - var $isData = it.opts.$data && $schema && $schema.$data - , $schemaValue; - }} - {{? $isData }} - var schema{{=$lvl}} = {{= it.util.getData($schema.$data, $dataLvl, it.dataPathArr) }}; - {{ $schemaValue = 'schema' + $lvl; }} - {{??}} - {{ $schemaValue = $schema; }} - {{?}} -#}} - - -{{## def.$dataNotType:_type: - {{?$isData}} ({{=$schemaValue}} !== undefined && typeof {{=$schemaValue}} != _type) || {{?}} -#}} - - -{{## def.check$dataIsArray: - if (schema{{=$lvl}} === undefined) {{=$valid}} = true; - else if (!Array.isArray(schema{{=$lvl}})) {{=$valid}} = false; - else { -#}} - - -{{## def.numberKeyword: - {{? !($isData || typeof $schema == 'number') }} - {{ throw new Error($keyword + ' must be number'); }} - {{?}} -#}} - - -{{## def.beginDefOut: - {{ - var $$outStack = $$outStack || []; - $$outStack.push(out); - out = ''; - }} -#}} - - -{{## def.storeDefOut:_variable: - {{ - var _variable = out; - out = $$outStack.pop(); - }} -#}} - - -{{## def.dataPath:(dataPath || ''){{? it.errorPath != '""'}} + {{= it.errorPath }}{{?}}#}} - -{{## def.setParentData: - {{ - var $parentData = $dataLvl ? 'data' + (($dataLvl-1)||'') : 'parentData' - , $parentDataProperty = $dataLvl ? it.dataPathArr[$dataLvl] : 'parentDataProperty'; - }} -#}} - -{{## def.passParentData: - {{# def.setParentData }} - , {{= $parentData }} - , {{= $parentDataProperty }} -#}} - - -{{## def.iterateProperties: - {{? $ownProperties }} - {{=$dataProperties}} = {{=$dataProperties}} || Object.keys({{=$data}}); - for (var {{=$idx}}=0; {{=$idx}}<{{=$dataProperties}}.length; {{=$idx}}++) { - var {{=$key}} = {{=$dataProperties}}[{{=$idx}}]; - {{??}} - for (var {{=$key}} in {{=$data}}) { - {{?}} -#}} - - -{{## def.noPropertyInData: - {{=$useData}} === undefined - {{? $ownProperties }} - || !{{# def.isOwnProperty }} - {{?}} -#}} - - -{{## def.isOwnProperty: - Object.prototype.hasOwnProperty.call({{=$data}}, '{{=it.util.escapeQuotes($propertyKey)}}') -#}} diff --git a/lib/dot/errors.def b/lib/dot/errors.def deleted file mode 100644 index b65e02521f..0000000000 --- a/lib/dot/errors.def +++ /dev/null @@ -1,122 +0,0 @@ -{{# def.definitions }} - -{{## def._error:_rule: - {{ 'istanbul ignore else'; }} - {{? it.createErrors !== false }} - { - keyword: '{{= $errorKeyword || _rule }}' - , dataPath: (dataPath || '') + {{= it.errorPath }} - , schemaPath: {{=it.util.toQuotedString($errSchemaPath)}} - , params: {{# def._errorParams[_rule] }} - {{? it.opts.messages !== false }} - , message: {{# def._errorMessages[_rule] }} - {{?}} - {{? it.opts.verbose }} - , schema: {{# def._errorSchemas[_rule] }} - , parentSchema: {{=it.topSchemaRef}}{{=it.schemaPath}} - , data: {{=$data}} - {{?}} - } - {{??}} - {} - {{?}} -#}} - - -{{## def._addError:_rule: - if (vErrors === null) vErrors = [err]; - else vErrors.push(err); - errors++; -#}} - - -{{## def.addError:_rule: - var err = {{# def._error:_rule }}; - {{# def._addError:_rule }} -#}} - - -{{## def.error:_rule: - {{# def.beginDefOut}} - {{# def._error:_rule }} - {{# def.storeDefOut:__err }} - - {{? !it.compositeRule && $breakOnError }} - {{ 'istanbul ignore if'; }} - {{? it.async }} - throw new ValidationError([{{=__err}}]); - {{??}} - validate.errors = [{{=__err}}]; - return false; - {{?}} - {{??}} - var err = {{=__err}}; - {{# def._addError:_rule }} - {{?}} -#}} - - -{{## def.extraError:_rule: - {{# def.addError:_rule}} - {{? !it.compositeRule && $breakOnError }} - {{ 'istanbul ignore if'; }} - {{? it.async }} - throw new ValidationError(vErrors); - {{??}} - validate.errors = vErrors; - return false; - {{?}} - {{?}} -#}} - - -{{## def.checkError:_rule: - if (!{{=$valid}}) { - {{# def.error:_rule }} - } -#}} - - -{{## def.resetErrors: - errors = {{=$errs}}; - if (vErrors !== null) { - if ({{=$errs}}) vErrors.length = {{=$errs}}; - else vErrors = null; - } -#}} - - -{{## def.concatSchemaEQ:{{?$isData}}' + {{=$schemaValue}} + '{{??}}{{=it.util.escapeQuotes($schema)}}{{?}}#}} - -{{## def._errorMessages = { - $ref: "'can\\\'t resolve reference {{=it.util.escapeQuotes($schema)}}'", - custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'", - patternRequired: "'should have property matching pattern \\'{{=$missingPattern}}\\''", - switch: "'should pass \"switch\" keyword validation'", - _formatLimit: "'should be {{=$opStr}} \"{{#def.concatSchemaEQ}}\"'", - _formatExclusiveLimit: "'{{=$exclusiveKeyword}} should be boolean'" -} #}} - - -{{## def.schemaRefOrQS: {{?$isData}}{{=it.topSchemaRef}}{{=$schemaPath}}{{??}}{{=it.util.toQuotedString($schema)}}{{?}} #}} - -{{## def._errorSchemas = { - $ref: "{{=it.util.toQuotedString($schema)}}", - custom: "{{=it.topSchemaRef}}{{=$schemaPath}}", - patternRequired: "{{=it.topSchemaRef}}{{=$schemaPath}}", - switch: "{{=it.topSchemaRef}}{{=$schemaPath}}", - _formatLimit: "{{#def.schemaRefOrQS}}", - _formatExclusiveLimit: "{{=it.topSchemaRef}}{{=$schemaPath}}" -} #}} - - -{{## def.schemaValueQS: {{?$isData}}{{=$schemaValue}}{{??}}{{=it.util.toQuotedString($schema)}}{{?}} #}} - -{{## def._errorParams = { - $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", - custom: "{ keyword: '{{=$rule.keyword}}' }", - patternRequired: "{ missingPattern: '{{=$missingPattern}}' }", - switch: "{ caseIndex: {{=$caseIndex}} }", - _formatLimit: "{ comparison: {{=$opExpr}}, limit: {{#def.schemaValueQS}}, exclusive: {{=$exclusive}} }", - _formatExclusiveLimit: "{}" -} #}} diff --git a/lib/dotjs/README.md b/lib/dotjs/README.md deleted file mode 100644 index 4d994846c8..0000000000 --- a/lib/dotjs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -These files are compiled dot templates from dot folder. - -Do NOT edit them directly, edit the templates and run `npm run build` from main ajv folder. diff --git a/lib/keyword.ts b/lib/keyword.ts index dd5d73f549..a52247286c 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -18,7 +18,6 @@ import {definitionSchema} from "./definition_schema" import keywordCode, {validateKeywordSchema, keywordError} from "./compile/validate/keyword" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i -const customRuleCode = require("./dotjs/custom") /** * Define vocabulary @@ -112,7 +111,7 @@ export function addKeyword( keyword, definition, custom: true, - code: ruleCode, // "code" in definition || !definition.$data ? ruleCode : customRuleCode, + code: ruleCode, implements: definition.implements, } diff --git a/lib/types.ts b/lib/types.ts index 51b5e4552d..c0332c469e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -136,7 +136,7 @@ export interface CompilationContext { self: any // TODO RULES: ValidationRules logger: Logger // TODO ? - isTop: boolean // TODO ? + isTop?: boolean // TODO ? root: SchemaRoot // TODO ? rootId: string // TODO ? topSchemaRef: string diff --git a/package.json b/package.json index 7a3d4113c5..6c97ca0392 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,13 @@ "test-ts": "tsc --target ES5 --noImplicitAny --noEmit spec/typescript/index.ts", "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", "bundle-beautify": "node ./scripts/bundle.js js-beautify", - "dot": "del-cli lib/dotjs/*.js \"!lib/dotjs/index.js\" && node scripts/compile-dots.js", - "tsc": "tsc || true && cp -r lib/refs dist/refs", - "build": "npm run dot && npm run tsc", + "build": "del-cli dist && tsc || true && cp -r lib/refs dist/refs", "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", "test": "npm run lint && npm run build && npm run test-cov", "prepublish": "npm run build", - "watch": "watch \"npm run build\" ./lib/dot" + "watch": "watch \"npm run build\" ./lib" }, "nyc": { "exclude": [ @@ -83,7 +81,6 @@ "chai": "^4.0.1", "coveralls": "^3.0.1", "del-cli": "^3.0.0", - "dot": "^1.0.3", "eslint": "^7.3.1", "eslint-config-prettier": "^6.11.0", "gh-pages-generator": "^0.2.3", @@ -91,7 +88,6 @@ "husky": "^4.2.5", "if-node-version": "^1.0.0", "js-beautify": "^1.7.3", - "jshint": "^2.10.2", "json-schema-test": "^2.0.0", "karma": "^5.0.0", "karma-chrome-launcher": "^3.0.0", @@ -100,7 +96,6 @@ "lint-staged": "^10.2.11", "mocha": "^8.0.1", "nyc": "^15.0.0", - "pre-commit": "^1.1.1", "prettier": "^2.0.5", "require-globify": "^1.3.0", "typescript": "^4.0.0", diff --git a/scripts/compile-dots.js b/scripts/compile-dots.js deleted file mode 100644 index a72e7ecb22..0000000000 --- a/scripts/compile-dots.js +++ /dev/null @@ -1,85 +0,0 @@ -//compile doT templates to js functions -"use strict" - -var glob = require("glob"), - fs = require("fs"), - path = require("path"), - doT = require("dot"), - beautify = require("js-beautify").js_beautify - -var defsRootPath = process.argv[2] || path.join(__dirname, "../lib") - -var defs = {} -var defFiles = glob.sync("./dot/**/*.def", {cwd: defsRootPath}) -defFiles.forEach((f) => { - var name = path.basename(f, ".def") - defs[name] = fs.readFileSync(path.join(defsRootPath, f)) -}) - -var filesRootPath = process.argv[3] || path.join(__dirname, "../lib") -var files = glob.sync("./dot/**/*.jst", {cwd: filesRootPath}) - -var dotjsPath = path.join(filesRootPath, "./dotjs") -try { - fs.mkdirSync(dotjsPath) -} catch (e) {} - -console.log("\n\nCompiling:") - -var FUNCTION_NAME = /function\s+anonymous\s*\(it[^)]*\)\s*{/ -var OUT_EMPTY_STRING = /out\s*\+=\s*'\s*';/g -var ISTANBUL = /'(istanbul[^']+)';/g -var ERROR_KEYWORD = /\$errorKeyword/g -var ERROR_KEYWORD_OR = /\$errorKeyword\s+\|\|/g -var VARS = [ - "$errs", - "$valid", - "$lvl", - "$data", - "$dataLvl", - "$errorKeyword", - "$closingBraces", - "$schemaPath", - "$validate", -] - -files.forEach((f) => { - var keyword = path.basename(f, ".jst") - var targetPath = path.join(dotjsPath, keyword + ".js") - var template = fs.readFileSync(path.join(filesRootPath, f)) - var code = doT.compile(template, defs) - code = code - .toString() - .replace(OUT_EMPTY_STRING, "") - .replace( - FUNCTION_NAME, - "function generate_" + keyword + "(it, $keyword, $ruleType) {" - ) - .replace(ISTANBUL, "/* $1 */") - removeAlwaysFalsyInOr() - VARS.forEach(removeUnusedVar) - code = "'use strict';\nmodule.exports = " + code - code = beautify(code, {indent_size: 2}) + "\n" - fs.writeFileSync(targetPath, code) - console.log("compiled", keyword) - - function removeUnusedVar(v) { - v = v.replace(/\$/g, "\\$$") - var regexp = new RegExp(v + "[^A-Za-z0-9_$]", "g") - var count = occurrences(regexp) - if (count === 1) { - regexp = new RegExp("var\\s+" + v + "\\s*=[^;]+;|var\\s+" + v + ";") - code = code.replace(regexp, "") - } - } - - function removeAlwaysFalsyInOr() { - var countUsed = occurrences(ERROR_KEYWORD) - var countOr = occurrences(ERROR_KEYWORD_OR) - if (countUsed === countOr + 1) code = code.replace(ERROR_KEYWORD_OR, "") - } - - function occurrences(regexp) { - return (code.match(regexp) || []).length - } -}) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 85926109a8..18bf89e40b 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -2,8 +2,7 @@ var getAjvInstances = require("./ajv_instances"), should = require("./chai").should(), - equal = require("../dist/compile/equal"), - customRules = require("./custom_rules") + equal = require("../dist/compile/equal") describe("Custom keywords", () => { var ajv, instances @@ -498,7 +497,7 @@ describe("Custom keywords", () => { } }) - // TODO replace with custom "code" keyword + // TODO replace with custom "code" keywords describe.skip("inline rules", () => { it('should add and validate rule with "inline" code keyword', () => { testEvenKeyword({type: "number", inline: inlineEven}) @@ -509,51 +508,47 @@ describe("Custom keywords", () => { }) it('should define "inline" keyword as template', () => { - var inlineRangeTemplate = customRules.range - - testRangeKeyword({ - type: "number", - inline: inlineRangeTemplate, - statements: true, - }) + // var inlineRangeTemplate = customRules.range + // testRangeKeyword({ + // type: "number", + // inline: inlineRangeTemplate, + // statements: true, + // }) }) it('should define "inline" keyword without errors', () => { - var inlineRangeTemplate = customRules.range - - testRangeKeyword({ - type: "number", - inline: inlineRangeTemplate, - statements: true, - errors: false, - }) + // var inlineRangeTemplate = customRules.range + // testRangeKeyword({ + // type: "number", + // inline: inlineRangeTemplate, + // statements: true, + // errors: false, + // }) }) it("should allow defining optional errors", () => { - var inlineRangeTemplate = customRules.rangeWithErrors - - testRangeKeyword( - { - type: "number", - inline: inlineRangeTemplate, - statements: true, - }, - true - ) + // var inlineRangeTemplate = customRules.rangeWithErrors + // testRangeKeyword( + // { + // type: "number", + // inline: inlineRangeTemplate, + // statements: true, + // }, + // true + // ) }) it("should allow defining required errors", () => { - var inlineRangeTemplate = customRules.rangeWithErrors - - testRangeKeyword( - { - type: "number", - inline: inlineRangeTemplate, - statements: true, - errors: true, - }, - true - ) + // var inlineRangeTemplate = customRules.rangeWithErrors + // testRangeKeyword( + // { + // type: "number", + // inline: inlineRangeTemplate, + // statements: true, + // errors: true, + // }, + // true + // ) }) function inlineEven(it, keyword, schema) { diff --git a/spec/custom_rules/index.js b/spec/custom_rules/index.js deleted file mode 100644 index 9dedfbdffe..0000000000 --- a/spec/custom_rules/index.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict" - -var fs = require("fs"), - path = require("path"), - doT = require("dot") - -module.exports = { - range: doT.compile( - fs.readFileSync(path.join(__dirname, "range.jst"), "utf8") - ), - rangeWithErrors: doT.compile( - fs.readFileSync(path.join(__dirname, "range_with_errors.jst"), "utf8") - ), -} diff --git a/spec/custom_rules/range.jst b/spec/custom_rules/range.jst deleted file mode 100644 index adb481e014..0000000000 --- a/spec/custom_rules/range.jst +++ /dev/null @@ -1,8 +0,0 @@ -{{ - var $data = 'data' + (it.dataLevel || '') - , $min = it.schema['x-range'][0] - , $max = it.schema['x-range'][1] - , $gt = it.schema.exclusiveRange ? '>' : '>=' - , $lt = it.schema.exclusiveRange ? '<' : '<='; -}} -var valid{{=it.level}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; diff --git a/spec/custom_rules/range_with_errors.jst b/spec/custom_rules/range_with_errors.jst deleted file mode 100644 index 6bfb6e5712..0000000000 --- a/spec/custom_rules/range_with_errors.jst +++ /dev/null @@ -1,42 +0,0 @@ -{{ - var $data = 'data' + (it.dataLevel || '') - , $min = it.schema['x-range'][0] - , $max = it.schema['x-range'][1] - , $exclusive = !!it.schema.exclusiveRange - , $gt = $exclusive ? '>' : '>=' - , $lt = $exclusive ? '<' : '<=' - , $lvl = it.level - , $err = 'err' + $lvl; -}} - -var minOk{{=$lvl}} = {{=$data}} {{=$gt}} {{=$min}}; -var valid{{=$lvl}} = minOk{{=$lvl}} && {{=$data}} {{=$lt}} {{=$max}}; - -if (!valid{{=$lvl}}) { - var {{=$err}}; - if (minOk{{=$lvl}}) { - {{=$err}} = { - keyword: 'x-range', - message: 'should be {{=$lt}} {{=$max}}', - params: { - comparison: '{{=$lt}}', - limit: {{=$max}}, - exclusive: {{=$exclusive}} - } - }; - } else { - {{=$err}} = { - keyword: 'x-range', - message: 'should be {{=$gt}} {{=$min}}', - params: { - comparison: '{{=$gt}}', - limit: {{=$min}}, - exclusive: {{=$exclusive}} - } - }; - } - - errors++; - if (vErrors) vErrors[vErrors.length] = {{=$err}}; - else vErrors = [{{=$err}}]; -} From 0567b0e8a6553908fe081e556d7145221bc936ba Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 25 Aug 2020 09:46:53 -0400 Subject: [PATCH 091/322] refactor: remove unused code --- CUSTOM.md | 20 ++++++++------------ lib/compile/index.ts | 7 +------ lib/compile/util.ts | 33 --------------------------------- lib/types.ts | 4 ---- 4 files changed, 9 insertions(+), 55 deletions(-) diff --git a/CUSTOM.md b/CUSTOM.md index 41fa6f2ea7..46b95887b8 100644 --- a/CUSTOM.md +++ b/CUSTOM.md @@ -307,16 +307,12 @@ There is a number of variables and expressions you can use in the generated (val There are sevral useful functions you can use in your inline keywords. These functions are available as properties of `it.util` object: -##### .copy(Object obj[, Object target]) -> Object - -Clone or extend the object. If one object is passed, it is cloned. If two objects are passed, the second object is extended with the properties of the first. - ##### .toHash(Array arr) -> Object Converts the array of strings to the object where each string becomes the key with the value of `true`. ```javascript -it.util.toHash(["a", "b", "c"]) // { a: true, b: true, c: true } +toHash(["a", "b", "c"]) // { a: true, b: true, c: true } ``` ##### .equal(value1, value2) -> Boolean @@ -328,10 +324,10 @@ Performs deep equality comparison. This function is used in keywords `enum`, `co Converts the string that is the key/index to access the property/item to the JavaScript syntax to access the property (either "." notation or "[...]" notation). ```javascript -it.util.getProperty("a") // ".a" -it.util.getProperty("1") // "['1']" -it.util.getProperty("a'b") // "['a\\'b']" -it.util.getProperty(1) // "[1]" +getProperty("a") // ".a" +getProperty("1") // "['1']" +getProperty("a'b") // "['a\\'b']" +getProperty(1) // "[1]" ``` ##### .schemaHasRules(Object schema, Object rules) -> String @@ -339,7 +335,7 @@ it.util.getProperty(1) // "[1]" Determines whether the passed schema has rules that should be validated. This function should be used before calling `it.validate` to compile subschemas. ```javascript -it.util.schemaHasRules(schema, it.RULES.all) // true or false +schemaHasRules(schema, it.RULES.all) // true or false ``` ##### .escapeQuotes(String str) -> String @@ -351,7 +347,7 @@ Escapes single quotes in the string, so it can be inserted in the generated code Converts the string to the JavaScript string constant in single quotes (using the escaped string). ```javascript -it.util.toQuotedString("a'b") // "'a\\'b'" +toQuotedString("a'b") // "'a\\'b'" ``` ##### .getData(String jsonPointer, Number dataLevel, Array paths) -> String @@ -359,7 +355,7 @@ it.util.toQuotedString("a'b") // "'a\\'b'" Returns the validation-time expression to safely access data based on the passed [relative json pointer](https://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (See [examples](https://gist.github.com/geraintluff/5911303)). ```javascript -it.util.getData("2/test/1", it.dataLevel, it.dataPathArr) +getData("2/test/1", it.dataLevel, it.dataPathArr) // The result depends on the current level // if it.dataLevel is 3 the result is "data1 && data1.test && data1.test[1]" ``` diff --git a/lib/compile/index.ts b/lib/compile/index.ts index f6509cf605..564e237e59 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,7 +1,6 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" -import {MissingRefError} from "./error_classes" import validateCode from "./validate" import {validateKeywordSchema} from "./validate/keyword" import {ErrorObject, KeywordCompilationResult} from "../types" @@ -10,7 +9,6 @@ const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") var resolve = require("./resolve"), - util = require("./util"), stableStringify = require("fast-json-stable-stringify") /** @@ -128,11 +126,8 @@ function compile(schema, root, localRefs, baseId) { dataLevel: 0, data: "data", // TODO get unique name when passed from applicator keywords gen: new CodeGen(), - MissingRefError, - RULES, + RULES, // TODO refactor - it is available on the instance validateCode, - util, // TODO remove to imports - resolve, // TODO remove to imports resolveRef, // TODO remove to imports usePattern, // TODO remove to imports useDefault, // TODO remove to imports diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 4b007620db..ed109440c7 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,23 +1,3 @@ -// TODO switch to exports - below are used in dot templates -module.exports = { - checkDataType, - checkDataTypes, - toHash, - escapeQuotes, - varOccurrences, - varReplace, - schemaHasRules, - schemaHasRulesExcept, - schemaUnknownRules, - toQuotedString, - getPathExpr, - getPath, - getData, - getProperty, - unescapeFragment, - escapeFragment, -} - export function checkDataType( dataType: string, data: string, @@ -96,19 +76,6 @@ export function escapeQuotes(str: string): string { .replace(/\t/g, "\\t") } -export function varOccurrences(str: string, dataVar: string): number { - dataVar += "[^0-9]" - /* eslint-disable @typescript-eslint/prefer-regexp-exec */ - const matches = str.match(new RegExp(dataVar, "g")) - return matches ? matches.length : 0 -} - -export function varReplace(str: string, dataVar: string, expr: string): string { - dataVar += "([^0-9])" - expr = expr.replace(/\$/g, "$$$$") - return str.replace(new RegExp(dataVar, "g"), expr + "$1") -} - // TODO rules, schema? export function schemaHasRules(schema: object | boolean, rules: object): boolean | undefined { if (typeof schema == "boolean") return !schema diff --git a/lib/types.ts b/lib/types.ts index c0332c469e..5826199bd8 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,6 @@ import Cache from "./cache" import CodeGen from "./compile/codegen" import {ValidationRules} from "./compile/rules" -import {MissingRefError} from "./compile/error_classes" import {ResolvedRef} from "./compile" export interface Options { @@ -132,7 +131,6 @@ export interface CompilationContext { useDefault: (value: any) => string customRules: KeywordCompilationResult[] validateKeywordSchema: (it: CompilationContext, keyword: string, def: KeywordDefinition) => void // TODO remove - util: any // TODO self: any // TODO RULES: ValidationRules logger: Logger // TODO ? @@ -140,8 +138,6 @@ export interface CompilationContext { root: SchemaRoot // TODO ? rootId: string // TODO ? topSchemaRef: string - MissingRefError: typeof MissingRefError - resolve: any resolveRef: (...args: any[]) => ResolvedRef | void } From 7f03fdd12b6448484c7f80b2a4b337a8a2454af3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 25 Aug 2020 13:19:19 -0400 Subject: [PATCH 092/322] refactor: code generation, remove ifs from template literals --- lib/compile/context._ts | 79 ------------------- lib/compile/index.ts | 12 +-- lib/compile/subschema.ts | 5 +- lib/compile/validate/index.ts | 72 ++++++----------- lib/compile/validate/iterate.ts | 25 ++---- lib/compile/validate/keyword.ts | 11 +-- lib/keyword.ts | 11 +-- lib/types.ts | 1 - .../applicator/additionalProperties.ts | 2 +- lib/vocabularies/applicator/anyOf.ts | 7 +- lib/vocabularies/applicator/contains.ts | 7 +- lib/vocabularies/applicator/if.ts | 5 +- lib/vocabularies/applicator/items.ts | 2 +- lib/vocabularies/applicator/not.ts | 6 +- lib/vocabularies/applicator/oneOf.ts | 13 +-- lib/vocabularies/applicator/propertyNames.ts | 3 +- lib/vocabularies/core/ref.ts | 7 +- lib/vocabularies/validation/required.ts | 5 +- 18 files changed, 78 insertions(+), 195 deletions(-) delete mode 100644 lib/compile/context._ts diff --git a/lib/compile/context._ts b/lib/compile/context._ts deleted file mode 100644 index 79ce9d2621..0000000000 --- a/lib/compile/context._ts +++ /dev/null @@ -1,79 +0,0 @@ -interface ContextConstructorArgument { - ajv: any - schema: any - isRoot: boolean - baseId: string -} - -export default class SchemaCompilationContext { - isTop?: boolean = true - async: boolean - schema: any - isRoot: boolean - baseId: string - - // TODO ajv type - constructor({ajv, schema, isRoot, baseId}: ContextConstructorArgument) { - this.async = schema.$async === true - this.schema = schema - this.isRoot = isRoot - this.baseId = baseId - } -} - - -// root: _root, -// schemaPath: "", -// errSchemaPath: "#", -// errorPath: '""', -// dataPathArr: [""], -// level: 0, -// dataLevel: 0, -// data: "data", // TODO get unique name when passed from applicator keywords -// gen: new CodeGen(), -// MissingRefError, -// RULES: RULES, -// validate: validateCode, -// util: util, -// resolve: resolve, -// resolveRef: resolveRef, -// usePattern: usePattern, -// useDefault: useDefault, -// useCustomRule: useCustomRule, -// opts: opts, -// formats: formats, -// logger: self.logger, -// self: self, - - -// level: number -// dataLevel: number -// data: string -// dataPathArr: string[] -// schemaPath: string -// errorPath: string -// errSchemaPath: string -// gen: CodeGen -// createErrors?: boolean // TODO maybe remove later -// opts: Options -// formats: { -// [index: string]: Format | undefined -// } -// // keywords: { -// // [index: string]: KeywordDefinition | undefined -// // } -// compositeRule?: boolean -// validate: (it: CompilationContext) => string -// usePattern: (str: string) => string -// useDefault: (value: any) => string -// useCustomRule: (rule: Rule, schema: any, parentSchema: object, it: CompilationContext) => any -// util: object // TODO -// self: object // TODO -// RULES: ValidationRules -// logger: Logger // TODO ? -// isTop: boolean // TODO ? -// root: SchemaRoot // TODO ? -// rootId?: string // TODO ? -// MissingRefError: typeof MissingRefError -// resolve: any -// resolveRef: (...args: any[]) => any \ No newline at end of file diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 564e237e59..0352a24ed8 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,7 +1,7 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" -import validateCode from "./validate" +import {validateCode} from "./validate" import {validateKeywordSchema} from "./validate/keyword" import {ErrorObject, KeywordCompilationResult} from "../types" @@ -107,8 +107,9 @@ function compile(schema, root, localRefs, baseId) { var $async = _schema.$async === true const rootId = resolve.fullPath(_root.schema.$id) + const gen = new CodeGen() // TODO refactor to extract code from gen - let sourceCode = validateCode({ + validateCode({ allErrors: !!opts.allErrors, isTop: true, topSchemaRef: "validate.schema", @@ -125,9 +126,8 @@ function compile(schema, root, localRefs, baseId) { level: 0, dataLevel: 0, data: "data", // TODO get unique name when passed from applicator keywords - gen: new CodeGen(), + gen, RULES, // TODO refactor - it is available on the instance - validateCode, resolveRef, // TODO remove to imports usePattern, // TODO remove to imports useDefault, // TODO remove to imports @@ -139,12 +139,12 @@ function compile(schema, root, localRefs, baseId) { self, }) - sourceCode = + let sourceCode = vars(refVal, refValCode) + vars(patterns, patternCode) + vars(defaults, defaultCode) + vars(customRules, customRuleCode) + - sourceCode + gen._out if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) // console.log("\n\n\n *** \n", sourceCode) diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 0f782051f8..192c136650 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,5 +1,5 @@ import {CompilationContext} from "../types" -import validateCode from "./validate" +import {validateCode} from "./validate" import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" import {quotedString} from "../vocabularies/util" @@ -48,8 +48,7 @@ export function applySubschema( extendSubschemaData(subschema, it, appl) extendSubschemaMode(subschema, appl) const nextContext = {...it, ...subschema, level: it.level + 1} - // TODO remove "true" once appendGen is removed - validateCode(nextContext, valid, true) + validateCode(nextContext, valid) } function getSubschema( diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 76f444b3ba..b9867cc0a8 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -8,84 +8,64 @@ import {schemaKeywords} from "./iterate" const resolve = require("../resolve") // schema compilation (render) time: -// it = { schema, RULES, _validate, opts } -// it.validate - this function (validateCode), -// it is used recursively to generate code for sub-schemas +// this function is used recursively to generate code for sub-schemas // // runtime: // "validate" is a variable name to which this function will be assigned // validateRef etc. are defined in the parent scope in index.js -export default function validateCode( - it: CompilationContext, - valid?: string, - appendGen?: true // TODO remove once all callers pass true -): string | void { +export function validateCode(it: CompilationContext, valid?: string): string | void { const { isTop, schema, - RULES, level, gen, opts: {$comment}, } = it - let _out - if (!appendGen) { - // TODO _out - _out = gen._out - gen._out = "" - } - // TODO valid must be non-optional or maybe it must be returned if (!valid) valid = `valid${level}` - checkUnknownKeywords(it) - checkRefsAndKeywords(it) + checkKeywords(it) if (isTop) startFunction(it) - if (booleanOrEmpty()) return _out + if (booleanOrEmpty(it, valid)) return if ($comment && schema.$comment) commentKeyword(it) if (isTop) { delete it.isTop checkNoDefault(it) initializeTop(it) - typeAndKeywords() + typeAndKeywords(it) endFunction(it) } else { updateContext(it) checkAsync(it) - // TODO level, var - it is coupled with errs count in keyword.ts - gen.code(`var errs_${level} = errors;`) - typeAndKeywords() - // TODO level, var - gen.code(`var ${valid} = errors === errs_${level};`) - } - - if (!appendGen) { - // TODO _out - ;[_out, gen._out] = [gen._out, _out] - return _out + const errsCount = gen.name("_errs") + // TODO var - async validation fails, possibly because of nodent + gen.code(`var ${errsCount} = errors;`) + typeAndKeywords(it, errsCount) + // TODO var, level + gen.code(`var ${valid} = ${errsCount} === errors;`) } +} - function booleanOrEmpty(): true | void { - if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { - // TODO remove type cast once valid is non optional - booleanOrEmptySchema(it, valid) +function checkKeywords(it: CompilationContext) { + checkUnknownKeywords(it) + checkRefsAndKeywords(it) +} - if (!appendGen) { - // TODO _out - ;[_out, gen._out] = [gen._out, _out] - } - return true - } +function booleanOrEmpty(it: CompilationContext, valid: string): true | void { + const {schema, RULES} = it + if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { + booleanOrEmptySchema(it, valid) + return true } +} - function typeAndKeywords(): void { - const types = getSchemaTypes(it) - const checkedTypes = coerceAndCheckDataType(it, types) - schemaKeywords(it, types, !checkedTypes, isTop) - } +function typeAndKeywords(it: CompilationContext, errsCount?: string): void { + const types = getSchemaTypes(it) + const checkedTypes = coerceAndCheckDataType(it, types) + schemaKeywords(it, types, !checkedTypes, errsCount) } function checkUnknownKeywords({ diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index d56c74574a..f6428ac7b2 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -9,12 +9,11 @@ export function schemaKeywords( it: CompilationContext, types: string[], typeErrors: boolean, - top?: boolean + errsCount?: string ): void { const { gen, schema, - level, dataLevel, RULES, allErrors, @@ -22,8 +21,7 @@ export function schemaKeywords( } = it if (schema.$ref && !(extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref"))) { // TODO remove Rule type cast - ;(RULES.all.$ref as Rule).code(it, "$ref") - if (!allErrors) gen.code("}") + gen.block(() => (RULES.all.$ref as Rule).code(it, "$ref")) return } const ruleGroups = RULES.rules.filter((group) => shouldUseGroup(schema, group)) @@ -44,8 +42,7 @@ export function schemaKeywords( iterateKeywords(it, group) } if (!allErrors && i < last) { - const errCount = top ? "0" : `errs_${level}` - gen.if(`errors === ${errCount}`) + gen.if(`errors === ${errsCount || 0}`) } }) ) @@ -55,20 +52,14 @@ function iterateKeywords(it: CompilationContext, group: RuleGroup) { const { gen, schema, - allErrors, opts: {useDefaults}, } = it if (useDefaults) assignDefaults(it, group.type) - let closeBlocks = "" - for (const rule of group.rules) { - if (shouldUseRule(schema, rule)) { - // TODO _outLen - const _outLen = gen._out.length - rule.code(it, rule.keyword, group.type) - if (_outLen < gen._out.length) { - if (!allErrors) closeBlocks += "}" + gen.block(() => { + for (const rule of group.rules) { + if (shouldUseRule(schema, rule)) { + rule.code(it, rule.keyword, group.type) } } - } - if (!allErrors) gen.code(closeBlocks) + }) } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 0d7d76d696..1977557cae 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -51,9 +51,10 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { ) // TODO refactor ifs - gen.code(`if (!${valid}) {`) + gen.if(`!${valid}`) reportExtraError(cxt, keywordError) - gen.code(it.allErrors ? "}" : "} else {") + if (it.allErrors) gen.endIf() + else gen.else() } function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { @@ -167,10 +168,10 @@ function reportKeywordErrors( return ok("false") default: // TODO refactor ifs - gen.code(`if (!${valid}) {`) + gen.if(`!${valid}`) addKeywordErrors(cxt, ruleErrs, errsCount) - gen.code(`}`) - if (!it.allErrors) gen.code(" else {") + if (it.allErrors) gen.endIf() + else gen.else() } } diff --git a/lib/keyword.ts b/lib/keyword.ts index a52247286c..a2f88f5328 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -179,12 +179,13 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v // TODO replace with fail_ below function fail(condition?: string, context?: KeywordContext): void { if (condition) { - gen.code(`if (${condition}) {`) + gen.if(condition) _reportError() - gen.code(allErrors ? "}" : "} else {") + if (allErrors) gen.endIf() + else gen.else() } else { _reportError() - if (!allErrors) gen.code("if (false) {") + if (!allErrors) gen.if("false") } function _reportError() { @@ -194,8 +195,8 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v function ok(condition?: string): void { if (!allErrors) { - if (condition) gen.code(`if (${condition}) {`) - else gen.code("if (true) {") + if (condition) gen.if(condition) + else gen.if("true") } } diff --git a/lib/types.ts b/lib/types.ts index 5826199bd8..0a74c7fc42 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -126,7 +126,6 @@ export interface CompilationContext { // [index: string]: KeywordDefinition | undefined // } compositeRule?: boolean - validateCode: (it: CompilationContext) => string | void // TODO remove string usePattern: (str: string) => string useDefault: (value: any) => string customRules: KeywordCompilationResult[] diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index f62848bc3a..6af215c6e9 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -38,7 +38,7 @@ const def: CodeKeywordDefinition = { const errsCount = gen.name("_errs") gen.code(`const ${errsCount} = errors;`) checkAdditionalProperties() - if (!allErrors) gen.code(`if (${errsCount} === errors) {`) + if (!allErrors) gen.if(`${errsCount} === errors`) function checkAdditionalProperties(): void { loopPropertiesCode(cxt, (key: string) => { diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 536cd18d7d..d014b2d6a1 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -36,12 +36,11 @@ const def: CodeKeywordDefinition = { }, schema.length) // TODO refactor failCompoundOrReset? - // TODO refactor ifs - gen.code(`if (!${valid}) {`) + gen.if(`!${valid}`) reportExtraError(cxt, def.error as KeywordErrorDefinition) - gen.code(`} else {`) + gen.else() resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.code(`}`) + if (it.allErrors) gen.endIf() }, error: { message: "should match some schema in anyOf", diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 32f94770cd..0368713921 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -32,12 +32,11 @@ const def: CodeKeywordDefinition = { }) // TODO refactor failCompoundOrReset? It is different from anyOf though - // TODO refactor ifs - gen.code(`if (!${valid}) {`) + gen.if(`!${valid}`) reportError(cxt, def.error as KeywordErrorDefinition) - gen.code(`} else {`) + gen.else() resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.code(`}`) + if (it.allErrors) gen.endIf() }, error: { message: "should contain a valid item", diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index d70e67b488..c607a267ec 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -43,9 +43,10 @@ const def: CodeKeywordDefinition = { // // TODO refactor failCompoundOrReset? // // TODO refactor ifs - gen.code(`if (!${valid}) {`) + gen.if(`!${valid}`) reportExtraError(cxt, def.error as KeywordErrorDefinition) - gen.code(it.allErrors ? "}" : "} else {") + if (it.allErrors) gen.endIf() + else gen.else() function validateIf(): void { applySubschema( diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 65500bcf60..3632282be4 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -74,7 +74,7 @@ const def: CodeKeywordDefinition = { const valid = gen.name("valid") gen.for(`let ${i}=${startFrom}; ${i}<${len}; ${i}++`, () => { applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) - if (!it.allErrors) gen.code(`if(!${valid}){break}`) + if (!it.allErrors) gen.if(`!${valid}`, "break") }) } }, diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 5edac49b48..8f63de3156 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -26,11 +26,11 @@ const def: CodeKeywordDefinition = { // TODO refactor failCompoundOrReset? // TODO refactor ifs - gen.code(`if (${valid}) {`) + gen.if(valid) reportError(cxt, def.error as KeywordErrorDefinition) - gen.code(`} else {`) + gen.else() resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.code(`}`) + if (it.allErrors) gen.endIf() }, error: { message: "should NOT be valid", diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 17b9c4ed7d..d42ce9709f 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -25,11 +25,11 @@ const def: CodeKeywordDefinition = { // TODO refactor failCompoundOrReset? // TODO refactor ifs - gen.code(`if (!${valid}) {`) + gen.if(`!${valid}`) reportExtraError(cxt, def.error as KeywordErrorDefinition) - gen.code(`} else {`) + gen.else() resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.code(`}`) + if (it.allErrors) gen.endIf() function validateOneOf() { schema.forEach((sch, i: number) => { @@ -57,12 +57,7 @@ const def: CodeKeywordDefinition = { .else() } - gen.code( - `if (${schValid}) { - ${valid} = true; - ${passing} = ${i}; - }` - ) + gen.if(schValid, `${valid} = true; ${passing} = ${i};`) }) } }, diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 92b032dc1a..0c0c5da52d 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -28,8 +28,7 @@ const def: CodeKeywordDefinition = { }) }) - // TODO refactor ifs - if (!it.allErrors) gen.code(`if (${errsCount} === errors) {`) + ok(`${errsCount} === errors`) }, error: { message: ({params}) => `"property name '" + ${params.propertyName} + "' is invalid"`, diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index f58c681289..50c3e26e9e 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -72,11 +72,10 @@ const def: CodeKeywordDefinition = { } function validateRef(v: string): void { - // TODO refactor ifs - gen.code(`if (!${callValidate(v)}) {`) + gen.if(`!${callValidate(v)}`) addErrorsFrom(v) - // refactor ifs - gen.code(allErrors ? "}" : "} else {") + if (allErrors) gen.endIf() + else gen.else() } function callValidate(v: string): string { diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 70e4662ec1..cfd55c95e1 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -69,10 +69,9 @@ const def: CodeKeywordDefinition = { fail(`!${valid}`) } else { - // TODO refactor ifs - gen.code(`if (${checkMissingProp(cxt, schema, missing)}) {`) + gen.if(`${checkMissingProp(cxt, schema, missing)}`) reportMissingProp(cxt, missing, error) - gen.code(`} else {`) + gen.else() } } From 0aad9598f63a05aa1f4d967db6762d55db7de491 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 25 Aug 2020 16:08:54 -0400 Subject: [PATCH 093/322] refactor: split validate code generation --- lib/compile/codegen.ts | 18 ++- lib/compile/index.ts | 6 +- lib/compile/subschema.ts | 6 +- lib/compile/validate/boolSchema.ts | 37 +++---- lib/compile/validate/index.ts | 172 +++++++++++++---------------- lib/keyword.ts | 5 +- lib/types.ts | 2 - 7 files changed, 115 insertions(+), 131 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index ca343870f4..282e5c9c57 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -2,9 +2,10 @@ enum Block { If, Else, For, + Func, } -type Code = string | (() => void) +export type Code = string | (() => void) export default class CodeGen { #names: {[key: string]: number} = {} @@ -108,6 +109,21 @@ export default class CodeGen { return this } + func(name = "", args = "", async?: boolean, funcBody?: Code): CodeGen { + this.#blocks.push(Block.Func) + this.code(`${async ? "async " : ""}function ${name}(${args}){`) + if (funcBody) this.code(funcBody).endFunc() + return this + } + + endFunc(): CodeGen { + const b = this._lastBlock + if (b !== Block.Func) throw new Error('CodeGen: "endFunc" without "func"') + this.#blocks.pop() + this.code(`}`) + return this + } + get _lastBlock(): Block { return this.#blocks[this._last()] } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 0352a24ed8..6de06cca34 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,7 +1,7 @@ import CodeGen from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" -import {validateCode} from "./validate" +import {validateFunctionCode} from "./validate" import {validateKeywordSchema} from "./validate/keyword" import {ErrorObject, KeywordCompilationResult} from "../types" @@ -109,9 +109,8 @@ function compile(schema, root, localRefs, baseId) { const gen = new CodeGen() // TODO refactor to extract code from gen - validateCode({ + validateFunctionCode({ allErrors: !!opts.allErrors, - isTop: true, topSchemaRef: "validate.schema", async: _schema.$async === true, schema: _schema, @@ -123,7 +122,6 @@ function compile(schema, root, localRefs, baseId) { errSchemaPath: "#", errorPath: '""', dataPathArr: [""], - level: 0, dataLevel: 0, data: "data", // TODO get unique name when passed from applicator keywords gen, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 192c136650..963f67ce36 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,5 +1,5 @@ import {CompilationContext} from "../types" -import {validateCode} from "./validate" +import {subschemaCode} from "./validate" import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" import {quotedString} from "../vocabularies/util" @@ -47,8 +47,8 @@ export function applySubschema( const subschema = getSubschema(it, appl) extendSubschemaData(subschema, it, appl) extendSubschemaMode(subschema, appl) - const nextContext = {...it, ...subschema, level: it.level + 1} - validateCode(nextContext, valid) + const nextContext = {...it, ...subschema} + subschemaCode(nextContext, valid) } function getSubschema( diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index eeff4eeaea..573a5f6234 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -6,27 +6,24 @@ const boolError: KeywordErrorDefinition = { params: () => "{}", } -export function booleanOrEmptySchema(it: CompilationContext, valid: string): void { - const {gen, isTop, schema} = it - if (isTop) { - if (schema === false) { - falseSchemaError(it, false) - } else if (schema.$async === true) { - gen.code("return data;") - } else { - gen.code("validate.errors = null; return true;") - } - gen.code( - `}; - return validate;` - ) +export function topBoolOrEmptySchema(it: CompilationContext): void { + const {gen, schema} = it + if (schema === false) { + falseSchemaError(it, false) + } else if (schema.$async === true) { + gen.code("return data;") } else { - if (schema === false) { - gen.code(`var ${valid} = false;`) // TODO var - falseSchemaError(it) - } else { - gen.code(`var ${valid} = true;`) // TODO var - } + gen.code("validate.errors = null; return true;") + } +} + +export function boolOrEmptySchema(it: CompilationContext, valid: string): void { + const {gen, schema} = it + if (schema === false) { + gen.code(`var ${valid} = false;`) // TODO var + falseSchemaError(it) + } else { + gen.code(`var ${valid} = true;`) // TODO var } } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index b9867cc0a8..76d88460f1 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,52 +1,63 @@ import {CompilationContext} from "../../types" import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util" import {quotedString} from "../../vocabularies/util" -import {booleanOrEmptySchema} from "./boolSchema" +import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" import {schemaKeywords} from "./iterate" +import CodeGen, {Code} from "../codegen" const resolve = require("../resolve") -// schema compilation (render) time: -// this function is used recursively to generate code for sub-schemas -// -// runtime: -// "validate" is a variable name to which this function will be assigned -// validateRef etc. are defined in the parent scope in index.js -export function validateCode(it: CompilationContext, valid?: string): string | void { - const { - isTop, - schema, - level, - gen, - opts: {$comment}, - } = it - - // TODO valid must be non-optional or maybe it must be returned - if (!valid) valid = `valid${level}` - +// schema compilation - generates validation function, subschemaCode (below) is used for subschemas +export function validateFunctionCode(it: CompilationContext): void { + const {schema, opts} = it checkKeywords(it) - - if (isTop) startFunction(it) - if (booleanOrEmpty(it, valid)) return - if ($comment && schema.$comment) commentKeyword(it) - - if (isTop) { - delete it.isTop + if (isBoolOrEmpty(it)) { + validateFunction(it, () => topBoolOrEmptySchema(it)) + return + } + validateFunction(it, () => { + if (opts.$comment && schema.$comment) commentKeyword(it) checkNoDefault(it) - initializeTop(it) + initializeTop(it.gen) typeAndKeywords(it) - endFunction(it) - } else { - updateContext(it) - checkAsync(it) - const errsCount = gen.name("_errs") - // TODO var - async validation fails, possibly because of nodent - gen.code(`var ${errsCount} = errors;`) - typeAndKeywords(it, errsCount) - // TODO var, level - gen.code(`var ${valid} = ${errsCount} === errors;`) + returnResults(it) + }) +} + +function validateFunction(it: CompilationContext, body: Code) { + const {gen} = it + gen.func("validate", "data, dataPath, parentData, parentDataProperty, rootData", it.async, body) + gen.code( + `"use strict"; + ${funcSourceUrl(it)}` + ) + gen.code(`return validate;`) +} + +function funcSourceUrl({schema, opts}: CompilationContext): string { + return schema.$id && (opts.sourceCode || opts.processCode) + ? `/*# sourceURL=${schema.$id as string} */` + : "" +} + +// schema compilation - this function is used recursively to generate code for sub-schemas +export function subschemaCode(it: CompilationContext, valid: string): void { + const {schema, gen, opts} = it + checkKeywords(it) + if (isBoolOrEmpty(it)) { + boolOrEmptySchema(it, valid) + return } + if (opts.$comment && schema.$comment) commentKeyword(it) + updateContext(it) + checkAsync(it) + const errsCount = gen.name("_errs") + // TODO var - async validation fails if var replaced, possibly because of nodent + gen.code(`var ${errsCount} = errors;`) + typeAndKeywords(it, errsCount) + // TODO var + gen.code(`var ${valid} = ${errsCount} === errors;`) } function checkKeywords(it: CompilationContext) { @@ -54,12 +65,8 @@ function checkKeywords(it: CompilationContext) { checkRefsAndKeywords(it) } -function booleanOrEmpty(it: CompilationContext, valid: string): true | void { - const {schema, RULES} = it - if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) { - booleanOrEmptySchema(it, valid) - return true - } +function isBoolOrEmpty({schema, RULES}: CompilationContext): boolean { + return typeof schema == "boolean" || !schemaHasRules(schema, RULES.all) } function typeAndKeywords(it: CompilationContext, errsCount?: string): void { @@ -68,17 +75,12 @@ function typeAndKeywords(it: CompilationContext, errsCount?: string): void { schemaKeywords(it, types, !checkedTypes, errsCount) } -function checkUnknownKeywords({ - schema, - RULES, - opts: {strictKeywords}, - logger, -}: CompilationContext): void { - if (strictKeywords) { +function checkUnknownKeywords({schema, RULES, opts, logger}: CompilationContext): void { + if (opts.strictKeywords) { const unknownKeyword = schemaUnknownRules(schema, RULES.keywords) if (unknownKeyword) { const msg = `unknown keyword: "${unknownKeyword}"` - if (strictKeywords === "log") logger.warn(msg) + if (opts.strictKeywords === "log") logger.warn(msg) else throw new Error(msg) } } @@ -88,53 +90,33 @@ function checkRefsAndKeywords({ schema, errSchemaPath, RULES, - opts: {extendRefs}, + opts, logger, }: CompilationContext): void { if (schema.$ref && schemaHasRulesExcept(schema, RULES.all, "$ref")) { - if (extendRefs === "fail") { + if (opts.extendRefs === "fail") { throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`) - } else if (extendRefs !== true) { + } else if (opts.extendRefs !== true) { logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) } } } -function startFunction({ - gen, - schema, - async, - opts: {sourceCode, processCode}, -}: CompilationContext): void { - const asyncFunc = async ? "async" : "" - const sourceUrl = - schema.$id && (sourceCode || processCode) ? `/*# sourceURL=${schema.$id as string} */` : "" - gen.code( - `const validate = ${asyncFunc} function(data, dataPath, parentData, parentDataProperty, rootData) { - 'use strict'; - ${sourceUrl}` - ) -} - -function checkNoDefault({ - schema, - opts: {useDefaults, strictDefaults}, - logger, -}: CompilationContext): void { - if (schema.default !== undefined && useDefaults && strictDefaults) { +function checkNoDefault({schema, opts, logger}: CompilationContext): void { + if (schema.default !== undefined && opts.useDefaults && opts.strictDefaults) { const msg = "default is ignored in the schema root" - if (strictDefaults === "log") logger.warn(msg) + if (opts.strictDefaults === "log") logger.warn(msg) else throw new Error(msg) } } -function initializeTop({gen}: CompilationContext): void { - // TODO old comment: "don't edit, used in replace". Should be removed? - gen.code( - `let vErrors = null; - let errors = 0; - if (rootData === undefined) rootData = data;` - ) +function initializeTop(gen: CodeGen): void { + gen + .code( + `let vErrors = null; + let errors = 0;` + ) + .if(`rootData === undefined`, `rootData = data;`) } function updateContext(it: CompilationContext): void { @@ -155,17 +137,13 @@ function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: Compilat } } -function endFunction({gen, async}: CompilationContext) { - // TODO old comment: "don't edit, used in replace". Should be removed? - gen.code( - async - ? `if (errors === 0) return data; - else throw new ValidationError(vErrors);` - : `validate.errors = vErrors; - return errors === 0;` - ) - gen.code( - `}; - return validate;` - ) +function returnResults({gen, async}: CompilationContext) { + if (async) { + gen.if("errors === 0", "return data", "throw new ValidationError(vErrors)") + } else { + gen.code( + `validate.errors = vErrors; + return errors === 0;` + ) + } } diff --git a/lib/keyword.ts b/lib/keyword.ts index a2f88f5328..97b4c10172 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -194,10 +194,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } function ok(condition?: string): void { - if (!allErrors) { - if (condition) gen.if(condition) - else gen.if("true") - } + if (!allErrors) gen.if(condition || "true") } function errorParams(obj: KeywordContextParams, assign?: true) { diff --git a/lib/types.ts b/lib/types.ts index 0a74c7fc42..f96362f76c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -104,7 +104,6 @@ export type KeywordCompilationResult = object | boolean | SchemaValidateFunction export interface CompilationContext { allErrors: boolean - level: number dataLevel: number data: string dataPathArr: (string | number)[] @@ -133,7 +132,6 @@ export interface CompilationContext { self: any // TODO RULES: ValidationRules logger: Logger // TODO ? - isTop?: boolean // TODO ? root: SchemaRoot // TODO ? rootId: string // TODO ? topSchemaRef: string From f5a46db4df31802d3826490d08991cc525f50720 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 25 Aug 2020 17:38:20 -0400 Subject: [PATCH 094/322] refactor: simplify code generation by using different number of open blocks --- lib/compile/validate/keyword.ts | 26 +++------ lib/keyword.ts | 21 ++----- lib/types.ts | 4 +- .../applicator/additionalProperties.ts | 4 +- lib/vocabularies/applicator/allOf.ts | 18 ++---- lib/vocabularies/applicator/anyOf.ts | 4 +- lib/vocabularies/applicator/dependencies.ts | 15 +---- lib/vocabularies/applicator/if.ts | 20 +++---- lib/vocabularies/applicator/items.ts | 56 ++++++++----------- .../applicator/patternProperties.ts | 11 +--- lib/vocabularies/applicator/properties.ts | 28 ++++------ lib/vocabularies/applicator/propertyNames.ts | 7 +-- lib/vocabularies/core/ref.ts | 7 +-- lib/vocabularies/format/format.ts | 8 +-- lib/vocabularies/validation/required.ts | 4 +- lib/vocabularies/validation/uniqueItems.ts | 5 +- 16 files changed, 85 insertions(+), 153 deletions(-) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 1977557cae..ad580827c4 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -22,6 +22,7 @@ export default function keywordCode( def: KeywordDefinition ): void { // TODO "code" keyword + // TODO refactor if (cxt.$data && "validate" in def) { funcKeywordCode(cxt, def as FuncKeywordDefinition) } else if ("macro" in def) { @@ -32,7 +33,7 @@ export default function keywordCode( } function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { - const {gen, keyword, schema, parentSchema, it} = cxt + const {gen, fail, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) const schemaRef = addCustomRule(it, keyword, macroSchema) if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true) @@ -50,15 +51,11 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { valid ) - // TODO refactor ifs - gen.if(`!${valid}`) - reportExtraError(cxt, keywordError) - if (it.allErrors) gen.endIf() - else gen.else() + fail(`!${valid}`, () => reportExtraError(cxt, keywordError)) } function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { - const {gen, ok, fail, keyword, schema, schemaCode, parentSchema, data, $data, it} = cxt + const {gen, fail, keyword, schema, schemaCode, parentSchema, data, $data, it} = cxt checkAsync(it, def) const validate = "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate @@ -99,8 +96,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { gen.code(`${valid} = ${def.async ? "await " : ""}${callValidate(validateRef)};`) if (def.modifying) modifyData(cxt) gen.endBlock() - if (def.valid) return ok() - fail(`!${valid}`) + if (!def.valid) fail(`!${valid}`) } function validateAsyncRule() { @@ -159,19 +155,15 @@ function reportKeywordErrors( ruleErrs: string, errsCount: string ): void { - const {gen, ok, it} = cxt + const {ok, fail} = cxt switch (def.valid) { case true: - return ok() + return case false: addKeywordErrors(cxt, ruleErrs, errsCount) - return ok("false") + return ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? default: - // TODO refactor ifs - gen.if(`!${valid}`) - addKeywordErrors(cxt, ruleErrs, errsCount) - if (it.allErrors) gen.endIf() - else gen.else() + fail(`!${valid}`, () => addKeywordErrors(cxt, ruleErrs, errsCount)) } } diff --git a/lib/keyword.ts b/lib/keyword.ts index 97b4c10172..827aaac368 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,7 +1,6 @@ import { KeywordDefinition, CodeKeywordDefinition, - KeywordErrorDefinition, Vocabulary, ErrorObject, ValidateFunction, @@ -177,14 +176,15 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v ;(def.code || keywordCode)(cxt, ruleType, this.definition) // TODO replace with fail_ below - function fail(condition?: string, context?: KeywordContext): void { + function fail(condition?: string, failAction?: () => void, context?: KeywordContext): void { + const action = failAction || _reportError if (condition) { gen.if(condition) - _reportError() + action() if (allErrors) gen.endIf() else gen.else() } else { - _reportError() + action() if (!allErrors) gen.if("false") } @@ -193,8 +193,8 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } } - function ok(condition?: string): void { - if (!allErrors) gen.if(condition || "true") + function ok(condition: string): void { + if (!allErrors) gen.if(condition) } function errorParams(obj: KeywordContextParams, assign?: true) { @@ -203,15 +203,6 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } } -// TODO remove when "fail" replaced -export function fail_(condition: string, cxt: KeywordContext, error: KeywordErrorDefinition): void { - const {gen, allErrors} = cxt.it - gen.if(condition) - reportError(cxt, error) - if (allErrors) gen.endIf() - else gen.else() -} - function validSchemaType(schema: any, schemaType: string | string[]): boolean { // TODO add tests if (Array.isArray(schemaType)) { diff --git a/lib/types.ts b/lib/types.ts index f96362f76c..865f95c301 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -213,8 +213,8 @@ export type Vocabulary = KeywordDefinition[] export interface KeywordContext { gen: CodeGen - fail: (condition?: string, context?: KeywordContext) => void - ok: (condition?: string) => void + fail: (condition?: string, failAction?: () => void, context?: KeywordContext) => void + ok: (condition: string) => void errorParams: (obj: KeywordContextParams, assing?: true) => void keyword: string data: string diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 6af215c6e9..344b1445ff 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -21,7 +21,7 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "boolean"], error, code(cxt) { - const {gen, ok, errorParams, schema, parentSchema, data, it} = cxt + const {gen, errorParams, schema, parentSchema, data, it} = cxt const { allErrors, usePattern, @@ -29,7 +29,7 @@ const def: CodeKeywordDefinition = { } = it if ((schema === undefined || alwaysValidSchema(it, schema)) && removeAdditional !== "all") { - return ok() + return } const props = allSchemaProperties(parentSchema.properties) diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index e06641418d..bc5d4fba21 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -6,20 +6,12 @@ const def: CodeKeywordDefinition = { keyword: "allOf", schemaType: "array", code({gen, ok, schema, it}) { - let emptySchemas = true const valid = gen.name("valid") - gen.block(() => - schema.forEach((sch: object | boolean, i: number) => { - if (alwaysValidSchema(it, sch)) return - emptySchemas = false - applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) - if (!it.allErrors) gen.if(valid) - }) - ) - - if (emptySchemas) ok() - else ok(valid) - + schema.forEach((sch: object | boolean, i: number) => { + if (alwaysValidSchema(it, sch)) return + applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) + ok(valid) + }) // TODO possibly add allOf error }, } diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index d014b2d6a1..9ecd3466ed 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -7,9 +7,9 @@ const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", code(cxt) { - const {gen, ok, schema, it} = cxt + const {gen, schema, it} = cxt const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) - if (alwaysValid) return ok() + if (alwaysValid) return const valid = gen.name("valid") const schValid = gen.name("_valid") diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 186bb32fa4..f385d2bee2 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -17,19 +17,10 @@ const def: CodeKeywordDefinition = { schemaType: "object", code(cxt) { const {gen, ok, errorParams, schema, data, it} = cxt - const [propDeps, schDeps] = splitDependencies() - const valid = gen.name("valid") - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) - - gen.block(() => { - validatePropertyDeps(propDeps) - validateSchemaDeps(schDeps) - }) - - ok(`${errsCount} === errors`) + validatePropertyDeps(propDeps) + validateSchemaDeps(schDeps) function splitDependencies(): [PropertyDependencies, SchemaDependencies] { const propertyDeps: PropertyDependencies = {} @@ -78,7 +69,7 @@ const def: CodeKeywordDefinition = { () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), `var ${valid} = true;` // TODO refactor var ) - if (!it.allErrors) gen.if(valid) + ok(valid) } } }, diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index c607a267ec..55744cbfcc 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -3,18 +3,22 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" +const error: KeywordErrorDefinition = { + message: ({params}) => `'should match "' + ${params.ifClause} + '" schema'`, + params: ({params}) => `{failingKeyword: ${params.ifClause}}`, +} + const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], // TODO // implements: ["then", "else"], code(cxt) { - const {gen, ok, errorParams, it} = cxt + const {gen, fail, errorParams, it} = cxt const hasThen = hasSchema(it, "then") const hasElse = hasSchema(it, "else") if (!hasThen && !hasElse) { // TODO strict mode: fail or warning if both "then" and "else" are not present - ok() return } @@ -41,12 +45,7 @@ const def: CodeKeywordDefinition = { gen.if(`!${schValid}`, validateClause("else")) } - // // TODO refactor failCompoundOrReset? - // // TODO refactor ifs - gen.if(`!${valid}`) - reportExtraError(cxt, def.error as KeywordErrorDefinition) - if (it.allErrors) gen.endIf() - else gen.else() + fail(`!${valid}`, () => reportExtraError(cxt, error)) function validateIf(): void { applySubschema( @@ -70,10 +69,7 @@ const def: CodeKeywordDefinition = { } } }, - error: { - message: ({params}) => `'should match "' + ${params.ifClause} + '" schema'`, - params: ({params}) => `{failingKeyword: ${params.ifClause}}`, - }, + error, } module.exports = def diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 3632282be4..3fc5a72b4e 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,7 +1,6 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" -import {fail_} from "../../keyword" const def: CodeKeywordDefinition = { keyword: "items", @@ -12,41 +11,31 @@ const def: CodeKeywordDefinition = { // implements: ["additionalItems"], code(cxt) { // TODO strict mode: fail or warning if "additionalItems" is present without "items" - - const {gen, ok, schema, parentSchema, data, it} = cxt - const errsCount = gen.name("_errs") + const {gen, ok, fail, schema, parentSchema, data, it} = cxt const len = gen.name("len") - gen.code( - `const ${errsCount} = errors; - const ${len} = ${data}.length;` - ) - gen.block(validateItemsKeyword) - ok(`${errsCount} === errors`) + gen.code(`const ${len} = ${data}.length;`) + + if (Array.isArray(schema)) { + validateItemsArray(schema) + } else if (!alwaysValidSchema(it, schema)) { + validateItems("items", 0) + } - function validateItemsKeyword(): void { - if (Array.isArray(schema)) { - const addIts = parentSchema.additionalItems - if (addIts === false) validateDataLength(schema) - validateDefinedItems() - if (typeof addIts == "object" && !alwaysValidSchema(it, addIts)) { - gen.if(`${len} > ${schema.length}`, () => validateItems("additionalItems", schema.length)) - } - } else if (!alwaysValidSchema(it, schema)) { - validateItems("items", 0) + function validateItemsArray(sch: (object | boolean)[]) { + const addIts = parentSchema.additionalItems + if (addIts === false) validateDataLength(sch) + validateDefinedItems() + if (typeof addIts == "object" && !alwaysValidSchema(it, addIts)) { + gen.if(`${len} > ${sch.length}`, () => validateItems("additionalItems", sch.length)) } } - function validateDataLength(sch: any[]): void { - // TODO replace with "fail" - fail_( - `${len} > ${sch.length}`, - { - ...cxt, - keyword: "additionalItems", - schemaValue: false, - }, - def.error as KeywordErrorDefinition - ) + function validateDataLength(sch: (object | boolean)[]): void { + fail(`${len} > ${sch.length}`, undefined, { + ...cxt, + keyword: "additionalItems", + schemaValue: false, + }) } function validateDefinedItems(): void { @@ -65,7 +54,7 @@ const def: CodeKeywordDefinition = { valid ) ) - if (!it.allErrors) gen.if(valid) + ok(valid) }) } @@ -76,6 +65,7 @@ const def: CodeKeywordDefinition = { applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) if (!it.allErrors) gen.if(`!${valid}`, "break") }) + ok(valid) } }, error: { diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 30d475e154..06f8737d14 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -7,16 +7,11 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "object", code(cxt) { - const {gen, ok, schema, it} = cxt + const {gen, schema, it} = cxt const patterns = schemaProperties(it, schema) - if (patterns.length === 0) return ok() - + if (patterns.length === 0) return const valid = gen.name("valid") - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) - - gen.block(validatePatternProperties) - ok(`${errsCount} === errors`) + validatePatternProperties() function validatePatternProperties() { for (const pat of patterns) { diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 4ccc90e310..256e77171f 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -13,27 +13,19 @@ const def: CodeKeywordDefinition = { // remove all additional properties - it will fix skipped tests // } const properties = schemaProperties(it, schema) - if (properties.length === 0) return ok() - + if (properties.length === 0) return const valid = gen.name("valid") - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) - - gen.block(validateProperties) - ok(`${errsCount} === errors`) - function validateProperties() { - for (const prop of properties) { - if (hasDefault(prop)) { - applyPropertySchema(prop) - } else { - gen.if(propertyInData(data, prop, Expr.Const, it.opts.ownProperties)) - applyPropertySchema(prop) - if (!it.allErrors) gen.else().code(`var ${valid} = true;`) - gen.endIf() - } - if (!it.allErrors) gen.if(valid) + for (const prop of properties) { + if (hasDefault(prop)) { + applyPropertySchema(prop) + } else { + gen.if(propertyInData(data, prop, Expr.Const, it.opts.ownProperties)) + applyPropertySchema(prop) + if (!it.allErrors) gen.else().code(`var ${valid} = true;`) + gen.endIf() } + ok(valid) } function hasDefault(prop: string): boolean | undefined { diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 0c0c5da52d..6e0dc1afe0 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -9,11 +9,8 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "boolean"], code(cxt) { const {gen, ok, errorParams, schema, it} = cxt - if (alwaysValidSchema(it, schema)) return ok() - + if (alwaysValidSchema(it, schema)) return const valid = gen.name("valid") - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) loopPropertiesCode(cxt, (key) => { errorParams({propertyName: key}) @@ -28,7 +25,7 @@ const def: CodeKeywordDefinition = { }) }) - ok(`${errsCount} === errors`) + ok(valid) }, error: { message: ({params}) => `"property name '" + ${params.propertyName} + "' is invalid"`, diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 50c3e26e9e..b6db894774 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -32,7 +32,7 @@ const def: CodeKeywordDefinition = { return fail() case "ignore": logger.warn(msg) - return ok() + return default: throw new MissingRefError(baseId, schema, msg) } @@ -72,10 +72,7 @@ const def: CodeKeywordDefinition = { } function validateRef(v: string): void { - gen.if(`!${callValidate(v)}`) - addErrorsFrom(v) - if (allErrors) gen.endIf() - else gen.else() + fail(`!${callValidate(v)}`, () => addErrorsFrom(v)) } function callValidate(v: string): string { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index ea573e0a5d..21b9f0a566 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -7,9 +7,9 @@ const def: CodeKeywordDefinition = { type: ["number", "string"], schemaType: "string", $data: true, - code({gen, ok, fail, data, $data, schema, schemaCode, it}, ruleType) { + code({gen, fail, data, $data, schema, schemaCode, it}, ruleType) { const {formats, opts, logger, errSchemaPath} = it - if (opts.format === false) return ok() + if (opts.format === false) return if ($data) validate$DataFormat() else validateFormat() @@ -56,12 +56,10 @@ const def: CodeKeywordDefinition = { const formatDef: AddedFormat = formats[schema] if (!formatDef) { unknownFormat() - ok() return } const [fmtType, format, fmtRef] = getFormat(formatDef) - if (fmtType !== ruleType) ok() - else fail(`!(${validCondition()})`) + if (fmtType === ruleType) fail(`!(${validCondition()})`) function unknownFormat() { if (opts.unknownFormats === "ignore") return logger.warn(unknownMsg()) diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index cfd55c95e1..ee1446bc9f 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -20,8 +20,8 @@ const def: CodeKeywordDefinition = { schemaType: ["array"], $data: true, code(cxt) { - const {gen, ok, fail, errorParams, schema, schemaCode, data, $data, it} = cxt - if (!$data && schema.length === 0) return ok() + const {gen, fail, errorParams, schema, schemaCode, data, $data, it} = cxt + if (!$data && schema.length === 0) return const loopRequired = $data || schema.length >= it.opts.loopRequired diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 4a132484f2..1aa385694a 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -6,8 +6,8 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: "boolean", $data: true, - code({gen, fail, ok, errorParams, data, $data, schema, parentSchema, schemaCode, it: {opts}}) { - if (opts.uniqueItems === false || !($data || schema)) return ok() + code({gen, fail, errorParams, data, $data, schema, parentSchema, schemaCode, it: {opts}}) { + if (opts.uniqueItems === false || !($data || schema)) return const i = gen.name("i") const j = gen.name("j") const valid = gen.name("valid") @@ -15,6 +15,7 @@ const def: CodeKeywordDefinition = { gen.code(`let ${valid}, ${i}, ${j};`) const itemType = parentSchema.items?.type + // TODO refactor to have two open blocks? same as in required if ($data) { gen.if(`${schemaCode} === false || ${schemaCode} === undefined`, `${valid} = true`, () => gen.if(`typeof ${schemaCode} != "boolean"`, `${valid} = false`, validateUniqueItems) From 94ed4bd0a7bbbb2cdd7ea2f75589e815ec9ea162 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 26 Aug 2020 04:59:26 -0400 Subject: [PATCH 095/322] refactor: "compile/validate keywords code generation --- lib/compile/codegen.ts | 4 ++ lib/compile/index.ts | 6 +- lib/compile/validate/iterate.ts | 43 ++++++------ lib/compile/validate/keyword.ts | 117 ++++++++++++++------------------ lib/keyword.ts | 2 - lib/vocabularies/core/ref.ts | 20 ++---- lib/vocabularies/util.ts | 15 ++++ package.json | 2 +- 8 files changed, 104 insertions(+), 105 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 282e5c9c57..2c0e55b4c6 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -14,6 +14,10 @@ export default class CodeGen { #blocks: Block[] = [] #blockStarts: number[] = [] + toString(): string { + return this._out + } + name(prefix: string): string { if (!this.#names[prefix]) this.#names[prefix] = 0 const num = this.#names[prefix]++ diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 6de06cca34..65a14966ee 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -108,8 +108,9 @@ function compile(schema, root, localRefs, baseId) { const rootId = resolve.fullPath(_root.schema.$id) const gen = new CodeGen() - // TODO refactor to extract code from gen + validateFunctionCode({ + gen, allErrors: !!opts.allErrors, topSchemaRef: "validate.schema", async: _schema.$async === true, @@ -124,7 +125,6 @@ function compile(schema, root, localRefs, baseId) { dataPathArr: [""], dataLevel: 0, data: "data", // TODO get unique name when passed from applicator keywords - gen, RULES, // TODO refactor - it is available on the instance resolveRef, // TODO remove to imports usePattern, // TODO remove to imports @@ -142,7 +142,7 @@ function compile(schema, root, localRefs, baseId) { vars(patterns, patternCode) + vars(defaults, defaultCode) + vars(customRules, customRuleCode) + - gen._out + gen.toString() if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) // console.log("\n\n\n *** \n", sourceCode) diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index f6428ac7b2..0e13672716 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -24,28 +24,31 @@ export function schemaKeywords( gen.block(() => (RULES.all.$ref as Rule).code(it, "$ref")) return } - const ruleGroups = RULES.rules.filter((group) => shouldUseGroup(schema, group)) - const last = ruleGroups.length - 1 - gen.block(() => - ruleGroups.forEach((group, i) => { - if (group.type) { - // TODO refactor `data${dataLevel || ""}` - const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) - gen.if(checkType) - iterateKeywords(it, group) - if (types.length === 1 && types[0] === group.type && typeErrors) { - gen.else() - reportTypeError(it) - } - gen.endIf() - } else { - iterateKeywords(it, group) + gen.block(() => { + for (const group of RULES.rules) { + if (shouldUseGroup(schema, group)) { + groupKeywords(group) } - if (!allErrors && i < last) { - gen.if(`errors === ${errsCount || 0}`) + } + }) + + function groupKeywords(group: RuleGroup): void { + if (group.type) { + // TODO refactor `data${dataLevel || ""}` + const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) + gen.if(checkType) + iterateKeywords(it, group) + if (types.length === 1 && types[0] === group.type && typeErrors) { + gen.else() + reportTypeError(it) } - }) - ) + gen.endIf() + } else { + iterateKeywords(it, group) + } + // TODO make it "ok" call + if (!allErrors) gen.if(`errors === ${errsCount || 0}`) + } } function iterateKeywords(it: CompilationContext, group: RuleGroup) { diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index ad580827c4..51a619523b 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -9,7 +9,7 @@ import { } from "../../types" import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" -import {getParentData} from "../../vocabularies/util" +import {getParentData, callValidate} from "../../vocabularies/util" export const keywordError: KeywordErrorDefinition = { message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, @@ -55,7 +55,7 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { } function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { - const {gen, fail, keyword, schema, schemaCode, parentSchema, data, $data, it} = cxt + const {gen, ok, fail, keyword, schema, schemaCode, parentSchema, $data, it} = cxt checkAsync(it, def) const validate = "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate @@ -63,18 +63,33 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { const valid = gen.name("valid") gen.code(`let ${valid};`) - gen.block() - - if ($data) check$data() - if (def.errors === false) { validateNoErrorsRule() } else { - if (def.async) validateAsyncRule() - else validateRule() + validateRuleWithErrors() + } + + function validateNoErrorsRule(): void { + gen.block(() => { + if ($data) check$data() + assignValid() + if (def.modifying) modifyData(cxt) + }) + if (!def.valid) fail(`!${valid}`) + } + + function validateRuleWithErrors(): void { + gen.block() + if ($data) check$data() + const errsCount = gen.name("_errs") + gen.code(`const ${errsCount} = errors;`) + const ruleErrs = def.async ? validateAsyncRule() : validateSyncRule() + if (def.modifying) modifyData(cxt) + gen.endBlock() + reportKeywordErrors(ruleErrs, errsCount) } - function check$data() { + function check$data(): void { gen // TODO add support for schemaType in keyword definition // .if(`${dataNotType(schemaCode, def.schemaType, $data)} false`) // TODO refactor @@ -92,53 +107,42 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { } } - function validateNoErrorsRule() { - gen.code(`${valid} = ${def.async ? "await " : ""}${callValidate(validateRef)};`) - if (def.modifying) modifyData(cxt) - gen.endBlock() - if (!def.valid) fail(`!${valid}`) - } - - function validateAsyncRule() { - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) + function validateAsyncRule(): string { const ruleErrs = gen.name("ruleErrs") - gen - .code(`let ${ruleErrs} = null;`) - .try(`${valid} = await ${callValidate(validateRef)};`, (e) => + gen.code(`let ${ruleErrs} = null;`) + gen.try( + () => assignValid("await "), + (e) => gen .code(`${valid} = false;`) .if(`${e} instanceof ValidationError`, `${ruleErrs} = ${e}.errors;`, `throw ${e};`) - ) - if (def.modifying) modifyData(cxt) - gen.endBlock() - reportKeywordErrors(cxt, def, valid, ruleErrs, errsCount) + ) + return ruleErrs } - function validateRule() { + function validateSyncRule(): string { const validateErrs = `${validateRef}.errors` - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) - gen.code( - `${validateErrs} = null; - ${valid} = ${callValidate(validateRef)};` - ) - if (def.modifying) modifyData(cxt) - gen.endBlock() - reportKeywordErrors(cxt, def, valid, validateErrs, errsCount) + gen.code(`${validateErrs} = null;`) + assignValid("") + return validateErrs } - // TODO refactor with ref? - function callValidate(v: string): string { - const {errorPath} = it - const context = it.opts.passContext ? "this" : "self" - const dataAndSchema = - ("compile" in def && !$data) || def.schema === false - ? `${data}` - : `${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` - const dataPath = `(dataPath || '')${errorPath === '""' ? "" : ` + ${errorPath}`}` // TODO joinPaths? - const parent = getParentData(it) - return `${v}.call(${context}, ${dataAndSchema}, ${dataPath}, ${parent.data}, ${parent.property}, rootData)` + function assignValid(await: string = def.async ? "await " : ""): void { + const passCxt = it.opts.passContext ? "this" : "self" + const passSchema = !(("compile" in def && !$data) || def.schema === false) + gen.code(`${valid} = ${await}${callValidate(cxt, validateRef, passCxt, passSchema)};`) + } + + function reportKeywordErrors(ruleErrs: string, errsCount: string): void { + switch (def.valid) { + case true: + return + case false: + addKeywordErrors(cxt, ruleErrs, errsCount) + return ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? + default: + fail(`!${valid}`, () => addKeywordErrors(cxt, ruleErrs, errsCount)) + } } } @@ -148,25 +152,6 @@ function modifyData(cxt: KeywordContext) { gen.if(parent.data, `${data} = ${parent.data}[${parent.property}];`) } -function reportKeywordErrors( - cxt: KeywordContext, - def: FuncKeywordDefinition, - valid: string, - ruleErrs: string, - errsCount: string -): void { - const {ok, fail} = cxt - switch (def.valid) { - case true: - return - case false: - addKeywordErrors(cxt, ruleErrs, errsCount) - return ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? - default: - fail(`!${valid}`, () => addKeywordErrors(cxt, ruleErrs, errsCount)) - } -} - function addKeywordErrors(cxt: KeywordContext, ruleErrs: string, errsCount: string): void { const {gen} = cxt gen.if( diff --git a/lib/keyword.ts b/lib/keyword.ts index 827aaac368..073fbf716c 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -172,10 +172,8 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } else if (schemaType && !validSchemaType(schema, schemaType)) { throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) } - // TODO check that code called "fail" or another valid way to return code ;(def.code || keywordCode)(cxt, ruleType, this.definition) - // TODO replace with fail_ below function fail(condition?: string, failAction?: () => void, context?: KeywordContext): void { const action = failAction || _reportError if (condition) { diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index b6db894774..553a48ed35 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,15 +1,17 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordContext} from "../../types" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" -import {getParentData} from "../util" +import {callValidate} from "../util" const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", - code({gen, ok, fail, data, schema, it}) { + code(cxt: KeywordContext) { + const {gen, ok, fail, schema, it} = cxt const {resolveRef, allErrors, baseId, isRoot, root, opts, logger} = it const ref = getRef() + const passCxt = opts.passContext ? "this" : "" if (ref === undefined) missingRef() else if (ref.inline) applyRefSchema(ref) else if (ref.$async || it.async) validateAsyncRef(ref.code) @@ -59,7 +61,7 @@ const def: CodeKeywordDefinition = { if (!allErrors) gen.code(`let ${valid};`) gen.try( () => { - gen.code(`await ${callValidate(v)};`) + gen.code(`await ${callValidate(cxt, v, passCxt)};`) if (!allErrors) gen.code(`${valid} = true;`) }, (e) => { @@ -72,15 +74,7 @@ const def: CodeKeywordDefinition = { } function validateRef(v: string): void { - fail(`!${callValidate(v)}`, () => addErrorsFrom(v)) - } - - function callValidate(v: string): string { - const {errorPath} = it - const dataPath = `(dataPath || '')${errorPath === '""' ? "" : ` + ${errorPath}`}` // TODO joinPaths? - const parent = getParentData(it) - const args = `${data}, ${dataPath}, ${parent.data}, ${parent.property}, rootData` - return opts.passContext ? `${v}.call(this, ${args})` : `${v}(${args})` + fail(`!${callValidate(cxt, v, passCxt)}`, () => addErrorsFrom(v)) } function addErrorsFrom(source: string): void { diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 4c32000bc4..3f53ba2827 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -117,3 +117,18 @@ export function getParentData({dataLevel, dataPathArr}: CompilationContext): Par ? {data: `data${dataLevel - 1 || ""}`, property: `${dataPathArr[dataLevel]}`} : {data: "parentData", property: "parentDataProperty"} } + +export function callValidate( + {schemaCode, data, it}: KeywordContext, + func: string, + context?: string, + passSchema?: boolean +): string { + const dataAndSchema = passSchema + ? `${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` + : data + const dataPath = `(dataPath || '')${it.errorPath === '""' ? "" : ` + ${it.errorPath}`}` // TODO joinPaths? + const parent = getParentData(it) + const args = `${dataAndSchema}, ${dataPath}, ${parent.data}, ${parent.property}, rootData` + return context ? `${func}.call(${context}, ${args})` : `${func}(${args})` +} diff --git a/package.json b/package.json index 6c97ca0392..9996ecb75b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "lint": "npm run eslint", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", - "test-spec": "mocha spec/{**/,}*.spec.js -R spec", + "test-spec": "mocha spec/{**/,}*.spec.js -R dot", "test-fast": "AJV_FAST_TEST=true npm run test-spec", "test-debug": "npm run test-spec -- --inspect", "test-cov": "nyc npm run test-spec", From 6e21dd0461f19b3457404ce77713bfc55dd55a95 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 26 Aug 2020 07:00:01 -0400 Subject: [PATCH 096/322] feat: codegen Name class for variables in generated code --- .eslintrc.yml | 2 +- lib/compile/codegen.ts | 41 ++++++++++++++----- lib/compile/errors.ts | 6 +-- lib/compile/index.ts | 6 +-- lib/compile/subschema.ts | 24 ++++++----- lib/compile/util.ts | 10 +++-- lib/compile/validate/boolSchema.ts | 3 +- lib/compile/validate/dataType.ts | 6 ++- lib/compile/validate/index.ts | 8 ++-- lib/compile/validate/iterate.ts | 3 +- lib/compile/validate/keyword.ts | 7 ++-- lib/keyword.ts | 5 ++- lib/types.ts | 16 ++++---- .../applicator/additionalProperties.ts | 11 ++--- lib/vocabularies/applicator/dependencies.ts | 6 +-- lib/vocabularies/applicator/if.ts | 3 +- lib/vocabularies/applicator/properties.ts | 2 +- lib/vocabularies/core/ref.ts | 7 ++-- lib/vocabularies/missing.ts | 10 ++--- lib/vocabularies/util.ts | 36 ++++++++-------- lib/vocabularies/validation/enum.ts | 5 ++- lib/vocabularies/validation/required.ts | 10 ++--- 22 files changed, 130 insertions(+), 97 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 82985087ef..90edefbbb8 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -21,7 +21,7 @@ overrides: plugins: ["@typescript-eslint"] rules: no-var: 0 - "@typescript-eslint/restrict-template-expressions": [error, allowBoolean: true] + "@typescript-eslint/restrict-template-expressions": off "@typescript-eslint/ban-types": off "@typescript-eslint/no-empty-interface": off "@typescript-eslint/no-explicit-any": off diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 2c0e55b4c6..6467640779 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -5,7 +5,26 @@ enum Block { Func, } -export type Code = string | (() => void) +enum VarKind { + Const, + Let, + VFar, +} + +export class Name { + #name: string + constructor(name: string) { + this.#name = name + } + + toString(): string { + return this.#name + } +} + +export type Expression = string | Name + +export type _Code = string | (() => void) export default class CodeGen { #names: {[key: string]: number} = {} @@ -18,20 +37,22 @@ export default class CodeGen { return this._out } - name(prefix: string): string { + name(prefix: string): Name { if (!this.#names[prefix]) this.#names[prefix] = 0 const num = this.#names[prefix]++ - return `${prefix}_${num}` + return new Name(`${prefix}_${num}`) } - code(c?: Code): CodeGen { + // _def(varKind: VarKind, prefix: string, rhs?: string): Variable {} + + code(c?: _Code): CodeGen { // TODO optionally strip whitespace if (typeof c == "function") c() else if (c) this._out += c + "\n" return this } - if(condition: string, thenBody?: Code, elseBody?: Code): CodeGen { + if(condition: Expression, thenBody?: _Code, elseBody?: _Code): CodeGen { this.#blocks.push(Block.If) this.code(`if(${condition}){`) if (thenBody && elseBody) { @@ -44,7 +65,7 @@ export default class CodeGen { return this } - elseIf(condition: string): CodeGen { + elseIf(condition: Expression): CodeGen { if (this._lastBlock !== Block.If) throw new Error('CodeGen: "else if" without "if"') this.code(`}else if(${condition}){`) return this @@ -66,7 +87,7 @@ export default class CodeGen { return this } - for(iteration: string, forBody?: Code): CodeGen { + for(iteration: string, forBody?: _Code): CodeGen { this.#blocks.push(Block.For) this.code(`for(${iteration}){`) if (forBody) this.code(forBody).endFor() @@ -81,7 +102,7 @@ export default class CodeGen { return this } - try(tryBody: Code, catchCode?: (e: string) => void, finallyCode?: Code): CodeGen { + try(tryBody: _Code, catchCode?: (e: Name) => void, finallyCode?: _Code): CodeGen { if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') this.code("try{").code(tryBody) if (catchCode) { @@ -94,7 +115,7 @@ export default class CodeGen { return this } - block(body?: Code, expectedToClose?: number): CodeGen { + block(body?: _Code, expectedToClose?: number): CodeGen { this.#blockStarts.push(this.#blocks.length) if (body) this.code(body).endBlock(expectedToClose) return this @@ -113,7 +134,7 @@ export default class CodeGen { return this } - func(name = "", args = "", async?: boolean, funcBody?: Code): CodeGen { + func(name = "", args = "", async?: boolean, funcBody?: _Code): CodeGen { this.#blocks.push(Block.Func) this.code(`${async ? "async " : ""}function ${name}(${args}){`) if (funcBody) this.code(funcBody).endFunc() diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 238ca35d61..4b252459a8 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,6 +1,6 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {quotedString} from "../vocabularies/util" -import CodeGen from "./codegen" +import CodeGen, {Name} from "./codegen" export function reportError( cxt: KeywordContext, @@ -25,7 +25,7 @@ export function reportExtraError(cxt: KeywordContext, error: KeywordErrorDefinit } } -export function resetErrorsCount(gen: CodeGen, errsCount: string): void { +export function resetErrorsCount(gen: CodeGen, errsCount: Name): void { gen.code(`errors = ${errsCount};`) gen.if(`vErrors !== null`, () => gen.if(errsCount, `vErrors.length = ${errsCount}`, "vErrors = null") @@ -34,7 +34,7 @@ export function resetErrorsCount(gen: CodeGen, errsCount: string): void { export function extendErrors( {gen, keyword, schemaValue, data, it}: KeywordContext, - errsCount: string + errsCount: Name ): void { gen.for(`let i=${errsCount}; i { gen.code(`const err = vErrors[i];`) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 65a14966ee..24e9e4f3f8 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,4 @@ -import CodeGen from "./codegen" +import CodeGen, {Expression} from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" import {validateFunctionCode} from "./validate" @@ -23,13 +23,13 @@ module.exports = compile export type ResolvedRef = InlineResolvedRef | FuncResolvedRef export interface InlineResolvedRef { - code: string + code: Expression schema: object | boolean inline: true } export interface FuncResolvedRef { - code: string + code: Expression $async?: boolean inline?: false } diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 963f67ce36..f9ea4d8166 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -2,16 +2,17 @@ import {CompilationContext} from "../types" import {subschemaCode} from "./validate" import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" import {quotedString} from "../vocabularies/util" +import {Name, Expression} from "./codegen" export interface SubschemaContext { schema: object | boolean schemaPath: string errSchemaPath: string - topSchemaRef?: string + topSchemaRef?: Expression errorPath?: string - dataPathArr?: (string | number)[] + dataPathArr?: (Expression | number)[] dataLevel?: number - propertyName?: string + propertyName?: Name compositeRule?: true createErrors?: boolean allErrors?: boolean @@ -29,10 +30,10 @@ export interface SubschemaApplication { schema?: object | boolean schemaPath?: string errSchemaPath?: string - topSchemaRef?: string - data?: string - dataProp?: string | number - propertyName?: string + topSchemaRef?: Expression + data?: Name + dataProp?: Expression | number + propertyName?: Name expr?: Expr compositeRule?: true createErrors?: boolean @@ -42,7 +43,7 @@ export interface SubschemaApplication { export function applySubschema( it: CompilationContext, appl: SubschemaApplication, - valid: string + valid: Name ): void { const subschema = getSubschema(it, appl) extendSubschemaData(subschema, it, appl) @@ -103,9 +104,10 @@ function extendSubschemaData( // TODO possibly refactor getPath and getPathExpr to one function using Expr enum const nextLevel = dataLevel + 1 subschema.errorPath = - expr === Expr.Const - ? getPath(errorPath, dataProp, opts.jsonPointers) - : getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num) + dataProp instanceof Name + ? getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num) + : getPath(errorPath, dataProp, opts.jsonPointers) + subschema.dataPathArr = [ ...dataPathArr, expr === Expr.Const && typeof dataProp == "string" ? quotedString(dataProp) : dataProp, diff --git a/lib/compile/util.ts b/lib/compile/util.ts index ed109440c7..8e2897b233 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,3 +1,5 @@ +import {Name, Expression} from "./codegen" + export function checkDataType( dataType: string, data: string, @@ -59,10 +61,10 @@ export function toHash(arr: string[]): {[key: string]: true} { const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i const SINGLE_QUOTE = /'|\\/g -export function getProperty(key: string | number): string { +export function getProperty(key: Expression | number): string { return typeof key === "number" ? `[${key}]` - : IDENTIFIER.test(key) + : key instanceof Name || IDENTIFIER.test(key) ? `.${key}` : `['${escapeQuotes(key)}']` } @@ -104,7 +106,7 @@ export function toQuotedString(str: string): string { export function getPathExpr( currentPath: string, - expr: string, + expr: Expression, jsonPointers?: boolean, isNumber?: boolean ): string { @@ -131,7 +133,7 @@ export function getPath( const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ -export function getData($data: string, lvl: number, paths: (number | string)[]): string { +export function getData($data: string, lvl: number, paths: (Expression | number)[]): string { let jsonPointer, data if ($data === "") return "rootData" if ($data[0] === "/") { diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 573a5f6234..47b75a6150 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,5 +1,6 @@ import {KeywordErrorDefinition, CompilationContext, KeywordContext} from "../../types" import {reportError} from "../errors" +import {Name} from "../codegen" const boolError: KeywordErrorDefinition = { message: () => '"boolean schema is false"', @@ -17,7 +18,7 @@ export function topBoolOrEmptySchema(it: CompilationContext): void { } } -export function boolOrEmptySchema(it: CompilationContext, valid: string): void { +export function boolOrEmptySchema(it: CompilationContext, valid: Name): void { const {gen, schema} = it if (schema === false) { gen.code(`var ${valid} = false;`) // TODO var diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 35713f65df..c5a18f482f 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -3,6 +3,7 @@ import {toHash, checkDataType, checkDataTypes} from "../util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" import {getKeywordContext} from "../../keyword" +import {Name} from "../codegen" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { const st: undefined | string | string[] = schema.type @@ -135,7 +136,10 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { } } -function assignParentData({gen, dataLevel, dataPathArr}: CompilationContext, expr: string): void { +function assignParentData( + {gen, dataLevel, dataPathArr}: CompilationContext, + expr: string | Name +): void { // TODO replace dataLevel if (dataLevel) { const parentData = "data" + (dataLevel - 1 || "") diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 76d88460f1..bf1a351686 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -4,7 +4,7 @@ import {quotedString} from "../../vocabularies/util" import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" import {schemaKeywords} from "./iterate" -import CodeGen, {Code} from "../codegen" +import CodeGen, {_Code, Name} from "../codegen" const resolve = require("../resolve") @@ -25,7 +25,7 @@ export function validateFunctionCode(it: CompilationContext): void { }) } -function validateFunction(it: CompilationContext, body: Code) { +function validateFunction(it: CompilationContext, body: _Code) { const {gen} = it gen.func("validate", "data, dataPath, parentData, parentDataProperty, rootData", it.async, body) gen.code( @@ -42,7 +42,7 @@ function funcSourceUrl({schema, opts}: CompilationContext): string { } // schema compilation - this function is used recursively to generate code for sub-schemas -export function subschemaCode(it: CompilationContext, valid: string): void { +export function subschemaCode(it: CompilationContext, valid: Name): void { const {schema, gen, opts} = it checkKeywords(it) if (isBoolOrEmpty(it)) { @@ -69,7 +69,7 @@ function isBoolOrEmpty({schema, RULES}: CompilationContext): boolean { return typeof schema == "boolean" || !schemaHasRules(schema, RULES.all) } -function typeAndKeywords(it: CompilationContext, errsCount?: string): void { +function typeAndKeywords(it: CompilationContext, errsCount?: Name): void { const types = getSchemaTypes(it) const checkedTypes = coerceAndCheckDataType(it, types) schemaKeywords(it, types, !checkedTypes, errsCount) diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 0e13672716..d5321fa7db 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -4,12 +4,13 @@ import {checkDataType, schemaHasRulesExcept} from "../util" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" import {RuleGroup, Rule} from "../rules" +import {Name} from "../codegen" export function schemaKeywords( it: CompilationContext, types: string[], typeErrors: boolean, - errsCount?: string + errsCount?: Name ): void { const { gen, diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 51a619523b..a500063e18 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -10,6 +10,7 @@ import { import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" import {getParentData, callValidate} from "../../vocabularies/util" +import {Name, Expression} from "../codegen" export const keywordError: KeywordErrorDefinition = { message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, @@ -107,7 +108,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { } } - function validateAsyncRule(): string { + function validateAsyncRule(): Name { const ruleErrs = gen.name("ruleErrs") gen.code(`let ${ruleErrs} = null;`) gen.try( @@ -133,7 +134,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { gen.code(`${valid} = ${await}${callValidate(cxt, validateRef, passCxt, passSchema)};`) } - function reportKeywordErrors(ruleErrs: string, errsCount: string): void { + function reportKeywordErrors(ruleErrs: Expression, errsCount: Name): void { switch (def.valid) { case true: return @@ -152,7 +153,7 @@ function modifyData(cxt: KeywordContext) { gen.if(parent.data, `${data} = ${parent.data}[${parent.property}];`) } -function addKeywordErrors(cxt: KeywordContext, ruleErrs: string, errsCount: string): void { +function addKeywordErrors(cxt: KeywordContext, ruleErrs: Expression, errsCount: Name): void { const {gen} = cxt gen.if( `Array.isArray(${ruleErrs})`, diff --git a/lib/keyword.ts b/lib/keyword.ts index 073fbf716c..c40dc2d2e1 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -15,6 +15,7 @@ import {getData} from "./compile/util" import {schemaRefOrVal} from "./vocabularies/util" import {definitionSchema} from "./definition_schema" import keywordCode, {validateKeywordSchema, keywordError} from "./compile/validate/keyword" +import {Expression} from "./compile/codegen" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i @@ -174,7 +175,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } ;(def.code || keywordCode)(cxt, ruleType, this.definition) - function fail(condition?: string, failAction?: () => void, context?: KeywordContext): void { + function fail(condition?: Expression, failAction?: () => void, context?: KeywordContext): void { const action = failAction || _reportError if (condition) { gen.if(condition) @@ -191,7 +192,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } } - function ok(condition: string): void { + function ok(condition: Expression): void { if (!allErrors) gen.if(condition) } diff --git a/lib/types.ts b/lib/types.ts index 865f95c301..94495b1dd0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import Cache from "./cache" -import CodeGen from "./compile/codegen" +import CodeGen, {Name, Expression} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" @@ -106,13 +106,13 @@ export interface CompilationContext { allErrors: boolean dataLevel: number data: string - dataPathArr: (string | number)[] + dataPathArr: (Expression | number)[] schema: any isRoot: boolean schemaPath: string errorPath: string errSchemaPath: string - propertyName?: string + propertyName?: Name gen: CodeGen createErrors?: boolean // TODO maybe remove later baseId: string @@ -134,7 +134,7 @@ export interface CompilationContext { logger: Logger // TODO ? root: SchemaRoot // TODO ? rootId: string // TODO ? - topSchemaRef: string + topSchemaRef: Expression resolveRef: (...args: any[]) => ResolvedRef | void } @@ -213,21 +213,21 @@ export type Vocabulary = KeywordDefinition[] export interface KeywordContext { gen: CodeGen - fail: (condition?: string, failAction?: () => void, context?: KeywordContext) => void - ok: (condition: string) => void + fail: (condition?: Expression, failAction?: () => void, context?: KeywordContext) => void + ok: (condition: Expression) => void errorParams: (obj: KeywordContextParams, assing?: true) => void keyword: string data: string $data?: string | false schema: any parentSchema: any - schemaCode: string | number | boolean + schemaCode: Expression | number | boolean schemaValue: string | number | boolean params: KeywordContextParams it: CompilationContext } -export type KeywordContextParams = {[x: string]: string} +export type KeywordContextParams = {[x: string]: Expression} export type FormatMode = "fast" | "full" diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 344b1445ff..fb4c8cfb70 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -9,6 +9,7 @@ import { } from "../util" import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" +import {Name} from "../../compile/codegen" const error: KeywordErrorDefinition = { message: "should NOT have additional properties", @@ -41,13 +42,13 @@ const def: CodeKeywordDefinition = { if (!allErrors) gen.if(`${errsCount} === errors`) function checkAdditionalProperties(): void { - loopPropertiesCode(cxt, (key: string) => { + loopPropertiesCode(cxt, (key: Name) => { if (!props.length && !patProps.length) additionalPropertyCode(key) else gen.if(isAdditional(key), () => additionalPropertyCode(key)) }) } - function isAdditional(key: string): string { + function isAdditional(key: Name): string { let definedProp = "" if (props.length > 8) { // TODO maybe an option instead of hard-coded 8? @@ -63,11 +64,11 @@ const def: CodeKeywordDefinition = { return `!(${definedProp})` } - function deleteAdditional(key: string): void { + function deleteAdditional(key: Name): void { gen.code(`delete ${data}[${key}];`) } - function additionalPropertyCode(key: string): void { + function additionalPropertyCode(key: Name): void { if (removeAdditional === "all" || (removeAdditional && schema === false)) { deleteAdditional(key) return @@ -95,7 +96,7 @@ const def: CodeKeywordDefinition = { } } - function applyAdditionalSchema(key: string, valid: string, errors?: false): void { + function applyAdditionalSchema(key: Name, valid: Name, errors?: false): void { const subschema: SubschemaApplication = { keyword: "additionalProperties", dataProp: key, diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index f385d2bee2..3d8c3b1fdf 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema, quotedString, propertyInData} from "../util" -import {applySubschema, Expr} from "../../compile/subschema" +import {applySubschema} from "../../compile/subschema" import {escapeQuotes} from "../../compile/util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" @@ -37,7 +37,7 @@ const def: CodeKeywordDefinition = { for (const prop in propertyDeps) { const deps = propertyDeps[prop] if (deps.length === 0) continue - const hasProperty = propertyInData(data, prop, Expr.Const, it.opts.ownProperties) + const hasProperty = propertyInData(data, prop, it.opts.ownProperties) errorParams({ property: prop, depsCount: "" + deps.length, @@ -65,7 +65,7 @@ const def: CodeKeywordDefinition = { for (const prop in schemaDeps) { if (alwaysValidSchema(it, schemaDeps[prop])) continue gen.if( - propertyInData(data, prop, Expr.Const, it.opts.ownProperties), + propertyInData(data, prop, it.opts.ownProperties), () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), `var ${valid} = true;` // TODO refactor var ) diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 55744cbfcc..c236dcc5fe 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -2,6 +2,7 @@ import {CodeKeywordDefinition, KeywordErrorDefinition, CompilationContext} from import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" +import {Name} from "../../compile/codegen" const error: KeywordErrorDefinition = { message: ({params}) => `'should match "' + ${params.ifClause} + '" schema'`, @@ -60,7 +61,7 @@ const def: CodeKeywordDefinition = { ) } - function validateClause(keyword: string, ifClause?: string): () => void { + function validateClause(keyword: string, ifClause?: Name): () => void { return () => { applySubschema(it, {keyword}, schValid) gen.code(`${valid} = ${schValid};`) diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 256e77171f..03d4c462a1 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -20,7 +20,7 @@ const def: CodeKeywordDefinition = { if (hasDefault(prop)) { applyPropertySchema(prop) } else { - gen.if(propertyInData(data, prop, Expr.Const, it.opts.ownProperties)) + gen.if(propertyInData(data, prop, it.opts.ownProperties)) applyPropertySchema(prop) if (!it.allErrors) gen.else().code(`var ${valid} = true;`) gen.endIf() diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 553a48ed35..911b6054d6 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -3,6 +3,7 @@ import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" import {callValidate} from "../util" +import {Expression} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "$ref", @@ -55,7 +56,7 @@ const def: CodeKeywordDefinition = { ok(valid) } - function validateAsyncRef(v: string): void { + function validateAsyncRef(v: Expression): void { const valid = gen.name("valid") if (!it.async) throw new Error("async schema referenced by sync schema") if (!allErrors) gen.code(`let ${valid};`) @@ -73,11 +74,11 @@ const def: CodeKeywordDefinition = { ok(valid) } - function validateRef(v: string): void { + function validateRef(v: Expression): void { fail(`!${callValidate(cxt, v, passCxt)}`, () => addErrorsFrom(v)) } - function addErrorsFrom(source: string): void { + function addErrorsFrom(source: Expression): void { gen.if( "vErrors === null", `vErrors = ${source}.errors`, diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 809676600d..cc24704e67 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,7 +1,7 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {noPropertyInData, quotedString, orExpr} from "./util" import {reportError} from "../compile/errors" -import {Expr} from "../compile/subschema" +import {Name} from "../compile/codegen" export function checkReportMissingProp( cxt: KeywordContext, @@ -14,7 +14,7 @@ export function checkReportMissingProp( data, it: {opts}, } = cxt - gen.if(noPropertyInData(data, prop, Expr.Const, opts.ownProperties), () => { + gen.if(noPropertyInData(data, prop, opts.ownProperties), () => { errorParams({missingProperty: quotedString(prop)}, true) reportError(cxt, error) }) @@ -23,17 +23,17 @@ export function checkReportMissingProp( export function checkMissingProp( {data, it: {opts}}: KeywordContext, properties: string[], - missing: string + missing: Name ): string { return orExpr(properties, (prop) => { - const hasNoProp = noPropertyInData(data, prop, Expr.Const, opts.ownProperties) + const hasNoProp = noPropertyInData(data, prop, opts.ownProperties) return `(${hasNoProp} && (${missing} = ${quotedString(prop)}))` }) } export function reportMissingProp( cxt: KeywordContext, - missing: string, + missing: Name, error: KeywordErrorDefinition ): void { cxt.errorParams({missingProperty: missing}, true) diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 3f53ba2827..3380d8d62c 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,18 +1,18 @@ import {getProperty, schemaHasRules} from "../compile/util" import {CompilationContext, KeywordContext} from "../types" -import {Expr} from "../compile/subschema" +import {Name, Expression} from "../compile/codegen" export function appendSchema( - schemaCode: string | number | boolean, + schemaCode: Expression | number | boolean, $data?: string | false ): string { return $data ? `" + ${schemaCode}` : `${schemaCode}"` } export function concatSchema( - schemaCode: string | number | boolean, + schemaCode: Expression | number | boolean, $data?: string | false -): string | number | boolean { +): Expression | number | boolean { return $data ? `" + ${schemaCode} + "` : schemaCode } @@ -23,7 +23,7 @@ export function quotedString(str: string): string { } export function dataNotType( - schemaCode: string | number | boolean, + schemaCode: Expression | number | boolean, schemaType: string, $data?: string | false ): string { @@ -62,40 +62,38 @@ export function schemaProperties(it: CompilationContext, schema: object): string return allSchemaProperties(schema).filter((p) => !alwaysValidSchema(it, schema[p])) } -export function isOwnProperty(data: string, property: string, expr: Expr): string { - const prop = expr === Expr.Const ? quotedString(property) : property +export function isOwnProperty(data: string, property: Expression): Expression { + const prop = property instanceof Name ? property : quotedString(property) return `Object.prototype.hasOwnProperty.call(${data}, ${prop})` } export function propertyInData( data: string, - property: string, - expr: Expr, + property: Expression, ownProperties?: boolean ): string { - let cond = `${data}${accessProperty(property, expr)} !== undefined` - if (ownProperties) cond += ` && ${isOwnProperty(data, property, expr)}` + let cond = `${data}${accessProperty(property)} !== undefined` + if (ownProperties) cond += ` && ${isOwnProperty(data, property)}` return cond } export function noPropertyInData( data: string, - property: string, - expr: Expr, + property: Expression, ownProperties?: boolean ): string { - let cond = `${data}${accessProperty(property, expr)} === undefined` - if (ownProperties) cond += ` || !${isOwnProperty(data, property, expr)}` + let cond = `${data}${accessProperty(property)} === undefined` + if (ownProperties) cond += ` || !${isOwnProperty(data, property)}` return cond } -function accessProperty(property: string | number, expr: Expr): string { - return expr === Expr.Const ? getProperty(property) : `[${property}]` +function accessProperty(property: Expression | number): string { + return property instanceof Name ? `[${property}]` : getProperty(property) } export function loopPropertiesCode( {gen, data, it}: KeywordContext, - loopBody: (key: string) => void + loopBody: (key: Name) => void ): void { // TODO maybe always iterate own properties in v7? const key = gen.name("key") @@ -120,7 +118,7 @@ export function getParentData({dataLevel, dataPathArr}: CompilationContext): Par export function callValidate( {schemaCode, data, it}: KeywordContext, - func: string, + func: Expression, context?: string, passSchema?: boolean ): string { diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 5bbf076501..b2a9d13814 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import {quotedString, orExpr} from "../util" +import {Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "enum", @@ -28,13 +29,13 @@ const def: CodeKeywordDefinition = { } } - function loopEnum(valid: string): void { + function loopEnum(valid: Name): void { gen.for(`const v of ${schemaCode}`, () => gen.if(`equal(${data}, v)`, `${valid} = true; break;`) ) } - function equalCode(vSchema: string, i: number): string { + function equalCode(vSchema: Name, i: number): string { let sch: string = schema[i] if (sch && typeof sch === "object") { return `equal(${data}, ${vSchema}[${i}])` diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index ee1446bc9f..be2be8f883 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,8 +1,8 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {propertyInData, noPropertyInData} from "../util" -import {Expr} from "../../compile/subschema" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {reportError} from "../../compile/errors" +import {Name} from "../../compile/codegen" const error: KeywordErrorDefinition = { message: ({params: {missingProperty}}) => { @@ -79,16 +79,14 @@ const def: CodeKeywordDefinition = { const prop = gen.name("prop") errorParams({missingProperty: prop}) gen.for(`const ${prop} of ${schemaCode}`, () => - gen.if(noPropertyInData(data, prop, Expr.Str, it.opts.ownProperties), () => - reportError(cxt, error) - ) + gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => reportError(cxt, error)) ) } - function loopUntilMissing(missing: string, valid: string): void { + function loopUntilMissing(missing: Name, valid: Name): void { gen.for(`${missing} of ${schemaCode}`, () => gen - .code(`${valid} = ${propertyInData(data, missing, Expr.Str, it.opts.ownProperties)};`) + .code(`${valid} = ${propertyInData(data, missing, it.opts.ownProperties)};`) .if(`!${valid}`, "break") ) } From e98739d7d31e9535205f373981106e6c99abce67 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 26 Aug 2020 08:16:58 -0400 Subject: [PATCH 097/322] feat: safe code generation with template literals --- lib/compile/codegen.ts | 130 +++++++++++++----- lib/compile/errors.ts | 29 ++-- lib/compile/index.ts | 4 +- lib/compile/subschema.ts | 13 +- lib/compile/util.ts | 16 ++- lib/compile/validate/boolSchema.ts | 18 +-- lib/compile/validate/dataType.ts | 18 ++- lib/compile/validate/index.ts | 9 +- lib/compile/validate/iterate.ts | 2 +- lib/compile/validate/keyword.ts | 11 +- lib/keyword.ts | 29 ++-- lib/types.ts | 17 +-- .../applicator/additionalProperties.ts | 5 +- lib/vocabularies/applicator/anyOf.ts | 15 +- lib/vocabularies/applicator/contains.ts | 3 +- lib/vocabularies/applicator/dependencies.ts | 17 ++- lib/vocabularies/applicator/if.ts | 24 ++-- lib/vocabularies/applicator/items.ts | 14 +- lib/vocabularies/applicator/not.ts | 3 +- lib/vocabularies/applicator/oneOf.ts | 29 ++-- .../applicator/patternProperties.ts | 2 +- lib/vocabularies/applicator/properties.ts | 2 +- lib/vocabularies/applicator/propertyNames.ts | 5 +- lib/vocabularies/core/ref.ts | 7 +- lib/vocabularies/format/format.ts | 34 ++--- lib/vocabularies/missing.ts | 4 +- lib/vocabularies/util.ts | 23 ++-- lib/vocabularies/validation/enum.ts | 24 ++-- lib/vocabularies/validation/limit.ts | 8 +- lib/vocabularies/validation/multipleOf.ts | 10 +- lib/vocabularies/validation/pattern.ts | 2 +- lib/vocabularies/validation/required.ts | 10 +- lib/vocabularies/validation/uniqueItems.ts | 38 ++--- 33 files changed, 308 insertions(+), 267 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 6467640779..43fc76fb9d 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -1,36 +1,71 @@ -enum Block { +enum BlockKind { If, Else, For, Func, } -enum VarKind { - Const, - Let, - VFar, -} +export type Expression = string | Name | Code + +export type Block = string | Name | Code | (() => void) + +export class Code { + _str: string -export class Name { - #name: string constructor(name: string) { - this.#name = name + this._str = name } toString(): string { - return this.#name + return this._str + } + + isQuoted(): boolean { + const len = this._str.length + return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"' } } -export type Expression = string | Name +export class Name extends Code {} + +const varKinds = { + const: new Name("const"), + let: new Name("let"), + var: new Name("var"), +} + +type TemplateArg = Expression | number | boolean -export type _Code = string | (() => void) +export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { + return new Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) +} + +// TODO this is unsafe tagged template that should be removed later +export function $(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { + return new Code(strs.reduce((res, s, i) => res + args[i - 1] + s)) +} + +export function str(strings: TemplateStringsArray, ...args: TemplateArg[]): Code { + return new Code( + strings.map(quoteString).reduce((res, s, i) => { + let aStr = interpolate(args[i - 1]) + if (aStr instanceof Code && aStr.isQuoted()) aStr = aStr.toString() + return typeof aStr === "string" + ? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1) + : `${res} + ${aStr} + ${s}` + }) + ) +} + +function interpolate(x: TemplateArg): TemplateArg { + return x instanceof Code || typeof x == "number" || typeof x == "boolean" ? x : quoteString(x) +} export default class CodeGen { #names: {[key: string]: number} = {} // TODO make private. Possibly stack? _out = "" - #blocks: Block[] = [] + #blocks: BlockKind[] = [] #blockStarts: number[] = [] toString(): string { @@ -43,17 +78,39 @@ export default class CodeGen { return new Name(`${prefix}_${num}`) } - // _def(varKind: VarKind, prefix: string, rhs?: string): Variable {} + _def(varKind: Name, nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + if (rhs === undefined) this.code(`${varKind} ${name};`) + else this.code(`${varKind} ${name} = ${rhs};`) + return name + } + + const(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + return this._def(varKinds.const, nameOrPrefix, rhs) + } + + let(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + return this._def(varKinds.let, nameOrPrefix, rhs) + } + + var(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + return this._def(varKinds.var, nameOrPrefix, rhs) + } - code(c?: _Code): CodeGen { + assign(name: Name, rhs: Expression | number | boolean): CodeGen { + this.code(`${name} = ${rhs};`) + return this + } + + code(c?: Block): CodeGen { // TODO optionally strip whitespace if (typeof c == "function") c() else if (c) this._out += c + "\n" return this } - if(condition: Expression, thenBody?: _Code, elseBody?: _Code): CodeGen { - this.#blocks.push(Block.If) + if(condition: Expression, thenBody?: Block, elseBody?: Block): CodeGen { + this.#blocks.push(BlockKind.If) this.code(`if(${condition}){`) if (thenBody && elseBody) { this.code(thenBody).else().code(elseBody).endIf() @@ -66,14 +123,14 @@ export default class CodeGen { } elseIf(condition: Expression): CodeGen { - if (this._lastBlock !== Block.If) throw new Error('CodeGen: "else if" without "if"') + if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"') this.code(`}else if(${condition}){`) return this } else(): CodeGen { - if (this._lastBlock !== Block.If) throw new Error('CodeGen: "else" without "if"') - this._lastBlock = Block.Else + if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" without "if"') + this._lastBlock = BlockKind.Else this.code(`}else{`) return this } @@ -81,14 +138,14 @@ export default class CodeGen { endIf(): CodeGen { // TODO possibly remove empty branches here const b = this._lastBlock - if (b !== Block.If && b !== Block.Else) throw new Error('CodeGen: "endIf" without "if"') + if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" without "if"') this.#blocks.pop() this.code(`}`) return this } - for(iteration: string, forBody?: _Code): CodeGen { - this.#blocks.push(Block.For) + for(iteration: string | Code, forBody?: Block): CodeGen { + this.#blocks.push(BlockKind.For) this.code(`for(${iteration}){`) if (forBody) this.code(forBody).endFor() return this @@ -96,13 +153,18 @@ export default class CodeGen { endFor(): CodeGen { const b = this._lastBlock - if (b !== Block.For) throw new Error('CodeGen: "endFor" without "for"') + if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"') this.#blocks.pop() this.code(`}`) return this } - try(tryBody: _Code, catchCode?: (e: Name) => void, finallyCode?: _Code): CodeGen { + break(): CodeGen { + this.code("break;") + return this + } + + try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen { if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') this.code("try{").code(tryBody) if (catchCode) { @@ -115,7 +177,7 @@ export default class CodeGen { return this } - block(body?: _Code, expectedToClose?: number): CodeGen { + block(body?: Block, expectedToClose?: number): CodeGen { this.#blockStarts.push(this.#blocks.length) if (body) this.code(body).endBlock(expectedToClose) return this @@ -134,8 +196,8 @@ export default class CodeGen { return this } - func(name = "", args = "", async?: boolean, funcBody?: _Code): CodeGen { - this.#blocks.push(Block.Func) + func(name = "", args = "", async?: boolean, funcBody?: Block): CodeGen { + this.#blocks.push(BlockKind.Func) this.code(`${async ? "async " : ""}function ${name}(${args}){`) if (funcBody) this.code(funcBody).endFunc() return this @@ -143,17 +205,17 @@ export default class CodeGen { endFunc(): CodeGen { const b = this._lastBlock - if (b !== Block.Func) throw new Error('CodeGen: "endFunc" without "func"') + if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" without "func"') this.#blocks.pop() this.code(`}`) return this } - get _lastBlock(): Block { + get _lastBlock(): BlockKind { return this.#blocks[this._last()] } - set _lastBlock(b: Block) { + set _lastBlock(b: BlockKind) { this.#blocks[this._last()] = b } @@ -163,3 +225,9 @@ export default class CodeGen { return len - 1 } } + +export function quoteString(s: string): string { + return JSON.stringify(s) + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029") +} diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 4b252459a8..fbe9577982 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,6 +1,6 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {quotedString} from "../vocabularies/util" -import CodeGen, {Name} from "./codegen" +import CodeGen, {_, Name} from "./codegen" export function reportError( cxt: KeywordContext, @@ -26,9 +26,9 @@ export function reportExtraError(cxt: KeywordContext, error: KeywordErrorDefinit } export function resetErrorsCount(gen: CodeGen, errsCount: Name): void { - gen.code(`errors = ${errsCount};`) - gen.if(`vErrors !== null`, () => - gen.if(errsCount, `vErrors.length = ${errsCount}`, "vErrors = null") + gen.code(_`errors = ${errsCount};`) + gen.if(_`vErrors !== null`, () => + gen.if(errsCount, _`vErrors.length = ${errsCount}`, _`vErrors = null`) ) } @@ -36,25 +36,24 @@ export function extendErrors( {gen, keyword, schemaValue, data, it}: KeywordContext, errsCount: Name ): void { - gen.for(`let i=${errsCount}; i { - gen.code(`const err = vErrors[i];`) - gen.if("err.dataPath === undefined", `err.dataPath = (dataPath || '') + ${it.errorPath}`) - gen.code(`err.schemaPath = ${quotedString(it.errSchemaPath + "/" + keyword)};`) + const err = gen.name("err") + gen.for(_`let i=${errsCount}; i { + gen.const(err, _`vErrors[i]`) + gen.if(_`${err}.dataPath === undefined`, `${err}.dataPath = (dataPath || '') + ${it.errorPath}`) + gen.code(_`${err}.schemaPath = ${it.errSchemaPath + "/" + keyword};`) if (it.opts.verbose) { gen.code( - `err.schema = ${schemaValue}; - err.data = ${data};` + `${err}.schema = ${schemaValue}; + ${err}.data = ${data};` ) } }) } function addError(gen: CodeGen, errObj: string): void { - const err = gen.name("err") - gen - .code(`const ${err} = ${errObj};`) - .if("vErrors === null", `vErrors = [${err}]`, `vErrors.push(${err})`) - .code(`errors++;`) + const err = gen.const("err", errObj) + gen.if(_`vErrors === null`, _`vErrors = [${err}]`, _`vErrors.push(${err})`) + gen.code(_`errors++;`) } function returnErrors(gen: CodeGen, async: boolean, errs: string): void { diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 24e9e4f3f8..1012866b78 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,4 @@ -import CodeGen, {Expression} from "./codegen" +import CodeGen, {Expression, Code} from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" import {validateFunctionCode} from "./validate" @@ -112,7 +112,7 @@ function compile(schema, root, localRefs, baseId) { validateFunctionCode({ gen, allErrors: !!opts.allErrors, - topSchemaRef: "validate.schema", + topSchemaRef: new Code("validate.schema"), async: _schema.$async === true, schema: _schema, isRoot, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index f9ea4d8166..aed0a7f525 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,14 +1,14 @@ import {CompilationContext} from "../types" import {subschemaCode} from "./validate" import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" -import {quotedString} from "../vocabularies/util" -import {Name, Expression} from "./codegen" +import {quotedString, accessProperty} from "../vocabularies/util" +import {Code, Name, Expression} from "./codegen" export interface SubschemaContext { schema: object | boolean schemaPath: string errSchemaPath: string - topSchemaRef?: Expression + topSchemaRef?: Code errorPath?: string dataPathArr?: (Expression | number)[] dataLevel?: number @@ -30,7 +30,7 @@ export interface SubschemaApplication { schema?: object | boolean schemaPath?: string errSchemaPath?: string - topSchemaRef?: Expression + topSchemaRef?: Code data?: Name dataProp?: Expression | number propertyName?: Name @@ -104,7 +104,7 @@ function extendSubschemaData( // TODO possibly refactor getPath and getPathExpr to one function using Expr enum const nextLevel = dataLevel + 1 subschema.errorPath = - dataProp instanceof Name + dataProp instanceof Code ? getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num) : getPath(errorPath, dataProp, opts.jsonPointers) @@ -114,8 +114,7 @@ function extendSubschemaData( ] subschema.dataLevel = nextLevel - // TODO refactor - use accessProperty - const passDataProp = expr === Expr.Const ? getProperty(dataProp) : `[${dataProp}]` + const passDataProp = accessProperty(dataProp) gen.code(`var data${nextLevel} = data${dataLevel || ""}${passDataProp};`) } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 8e2897b233..6ecba930ef 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,8 +1,8 @@ -import {Name, Expression} from "./codegen" +import {_, Code, Name, Expression} from "./codegen" export function checkDataType( dataType: string, - data: string, + data: Name, strictNumbers?: boolean, negate?: boolean ): string { @@ -30,7 +30,7 @@ export function checkDataType( export function checkDataTypes( dataTypes: string[], - data: string, + data: Name, strictNumbers?: boolean, negate?: true ): string { @@ -62,10 +62,14 @@ export function toHash(arr: string[]): {[key: string]: true} { const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i const SINGLE_QUOTE = /'|\\/g export function getProperty(key: Expression | number): string { - return typeof key === "number" - ? `[${key}]` - : key instanceof Name || IDENTIFIER.test(key) + // return key instanceof Name || (typeof key == "string" && IDENTIFIER.test(key)) + // ? _`.${key}` + // : _`[${key}]` + + return key instanceof Name || (typeof key == "string" && IDENTIFIER.test(key)) ? `.${key}` + : key instanceof Code || typeof key === "number" + ? `[${key}]` : `['${escapeQuotes(key)}']` } diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 47b75a6150..72aab827d9 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,10 +1,9 @@ import {KeywordErrorDefinition, CompilationContext, KeywordContext} from "../../types" import {reportError} from "../errors" -import {Name} from "../codegen" +import {_, Name} from "../codegen" const boolError: KeywordErrorDefinition = { - message: () => '"boolean schema is false"', - params: () => "{}", + message: "boolean schema is false", } export function topBoolOrEmptySchema(it: CompilationContext): void { @@ -12,19 +11,19 @@ export function topBoolOrEmptySchema(it: CompilationContext): void { if (schema === false) { falseSchemaError(it, false) } else if (schema.$async === true) { - gen.code("return data;") + gen.code(_`return data;`) } else { - gen.code("validate.errors = null; return true;") + gen.code(_`validate.errors = null; return true;`) } } export function boolOrEmptySchema(it: CompilationContext, valid: Name): void { const {gen, schema} = it if (schema === false) { - gen.code(`var ${valid} = false;`) // TODO var + gen.var(valid, false) // TODO var falseSchemaError(it) } else { - gen.code(`var ${valid} = true;`) // TODO var + gen.var(valid, true) // TODO var } } @@ -33,11 +32,12 @@ function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { // TODO maybe some other interface should be used for non-keyword validation errors... const cxt: KeywordContext = { gen, - fail: exception, ok: exception, + pass: exception, + fail: exception, errorParams: exception, keyword: "false schema", - data: "data" + (dataLevel || ""), + data: new Name("data" + (dataLevel || "")), // TODO refactor dataLevel schema: false, schemaCode: false, schemaValue: false, diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index c5a18f482f..ab6af31f96 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -3,7 +3,7 @@ import {toHash, checkDataType, checkDataTypes} from "../util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" import {getKeywordContext} from "../../keyword" -import {Name} from "../codegen" +import {_, Name} from "../codegen" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { const st: undefined | string | string[] = schema.type @@ -37,7 +37,7 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]): !(coerceTo.length === 0 && types.length === 1 && schemaHasRulesForType(it, types[0])) if (checkTypes) { // TODO refactor `data${dataLevel || ""}` - const wrongType = checkDataTypes(types, `data${dataLevel || ""}`, strictNumbers, true) + const wrongType = checkDataTypes(types, new Name(`data${dataLevel || ""}`), strictNumbers, true) gen.if(wrongType, () => { if (coerceTo.length) coerceData(it, coerceTo) else reportTypeError(it) @@ -61,16 +61,14 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { opts: {coerceTypes, strictNumbers}, } = it // TODO move "data" to CompilationContext - const data = `data${dataLevel || ""}` - const dataType = gen.name("dataType") - const coerced = gen.name("coerced") - gen.code(`let ${coerced};`) - gen.code(`let ${dataType} = typeof ${data};`) + const data = new Name(`data${dataLevel || ""}`) + const dataType = gen.let("dataType", `typeof ${data}`) + const coerced = gen.let("coerced") if (coerceTypes === "array") { - gen.if(`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => + gen.if(_`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen - .code(`${data} = ${data}[0]; ${dataType} = typeof ${data};`) - .if(`${checkDataType(schema.type, data, strictNumbers)}`, `${coerced} = ${data}`) + .code(_`${data} = ${data}[0]; ${dataType} = typeof ${data};`) + .if(checkDataType(schema.type, data, strictNumbers), _`${coerced} = ${data}`) ) } gen.if(`${coerced} !== undefined`) diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index bf1a351686..c0ccdc9aa5 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -4,7 +4,7 @@ import {quotedString} from "../../vocabularies/util" import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" import {schemaKeywords} from "./iterate" -import CodeGen, {_Code, Name} from "../codegen" +import CodeGen, {_, Block, Name} from "../codegen" const resolve = require("../resolve") @@ -25,7 +25,7 @@ export function validateFunctionCode(it: CompilationContext): void { }) } -function validateFunction(it: CompilationContext, body: _Code) { +function validateFunction(it: CompilationContext, body: Block) { const {gen} = it gen.func("validate", "data, dataPath, parentData, parentDataProperty, rootData", it.async, body) gen.code( @@ -52,12 +52,11 @@ export function subschemaCode(it: CompilationContext, valid: Name): void { if (opts.$comment && schema.$comment) commentKeyword(it) updateContext(it) checkAsync(it) - const errsCount = gen.name("_errs") // TODO var - async validation fails if var replaced, possibly because of nodent - gen.code(`var ${errsCount} = errors;`) + const errsCount = gen.var("_errs", "errors") typeAndKeywords(it, errsCount) // TODO var - gen.code(`var ${valid} = ${errsCount} === errors;`) + gen.var(valid, _`${errsCount} === errors`) } function checkKeywords(it: CompilationContext) { diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index d5321fa7db..51d193899f 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -36,7 +36,7 @@ export function schemaKeywords( function groupKeywords(group: RuleGroup): void { if (group.type) { // TODO refactor `data${dataLevel || ""}` - const checkType = checkDataType(group.type, `data${dataLevel || ""}`, strictNumbers) + const checkType = checkDataType(group.type, new Name(`data${dataLevel || ""}`), strictNumbers) gen.if(checkType) iterateKeywords(it, group) if (types.length === 1 && types[0] === group.type && typeErrors) { diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index a500063e18..4b95433923 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -10,7 +10,7 @@ import { import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" import {getParentData, callValidate} from "../../vocabularies/util" -import {Name, Expression} from "../codegen" +import {_, Name, Expression} from "../codegen" export const keywordError: KeywordErrorDefinition = { message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, @@ -61,8 +61,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { const validate = "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate const validateRef = addCustomRule(it, keyword, validate) - const valid = gen.name("valid") - gen.code(`let ${valid};`) + const valid = gen.let("valid") if (def.errors === false) { validateNoErrorsRule() @@ -82,8 +81,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function validateRuleWithErrors(): void { gen.block() if ($data) check$data() - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) + const errsCount = gen.const("_errs", "errors") const ruleErrs = def.async ? validateAsyncRule() : validateSyncRule() if (def.modifying) modifyData(cxt) gen.endBlock() @@ -109,8 +107,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { } function validateAsyncRule(): Name { - const ruleErrs = gen.name("ruleErrs") - gen.code(`let ${ruleErrs} = null;`) + const ruleErrs = gen.let("ruleErrs", "null") gen.try( () => assignValid("await "), (e) => diff --git a/lib/keyword.ts b/lib/keyword.ts index c40dc2d2e1..e252a62be3 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -15,7 +15,7 @@ import {getData} from "./compile/util" import {schemaRefOrVal} from "./vocabularies/util" import {definitionSchema} from "./definition_schema" import keywordCode, {validateKeywordSchema, keywordError} from "./compile/validate/keyword" -import {Expression} from "./compile/codegen" +import {_, Name, Expression} from "./compile/codegen" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i @@ -151,12 +151,13 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v // TODO // if (!code) throw new Error('"code" and "error" must be defined') const $data = $defData && opts.$data && schema && schema.$data - const data = "data" + (dataLevel || "") + const data = new Name("data" + (dataLevel || "")) // TODO remove dataLevel const schemaValue = schemaRefOrVal(it, schema, keyword, $data) const cxt: KeywordContext = { gen, - fail, ok, + pass, + fail, errorParams, keyword, data, @@ -169,16 +170,16 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v it, } if ($data) { - gen.code(`const ${cxt.schemaCode} = ${getData($data, dataLevel, dataPathArr)};`) + gen.const(cxt.schemaCode, `${getData($data, dataLevel, dataPathArr)}`) } else if (schemaType && !validSchemaType(schema, schemaType)) { throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) } ;(def.code || keywordCode)(cxt, ruleType, this.definition) - function fail(condition?: Expression, failAction?: () => void, context?: KeywordContext): void { + function fail(cond?: Expression, failAction?: () => void, context?: KeywordContext): void { const action = failAction || _reportError - if (condition) { - gen.if(condition) + if (cond) { + gen.if(cond) action() if (allErrors) gen.endIf() else gen.else() @@ -192,8 +193,13 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v } } - function ok(condition: Expression): void { - if (!allErrors) gen.if(condition) + function pass(cond: Expression, failAction?: () => void, context?: KeywordContext): void { + cond = cond instanceof Name ? cond : `(${cond})` + fail(`!${cond}`, failAction, context) + } + + function ok(cond: Expression): void { + if (!allErrors) gen.if(cond) } function errorParams(obj: KeywordContextParams, assign?: true) { @@ -219,11 +225,12 @@ export function getKeywordContext(it: CompilationContext, keyword: string): Keyw const schemaCode = schemaRefOrVal(it, schema, keyword) return { gen, - fail: exception, ok: exception, + pass: exception, + fail: exception, errorParams: exception, keyword, - data: "data" + (dataLevel || ""), + data: new Name("data" + (dataLevel || "")), schema: schema[keyword], schemaCode, schemaValue: schemaCode, diff --git a/lib/types.ts b/lib/types.ts index 94495b1dd0..f9a759e9f6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import Cache from "./cache" -import CodeGen, {Name, Expression} from "./compile/codegen" +import CodeGen, {Name, Code, Expression} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" @@ -134,7 +134,7 @@ export interface CompilationContext { logger: Logger // TODO ? root: SchemaRoot // TODO ? rootId: string // TODO ? - topSchemaRef: Expression + topSchemaRef: Code resolveRef: (...args: any[]) => ResolvedRef | void } @@ -205,29 +205,30 @@ export type KeywordDefinition = | CodeKeywordDefinition export interface KeywordErrorDefinition { - message: string | ((cxt: KeywordContext) => string) - params?: (cxt: KeywordContext) => string + message: string | ((cxt: KeywordContext) => Expression) + params?: (cxt: KeywordContext) => Expression } export type Vocabulary = KeywordDefinition[] export interface KeywordContext { gen: CodeGen - fail: (condition?: Expression, failAction?: () => void, context?: KeywordContext) => void ok: (condition: Expression) => void + pass: (condition: Expression, failAction?: () => void, context?: KeywordContext) => void + fail: (condition?: Expression, failAction?: () => void, context?: KeywordContext) => void errorParams: (obj: KeywordContextParams, assing?: true) => void keyword: string - data: string + data: Name $data?: string | false schema: any parentSchema: any schemaCode: Expression | number | boolean - schemaValue: string | number | boolean + schemaValue: Expression | number | boolean params: KeywordContextParams it: CompilationContext } -export type KeywordContextParams = {[x: string]: Expression} +export type KeywordContextParams = {[x: string]: Expression | number} export type FormatMode = "fast" | "full" diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index fb4c8cfb70..b89dee2d19 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -36,8 +36,7 @@ const def: CodeKeywordDefinition = { const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) + const errsCount = gen.const("_errs", "errors") checkAdditionalProperties() if (!allErrors) gen.if(`${errsCount} === errors`) @@ -77,7 +76,7 @@ const def: CodeKeywordDefinition = { if (schema === false) { errorParams({additionalProperty: key}) reportError(cxt, error) - if (!allErrors) gen.code("break;") + if (!allErrors) gen.break() return } diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 9ecd3466ed..8979300da8 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -2,6 +2,7 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "anyOf", @@ -11,16 +12,12 @@ const def: CodeKeywordDefinition = { const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) if (alwaysValid) return - const valid = gen.name("valid") + const errsCount = gen.const("_errs", "errors") + const valid = gen.let("valid", false) const schValid = gen.name("_valid") - const errsCount = gen.name("_errs") - gen.code( - `let ${valid} = false; - const ${errsCount} = errors;` - ) gen.block(() => { - schema.forEach((_, i: number) => { + schema.forEach((_sch, i: number) => { applySubschema( it, { @@ -30,8 +27,8 @@ const def: CodeKeywordDefinition = { }, schValid ) - gen.code(`${valid} = ${valid} || ${schValid};`) - gen.if(`!${valid}`) + gen.code(_`${valid} = ${valid} || ${schValid};`) + gen.if(_`!${valid}`) }) }, schema.length) diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 0368713921..24be3f1518 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -10,8 +10,7 @@ const def: CodeKeywordDefinition = { before: "uniqueItems", code(cxt) { const {gen, fail, schema, data, it} = cxt - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) + const errsCount = gen.const("_errs", "errors") if (alwaysValidSchema(it, schema)) return fail(`${data}.length === 0`) diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 3d8c3b1fdf..29e1501009 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,8 +1,8 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" -import {alwaysValidSchema, quotedString, propertyInData} from "../util" +import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" -import {escapeQuotes} from "../../compile/util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" +import {_, str} from "../../compile/codegen" interface PropertyDependencies { [x: string]: string[] @@ -40,7 +40,7 @@ const def: CodeKeywordDefinition = { const hasProperty = propertyInData(data, prop, it.opts.ownProperties) errorParams({ property: prop, - depsCount: "" + deps.length, + depsCount: deps.length, deps: deps.join(", "), }) if (it.allErrors) { @@ -52,8 +52,7 @@ const def: CodeKeywordDefinition = { } else { // TODO refactor: maybe use one variable for all dependencies // or not use this variable at all? - const missing = gen.name("missing") - gen.code(`let ${missing};`) + const missing = gen.let("missing") gen.if(`${hasProperty} && (${checkMissingProp(cxt, deps, missing)})`) reportMissingProp(cxt, missing, def.error as KeywordErrorDefinition) gen.else() @@ -75,14 +74,14 @@ const def: CodeKeywordDefinition = { }, error: { message: ({params: {property, depsCount, deps}}) => { - const requiredProps = (depsCount === "1" ? "property " : "properties ") + escapeQuotes(deps) - return `'should have ${requiredProps} when property ${escapeQuotes(property)} is present'` + const property_ies = depsCount === 1 ? "property" : "properties" + return str`should have ${property_ies} ${deps} when property ${property} is present` }, params: ({params: {property, depsCount, deps, missingProperty}}) => - `{property: ${quotedString(property)}, + _`{property: ${property}, missingProperty: ${missingProperty}, depsCount: ${depsCount}, - deps: ${quotedString(deps)}}`, // TODO change to reference? + deps: ${deps}}`, // TODO change to reference? }, } diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index c236dcc5fe..ef79ab039f 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -2,11 +2,11 @@ import {CodeKeywordDefinition, KeywordErrorDefinition, CompilationContext} from import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" -import {Name} from "../../compile/codegen" +import {_, str, Name} from "../../compile/codegen" const error: KeywordErrorDefinition = { - message: ({params}) => `'should match "' + ${params.ifClause} + '" schema'`, - params: ({params}) => `{failingKeyword: ${params.ifClause}}`, + message: ({params}) => str`should match "${params.ifClause}" schema`, + params: ({params}) => _`{failingKeyword: ${params.ifClause}}`, } const def: CodeKeywordDefinition = { @@ -15,7 +15,7 @@ const def: CodeKeywordDefinition = { // TODO // implements: ["then", "else"], code(cxt) { - const {gen, fail, errorParams, it} = cxt + const {gen, pass, errorParams, it} = cxt const hasThen = hasSchema(it, "then") const hasElse = hasSchema(it, "else") if (!hasThen && !hasElse) { @@ -23,22 +23,16 @@ const def: CodeKeywordDefinition = { return } - const valid = gen.name("valid") + const valid = gen.let("valid", true) + const errsCount = gen.const("_errs", "errors") const schValid = gen.name("_valid") - const errsCount = gen.name("_errs") - - gen.code( - `const ${errsCount} = errors; - let ${valid} = true;` - ) validateIf() resetErrorsCount(gen, errsCount) if (hasThen && hasElse) { - const ifClause = gen.name("ifClause") + const ifClause = gen.let("ifClause") errorParams({ifClause}) - gen.code(`let ${ifClause};`) gen.if(schValid, validateClause("then", ifClause), validateClause("else", ifClause)) } else if (hasThen) { gen.if(schValid, validateClause("then")) @@ -46,7 +40,7 @@ const def: CodeKeywordDefinition = { gen.if(`!${schValid}`, validateClause("else")) } - fail(`!${valid}`, () => reportExtraError(cxt, error)) + pass(valid, () => reportExtraError(cxt, error)) function validateIf(): void { applySubschema( @@ -66,7 +60,7 @@ const def: CodeKeywordDefinition = { applySubschema(it, {keyword}, schValid) gen.code(`${valid} = ${schValid};`) if (ifClause) gen.code(`${ifClause} = "${keyword}";`) - else errorParams({ifClause: `"${keyword}"`}) + else errorParams({ifClause: keyword}) } } }, diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 3fc5a72b4e..467ccbe97c 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,6 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "items", @@ -12,8 +13,7 @@ const def: CodeKeywordDefinition = { code(cxt) { // TODO strict mode: fail or warning if "additionalItems" is present without "items" const {gen, ok, fail, schema, parentSchema, data, it} = cxt - const len = gen.name("len") - gen.code(`const ${len} = ${data}.length;`) + const len = gen.const("len", `${data}.length`) if (Array.isArray(schema)) { validateItemsArray(schema) @@ -26,7 +26,7 @@ const def: CodeKeywordDefinition = { if (addIts === false) validateDataLength(sch) validateDefinedItems() if (typeof addIts == "object" && !alwaysValidSchema(it, addIts)) { - gen.if(`${len} > ${sch.length}`, () => validateItems("additionalItems", sch.length)) + gen.if(_`${len} > ${sch.length}`, () => validateItems("additionalItems", sch.length)) } } @@ -42,7 +42,7 @@ const def: CodeKeywordDefinition = { const valid = gen.name("valid") schema.forEach((sch: any, i: number) => { if (alwaysValidSchema(it, sch)) return - gen.if(`${len} > ${i}`, () => + gen.if(_`${len} > ${i}`, () => applySubschema( it, { @@ -61,7 +61,7 @@ const def: CodeKeywordDefinition = { function validateItems(keyword: string, startFrom: number): void { const i = gen.name("i") const valid = gen.name("valid") - gen.for(`let ${i}=${startFrom}; ${i}<${len}; ${i}++`, () => { + gen.for(_`let ${i}=${startFrom}; ${i}<${len}; ${i}++`, () => { applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) if (!it.allErrors) gen.if(`!${valid}`, "break") }) @@ -69,8 +69,8 @@ const def: CodeKeywordDefinition = { } }, error: { - message: ({schema}) => `"should NOT have more than ${schema.length as number} items"`, - params: ({schema}) => `{limit: ${schema.length as number}}`, + message: ({schema}) => str`should NOT have more than ${schema.length as number} items`, + params: ({schema}) => _`{limit: ${schema.length as number}}`, }, } diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 8f63de3156..03b03c2ecf 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -11,8 +11,7 @@ const def: CodeKeywordDefinition = { if (alwaysValidSchema(it, schema)) return fail() const valid = gen.name("valid") - const errsCount = gen.name("_errs") - gen.code(`const ${errsCount} = errors;`) + const errsCount = gen.const("_errs", "errors") applySubschema( it, { diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index d42ce9709f..76cd4cd95d 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -2,30 +2,25 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", code(cxt) { const {gen, errorParams, schema, it} = cxt - const valid = gen.name("valid") + const valid = gen.let("valid", false) + const errsCount = gen.const("_errs", "errors") + const passing = gen.let("passing", "null") const schValid = gen.name("_valid") - const errsCount = gen.name("_errs") - const passing = gen.name("passing") errorParams({passing}) // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas - gen - .code( - `const ${errsCount} = errors; - let ${valid} = false; - let ${passing} = null;` - ) - .block(validateOneOf) + gen.block(validateOneOf) // TODO refactor failCompoundOrReset? // TODO refactor ifs - gen.if(`!${valid}`) + gen.if(_`!${valid}`) reportExtraError(cxt, def.error as KeywordErrorDefinition) gen.else() resetErrorsCount(gen, errsCount) @@ -34,7 +29,7 @@ const def: CodeKeywordDefinition = { function validateOneOf() { schema.forEach((sch, i: number) => { if (alwaysValidSchema(it, sch)) { - gen.code(`var ${schValid} = true;`) + gen.var(schValid, true) } else { applySubschema( it, @@ -49,21 +44,21 @@ const def: CodeKeywordDefinition = { if (i > 0) { gen - .if(`${schValid} && ${valid}`) + .if(_`${schValid} && ${valid}`) .code( - `${valid} = false; - ${passing} = [${passing}, ${i}];` + _`${valid} = false; + ${passing} = [${passing}, ${i}];` ) .else() } - gen.if(schValid, `${valid} = true; ${passing} = ${i};`) + gen.if(schValid, _`${valid} = true; ${passing} = ${i};`) }) } }, error: { message: "should match exactly one schema in oneOf", - params: ({params}) => `{passingSchemas: ${params.passing}}`, + params: ({params}) => _`{passingSchemas: ${params.passing}}`, }, } diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 06f8737d14..90005c2202 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -18,7 +18,7 @@ const def: CodeKeywordDefinition = { if (it.allErrors) { validateProperties(pat) } else { - gen.code(`var ${valid} = true`) // TODO var + gen.var(valid, true) // TODO var validateProperties(pat) gen.if(valid) } diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 03d4c462a1..e0a62f93c4 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -22,7 +22,7 @@ const def: CodeKeywordDefinition = { } else { gen.if(propertyInData(data, prop, it.opts.ownProperties)) applyPropertySchema(prop) - if (!it.allErrors) gen.else().code(`var ${valid} = true;`) + if (!it.allErrors) gen.else().var(valid, true) gen.endIf() } ok(valid) diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 6e0dc1afe0..ce1f695a94 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -2,6 +2,7 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema, loopPropertiesCode} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError} from "../../compile/errors" +import {str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "propertyNames", @@ -21,14 +22,14 @@ const def: CodeKeywordDefinition = { ) gen.if(`!${valid}`, () => { reportExtraError(cxt, def.error as KeywordErrorDefinition) - if (!it.allErrors) gen.code("break;") + if (!it.allErrors) gen.break() }) }) ok(valid) }, error: { - message: ({params}) => `"property name '" + ${params.propertyName} + "' is invalid"`, + message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? params: ({params}) => `{propertyName: ${params.propertyName}}`, }, } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 911b6054d6..0cf6325985 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -3,7 +3,7 @@ import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" import {callValidate} from "../util" -import {Expression} from "../../compile/codegen" +import {_, Expression} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "$ref", @@ -57,16 +57,15 @@ const def: CodeKeywordDefinition = { } function validateAsyncRef(v: Expression): void { - const valid = gen.name("valid") if (!it.async) throw new Error("async schema referenced by sync schema") - if (!allErrors) gen.code(`let ${valid};`) + const valid = gen.let("valid") gen.try( () => { gen.code(`await ${callValidate(cxt, v, passCxt)};`) if (!allErrors) gen.code(`${valid} = true;`) }, (e) => { - gen.if(`!(${e} instanceof ValidationError)`, `throw ${e}`) + gen.if(_`!(${e} instanceof ValidationError)`, `throw ${e}`) addErrorsFrom(e) if (!allErrors) gen.code(`${valid} = false;`) } diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 21b9f0a566..0bb177b50d 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,13 +1,14 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import {dataNotType} from "../util" import {getProperty} from "../../compile/util" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "format", type: ["number", "string"], schemaType: "string", $data: true, - code({gen, fail, data, $data, schema, schemaCode, it}, ruleType) { + code({gen, pass, fail, data, $data, schema, schemaCode, it}, ruleType) { const {formats, opts, logger, errSchemaPath} = it if (opts.format === false) return @@ -15,25 +16,16 @@ const def: CodeKeywordDefinition = { else validateFormat() function validate$DataFormat() { - const fmtDef = gen.name("fmtDef") - const fmtType = gen.name("fmtType") - const format = gen.name("format") - prepare() - fail(invalidCondition()) - - function prepare() { - gen.code(`const ${fmtDef} = formats[${schemaCode}]; let ${fmtType}, ${format};`) - gen.if( - `typeof ${fmtDef} == "object" && !(${fmtDef} instanceof RegExp)`, - `${fmtType} = ${fmtDef}.type || "string"; ${format} = ${fmtDef}.validate;`, - `${fmtType} = "string"; ${format} = ${fmtDef}` - ) - } - - function invalidCondition(): string { - const dnt = dataNotType(schemaCode, def.schemaType, $data) - return dnt + unknownFmt() + invalidFmt() - } + const fmtDef = gen.const("fmtDef", `formats[${schemaCode}]`) + const fmtType = gen.let("fmtType") + const format = gen.let("format") + gen.if( + _`typeof ${fmtDef} == "object" && !(${fmtDef} instanceof RegExp)`, + _`${fmtType} = ${fmtDef}.type || "string"; ${format} = ${fmtDef}.validate;`, + _`${fmtType} = "string"; ${format} = ${fmtDef}` + ) + const dnt = dataNotType(schemaCode, def.schemaType, $data) + fail(dnt + unknownFmt() + invalidFmt()) function unknownFmt(): string { if (opts.unknownFormats === "ignore") return "" @@ -59,7 +51,7 @@ const def: CodeKeywordDefinition = { return } const [fmtType, format, fmtRef] = getFormat(formatDef) - if (fmtType === ruleType) fail(`!(${validCondition()})`) + if (fmtType === ruleType) pass(validCondition()) function unknownFormat() { if (opts.unknownFormats === "ignore") return logger.warn(unknownMsg()) diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index cc24704e67..c0ef9cfffc 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,7 +1,7 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {noPropertyInData, quotedString, orExpr} from "./util" import {reportError} from "../compile/errors" -import {Name} from "../compile/codegen" +import {Name, _} from "../compile/codegen" export function checkReportMissingProp( cxt: KeywordContext, @@ -15,7 +15,7 @@ export function checkReportMissingProp( it: {opts}, } = cxt gen.if(noPropertyInData(data, prop, opts.ownProperties), () => { - errorParams({missingProperty: quotedString(prop)}, true) + errorParams({missingProperty: _`${prop}`}, true) reportError(cxt, error) }) } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 3380d8d62c..19cd4fb31f 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,6 +1,6 @@ import {getProperty, schemaHasRules} from "../compile/util" import {CompilationContext, KeywordContext} from "../types" -import {Name, Expression} from "../compile/codegen" +import {_, Code, Name, Expression} from "../compile/codegen" export function appendSchema( schemaCode: Expression | number | boolean, @@ -35,7 +35,10 @@ export function schemaRefOrVal( schema: unknown, keyword: string, $data?: string | false -): string | number | boolean { +): Expression | number | boolean { + // return $data || typeof schema === "object" + // ? `${topSchemaRef}${schemaPath + getProperty(keyword)}` + // : _`${schema}` if (!$data) { if (typeof schema == "number" || typeof schema == "boolean") return schema if (typeof schema == "string") return quotedString(schema) @@ -62,23 +65,19 @@ export function schemaProperties(it: CompilationContext, schema: object): string return allSchemaProperties(schema).filter((p) => !alwaysValidSchema(it, schema[p])) } -export function isOwnProperty(data: string, property: Expression): Expression { - const prop = property instanceof Name ? property : quotedString(property) +export function isOwnProperty(data: Name, property: Expression): Expression { + const prop = property instanceof Code ? property : quotedString(property) return `Object.prototype.hasOwnProperty.call(${data}, ${prop})` } -export function propertyInData( - data: string, - property: Expression, - ownProperties?: boolean -): string { +export function propertyInData(data: Name, property: Expression, ownProperties?: boolean): string { let cond = `${data}${accessProperty(property)} !== undefined` if (ownProperties) cond += ` && ${isOwnProperty(data, property)}` return cond } export function noPropertyInData( - data: string, + data: Name, property: Expression, ownProperties?: boolean ): string { @@ -87,8 +86,8 @@ export function noPropertyInData( return cond } -function accessProperty(property: Expression | number): string { - return property instanceof Name ? `[${property}]` : getProperty(property) +export function accessProperty(property: Expression | number): string { + return property instanceof Code ? `[${property}]` : getProperty(property) } export function loopPropertiesCode( diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index b2a9d13814..b65acc3cb2 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,37 +1,35 @@ import {CodeKeywordDefinition} from "../../types" import {quotedString, orExpr} from "../util" -import {Name} from "../../compile/codegen" +import {_, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "enum", schemaType: "array", $data: true, - code({gen, fail, data, $data, schema, schemaCode, it: {opts}}) { + code({gen, pass, data, $data, schema, schemaCode, it: {opts}}) { if ($data) { - const valid = gen.name("valid") - gen.code(`let ${valid};`) + const valid = gen.let("valid") gen.if(`${schemaCode} === undefined`, `${valid} = true;`, () => gen.code(`${valid} = false;`).if(`Array.isArray(${schemaCode})`, () => loopEnum(valid)) ) - fail(`!${valid}`) + pass(valid) } else { if (schema.length === 0) throw new Error("enum must have non-empty array") if (schema.length > (opts.loopEnum as number)) { - const valid = gen.name("valid") - gen.code(`let ${valid} = false;`) + const valid = gen.let("valid", "false") loopEnum(valid) - fail(`!${valid}`) + pass(valid) } else { - const vSchema = gen.name("schema") - gen.code(`const ${vSchema} = ${schemaCode};`) + const vSchema = gen.const("schema", schemaCode) const cond: string = orExpr(schema, (_, i) => equalCode(vSchema, i)) - fail(`!(${cond})`) + pass(cond) } } function loopEnum(valid: Name): void { - gen.for(`const v of ${schemaCode}`, () => - gen.if(`equal(${data}, v)`, `${valid} = true; break;`) + const v = gen.name("v") + gen.for(`const ${v} of ${schemaCode}`, () => + gen.if(_`equal(${data}, ${v})`, _`${valid} = true; break;`) ) } diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index f69a00e49f..5ab18e59df 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" -import {appendSchema, dataNotType} from "../util" +import {dataNotType} from "../util" +import {_, str} from "../../compile/codegen" const OPS: {[index: string]: {fail: string; ok: string}} = { maximum: {fail: ">", ok: "<="}, @@ -18,9 +19,8 @@ const def: CodeKeywordDefinition = { fail(dnt + data + OPS[keyword].fail + schemaCode + ` || isNaN(${data})`) }, error: { - message: ({keyword, $data, schemaCode}) => - `"should be ${OPS[keyword].ok} ${appendSchema(schemaCode, $data)}`, - params: ({keyword, schemaCode}) => `{comparison: "${OPS[keyword].ok}", limit: ${schemaCode}}`, + message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].ok} ${schemaCode}`, + params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].ok}, limit: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 338d446160..1011b0a700 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import {appendSchema, dataNotType} from "../util" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "multipleOf", @@ -8,13 +9,12 @@ const def: CodeKeywordDefinition = { $data: true, code({gen, fail, data, $data, schemaCode, it: {opts}}) { const dnt = dataNotType(schemaCode, def.schemaType, $data) - const res = gen.name("res") + const res = gen.let("res") const prec = opts.multipleOfPrecision const invalid = prec - ? `Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` - : `${res} !== parseInt(${res})` - gen.code(`let ${res};`) - fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) + ? _`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` + : _`${res} !== parseInt(${res})` + fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) // TODO pass }, error: { message: ({$data, schemaCode}) => `"should be multiple of ${appendSchema(schemaCode, $data)}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 5f05dec4d7..fdc583e888 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { code({fail, data, $data, schema, schemaCode, it: {usePattern}}) { const dnt = dataNotType(schemaCode, def.schemaType, $data) const regExp = $data ? `(new RegExp(${schemaCode}))` : usePattern(schema) - fail(dnt + `!${regExp}.test(${data})`) + fail(dnt + `!${regExp}.test(${data})`) // TODO pass? }, error: { message: ({$data, schemaCode}) => diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index be2be8f883..319461ca72 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -20,7 +20,7 @@ const def: CodeKeywordDefinition = { schemaType: ["array"], $data: true, code(cxt) { - const {gen, fail, errorParams, schema, schemaCode, data, $data, it} = cxt + const {gen, pass, errorParams, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return const loopRequired = $data || schema.length >= it.opts.loopRequired @@ -47,13 +47,11 @@ const def: CodeKeywordDefinition = { } function exitOnErrorMode(): void { - const missing = gen.name("missing") - gen.code(`let ${missing};`) + const missing = gen.let("missing") errorParams({missingProperty: missing}) if (loopRequired) { - const valid = gen.name("valid") - gen.code(`let ${valid} = true;`) + const valid = gen.let("valid", "true") // TODO refactor and enable/fix test in errors.spec.js line 301 // it can be simpler once blocks are globally supported - endIf can be removed, so there will be 2 open blocks @@ -67,7 +65,7 @@ const def: CodeKeywordDefinition = { loopUntilMissing(missing, valid) } - fail(`!${valid}`) + pass(valid) } else { gen.if(`${checkMissingProp(cxt, schema, missing)}`) reportMissingProp(cxt, missing, error) diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 1aa385694a..3c17a61cc5 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,18 +1,18 @@ import {CodeKeywordDefinition} from "../../types" import {checkDataType, checkDataTypes} from "../../compile/util" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "uniqueItems", type: "array", schemaType: "boolean", $data: true, - code({gen, fail, errorParams, data, $data, schema, parentSchema, schemaCode, it: {opts}}) { + code({gen, pass, errorParams, data, $data, schema, parentSchema, schemaCode, it: {opts}}) { if (opts.uniqueItems === false || !($data || schema)) return - const i = gen.name("i") - const j = gen.name("j") - const valid = gen.name("valid") + const i = gen.let("i") + const j = gen.let("j") + const valid = gen.let("valid") errorParams({i, j}) - gen.code(`let ${valid}, ${i}, ${j};`) const itemType = parentSchema.items?.type // TODO refactor to have two open blocks? same as in required @@ -24,7 +24,7 @@ const def: CodeKeywordDefinition = { validateUniqueItems() } - fail(`!${valid}`) + pass(valid) function validateUniqueItems() { gen.code( @@ -41,33 +41,33 @@ const def: CodeKeywordDefinition = { } function loopN(): void { + const item = gen.name("item") const wrongType = (Array.isArray(itemType) ? checkDataTypes : checkDataType)( itemType, - "item", + item, opts.strictNumbers, true ) - const indices = gen.name("indices") - gen.code(`const ${indices} = {};`) - gen.for(`;${i}--;`, () => { - gen.code(`let item = ${data}[${i}];`) + const indices = gen.const("indices", "{}") + gen.for(_`;${i}--;`, () => { + gen.let(item, `${data}[${i}];`) gen.if(wrongType, "continue") - if (Array.isArray(itemType)) gen.if('typeof item == "string"', 'item += "_"') + if (Array.isArray(itemType)) gen.if(_`typeof ${item} == "string"`, _`${item} += "_"`) gen .if( - `typeof ${indices}[item] == "number"`, - `${valid} = false; ${j} = ${indices}[item]; break;` + _`typeof ${indices}[${item}] == "number"`, + _`${valid} = false; ${j} = ${indices}[${item}]; break;` ) - .code(`${indices}[item] = ${i};`) + .code(_`${indices}[${item}] = ${i};`) }) } function loopN2(): void { gen - .code(`outer:`) - .for(`;${i}--;`, () => - gen.for(`${j} = ${i}; ${j}--;`, () => - gen.if(`equal(${data}[${i}], ${data}[${j}])`, `${valid} = false; break outer;`) + .code(_`outer:`) + .for(_`;${i}--;`, () => + gen.for(_`${j} = ${i}; ${j}--;`, () => + gen.if(`equal(${data}[${i}], ${data}[${j}])`, _`${valid} = false; break outer;`) ) ) } From 3eb4c02fae8139a0c22a7aded3d0d821ecf51d2b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 26 Aug 2020 17:19:24 -0400 Subject: [PATCH 098/322] refactor: generate error messages using tagged templates --- lib/vocabularies/applicator/propertyNames.ts | 4 ++-- lib/vocabularies/core/ref.ts | 6 ++---- lib/vocabularies/format/format.ts | 9 +++------ lib/vocabularies/util.ts | 16 +--------------- lib/vocabularies/validation/enum.ts | 2 +- lib/vocabularies/validation/limitItems.ts | 10 +++++----- lib/vocabularies/validation/limitLength.ts | 10 +++++----- lib/vocabularies/validation/limitProperties.ts | 10 +++++----- lib/vocabularies/validation/multipleOf.ts | 8 ++++---- lib/vocabularies/validation/pattern.ts | 10 ++++------ lib/vocabularies/validation/required.ts | 8 ++++---- lib/vocabularies/validation/uniqueItems.ts | 10 +++++----- 12 files changed, 41 insertions(+), 62 deletions(-) diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index ce1f695a94..d37dd9e413 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -2,7 +2,7 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema, loopPropertiesCode} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError} from "../../compile/errors" -import {str} from "../../compile/codegen" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "propertyNames", @@ -30,7 +30,7 @@ const def: CodeKeywordDefinition = { }, error: { message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? - params: ({params}) => `{propertyName: ${params.propertyName}}`, + params: ({params}) => _`{propertyName: ${params.propertyName}}`, }, } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 0cf6325985..40312853e3 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -86,11 +86,9 @@ const def: CodeKeywordDefinition = { gen.code(`errors = vErrors.length;`) } }, + // TODO incorrect error message error: { - message: ({$data, schemaCode}) => - $data - ? `'should match format "' + ${schemaCode} + '"'` - : `"should match format \\"${(schemaCode).slice(1, -1)}\\""`, + message: ({schemaCode}) => `'should match format "' + ${schemaCode} + '"'`, params: ({schemaCode}) => `{format: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 0bb177b50d..40a991dd18 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import {dataNotType} from "../util" import {getProperty} from "../../compile/util" -import {_} from "../../compile/codegen" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "format", @@ -83,11 +83,8 @@ const def: CodeKeywordDefinition = { } }, error: { - message: ({$data, schemaCode}) => - $data - ? `'should match format "' + ${schemaCode} + '"'` - : `"should match format \\"${(schemaCode).slice(1, -1)}\\""`, - params: ({schemaCode}) => `{format: ${schemaCode}}`, + message: ({schemaCode}) => str`should match format "${schemaCode}"`, + params: ({schemaCode}) => _`{format: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 19cd4fb31f..5344ee52d5 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -2,20 +2,6 @@ import {getProperty, schemaHasRules} from "../compile/util" import {CompilationContext, KeywordContext} from "../types" import {_, Code, Name, Expression} from "../compile/codegen" -export function appendSchema( - schemaCode: Expression | number | boolean, - $data?: string | false -): string { - return $data ? `" + ${schemaCode}` : `${schemaCode}"` -} - -export function concatSchema( - schemaCode: Expression | number | boolean, - $data?: string | false -): Expression | number | boolean { - return $data ? `" + ${schemaCode} + "` : schemaCode -} - export function quotedString(str: string): string { return JSON.stringify(str) .replace(/\u2028/g, "\\u2028") @@ -41,7 +27,7 @@ export function schemaRefOrVal( // : _`${schema}` if (!$data) { if (typeof schema == "number" || typeof schema == "boolean") return schema - if (typeof schema == "string") return quotedString(schema) + if (typeof schema == "string") return _`${schema}` } return `${topSchemaRef}${schemaPath + getProperty(keyword)}` } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index b65acc3cb2..02f6b79485 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -44,7 +44,7 @@ const def: CodeKeywordDefinition = { }, error: { message: "should be equal to one of the allowed values", - params: ({schemaCode}) => `{allowedValues: ${schemaCode}}`, + params: ({schemaCode}) => _`{allowedValues: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 42eefe6999..ef019b9056 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" -import {concatSchema, dataNotType} from "../util" +import {dataNotType} from "../util" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxItems", "minItems"], @@ -12,12 +13,11 @@ const def: CodeKeywordDefinition = { fail(dnt + `${data}.length` + op + schemaCode) }, error: { - message({keyword, $data, schemaCode}) { + message({keyword, schemaCode}) { const comp = keyword === "maxItems" ? "more" : "fewer" - const sch = concatSchema(schemaCode, $data) - return `"should NOT have ${comp} than ${sch} items"` + return str`should NOT have ${comp} than ${schemaCode} items` }, - params: ({schemaCode}) => `{limit: ${schemaCode}}`, + params: ({schemaCode}) => _`{limit: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 591f7f3c74..cb0f91b083 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" -import {concatSchema, dataNotType} from "../util" +import {dataNotType} from "../util" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxLength", "minLength"], @@ -13,12 +14,11 @@ const def: CodeKeywordDefinition = { fail(dnt + len + op + schemaCode) }, error: { - message({keyword, $data, schemaCode}) { + message({keyword, schemaCode}) { const comp = keyword === "maxLength" ? "more" : "fewer" - const sch = concatSchema(schemaCode, $data) - return `"should NOT have ${comp} than ${sch} items"` + return str`should NOT have ${comp} than ${schemaCode} items` }, - params: ({schemaCode}) => `{limit: ${schemaCode}}`, + params: ({schemaCode}) => _`{limit: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index a05d7f6faf..ccc6c17edc 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" -import {concatSchema, dataNotType} from "../util" +import {dataNotType} from "../util" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxProperties", "minProperties"], @@ -12,12 +13,11 @@ const def: CodeKeywordDefinition = { fail(dnt + `Object.keys(${data}).length` + op + schemaCode) }, error: { - message({keyword, $data, schemaCode}) { + message({keyword, schemaCode}) { const comp = keyword === "maxProperties" ? "more" : "fewer" - const sch = concatSchema(schemaCode, $data) - return `"should NOT have ${comp} than ${sch} items"` + return str`should NOT have ${comp} than ${schemaCode} items` }, - params: ({schemaCode}) => `{limit: ${schemaCode}}`, + params: ({schemaCode}) => _`{limit: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 1011b0a700..bcd1d8c0be 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" -import {appendSchema, dataNotType} from "../util" -import {_} from "../../compile/codegen" +import {dataNotType} from "../util" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "multipleOf", @@ -17,8 +17,8 @@ const def: CodeKeywordDefinition = { fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) // TODO pass }, error: { - message: ({$data, schemaCode}) => `"should be multiple of ${appendSchema(schemaCode, $data)}`, - params: ({schemaCode}) => `{multipleOf: ${schemaCode}}`, + message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, + params: ({schemaCode}) => _`{multipleOf: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index fdc583e888..56afdbdde3 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import {dataNotType} from "../util" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "pattern", @@ -8,15 +9,12 @@ const def: CodeKeywordDefinition = { $data: true, code({fail, data, $data, schema, schemaCode, it: {usePattern}}) { const dnt = dataNotType(schemaCode, def.schemaType, $data) - const regExp = $data ? `(new RegExp(${schemaCode}))` : usePattern(schema) + const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(schema) fail(dnt + `!${regExp}.test(${data})`) // TODO pass? }, error: { - message: ({$data, schemaCode}) => - $data - ? `'should match pattern "' + ${schemaCode} + '"'` - : `"should match pattern \\"${(schemaCode).slice(1, -1)}\\""`, - params: ({schemaCode}) => `{pattern: ${schemaCode}}`, + message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, + params: ({schemaCode}) => _`{pattern: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 319461ca72..b938e1650d 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -2,16 +2,16 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {reportError} from "../../compile/errors" -import {Name} from "../../compile/codegen" +import {_, str, Name} from "../../compile/codegen" const error: KeywordErrorDefinition = { message: ({params: {missingProperty}}) => { return missingProperty - ? `"should have required property '" + ${missingProperty} + "'"` // TODO missingProperty can be string constant - : `'"required" keyword value must be array'` + ? str`should have required property '${missingProperty}'` + : str`"required" keyword value must be array` }, params: ({params: {missingProperty}}) => - missingProperty ? `{missingProperty: ${missingProperty}}` : "{}", + missingProperty ? _`{missingProperty: ${missingProperty}}` : _`{}`, } const def: CodeKeywordDefinition = { diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 3c17a61cc5..6aff26afe0 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import {checkDataType, checkDataTypes} from "../../compile/util" -import {_} from "../../compile/codegen" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "uniqueItems", @@ -74,12 +74,12 @@ const def: CodeKeywordDefinition = { }, error: { message: ({$data, params: {i, j}}) => { - const msg = `"should NOT have duplicate items (items ## " + ${j} + " and " + ${i} + " are identical)"` - return $data ? `(${i} === undefined ? "uniqueItems must be boolean ($data)" : ${msg})` : msg + const msg = str`should NOT have duplicate items (items ## ${j} and ${i} are identical)` + return $data ? _`(${i} === undefined ? "uniqueItems must be boolean ($data)" : ${msg})` : msg }, params: ({$data, params: {i, j}}) => { - const obj = `{i: ${i}, j: ${j}}` - return $data ? `(${i} === undefined ? {} : ${obj})` : obj + const obj = _`{i: ${i}, j: ${j}}` + return $data ? _`(${i} === undefined ? {} : ${obj})` : obj }, }, } From 60c4b3cbebf60cf650ad8edc390633d1c6bb57da Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 27 Aug 2020 04:45:23 -0400 Subject: [PATCH 099/322] refactor: pass data, parentData and parentDataProperty Names via CompilationContext --- lib/ajv.d._ts | 437 ----------------------------- lib/compile/codegen.ts | 13 +- lib/compile/index.ts | 11 +- lib/compile/names.ts | 9 + lib/compile/subschema.ts | 68 +++-- lib/compile/util.ts | 30 +- lib/compile/validate/boolSchema.ts | 4 +- lib/compile/validate/dataType.ts | 39 +-- lib/compile/validate/defaults.ts | 26 +- lib/compile/validate/index.ts | 15 +- lib/compile/validate/iterate.ts | 19 +- lib/compile/validate/keyword.ts | 5 +- lib/keyword.ts | 9 +- lib/types.ts | 7 +- lib/vocabularies/util.ts | 13 +- 15 files changed, 139 insertions(+), 566 deletions(-) delete mode 100644 lib/ajv.d._ts create mode 100644 lib/compile/names.ts diff --git a/lib/ajv.d._ts b/lib/ajv.d._ts deleted file mode 100644 index cb24845d69..0000000000 --- a/lib/ajv.d._ts +++ /dev/null @@ -1,437 +0,0 @@ -declare namespace AjvErrors { - class ValidationError extends Error { - constructor(errors: Array) - - message: string - errors: Array - ajv: true - validation: true - } - - class MissingRefError extends Error { - constructor(baseId: string, ref: string, message?: string) - static message: (baseId: string, ref: string) => string - - message: string - missingRef: string - missingSchema: string - } -} - -declare const ajv: { - (options?: ajv.Options): ajv.Ajv - new (options?: ajv.Options): ajv.Ajv - ValidationError: typeof AjvErrors.ValidationError - MissingRefError: typeof AjvErrors.MissingRefError - $dataMetaSchema: object -} - -declare namespace ajv { - type ValidationError = AjvErrors.ValidationError - - type MissingRefError = AjvErrors.MissingRefError - - interface Ajv { - /** - * Validate data using schema - * Schema will be compiled and cached (using serialized JSON as key, [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize by default). - * @param {string|object|Boolean} schemaKeyRef key, ref or schema object - * @param {Any} data to be validated - * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). - */ - validate( - schemaKeyRef: object | string | boolean, - data: any - ): boolean | PromiseLike - /** - * Create validating function for passed schema. - * @param {object|Boolean} schema schema object - * @return {Function} validating function - */ - compile(schema: object | boolean): ValidateFunction - /** - * Creates validating function for passed schema with asynchronous loading of missing schemas. - * `loadSchema` option should be a function that accepts schema uri and node-style callback. - * @this Ajv - * @param {object|Boolean} schema schema object - * @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped - * @param {Function} callback optional node-style callback, it is always called with 2 parameters: error (or null) and validating function. - * @return {PromiseLike} validating function - */ - compileAsync( - schema: object | boolean, - meta?: Boolean, - callback?: (err: Error, validate: ValidateFunction) => any - ): PromiseLike - /** - * Adds schema to the instance. - * @param {object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. - * @param {string} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. - * @return {Ajv} this for method chaining - */ - addSchema(schema: Array | object, key?: string): Ajv - /** - * Add schema that will be used to validate other schemas - * options in META_IGNORE_OPTIONS are alway set to false - * @param {object} schema schema object - * @param {string} key optional schema key - * @return {Ajv} this for method chaining - */ - addMetaSchema(schema: object, key?: string): Ajv - /** - * Validate schema - * @param {object|Boolean} schema schema to validate - * @return {Boolean} true if schema is valid - */ - validateSchema(schema: object | boolean): boolean - /** - * Get compiled schema from the instance by `key` or `ref`. - * @param {string} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id). - * @return {Function} schema validating function (with property `schema`). Returns undefined if keyRef can't be resolved to an existing schema. - */ - getSchema(keyRef: string): ValidateFunction | undefined - /** - * Remove cached schema(s). - * If no parameter is passed all schemas but meta-schemas are removed. - * If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. - * Even if schema is referenced by other schemas it still can be removed as other schemas have local references. - * @param {string|object|RegExp|Boolean} schemaKeyRef key, ref, pattern to match key/ref or schema object - * @return {Ajv} this for method chaining - */ - removeSchema(schemaKeyRef?: object | string | RegExp | boolean): Ajv - /** - * Add custom format - * @param {string} name format name - * @param {string|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) - * @return {Ajv} this for method chaining - */ - addFormat(name: string, format: FormatValidator | FormatDefinition): Ajv - /** - * Define custom keyword - * @this Ajv - * @param {string} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords. - * @param {object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. - * @return {Ajv} this for method chaining - */ - addKeyword(keyword: string, definition: KeywordDefinition): Ajv - /** - * Get keyword definition - * @this Ajv - * @param {string} keyword pre-defined or custom keyword. - * @return {object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. - */ - getKeyword(keyword: string): object | boolean - /** - * Remove keyword - * @this Ajv - * @param {string} keyword pre-defined or custom keyword. - * @return {Ajv} this for method chaining - */ - removeKeyword(keyword: string): Ajv - /** - * Validate keyword - * @this Ajv - * @param {object} definition keyword definition object - * @param {boolean} throwError true to throw exception if definition is invalid - * @return {boolean} validation result - */ - validateKeyword(definition: KeywordDefinition, throwError: boolean): boolean - /** - * Convert array of error message objects to string - * @param {Array} errors optional array of validation errors, if not passed errors from the instance are used. - * @param {object} options optional options with properties `separator` and `dataVar`. - * @return {string} human readable string with all errors descriptions - */ - errorsText( - errors?: Array | null, - options?: ErrorsTextOptions - ): string - errors?: Array | null - } - - interface CustomLogger { - log(...args: any[]): any - warn(...args: any[]): any - error(...args: any[]): any - } - - interface ValidateFunction { - ( - data: any, - dataPath?: string, - parentData?: object | Array, - parentDataProperty?: string | number, - rootData?: object | Array - ): boolean | PromiseLike - schema?: object | boolean - errors?: null | Array - refs?: object - refVal?: Array - root?: ValidateFunction | object - $async?: true - source?: object - } - - interface Options { - $data?: boolean - allErrors?: boolean - verbose?: boolean - jsonPointers?: boolean - uniqueItems?: boolean - unicode?: boolean - format?: false | string - formats?: object - keywords?: object - unknownFormats?: true | string[] | "ignore" - schemas?: Array | object - missingRefs?: true | "ignore" | "fail" - extendRefs?: true | "ignore" | "fail" - loadSchema?: ( - uri: string, - cb?: (err: Error, schema: object) => void - ) => PromiseLike - removeAdditional?: boolean | "all" | "failing" - useDefaults?: boolean | "empty" | "shared" - coerceTypes?: boolean | "array" - strictDefaults?: boolean | "log" - strictKeywords?: boolean | "log" - strictNumbers?: boolean - async?: boolean | string - transpile?: string | ((code: string) => string) - meta?: boolean | object - validateSchema?: boolean | "log" - addUsedSchema?: boolean - inlineRefs?: boolean | number - passContext?: boolean - loopRequired?: number - ownProperties?: boolean - multipleOfPrecision?: boolean | number - errorDataPath?: string - messages?: boolean - sourceCode?: boolean - processCode?: (code: string, schema: object) => string - cache?: object - logger?: CustomLogger | false - nullable?: boolean - serialize?: ((schema: object | boolean) => any) | false - } - - type FormatValidator = - | string - | RegExp - | ((data: string) => boolean | PromiseLike) - type NumberFormatValidator = (data: number) => boolean | PromiseLike - - interface NumberFormatDefinition { - type: "number" - validate: NumberFormatValidator - compare?: (data1: number, data2: number) => number - async?: boolean - } - - interface StringFormatDefinition { - type?: "string" - validate: FormatValidator - compare?: (data1: string, data2: string) => number - async?: boolean - } - - type FormatDefinition = NumberFormatDefinition | StringFormatDefinition - - interface KeywordDefinition { - type?: string | Array - async?: boolean - $data?: boolean - errors?: boolean | string - metaSchema?: object - // schema: false makes validate not to expect schema (ValidateFunction) - schema?: boolean - statements?: boolean - dependencies?: Array - modifying?: boolean - valid?: boolean - // one and only one of the following properties should be present - validate?: SchemaValidateFunction | ValidateFunction - compile?: ( - schema: any, - parentSchema: object, - it: CompilationContext - ) => ValidateFunction - macro?: ( - schema: any, - parentSchema: object, - it: CompilationContext - ) => object | boolean - inline?: ( - it: CompilationContext, - keyword: string, - schema: any, - parentSchema: object - ) => string - } - - interface CompilationContext { - level: number - dataLevel: number - dataPathArr: string[] - schema: any - schemaPath: string - baseId: string - async: boolean - opts: Options - formats: { - [index: string]: FormatDefinition | undefined - } - keywords: { - [index: string]: KeywordDefinition | undefined - } - compositeRule: boolean - validate: (schema: object) => boolean - util: { - copy(obj: any, target?: any): any - toHash(source: string[]): {[index: string]: true | undefined} - equal(obj: any, target: any): boolean - getProperty(str: string): string - schemaHasRules(schema: object, rules: any): string - escapeQuotes(str: string): string - toQuotedString(str: string): string - getData(jsonPointer: string, dataLevel: number, paths: string[]): string - escapeJsonPointer(str: string): string - unescapeJsonPointer(str: string): string - escapeFragment(str: string): string - unescapeFragment(str: string): string - } - self: Ajv - } - - interface SchemaValidateFunction { - ( - schema: any, - data: any, - parentSchema?: object, - dataPath?: string, - parentData?: object | Array, - parentDataProperty?: string | number, - rootData?: object | Array - ): boolean | PromiseLike - errors?: Array - } - - interface ErrorsTextOptions { - separator?: string - dataVar?: string - } - - interface ErrorObject { - keyword: string - dataPath: string - schemaPath: string - params: ErrorParameters - // Added to validation errors of propertyNames keyword schema - propertyName?: string - // Excluded if messages set to false. - message?: string - // These are added with the `verbose` option. - schema?: any - parentSchema?: object - data?: any - } - - type ErrorParameters = - | RefParams - | LimitParams - | AdditionalPropertiesParams - | DependenciesParams - | FormatParams - | ComparisonParams - | MultipleOfParams - | PatternParams - | RequiredParams - | TypeParams - | UniqueItemsParams - | CustomParams - | PatternRequiredParams - | PropertyNamesParams - | IfParams - | SwitchParams - | NoParams - | EnumParams - - interface RefParams { - ref: string - } - - interface LimitParams { - limit: number - } - - interface AdditionalPropertiesParams { - additionalProperty: string - } - - interface DependenciesParams { - property: string - missingProperty: string - depsCount: number - deps: string - } - - interface FormatParams { - format: string - } - - interface ComparisonParams { - comparison: string - limit: number | string - exclusive: boolean - } - - interface MultipleOfParams { - multipleOf: number - } - - interface PatternParams { - pattern: string - } - - interface RequiredParams { - missingProperty: string - } - - interface TypeParams { - type: string - } - - interface UniqueItemsParams { - i: number - j: number - } - - interface CustomParams { - keyword: string - } - - interface PatternRequiredParams { - missingPattern: string - } - - interface PropertyNamesParams { - propertyName: string - } - - interface IfParams { - failingKeyword: string - } - - interface SwitchParams { - caseIndex: number - } - - interface NoParams {} - - interface EnumParams { - allowedValues: Array - } -} - -export = ajv diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 43fc76fb9d..e0acf7ce67 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -26,6 +26,8 @@ export class Code { } } +export const nil = new Code("") + export class Name extends Code {} const varKinds = { @@ -97,7 +99,7 @@ export default class CodeGen { return this._def(varKinds.var, nameOrPrefix, rhs) } - assign(name: Name, rhs: Expression | number | boolean): CodeGen { + assign(name: Expression, rhs: Expression | number | boolean): CodeGen { this.code(`${name} = ${rhs};`) return this } @@ -164,6 +166,13 @@ export default class CodeGen { return this } + return(value: Block): CodeGen { + this._out += "return " + this.code(value) + this._out += ";" + return this + } + try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen { if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') this.code("try{").code(tryBody) @@ -196,7 +205,7 @@ export default class CodeGen { return this } - func(name = "", args = "", async?: boolean, funcBody?: Block): CodeGen { + func(name: Name, args: Code = nil, async?: boolean, funcBody?: Block): CodeGen { this.#blocks.push(BlockKind.Func) this.code(`${async ? "async " : ""}function ${name}(${args}){`) if (funcBody) this.code(funcBody).endFunc() diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 1012866b78..88f6f83cad 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,4 @@ -import CodeGen, {Expression, Code} from "./codegen" +import CodeGen, {nil, Expression, Code, Name} from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" import {validateFunctionCode} from "./validate" @@ -109,9 +109,16 @@ function compile(schema, root, localRefs, baseId) { const gen = new CodeGen() + const data = new Name("data") + validateFunctionCode({ gen, allErrors: !!opts.allErrors, + data, + parentData: new Name("parentData"), + parentDataProperty: new Name("parentDataProperty"), + dataNames: [data], + dataPathArr: [nil], topSchemaRef: new Code("validate.schema"), async: _schema.$async === true, schema: _schema, @@ -122,9 +129,7 @@ function compile(schema, root, localRefs, baseId) { schemaPath: "", errSchemaPath: "#", errorPath: '""', - dataPathArr: [""], dataLevel: 0, - data: "data", // TODO get unique name when passed from applicator keywords RULES, // TODO refactor - it is available on the instance resolveRef, // TODO remove to imports usePattern, // TODO remove to imports diff --git a/lib/compile/names.ts b/lib/compile/names.ts new file mode 100644 index 0000000000..588447a0df --- /dev/null +++ b/lib/compile/names.ts @@ -0,0 +1,9 @@ +import {Name} from "./codegen" + +const names = { + validate: new Name("validate"), + dataPath: new Name("dataPath"), + rootData: new Name("rootData"), +} + +export default names diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index aed0a7f525..301ab2ed39 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -5,13 +5,18 @@ import {quotedString, accessProperty} from "../vocabularies/util" import {Code, Name, Expression} from "./codegen" export interface SubschemaContext { + // TODO use Optional? schema: object | boolean schemaPath: string errSchemaPath: string topSchemaRef?: Code errorPath?: string - dataPathArr?: (Expression | number)[] dataLevel?: number + data?: Name + parentData?: Name + parentDataProperty?: Expression | number + dataNames?: Name[] + dataPathArr?: (Expression | number)[] propertyName?: Name compositeRule?: true createErrors?: boolean @@ -24,20 +29,22 @@ export enum Expr { Str, } -export interface SubschemaApplication { - keyword?: string - schemaProp?: string | number - schema?: object | boolean - schemaPath?: string - errSchemaPath?: string - topSchemaRef?: Code - data?: Name - dataProp?: Expression | number - propertyName?: Name - expr?: Expr - compositeRule?: true - createErrors?: boolean - allErrors?: boolean +export type SubschemaApplication = Partial + +interface SubschemaApplicationParams { + keyword: string + schemaProp: string | number + schema: object | boolean + schemaPath: string + errSchemaPath: string + topSchemaRef: Code + data: Name | Code + dataProp: Expression | number + propertyName: Name + expr: Expr + compositeRule: true + createErrors: boolean + allErrors: boolean } export function applySubschema( @@ -99,31 +106,36 @@ function extendSubschemaData( throw new Error('both "data" and "dataProp" passed, only one allowed') } + const {gen} = it + if (dataProp !== undefined) { - const {gen, errorPath, dataPathArr, dataLevel, opts} = it + const {errorPath, dataPathArr, opts} = it + const nextData = gen.var("data", `${it.data}${accessProperty(dataProp)}`) // TODO var, tagged + dataContextProps(nextData) // TODO possibly refactor getPath and getPathExpr to one function using Expr enum - const nextLevel = dataLevel + 1 subschema.errorPath = dataProp instanceof Code ? getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num) : getPath(errorPath, dataProp, opts.jsonPointers) - subschema.dataPathArr = [ - ...dataPathArr, - expr === Expr.Const && typeof dataProp == "string" ? quotedString(dataProp) : dataProp, - ] - subschema.dataLevel = nextLevel + subschema.parentDataProperty = + expr === Expr.Const && typeof dataProp == "string" ? quotedString(dataProp) : dataProp - const passDataProp = accessProperty(dataProp) - gen.code(`var data${nextLevel} = data${dataLevel || ""}${passDataProp};`) + subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty] } if (data !== undefined) { - const {gen, dataLevel} = it - const nextLevel = dataLevel + 1 - subschema.dataLevel = nextLevel + const nextData = data instanceof Name ? data : gen.var("data", data) // TODO var, replaceable if used once? + dataContextProps(nextData) if (propertyName !== undefined) subschema.propertyName = propertyName - gen.code(`var data${nextLevel} = ${data};`) + // TODO something is wrong here with not changing parentDataProperty and not appending dataPathArr + } + + function dataContextProps(_nextData: Name) { + subschema.data = _nextData + subschema.dataLevel = it.dataLevel + 1 + subschema.parentData = it.data + subschema.dataNames = [...it.dataNames, _nextData] } } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 6ecba930ef..1bcd24b945 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,4 +1,6 @@ import {_, Code, Name, Expression} from "./codegen" +import {CompilationContext} from "../types" +import names from "./names" export function checkDataType( dataType: string, @@ -137,33 +139,33 @@ export function getPath( const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ -export function getData($data: string, lvl: number, paths: (Expression | number)[]): string { +export function getData( + $data: string, + {dataLevel, dataNames, dataPathArr}: CompilationContext +): Expression | number { let jsonPointer, data - if ($data === "") return "rootData" + if ($data === "") return names.rootData if ($data[0] === "/") { if (!JSON_POINTER.test($data)) { throw new Error("Invalid JSON-pointer: " + $data) } jsonPointer = $data - data = "rootData" + data = names.rootData } else { const matches = RELATIVE_JSON_POINTER.exec($data) if (!matches) throw new Error("Invalid JSON-pointer: " + $data) const up: number = +matches[1] jsonPointer = matches[2] if (jsonPointer === "#") { - if (up >= lvl) { - throw new Error( - "Cannot access property/index " + up + " levels up, current level is " + lvl - ) + if (up >= dataLevel) { + throw new Error(errorMsg("property/index", up)) } - return "" + paths[lvl - up] + return dataPathArr[dataLevel - up] } - if (up > lvl) { - throw new Error("Cannot access data " + up + " levels up, current level is " + lvl) - } - data = "data" + (lvl - up || "") + if (up > dataLevel) throw new Error(errorMsg("data", up)) + + data = dataNames[dataLevel - up] if (!jsonPointer) return data } @@ -176,6 +178,10 @@ export function getData($data: string, lvl: number, paths: (Expression | number) } } return expr + + function errorMsg(pointerType: string, up: number): string { + return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}` + } } export function joinPaths(a: string, b: string): string { diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 72aab827d9..650ceed003 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -28,7 +28,7 @@ export function boolOrEmptySchema(it: CompilationContext, valid: Name): void { } function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { - const {gen, dataLevel} = it + const {gen, data} = it // TODO maybe some other interface should be used for non-keyword validation errors... const cxt: KeywordContext = { gen, @@ -37,7 +37,7 @@ function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { fail: exception, errorParams: exception, keyword: "false schema", - data: new Name("data" + (dataLevel || "")), // TODO refactor dataLevel + data, schema: false, schemaCode: false, schemaValue: false, diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index ab6af31f96..eab973cd14 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -26,18 +26,13 @@ export function getSchemaTypes({schema, opts}: CompilationContext): string[] { } export function coerceAndCheckDataType(it: CompilationContext, types: string[]): boolean { - const { - gen, - dataLevel, - opts: {coerceTypes, strictNumbers}, - } = it - const coerceTo = coerceToTypes(types, coerceTypes) + const {gen, data, opts} = it + const coerceTo = coerceToTypes(types, opts.coerceTypes) const checkTypes = types.length > 0 && !(coerceTo.length === 0 && types.length === 1 && schemaHasRulesForType(it, types[0])) if (checkTypes) { - // TODO refactor `data${dataLevel || ""}` - const wrongType = checkDataTypes(types, new Name(`data${dataLevel || ""}`), strictNumbers, true) + const wrongType = checkDataTypes(types, data, opts.strictNumbers, true) gen.if(wrongType, () => { if (coerceTo.length) coerceData(it, coerceTo) else reportTypeError(it) @@ -54,26 +49,19 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string } export function coerceData(it: CompilationContext, coerceTo: string[]): void { - const { - gen, - schema, - dataLevel, - opts: {coerceTypes, strictNumbers}, - } = it - // TODO move "data" to CompilationContext - const data = new Name(`data${dataLevel || ""}`) + const {gen, schema, data, opts} = it const dataType = gen.let("dataType", `typeof ${data}`) const coerced = gen.let("coerced") - if (coerceTypes === "array") { + if (opts.coerceTypes === "array") { gen.if(_`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen .code(_`${data} = ${data}[0]; ${dataType} = typeof ${data};`) - .if(checkDataType(schema.type, data, strictNumbers), _`${coerced} = ${data}`) + .if(checkDataType(schema.type, data, opts.strictNumbers), _`${coerced} = ${data}`) ) } gen.if(`${coerced} !== undefined`) for (const t of coerceTo) { - if (t in COERCIBLE || (t === "array" && coerceTypes === "array")) { + if (t in COERCIBLE || (t === "array" && opts.coerceTypes === "array")) { coerceSpecificType(t) } } @@ -135,16 +123,13 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { } function assignParentData( - {gen, dataLevel, dataPathArr}: CompilationContext, + {gen, parentData, parentDataProperty}: CompilationContext, expr: string | Name ): void { - // TODO replace dataLevel - if (dataLevel) { - const parentData = "data" + (dataLevel - 1 || "") - gen.code(`${parentData}[${dataPathArr[dataLevel]}] = ${expr};`) - } else { - gen.if("parentData !== undefined", `parentData[parentDataProperty] = ${expr};`) - } + // TODO use gen.property + gen.if(`${parentData} !== undefined`, () => + gen.assign(`${parentData}[${parentDataProperty}]`, expr) + ) } const typeError: KeywordErrorDefinition = { diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 04c1130d70..2b1307350e 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -13,33 +13,25 @@ export function assignDefaults(it: CompilationContext, ty?: string): void { } function assignDefault( - { - gen, - compositeRule, - dataLevel, - useDefault, - opts: {strictDefaults, useDefaults}, - logger, - }: CompilationContext, + {gen, compositeRule, data, useDefault, opts, logger}: CompilationContext, prop: string | number, defaultValue: any ): void { if (defaultValue === undefined) return - // TODO refactor `data${dataLevel || ""}` - const data = "data" + (dataLevel || "") + getProperty(prop) + const childData = `${data}${getProperty(prop)}` // TODO tagged if (compositeRule) { - if (strictDefaults) { - const msg = `default is ignored for: ${data}` - if (strictDefaults === "log") logger.warn(msg) + if (opts.strictDefaults) { + const msg = `default is ignored for: ${childData}` + if (opts.strictDefaults === "log") logger.warn(msg) else throw new Error(msg) } return } const condition = - `${data} === undefined` + - (useDefaults === "empty" ? ` || ${data} === null || ${data} === ""` : "") + `${childData} === undefined` + + (opts.useDefaults === "empty" ? ` || ${childData} === null || ${childData} === ""` : "") // TODO remove option `useDefaults === "shared"` - const defaultExpr = useDefaults === "shared" ? useDefault : JSON.stringify - gen.if(condition, `${data} = ${defaultExpr(defaultValue)}`) + const defaultExpr = opts.useDefaults === "shared" ? useDefault : JSON.stringify + gen.if(condition, `${childData} = ${defaultExpr(defaultValue)}`) } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index c0ccdc9aa5..c4d0c65038 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -5,6 +5,7 @@ import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" import {schemaKeywords} from "./iterate" import CodeGen, {_, Block, Name} from "../codegen" +import names from "../names" const resolve = require("../resolve") @@ -26,13 +27,15 @@ export function validateFunctionCode(it: CompilationContext): void { } function validateFunction(it: CompilationContext, body: Block) { - const {gen} = it - gen.func("validate", "data, dataPath, parentData, parentDataProperty, rootData", it.async, body) - gen.code( - `"use strict"; - ${funcSourceUrl(it)}` + const {gen, data, parentData, parentDataProperty} = it + gen.return(() => + gen.func( + names.validate, + _`${data}, ${names.dataPath}, ${parentData}, ${parentDataProperty}, ${names.rootData}`, + it.async, + () => gen.code(`"use strict"; ${funcSourceUrl(it)}`).code(body) + ) ) - gen.code(`return validate;`) } function funcSourceUrl({schema, opts}: CompilationContext): string { diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 51d193899f..47fb673063 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -12,15 +12,11 @@ export function schemaKeywords( typeErrors: boolean, errsCount?: Name ): void { - const { - gen, - schema, - dataLevel, - RULES, - allErrors, - opts: {extendRefs, strictNumbers}, - } = it - if (schema.$ref && !(extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref"))) { + const {gen, schema, data, RULES, allErrors, opts} = it + if ( + schema.$ref && + !(opts.extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref")) + ) { // TODO remove Rule type cast gen.block(() => (RULES.all.$ref as Rule).code(it, "$ref")) return @@ -35,8 +31,7 @@ export function schemaKeywords( function groupKeywords(group: RuleGroup): void { if (group.type) { - // TODO refactor `data${dataLevel || ""}` - const checkType = checkDataType(group.type, new Name(`data${dataLevel || ""}`), strictNumbers) + const checkType = checkDataType(group.type, data, opts.strictNumbers) gen.if(checkType) iterateKeywords(it, group) if (types.length === 1 && types[0] === group.type && typeErrors) { @@ -47,7 +42,7 @@ export function schemaKeywords( } else { iterateKeywords(it, group) } - // TODO make it "ok" call + // TODO make it "ok" call? if (!allErrors) gen.if(`errors === ${errsCount || 0}`) } } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 4b95433923..9d6a951e46 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -9,7 +9,7 @@ import { } from "../../types" import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" -import {getParentData, callValidate} from "../../vocabularies/util" +import {callValidate} from "../../vocabularies/util" import {_, Name, Expression} from "../codegen" export const keywordError: KeywordErrorDefinition = { @@ -146,8 +146,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function modifyData(cxt: KeywordContext) { const {gen, data, it} = cxt - const parent = getParentData(it) - gen.if(parent.data, `${data} = ${parent.data}[${parent.property}];`) + gen.if(it.parentData, () => gen.assign(data, `${it.parentData}[${it.parentDataProperty}];`)) } function addKeywordErrors(cxt: KeywordContext, ruleErrs: Expression, errsCount: Name): void { diff --git a/lib/keyword.ts b/lib/keyword.ts index e252a62be3..261e9dc68d 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -147,11 +147,10 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v const def: CodeKeywordDefinition = this.definition const {schemaType, $data: $defData} = def validateKeywordSchema(it, keyword, def) - const {gen, opts, dataLevel, dataPathArr, allErrors} = it + const {gen, data, opts, allErrors} = it // TODO // if (!code) throw new Error('"code" and "error" must be defined') const $data = $defData && opts.$data && schema && schema.$data - const data = new Name("data" + (dataLevel || "")) // TODO remove dataLevel const schemaValue = schemaRefOrVal(it, schema, keyword, $data) const cxt: KeywordContext = { gen, @@ -170,7 +169,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v it, } if ($data) { - gen.const(cxt.schemaCode, `${getData($data, dataLevel, dataPathArr)}`) + gen.const(cxt.schemaCode, `${getData($data, it)}`) } else if (schemaType && !validSchemaType(schema, schemaType)) { throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) } @@ -221,7 +220,7 @@ function validSchemaType(schema: any, schemaType: string | string[]): boolean { } export function getKeywordContext(it: CompilationContext, keyword: string): KeywordContext { - const {gen, schema, dataLevel} = it + const {gen, data, schema} = it const schemaCode = schemaRefOrVal(it, schema, keyword) return { gen, @@ -230,7 +229,7 @@ export function getKeywordContext(it: CompilationContext, keyword: string): Keyw fail: exception, errorParams: exception, keyword, - data: new Name("data" + (dataLevel || "")), + data, schema: schema[keyword], schemaCode, schemaValue: schemaCode, diff --git a/lib/types.ts b/lib/types.ts index f9a759e9f6..cbed239cf9 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -103,9 +103,13 @@ export interface ErrorObject { export type KeywordCompilationResult = object | boolean | SchemaValidateFunction | ValidateFunction export interface CompilationContext { + gen: CodeGen allErrors: boolean dataLevel: number - data: string + data: Name + parentData: Name + parentDataProperty: Expression | number + dataNames: Name[] dataPathArr: (Expression | number)[] schema: any isRoot: boolean @@ -113,7 +117,6 @@ export interface CompilationContext { errorPath: string errSchemaPath: string propertyName?: Name - gen: CodeGen createErrors?: boolean // TODO maybe remove later baseId: string async: boolean diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 5344ee52d5..010bc3a5c5 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -91,14 +91,8 @@ export function orExpr(items: string[], mapCondition: (s: string, i: number) => } export interface ParentData { - data: string - property: string -} - -export function getParentData({dataLevel, dataPathArr}: CompilationContext): ParentData { - return dataLevel - ? {data: `data${dataLevel - 1 || ""}`, property: `${dataPathArr[dataLevel]}`} - : {data: "parentData", property: "parentDataProperty"} + data: Name + property: Expression | number } export function callValidate( @@ -111,7 +105,6 @@ export function callValidate( ? `${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` : data const dataPath = `(dataPath || '')${it.errorPath === '""' ? "" : ` + ${it.errorPath}`}` // TODO joinPaths? - const parent = getParentData(it) - const args = `${dataAndSchema}, ${dataPath}, ${parent.data}, ${parent.property}, rootData` + const args = `${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, rootData` return context ? `${func}.call(${context}, ${args})` : `${func}(${args})` } From 47d172c1c6249fb4ad9d9ce48b781c7af82b747d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 27 Aug 2020 09:20:33 -0400 Subject: [PATCH 100/322] refactor: shared function scoped names --- lib/compile/codegen.ts | 17 ++++-- lib/compile/errors.ts | 42 +++++++------- lib/compile/index.ts | 15 +++-- lib/compile/names.ts | 15 ++++- lib/compile/util.ts | 6 +- lib/compile/validate/boolSchema.ts | 6 +- lib/compile/validate/dataType.ts | 49 ++++++++-------- lib/compile/validate/index.ts | 57 +++++++++---------- lib/compile/validate/iterate.ts | 3 +- lib/compile/validate/keyword.ts | 14 ++--- .../applicator/additionalProperties.ts | 5 +- lib/vocabularies/applicator/anyOf.ts | 3 +- lib/vocabularies/applicator/contains.ts | 3 +- lib/vocabularies/applicator/if.ts | 3 +- lib/vocabularies/applicator/not.ts | 3 +- lib/vocabularies/applicator/oneOf.ts | 3 +- lib/vocabularies/core/ref.ts | 12 ++-- lib/vocabularies/util.ts | 5 +- 18 files changed, 145 insertions(+), 116 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index e0acf7ce67..3fcb5ddb74 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -7,6 +7,8 @@ enum BlockKind { export type Expression = string | Name | Code +export type Value = string | Name | Code | number | boolean | null + export type Block = string | Name | Code | (() => void) export class Code { @@ -63,6 +65,8 @@ function interpolate(x: TemplateArg): TemplateArg { return x instanceof Code || typeof x == "number" || typeof x == "boolean" ? x : quoteString(x) } +const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i + export default class CodeGen { #names: {[key: string]: number} = {} // TODO make private. Possibly stack? @@ -99,15 +103,20 @@ export default class CodeGen { return this._def(varKinds.var, nameOrPrefix, rhs) } - assign(name: Expression, rhs: Expression | number | boolean): CodeGen { + assign(name: Expression, rhs: Value): CodeGen { this.code(`${name} = ${rhs};`) return this } - code(c?: Block): CodeGen { + prop(name: Code, key: Expression | number): Code { + name = name instanceof Name ? name : _`(${name})` + return typeof key == "string" && IDENTIFIER.test(key) ? _`${name}.${key}` : _`${name}[${key}]` + } + + code(c?: Block | Value): CodeGen { // TODO optionally strip whitespace if (typeof c == "function") c() - else if (c) this._out += c + "\n" + else if (c !== undefined) this._out += c + "\n" // TODO fails without line breaks return this } @@ -166,7 +175,7 @@ export default class CodeGen { return this } - return(value: Block): CodeGen { + return(value: Block | Value): CodeGen { this._out += "return " this.code(value) this._out += ";" diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index fbe9577982..24230b695e 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,6 +1,7 @@ import {KeywordContext, KeywordErrorDefinition} from "../types" import {quotedString} from "../vocabularies/util" -import CodeGen, {_, Name} from "./codegen" +import CodeGen, {_, Name, Expression} from "./codegen" +import N from "./names" export function reportError( cxt: KeywordContext, @@ -21,14 +22,14 @@ export function reportExtraError(cxt: KeywordContext, error: KeywordErrorDefinit const errObj = errorObjectCode(cxt, error) addError(gen, errObj) if (!(compositeRule || allErrors)) { - returnErrors(gen, async, "vErrors") + returnErrors(gen, async, N.vErrors) } } export function resetErrorsCount(gen: CodeGen, errsCount: Name): void { - gen.code(_`errors = ${errsCount};`) - gen.if(_`vErrors !== null`, () => - gen.if(errsCount, _`vErrors.length = ${errsCount}`, _`vErrors = null`) + gen.assign(N.errors, errsCount) + gen.if(_`${N.vErrors} !== null`, () => + gen.if(errsCount, _`${N.vErrors}.length = ${errsCount}`, _`${N.vErrors} = null`) ) } @@ -37,13 +38,16 @@ export function extendErrors( errsCount: Name ): void { const err = gen.name("err") - gen.for(_`let i=${errsCount}; i { - gen.const(err, _`vErrors[i]`) - gen.if(_`${err}.dataPath === undefined`, `${err}.dataPath = (dataPath || '') + ${it.errorPath}`) + gen.for(_`let i=${errsCount}; i<${N.errors}; i++`, () => { + gen.const(err, _`${N.vErrors}[i]`) + gen.if( + _`${err}.dataPath === undefined`, + `${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` + ) gen.code(_`${err}.schemaPath = ${it.errSchemaPath + "/" + keyword};`) if (it.opts.verbose) { gen.code( - `${err}.schema = ${schemaValue}; + _`${err}.schema = ${schemaValue}; ${err}.data = ${data};` ) } @@ -52,17 +56,17 @@ export function extendErrors( function addError(gen: CodeGen, errObj: string): void { const err = gen.const("err", errObj) - gen.if(_`vErrors === null`, _`vErrors = [${err}]`, _`vErrors.push(${err})`) - gen.code(_`errors++;`) + gen.if(_`${N.vErrors} === null`, _`${N.vErrors} = [${err}]`, _`${N.vErrors}.push(${err})`) + gen.code(_`${N.errors}++;`) } -function returnErrors(gen: CodeGen, async: boolean, errs: string): void { - gen.code( - async - ? `throw new ValidationError(${errs});` - : `validate.errors = ${errs}; - return false;` - ) +function returnErrors(gen: CodeGen, async: boolean, errs: Expression): void { + if (async) { + gen.code(`throw new ValidationError(${errs})`) + } else { + gen.assign(_`${N.validate}.errors`, errs) + gen.return("false") + } } function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): string { @@ -78,7 +82,7 @@ function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): st // TODO trim whitespace let out = `{ keyword: "${keyword}", - dataPath: (dataPath || "") + ${errorPath}, + dataPath: (${N.dataPath} || "") + ${errorPath}, schemaPath: ${quotedString(errSchemaPath + "/" + keyword)}, params: ${params ? params(cxt) : "{}"},` if (propertyName) out += `propertyName: ${propertyName},` diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 88f6f83cad..3d72793896 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,9 +1,10 @@ -import CodeGen, {nil, Expression, Code, Name} from "./codegen" +import CodeGen, {_, nil, Expression} from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" import {validateFunctionCode} from "./validate" import {validateKeywordSchema} from "./validate/keyword" import {ErrorObject, KeywordCompilationResult} from "../types" +import N from "./names" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -109,17 +110,15 @@ function compile(schema, root, localRefs, baseId) { const gen = new CodeGen() - const data = new Name("data") - validateFunctionCode({ gen, allErrors: !!opts.allErrors, - data, - parentData: new Name("parentData"), - parentDataProperty: new Name("parentDataProperty"), - dataNames: [data], + data: N.data, + parentData: N.parentData, + parentDataProperty: N.parentDataProperty, + dataNames: [N.data], dataPathArr: [nil], - topSchemaRef: new Code("validate.schema"), + topSchemaRef: _`${N.validate}.schema`, async: _schema.$async === true, schema: _schema, isRoot, diff --git a/lib/compile/names.ts b/lib/compile/names.ts index 588447a0df..77efaed0d4 100644 --- a/lib/compile/names.ts +++ b/lib/compile/names.ts @@ -1,9 +1,20 @@ import {Name} from "./codegen" const names = { - validate: new Name("validate"), + validate: new Name("validate"), // validation function name + // validation function arguments + data: new Name("data"), // data passed to validation function + // args passed from referencing schema dataPath: new Name("dataPath"), - rootData: new Name("rootData"), + parentData: new Name("parentData"), + parentDataProperty: new Name("parentDataProperty"), + rootData: new Name("rootData"), // data passed to the first/top validation function + // function scoped variables + vErrors: new Name("vErrors"), // null or array of validation errors + errors: new Name("errors"), // counter of validation errors + this: new Name("this"), + // "globals" + self: new Name("self"), } export default names diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 1bcd24b945..6e419e62f1 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,6 +1,6 @@ import {_, Code, Name, Expression} from "./codegen" import {CompilationContext} from "../types" -import names from "./names" +import N from "./names" export function checkDataType( dataType: string, @@ -144,13 +144,13 @@ export function getData( {dataLevel, dataNames, dataPathArr}: CompilationContext ): Expression | number { let jsonPointer, data - if ($data === "") return names.rootData + if ($data === "") return N.rootData if ($data[0] === "/") { if (!JSON_POINTER.test($data)) { throw new Error("Invalid JSON-pointer: " + $data) } jsonPointer = $data - data = names.rootData + data = N.rootData } else { const matches = RELATIVE_JSON_POINTER.exec($data) if (!matches) throw new Error("Invalid JSON-pointer: " + $data) diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 650ceed003..534a0db989 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,6 +1,7 @@ import {KeywordErrorDefinition, CompilationContext, KeywordContext} from "../../types" import {reportError} from "../errors" import {_, Name} from "../codegen" +import N from "../names" const boolError: KeywordErrorDefinition = { message: "boolean schema is false", @@ -11,9 +12,10 @@ export function topBoolOrEmptySchema(it: CompilationContext): void { if (schema === false) { falseSchemaError(it, false) } else if (schema.$async === true) { - gen.code(_`return data;`) + gen.return(N.data) } else { - gen.code(_`validate.errors = null; return true;`) + gen.assign(_`${N.validate}.errors`, "null") + gen.return(true) } } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index eab973cd14..e89287ba2c 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -3,7 +3,7 @@ import {toHash, checkDataType, checkDataTypes} from "../util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" import {getKeywordContext} from "../../keyword" -import {_, Name} from "../codegen" +import {_, str, Name} from "../codegen" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { const st: undefined | string | string[] = schema.type @@ -59,7 +59,7 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { .if(checkDataType(schema.type, data, opts.strictNumbers), _`${coerced} = ${data}`) ) } - gen.if(`${coerced} !== undefined`) + gen.if(_`${coerced} !== undefined`) for (const t of coerceTo) { if (t in COERCIBLE || (t === "array" && opts.coerceTypes === "array")) { coerceSpecificType(t) @@ -69,8 +69,8 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { reportTypeError(it) gen.endIf() - gen.if(`${coerced} !== undefined`, () => { - gen.code(`${data} = ${coerced};`) + gen.if(_`${coerced} !== undefined`, () => { + gen.code(_`${data} = ${coerced};`) assignParentData(it, coerced) }) @@ -78,44 +78,44 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { switch (t) { case "string": gen - .elseIf(`${dataType} == "number" || ${dataType} == "boolean"`) - .code(`${coerced} = "" + ${data}`) - .elseIf(`${data} === null`) - .code(`${coerced} = ""`) + .elseIf(_`${dataType} == "number" || ${dataType} == "boolean"`) + .code(_`${coerced} = "" + ${data}`) + .elseIf(_`${data} === null`) + .code(_`${coerced} = ""`) return case "number": gen .elseIf( - `${dataType} == "boolean" || ${data} === null - || (${dataType} == "string" && ${data} && ${data} == +${data})` + _`${dataType} == "boolean" || ${data} === null + || (${dataType} == "string" && ${data} && ${data} == +${data})` ) - .code(`${coerced} = +${data}`) + .code(_`${coerced} = +${data}`) return case "integer": gen .elseIf( - `${dataType} === "boolean" || ${data} === null - || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))` + _`${dataType} === "boolean" || ${data} === null + || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))` ) .code(`${coerced} = +${data}`) return case "boolean": gen - .elseIf(`${data} === "false" || ${data} === 0 || ${data} === null`) - .code(`${coerced} = false`) - .elseIf(`${data} === "true" || ${data} === 1`) - .code(`${coerced} = true`) + .elseIf(_`${data} === "false" || ${data} === 0 || ${data} === null`) + .code(_`${coerced} = false`) + .elseIf(_`${data} === "true" || ${data} === 1`) + .code(_`${coerced} = true`) return case "null": - gen.elseIf(`${data} === "" || ${data} === 0 || ${data} === false`) - gen.code(`${coerced} = null`) + gen.elseIf(_`${data} === "" || ${data} === 0 || ${data} === false`) + gen.code(_`${coerced} = null`) return case "array": gen .elseIf( - `${dataType} === "string" || ${dataType} === "number" - || ${dataType} === "boolean" || ${data} === null` + _`${dataType} === "string" || ${dataType} === "number" + || ${dataType} === "boolean" || ${data} === null` ) .code(`${coerced} = [${data}]`) } @@ -127,15 +127,16 @@ function assignParentData( expr: string | Name ): void { // TODO use gen.property - gen.if(`${parentData} !== undefined`, () => + gen.if(_`${parentData} !== undefined`, () => gen.assign(`${parentData}[${parentDataProperty}]`, expr) ) } const typeError: KeywordErrorDefinition = { - message: ({schema}) => `"should be ${Array.isArray(schema) ? schema.join(",") : schema}"`, + message: ({schema}) => + str`should be ${Array.isArray(schema) ? schema.join(",") : schema}`, // TODO change: return type as array here - params: ({schema}) => `{type: "${Array.isArray(schema) ? schema.join(",") : schema}"}`, + params: ({schema}) => _`{type: ${Array.isArray(schema) ? schema.join(",") : schema}}`, } export function reportTypeError(it: CompilationContext): void { diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index c4d0c65038..7dbcdcdc43 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,11 +1,10 @@ -import {CompilationContext} from "../../types" +import {CompilationContext, Options} from "../../types" import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util" -import {quotedString} from "../../vocabularies/util" import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" import {schemaKeywords} from "./iterate" -import CodeGen, {_, Block, Name} from "../codegen" -import names from "../names" +import CodeGen, {_, str, nil, Block, Code, Name} from "../codegen" +import N from "../names" const resolve = require("../resolve") @@ -26,22 +25,21 @@ export function validateFunctionCode(it: CompilationContext): void { }) } -function validateFunction(it: CompilationContext, body: Block) { - const {gen, data, parentData, parentDataProperty} = it +function validateFunction({gen, schema, async, opts}: CompilationContext, body: Block) { gen.return(() => gen.func( - names.validate, - _`${data}, ${names.dataPath}, ${parentData}, ${parentDataProperty}, ${names.rootData}`, - it.async, - () => gen.code(`"use strict"; ${funcSourceUrl(it)}`).code(body) + N.validate, + _`${N.data}, ${N.dataPath}, ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}`, + async, + () => gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`).code(body) ) ) } -function funcSourceUrl({schema, opts}: CompilationContext): string { +function funcSourceUrl(schema, opts: Options): Code { return schema.$id && (opts.sourceCode || opts.processCode) - ? `/*# sourceURL=${schema.$id as string} */` - : "" + ? _`/*# sourceURL=${schema.$id as string} */` + : nil } // schema compilation - this function is used recursively to generate code for sub-schemas @@ -56,10 +54,10 @@ export function subschemaCode(it: CompilationContext, valid: Name): void { updateContext(it) checkAsync(it) // TODO var - async validation fails if var replaced, possibly because of nodent - const errsCount = gen.var("_errs", "errors") + const errsCount = gen.var("_errs", N.errors) typeAndKeywords(it, errsCount) // TODO var - gen.var(valid, _`${errsCount} === errors`) + gen.var(valid, _`${errsCount} === ${N.errors}`) } function checkKeywords(it: CompilationContext) { @@ -113,12 +111,9 @@ function checkNoDefault({schema, opts, logger}: CompilationContext): void { } function initializeTop(gen: CodeGen): void { - gen - .code( - `let vErrors = null; - let errors = 0;` - ) - .if(`rootData === undefined`, `rootData = data;`) + gen.let(N.vErrors, "null") + gen.let(N.errors, 0) + gen.if(_`${N.rootData} === undefined`, () => gen.assign(N.rootData, N.data)) } function updateContext(it: CompilationContext): void { @@ -130,22 +125,24 @@ function checkAsync(it: CompilationContext): void { } function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: CompilationContext): void { - const msg = quotedString(schema.$comment) + const msg = schema.$comment if ($comment === true) { - gen.code(`console.log(${msg})`) + gen.code(_`console.log(${msg})`) // should it use logger? } else if (typeof $comment == "function") { - const schemaPath = quotedString(errSchemaPath + "/$comment") - gen.code(`self._opts.$comment(${msg}, ${schemaPath}, validate.root.schema)`) + const schemaPath = str`${errSchemaPath}/$comment` + gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${N.validate}.root.schema)`) // TODO chained properties? } } function returnResults({gen, async}: CompilationContext) { if (async) { - gen.if("errors === 0", "return data", "throw new ValidationError(vErrors)") - } else { - gen.code( - `validate.errors = vErrors; - return errors === 0;` + gen.if( + _`${N.errors} === 0`, + () => gen.return(N.data), + _`throw new ValidationError(${N.vErrors})` ) + } else { + gen.assign(_`${N.validate}.errors`, N.vErrors) + gen.return(_`${N.errors} === 0`) } } diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 47fb673063..9277821adb 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -5,6 +5,7 @@ import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" import {RuleGroup, Rule} from "../rules" import {Name} from "../codegen" +import N from "../names" export function schemaKeywords( it: CompilationContext, @@ -43,7 +44,7 @@ export function schemaKeywords( iterateKeywords(it, group) } // TODO make it "ok" call? - if (!allErrors) gen.if(`errors === ${errsCount || 0}`) + if (!allErrors) gen.if(`${N.errors} === ${errsCount || 0}`) } } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 9d6a951e46..64bc53e497 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -11,6 +11,7 @@ import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" import {callValidate} from "../../vocabularies/util" import {_, Name, Expression} from "../codegen" +import N from "../names" export const keywordError: KeywordErrorDefinition = { message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, @@ -81,7 +82,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function validateRuleWithErrors(): void { gen.block() if ($data) check$data() - const errsCount = gen.const("_errs", "errors") + const errsCount = gen.const("_errs", N.errors) const ruleErrs = def.async ? validateAsyncRule() : validateSyncRule() if (def.modifying) modifyData(cxt) gen.endBlock() @@ -118,7 +119,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { return ruleErrs } - function validateSyncRule(): string { + function validateSyncRule(): Expression { const validateErrs = `${validateRef}.errors` gen.code(`${validateErrs} = null;`) assignValid("") @@ -149,14 +150,13 @@ function modifyData(cxt: KeywordContext) { gen.if(it.parentData, () => gen.assign(data, `${it.parentData}[${it.parentDataProperty}];`)) } -function addKeywordErrors(cxt: KeywordContext, ruleErrs: Expression, errsCount: Name): void { +function addKeywordErrors(cxt: KeywordContext, errs: Expression, errsCount: Name): void { const {gen} = cxt gen.if( - `Array.isArray(${ruleErrs})`, + `Array.isArray(${errs})`, () => { - gen - .if("vErrors === null", `vErrors = ${ruleErrs}`, `vErrors = vErrors.concat(${ruleErrs})`) - .code("errors = vErrors.length;") + gen.assign(N.vErrors, `${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged + gen.assign(N.errors, _`${N.vErrors}.length;`) extendErrors(cxt, errsCount) }, () => reportError(cxt, keywordError) diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index b89dee2d19..581040fe55 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -10,6 +10,7 @@ import { import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" import {Name} from "../../compile/codegen" +import N from "../../compile/names" const error: KeywordErrorDefinition = { message: "should NOT have additional properties", @@ -36,9 +37,9 @@ const def: CodeKeywordDefinition = { const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) - const errsCount = gen.const("_errs", "errors") + const errsCount = gen.const("_errs", N.errors) checkAdditionalProperties() - if (!allErrors) gen.if(`${errsCount} === errors`) + if (!allErrors) gen.if(`${errsCount} === ${N.errors}`) function checkAdditionalProperties(): void { loopPropertiesCode(cxt, (key: Name) => { diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 8979300da8..7d56dcae88 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -3,6 +3,7 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_} from "../../compile/codegen" +import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "anyOf", @@ -12,7 +13,7 @@ const def: CodeKeywordDefinition = { const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) if (alwaysValid) return - const errsCount = gen.const("_errs", "errors") + const errsCount = gen.const("_errs", N.errors) const valid = gen.let("valid", false) const schValid = gen.name("_valid") diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 24be3f1518..52c1eff3c3 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -2,6 +2,7 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" +import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "contains", @@ -10,7 +11,7 @@ const def: CodeKeywordDefinition = { before: "uniqueItems", code(cxt) { const {gen, fail, schema, data, it} = cxt - const errsCount = gen.const("_errs", "errors") + const errsCount = gen.const("_errs", N.errors) if (alwaysValidSchema(it, schema)) return fail(`${data}.length === 0`) diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index ef79ab039f..05154be43b 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -3,6 +3,7 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_, str, Name} from "../../compile/codegen" +import N from "../../compile/names" const error: KeywordErrorDefinition = { message: ({params}) => str`should match "${params.ifClause}" schema`, @@ -24,7 +25,7 @@ const def: CodeKeywordDefinition = { } const valid = gen.let("valid", true) - const errsCount = gen.const("_errs", "errors") + const errsCount = gen.const("_errs", N.errors) const schValid = gen.name("_valid") validateIf() diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 03b03c2ecf..0b8e145fb6 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -2,6 +2,7 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" +import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "not", @@ -11,7 +12,7 @@ const def: CodeKeywordDefinition = { if (alwaysValidSchema(it, schema)) return fail() const valid = gen.name("valid") - const errsCount = gen.const("_errs", "errors") + const errsCount = gen.const("_errs", N.errors) applySubschema( it, { diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 76cd4cd95d..4150483700 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -3,6 +3,7 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_} from "../../compile/codegen" +import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "oneOf", @@ -10,7 +11,7 @@ const def: CodeKeywordDefinition = { code(cxt) { const {gen, errorParams, schema, it} = cxt const valid = gen.let("valid", false) - const errsCount = gen.const("_errs", "errors") + const errsCount = gen.const("_errs", N.errors) const passing = gen.let("passing", "null") const schValid = gen.name("_valid") errorParams({passing}) diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 40312853e3..1f98a38cb2 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -4,6 +4,7 @@ import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" import {callValidate} from "../util" import {_, Expression} from "../../compile/codegen" +import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "$ref", @@ -21,7 +22,7 @@ const def: CodeKeywordDefinition = { function getRef(): ResolvedRef | void { if (schema === "#" || schema === "#/") { return isRoot - ? {code: "validate", $async: it.async} + ? {code: N.validate, $async: it.async} : {code: "root.refVal[0]", $async: root.schema.$async === true} } return resolveRef(baseId, schema, isRoot) @@ -78,12 +79,9 @@ const def: CodeKeywordDefinition = { } function addErrorsFrom(source: Expression): void { - gen.if( - "vErrors === null", - `vErrors = ${source}.errors`, - `vErrors = vErrors.concat(${source}.errors)` - ) - gen.code(`errors = vErrors.length;`) + const errs = `${source}.errors` + gen.assign(N.vErrors, `${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged + gen.assign(N.errors, _`${N.vErrors}.length`) } }, // TODO incorrect error message diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 010bc3a5c5..1df465d381 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,6 +1,7 @@ import {getProperty, schemaHasRules} from "../compile/util" import {CompilationContext, KeywordContext} from "../types" import {_, Code, Name, Expression} from "../compile/codegen" +import N from "../compile/names" export function quotedString(str: string): string { return JSON.stringify(str) @@ -104,7 +105,7 @@ export function callValidate( const dataAndSchema = passSchema ? `${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` : data - const dataPath = `(dataPath || '')${it.errorPath === '""' ? "" : ` + ${it.errorPath}`}` // TODO joinPaths? - const args = `${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, rootData` + const dataPath = `(${N.dataPath} || '')${it.errorPath === '""' ? "" : ` + ${it.errorPath}`}` // TODO joinPaths? + const args = `${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` return context ? `${func}.call(${context}, ${args})` : `${func}(${args})` } From 9d1f18afe1bc33ecd9956cd3880270319ed34e75 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:40:34 -0400 Subject: [PATCH 101/322] refactor: keywordCode --- .eslintrc.yml | 1 + lib/compile/rules.ts | 12 +- lib/compile/subschema.ts | 4 +- lib/compile/validate/applicability.ts | 2 +- lib/compile/validate/dataType.ts | 2 +- lib/compile/validate/iterate.ts | 8 +- lib/compile/validate/keyword.ts | 124 ++++++++++- lib/keyword.ts | 293 +++++++------------------- lib/types.ts | 10 +- 9 files changed, 213 insertions(+), 243 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 90edefbbb8..b8152071b5 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -34,6 +34,7 @@ overrides: "@typescript-eslint/no-empty-function": off "@typescript-eslint/no-this-alias": off "@typescript-eslint/no-implied-eval": off + no-invalid-this: off rules: block-scoped-var: error callback-return: error diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 169f8db3fc..80993ef9a3 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -1,10 +1,10 @@ import {toHash} from "./util" -import {CompilationContext, KeywordDefinition} from "../types" +import {KeywordDefinition} from "../types" export interface ValidationRules { rules: RuleGroup[] - all: {[key: string]: boolean | Rule} - keywords: {[key: string]: boolean} + all: {[key: string]: boolean | Rule} // rules that have to be validated + keywords: {[key: string]: boolean} // all known keywords (superset of "all") types: {[key: string]: boolean | RuleGroup} custom: {[key: string]: Rule} } @@ -14,12 +14,10 @@ export interface RuleGroup { rules: Rule[] } +// This interface wraps KeywordDefinition because definition can have multiple keywords export interface Rule { keyword: string - code: (it: CompilationContext, keyword: string, ruleType?: string) => void - implements?: string[] - definition?: KeywordDefinition - custom?: true + definition: KeywordDefinition } export default function rules(): ValidationRules { diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 301ab2ed39..08dc218ebd 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -9,7 +9,7 @@ export interface SubschemaContext { schema: object | boolean schemaPath: string errSchemaPath: string - topSchemaRef?: Code + topSchemaRef?: Expression errorPath?: string dataLevel?: number data?: Name @@ -37,7 +37,7 @@ interface SubschemaApplicationParams { schema: object | boolean schemaPath: string errSchemaPath: string - topSchemaRef: Code + topSchemaRef: Expression data: Name | Code dataProp: Expression | number propertyName: Name diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 010de7e57f..0cc7fe1b71 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -15,5 +15,5 @@ export function shouldUseRule(schema: object, rule: Rule): boolean | undefined { } function ruleImplementsSomeKeyword(schema: object, rule: Rule): boolean | undefined { - return rule.implements?.some((kwd) => schema[kwd] !== undefined) + return rule.definition.implements?.some((kwd) => schema[kwd] !== undefined) } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index e89287ba2c..eef957b1fe 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -2,7 +2,7 @@ import {CompilationContext, KeywordErrorDefinition} from "../../types" import {toHash, checkDataType, checkDataTypes} from "../util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" -import {getKeywordContext} from "../../keyword" +import {getKeywordContext} from "./keyword" import {_, str, Name} from "../codegen" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 9277821adb..f2068862fa 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -1,9 +1,10 @@ import {CompilationContext} from "../../types" import {shouldUseGroup, shouldUseRule} from "./applicability" import {checkDataType, schemaHasRulesExcept} from "../util" +import {keywordCode} from "./keyword" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" -import {RuleGroup, Rule} from "../rules" +import {Rule, RuleGroup} from "../rules" import {Name} from "../codegen" import N from "../names" @@ -18,8 +19,7 @@ export function schemaKeywords( schema.$ref && !(opts.extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref")) ) { - // TODO remove Rule type cast - gen.block(() => (RULES.all.$ref as Rule).code(it, "$ref")) + gen.block(() => keywordCode(it, "$ref", (RULES.all.$ref).definition)) // TODO typecast return } gen.block(() => { @@ -58,7 +58,7 @@ function iterateKeywords(it: CompilationContext, group: RuleGroup) { gen.block(() => { for (const rule of group.rules) { if (shouldUseRule(schema, rule)) { - rule.code(it, rule.keyword, group.type) + keywordCode(it, rule.keyword, rule.definition, group.type) } } }) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 64bc53e497..20e9c9f79b 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -2,6 +2,7 @@ import { KeywordDefinition, KeywordErrorDefinition, KeywordContext, + KeywordContextParams, MacroKeywordDefinition, FuncKeywordDefinition, CompilationContext, @@ -9,7 +10,8 @@ import { } from "../../types" import {applySubschema} from "../subschema" import {reportError, reportExtraError, extendErrors} from "../errors" -import {callValidate} from "../../vocabularies/util" +import {callValidate, schemaRefOrVal} from "../../vocabularies/util" +import {getData} from "../util" import {_, Name, Expression} from "../codegen" import N from "../names" @@ -18,14 +20,16 @@ export const keywordError: KeywordErrorDefinition = { params: ({keyword}) => `{keyword: "${keyword}"}`, // TODO possibly remove it as keyword is reported in the object } -export default function keywordCode( - cxt: KeywordContext, - _ruleType: string, - def: KeywordDefinition +export function keywordCode( + it: CompilationContext, + keyword: string, + def: KeywordDefinition, + ruleType?: string ): void { - // TODO "code" keyword - // TODO refactor - if (cxt.$data && "validate" in def) { + const cxt = _getKeywordContext(it, keyword, def) + if ("code" in def) { + def.code(cxt, ruleType) + } else if (cxt.$data && "validate" in def) { funcKeywordCode(cxt, def as FuncKeywordDefinition) } else if ("macro" in def) { macroKeywordCode(cxt, def) @@ -34,6 +38,110 @@ export default function keywordCode( } } +function _getKeywordContext( + it: CompilationContext, + keyword: string, + def: KeywordDefinition +): KeywordContext { + const schema = it.schema[keyword] + const {schemaType, $data: $defData} = def + validateKeywordSchema(it, keyword, def) + const {gen, data, opts, allErrors} = it + // TODO + // if (!code) throw new Error('"code" and "error" must be defined') + const $data = $defData && opts.$data && schema && schema.$data + const schemaValue = schemaRefOrVal(it, schema, keyword, $data) + const cxt: KeywordContext = { + gen, + ok, + pass, + fail, + errorParams, + keyword, + data, + $data, + schema, + schemaCode: $data ? gen.name("schema") : schemaValue, // reference to resolved schema value + schemaValue, // actual schema reference or value for primitive values + parentSchema: it.schema, + params: {}, + it, + } + if ($data) { + gen.const(cxt.schemaCode, `${getData($data, it)}`) + } else if (schemaType && !validSchemaType(schema, schemaType)) { + throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) + } + return cxt + + function fail(cond?: Expression, failAction?: () => void, context?: KeywordContext): void { + const action = failAction || _reportError + if (cond) { + gen.if(cond) + action() + if (allErrors) gen.endIf() + else gen.else() + } else { + action() + if (!allErrors) gen.if("false") + } + + function _reportError() { + reportError(context || cxt, def.error || keywordError) + } + } + + function pass(cond: Expression, failAction?: () => void, context?: KeywordContext): void { + cond = cond instanceof Name ? cond : `(${cond})` + fail(`!${cond}`, failAction, context) + } + + function ok(cond: Expression): void { + if (!allErrors) gen.if(cond) + } + + function errorParams(obj: KeywordContextParams, assign?: true) { + if (assign) Object.assign(cxt.params, obj) + else cxt.params = obj + } +} + +function validSchemaType(schema: any, schemaType: string | string[]): boolean { + // TODO add tests + if (Array.isArray(schemaType)) { + return schemaType.some((st) => validSchemaType(schema, st)) + } + return schemaType === "array" + ? Array.isArray(schema) + : schemaType === "object" + ? schema && typeof schema == "object" && !Array.isArray(schema) + : typeof schema == schemaType +} + +export function getKeywordContext(it: CompilationContext, keyword: string): KeywordContext { + const {gen, data, schema} = it + const schemaCode = schemaRefOrVal(it, schema, keyword) + return { + gen, + ok: exception, + pass: exception, + fail: exception, + errorParams: exception, + keyword, + data, + schema: schema[keyword], + schemaCode, + schemaValue: schemaCode, + parentSchema: schema, + params: {}, + it, + } +} + +function exception() { + throw new Error("this function can only be used in keyword") +} + function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { const {gen, fail, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) diff --git a/lib/keyword.ts b/lib/keyword.ts index 261e9dc68d..a3b59328e1 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,258 +1,122 @@ -import { - KeywordDefinition, - CodeKeywordDefinition, - Vocabulary, - ErrorObject, - ValidateFunction, - CompilationContext, - KeywordContext, - KeywordContextParams, -} from "./types" +import {KeywordDefinition, Vocabulary, ErrorObject, ValidateFunction} from "./types" import {ValidationRules, Rule} from "./compile/rules" -import {reportError} from "./compile/errors" -import {getData} from "./compile/util" -import {schemaRefOrVal} from "./vocabularies/util" import {definitionSchema} from "./definition_schema" -import keywordCode, {validateKeywordSchema, keywordError} from "./compile/validate/keyword" -import {_, Name, Expression} from "./compile/codegen" +import {_} from "./compile/codegen" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i -/** - * Define vocabulary - * @this Ajv - * @param {Array} definitions array of keyword definitions - * @param {Boolean} _skipValidation skip definition validation - * @return {Ajv} this for method chaining - */ -export function addVocabulary(definitions: Vocabulary, _skipValidation?: boolean): object { +export function addVocabulary(this, definitions: Vocabulary, _skipValidation?: boolean): object { // TODO return type Ajv for (const def of definitions) { - if (!def.keyword) { - throw new Error('Vocabulary keywords must have "keyword" property in definition') - } - if (Array.isArray(def.keyword)) { - for (const keyword of def.keyword) { - this.addKeyword(keyword, def, _skipValidation) - } - } else { - this.addKeyword(def.keyword, def, _skipValidation) - } + if (!def.keyword) throw new Error('Keyword definition must have "keyword" property') + this.addKeyword(def, _skipValidation) } return this } -// TODO use overloading when switched to typescript to allow not passing keyword -/** - * Define keyword - * @this Ajv - * @param {String} keyword custom keyword, should be unique (including different from all standard, custom and macro keywords). - * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. - * @param {Boolean} _skipValidation of keyword definition - * @return {Ajv} this for method chaining - */ +// TODO Ajv +export function addKeyword(this, def: KeywordDefinition, _skipValidation?: boolean): object +export function addKeyword(this, keyword: string): object export function addKeyword( - keyword: string, - definition: KeywordDefinition, - _skipValidation?: boolean + this: any, // TODO Ajv + kwdOrDef: string | KeywordDefinition, + defOrSkip?: KeywordDefinition | boolean, // deprecated + _skipValidation?: boolean // deprecated ): object { - // TODO return type Ajv - /* eslint no-shadow: 0 */ - const RULES: ValidationRules = this.RULES - if (RULES.keywords[keyword]) { - throw new Error(`Keyword ${keyword} is already defined`) - } - - if (!IDENTIFIER.test(keyword)) { - throw new Error(`Keyword ${keyword} is not a valid identifier`) + let keyword: string | string[] + let definition: KeywordDefinition | undefined + if (typeof kwdOrDef == "string") { + keyword = kwdOrDef + if (typeof defOrSkip == "object") { + // this.logger.warn("this method signature is deprecated, see docs for addKeyword") + definition = defOrSkip + if (definition.keyword === undefined) definition.keyword = keyword + } + } else if (typeof kwdOrDef == "object" && typeof defOrSkip != "object") { + definition = kwdOrDef + keyword = definition.keyword + _skipValidation = defOrSkip + } else { + throw new Error("invalid addKeywords parameters") } - // TODO any - const self = this + /* eslint no-shadow: 0 */ + const RULES: ValidationRules = this.RULES + eachItem(keyword, (kwd) => { + if (RULES.keywords[kwd]) throw new Error(`Keyword ${kwd} is already defined`) + if (!IDENTIFIER.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`) + }) if (definition) { if (!_skipValidation) this.validateKeyword(definition, true) - const dataType = definition.type - if (Array.isArray(dataType)) { - for (const t of dataType) { - _addRule(keyword, t, definition) - } - } else { - _addRule(keyword, dataType, definition) - } - - let metaSchema = definition.metaSchema - if (metaSchema) { - if (definition.$data && this._opts.$data) { - metaSchema = { - anyOf: [ - metaSchema, - { - $ref: - "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", - }, - ], - } - } - definition.validateSchema = this.compile(metaSchema, true) - } + keywordMetaschema.call(this, definition) } + const types = definition?.type + eachItem(keyword, (kwd) => { + eachItem(types, (t) => _addRule.call(this, kwd, t, definition)) + }) - function _addRule(keyword: string, dataType: string | undefined, definition: KeywordDefinition) { - let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) - - if (!ruleGroup) { - ruleGroup = {type: dataType, rules: []} - RULES.rules.push(ruleGroup) - } - - const rule: Rule = { - keyword, - definition, - custom: true, - code: ruleCode, - implements: definition.implements, - } - - if (definition.before) { - const i = ruleGroup.rules.findIndex((rule) => rule.keyword === definition.before) - if (i >= 0) { - ruleGroup.rules.splice(i, 0, rule) - } else { - ruleGroup.rules.push(rule) - // TODO replace with Ajv this.logger - self.logger.log(`rule ${definition.before} is not defined`) - } - } else { - ruleGroup.rules.push(rule) - } + return this +} - RULES.custom[keyword] = RULES.all[keyword] = rule - RULES.keywords[keyword] = true +function _addRule(keyword: string, dataType?: string, definition?: KeywordDefinition) { + const RULES: ValidationRules = this.RULES + let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) + if (!ruleGroup) { + ruleGroup = {type: dataType, rules: []} + RULES.rules.push(ruleGroup) } - return this -} + RULES.keywords[keyword] = true + if (!definition) return -/** - * Generate keyword code - * @this rule - * @param {Object} it schema compilation context. - * @param {String} keyword pre-defined or custom keyword. - * @param {String} ruleType current data type the rule is applied to (for rules supporting multiple types). - */ -function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): void { - const schema = it.schema[keyword] - const def: CodeKeywordDefinition = this.definition - const {schemaType, $data: $defData} = def - validateKeywordSchema(it, keyword, def) - const {gen, data, opts, allErrors} = it - // TODO - // if (!code) throw new Error('"code" and "error" must be defined') - const $data = $defData && opts.$data && schema && schema.$data - const schemaValue = schemaRefOrVal(it, schema, keyword, $data) - const cxt: KeywordContext = { - gen, - ok, - pass, - fail, - errorParams, + const rule: Rule = { keyword, - data, - $data, - schema, - schemaCode: $data ? gen.name("schema") : schemaValue, // reference to resolved schema value - schemaValue, // actual schema reference or value for primitive values - parentSchema: it.schema, - params: {}, - it, + definition, } - if ($data) { - gen.const(cxt.schemaCode, `${getData($data, it)}`) - } else if (schemaType && !validSchemaType(schema, schemaType)) { - throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) - } - ;(def.code || keywordCode)(cxt, ruleType, this.definition) - function fail(cond?: Expression, failAction?: () => void, context?: KeywordContext): void { - const action = failAction || _reportError - if (cond) { - gen.if(cond) - action() - if (allErrors) gen.endIf() - else gen.else() + if (definition?.before) { + const i = ruleGroup.rules.findIndex((rule) => rule.keyword === definition.before) + if (i >= 0) { + ruleGroup.rules.splice(i, 0, rule) } else { - action() - if (!allErrors) gen.if("false") - } - - function _reportError() { - reportError(context || cxt, def.error || keywordError) + ruleGroup.rules.push(rule) + // TODO replace with Ajv this.logger + this.logger.log(`rule ${definition.before} is not defined`) } + } else { + ruleGroup.rules.push(rule) } - function pass(cond: Expression, failAction?: () => void, context?: KeywordContext): void { - cond = cond instanceof Name ? cond : `(${cond})` - fail(`!${cond}`, failAction, context) - } - - function ok(cond: Expression): void { - if (!allErrors) gen.if(cond) - } - - function errorParams(obj: KeywordContextParams, assign?: true) { - if (assign) Object.assign(cxt.params, obj) - else cxt.params = obj - } + RULES.all[keyword] = rule } -function validSchemaType(schema: any, schemaType: string | string[]): boolean { - // TODO add tests - if (Array.isArray(schemaType)) { - return schemaType.some((st) => validSchemaType(schema, st)) +function eachItem(xs: T | T[], f: (x: T) => void): void { + if (Array.isArray(xs)) { + for (const x of xs) f(x) + } else { + f(xs) } - return schemaType === "array" - ? Array.isArray(schema) - : schemaType === "object" - ? schema && typeof schema == "object" && !Array.isArray(schema) - : typeof schema == schemaType } -export function getKeywordContext(it: CompilationContext, keyword: string): KeywordContext { - const {gen, data, schema} = it - const schemaCode = schemaRefOrVal(it, schema, keyword) - return { - gen, - ok: exception, - pass: exception, - fail: exception, - errorParams: exception, - keyword, - data, - schema: schema[keyword], - schemaCode, - schemaValue: schemaCode, - parentSchema: schema, - params: {}, - it, - } +const $dataRef = { + $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", } -function exception() { - throw new Error("this function can only be used in keyword") +function keywordMetaschema(this: any, def: KeywordDefinition): void { + // TODO this Ajv + let metaSchema = def.metaSchema + if (metaSchema === undefined) return + if (def.$data && this._opts.$data) { + metaSchema = {anyOf: [metaSchema, $dataRef]} + } + def.validateSchema = this.compile(metaSchema, true) } -/** - * Get keyword - * @this Ajv - * @param {String} keyword pre-defined or custom keyword. - * @return {Object|Boolean} custom keyword definition, `true` if it is a predefined keyword, `false` otherwise. - */ -export function getKeyword(keyword: string): KeywordDefinition | boolean { - /* jshint validthis: true */ - const rule = this.RULES.custom[keyword] - return rule ? rule.definition : this.RULES.keywords[keyword] || false +export function getKeyword(this, keyword: string): KeywordDefinition | boolean { + const rule = this.RULES.all[keyword] + return rule?.definition || rule || false } /** @@ -267,7 +131,6 @@ export function removeKeyword(keyword: string): object { const RULES: ValidationRules = this.RULES delete RULES.keywords[keyword] delete RULES.all[keyword] - delete RULES.custom[keyword] for (const group of RULES.rules) { const i = group.rules.findIndex((rule) => rule.keyword === keyword) if (i >= 0) group.rules.splice(i, 1) diff --git a/lib/types.ts b/lib/types.ts index cbed239cf9..a86c44aa39 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import Cache from "./cache" -import CodeGen, {Name, Code, Expression} from "./compile/codegen" +import CodeGen, {Name, Expression} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" @@ -137,7 +137,7 @@ export interface CompilationContext { logger: Logger // TODO ? root: SchemaRoot // TODO ? rootId: string // TODO ? - topSchemaRef: Code + topSchemaRef: Expression // TODO must be Code - depends on global names resolveRef: (...args: any[]) => ResolvedRef | void } @@ -148,7 +148,7 @@ interface SchemaRoot { } interface _KeywordDef { - keyword?: string | string[] + keyword: string | string[] type?: string | string[] schemaType?: string | string[] implements?: string[] @@ -156,11 +156,11 @@ interface _KeywordDef { metaSchema?: object validateSchema?: ValidateFunction // compiled keyword metaSchema - should not be passed dependencies?: string[] // keywords that must be present in the same schema + error?: KeywordErrorDefinition // TODO all keyword types should support error } export interface CodeKeywordDefinition extends _KeywordDef { - code: (cxt: KeywordContext, ruleType?: string, def?: KeywordDefinition) => string | void - error?: KeywordErrorDefinition // TODO all keyword types should support error + code: (cxt: KeywordContext, ruleType?: string) => string | void $data?: boolean } From e710856207a4dcc49e7106e997010d642fa5bbda Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:57:32 -0400 Subject: [PATCH 102/322] refactor: KeywordContext --- lib/compile/context.ts | 115 +++++++++++++ lib/compile/errors.ts | 15 +- lib/compile/index.ts | 2 - lib/compile/validate/boolSchema.ts | 13 +- lib/compile/validate/dataType.ts | 22 ++- lib/compile/validate/keyword.ts | 153 ++---------------- lib/types.ts | 21 +-- .../applicator/additionalProperties.ts | 7 +- lib/vocabularies/applicator/allOf.ts | 6 +- lib/vocabularies/applicator/anyOf.ts | 3 +- lib/vocabularies/applicator/contains.ts | 10 +- lib/vocabularies/applicator/dependencies.ts | 9 +- lib/vocabularies/applicator/if.ts | 11 +- lib/vocabularies/applicator/items.ts | 11 +- lib/vocabularies/applicator/not.ts | 10 +- lib/vocabularies/applicator/oneOf.ts | 7 +- .../applicator/patternProperties.ts | 3 +- lib/vocabularies/applicator/properties.ts | 7 +- lib/vocabularies/applicator/propertyNames.ts | 9 +- lib/vocabularies/core/ref.ts | 13 +- lib/vocabularies/format/format.ts | 8 +- lib/vocabularies/missing.ts | 14 +- lib/vocabularies/util.ts | 3 +- lib/vocabularies/validation/const.ts | 3 +- lib/vocabularies/validation/enum.ts | 11 +- lib/vocabularies/validation/limit.ts | 6 +- lib/vocabularies/validation/limitItems.ts | 6 +- lib/vocabularies/validation/limitLength.ts | 8 +- .../validation/limitProperties.ts | 6 +- lib/vocabularies/validation/multipleOf.ts | 8 +- lib/vocabularies/validation/pattern.ts | 8 +- lib/vocabularies/validation/required.ts | 11 +- lib/vocabularies/validation/uniqueItems.ts | 12 +- 33 files changed, 292 insertions(+), 259 deletions(-) create mode 100644 lib/compile/context.ts diff --git a/lib/compile/context.ts b/lib/compile/context.ts new file mode 100644 index 0000000000..f54c98a379 --- /dev/null +++ b/lib/compile/context.ts @@ -0,0 +1,115 @@ +import { + KeywordDefinition, + KeywordErrorContext, + KeywordContextParams, + CompilationContext, +} from "../types" +import {schemaRefOrVal} from "../vocabularies/util" +import {getData} from "./util" +import {reportError, keywordError} from "./errors" +import CodeGen, {_, Name, Expression} from "./codegen" + +export default class KeywordContext implements KeywordErrorContext { + gen: CodeGen + allErrors: boolean + keyword: string + data: Name + $data?: string | false + schema: any + schemaCode: Expression | number | boolean + schemaValue: Expression | number | boolean + parentSchema: any + params: KeywordContextParams + it: CompilationContext + def: KeywordDefinition + + constructor(it: CompilationContext, keyword: string, def: KeywordDefinition) { + const schema = it.schema[keyword] + const {schemaType, $data: $defData} = def + validateKeywordSchema(it, keyword, def) + // TODO + // if (!code) throw new Error('"code" and "error" must be defined') + const $data = $defData && it.opts.$data && schema && schema.$data + const schemaValue = schemaRefOrVal(it, schema, keyword, $data) + this.gen = it.gen + this.allErrors = it.allErrors + this.keyword = keyword + this.data = it.data + this.$data = $data + this.schema = schema + this.schemaCode = $data ? it.gen.name("schema") : schemaValue // reference to resolved schema value + this.schemaValue = schemaValue // actual schema reference or value for primitive values + this.parentSchema = it.schema + this.params = {} + this.it = it + this.def = def + + if ($data) { + it.gen.const(this.schemaCode, `${getData($data, it)}`) + } else if (schemaType && !validSchemaType(schema, schemaType)) { + throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) + } + } + + fail(cond?: Expression, failAction?: () => void, context?: KeywordErrorContext): void { + if (cond) { + this.gen.if(cond) + failAction ? failAction() : this._reportError(context) + if (this.allErrors) this.gen.endIf() + else this.gen.else() + } else { + failAction ? failAction() : this._reportError(context) + if (!this.allErrors) this.gen.if("false") + } + } + + _reportError(context?: KeywordErrorContext) { + reportError(context || this, this.def.error || keywordError) + } + + pass(cond: Expression, failAction?: () => void, context?: KeywordErrorContext): void { + cond = cond instanceof Name ? cond : `(${cond})` + this.fail(`!${cond}`, failAction, context) + } + + ok(cond: Expression): void { + if (!this.allErrors) this.gen.if(cond) + } + + errorParams(obj: KeywordContextParams, assign?: true) { + if (assign) Object.assign(this.params, obj) + else this.params = obj + } +} + +function validSchemaType(schema: any, schemaType: string | string[]): boolean { + // TODO add tests + if (Array.isArray(schemaType)) { + return schemaType.some((st) => validSchemaType(schema, st)) + } + return schemaType === "array" + ? Array.isArray(schema) + : schemaType === "object" + ? schema && typeof schema == "object" && !Array.isArray(schema) + : typeof schema == schemaType +} + +function validateKeywordSchema( + it: CompilationContext, + keyword: string, + def: KeywordDefinition +): void { + const deps = def.dependencies + if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(it.schema, kwd))) { + throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) + } + + if (def.validateSchema) { + const valid = def.validateSchema(it.schema[keyword]) + if (!valid) { + const msg = "keyword schema is invalid: " + it.self.errorsText(def.validateSchema.errors) + if (it.opts.validateSchema === "log") it.logger.error(msg) + else throw new Error(msg) + } + } +} diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 24230b695e..991f7eeaf3 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,10 +1,15 @@ -import {KeywordContext, KeywordErrorDefinition} from "../types" +import {KeywordErrorContext, KeywordErrorDefinition} from "../types" import {quotedString} from "../vocabularies/util" import CodeGen, {_, Name, Expression} from "./codegen" import N from "./names" +export const keywordError: KeywordErrorDefinition = { + message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, + params: ({keyword}) => `{keyword: "${keyword}"}`, // TODO possibly remove it as keyword is reported in the object +} + export function reportError( - cxt: KeywordContext, + cxt: KeywordErrorContext, error: KeywordErrorDefinition, overrideAllErrors?: boolean ): void { @@ -17,7 +22,7 @@ export function reportError( } } -export function reportExtraError(cxt: KeywordContext, error: KeywordErrorDefinition): void { +export function reportExtraError(cxt: KeywordErrorContext, error: KeywordErrorDefinition): void { const {gen, compositeRule, allErrors, async} = cxt.it const errObj = errorObjectCode(cxt, error) addError(gen, errObj) @@ -34,7 +39,7 @@ export function resetErrorsCount(gen: CodeGen, errsCount: Name): void { } export function extendErrors( - {gen, keyword, schemaValue, data, it}: KeywordContext, + {gen, keyword, schemaValue, data, it}: KeywordErrorContext, errsCount: Name ): void { const err = gen.name("err") @@ -69,7 +74,7 @@ function returnErrors(gen: CodeGen, async: boolean, errs: Expression): void { } } -function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): string { +function errorObjectCode(cxt: KeywordErrorContext, error: KeywordErrorDefinition): string { const { keyword, data, diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 3d72793896..56eb18b8a9 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -2,7 +2,6 @@ import CodeGen, {_, nil, Expression} from "./codegen" import {toQuotedString} from "./util" import {quotedString} from "../vocabularies/util" import {validateFunctionCode} from "./validate" -import {validateKeywordSchema} from "./validate/keyword" import {ErrorObject, KeywordCompilationResult} from "../types" import N from "./names" @@ -133,7 +132,6 @@ function compile(schema, root, localRefs, baseId) { resolveRef, // TODO remove to imports usePattern, // TODO remove to imports useDefault, // TODO remove to imports - validateKeywordSchema, // TODO remove customRules, // TODO add to types opts, formats, diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 534a0db989..040eb7a982 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,4 +1,4 @@ -import {KeywordErrorDefinition, CompilationContext, KeywordContext} from "../../types" +import {KeywordErrorDefinition, CompilationContext, KeywordErrorContext} from "../../types" import {reportError} from "../errors" import {_, Name} from "../codegen" import N from "../names" @@ -32,12 +32,8 @@ export function boolOrEmptySchema(it: CompilationContext, valid: Name): void { function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { const {gen, data} = it // TODO maybe some other interface should be used for non-keyword validation errors... - const cxt: KeywordContext = { + const cxt: KeywordErrorContext = { gen, - ok: exception, - pass: exception, - fail: exception, - errorParams: exception, keyword: "false schema", data, schema: false, @@ -49,8 +45,3 @@ function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { } reportError(cxt, boolError, overrideAllErrors) } - -// TODO combine with exception from dataType -function exception() { - throw new Error("this function can only be used in keyword") -} diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index eef957b1fe..6d1462ef52 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,8 +1,8 @@ -import {CompilationContext, KeywordErrorDefinition} from "../../types" +import {CompilationContext, KeywordErrorDefinition, KeywordErrorContext} from "../../types" import {toHash, checkDataType, checkDataTypes} from "../util" +import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" -import {getKeywordContext} from "./keyword" import {_, str, Name} from "../codegen" export function getSchemaTypes({schema, opts}: CompilationContext): string[] { @@ -140,6 +140,22 @@ const typeError: KeywordErrorDefinition = { } export function reportTypeError(it: CompilationContext): void { - const cxt = getKeywordContext(it, "type") + const cxt = getErrorContext(it, "type") reportError(cxt, typeError) } + +function getErrorContext(it: CompilationContext, keyword: string): KeywordErrorContext { + const {gen, data, schema} = it + const schemaCode = schemaRefOrVal(it, schema, keyword) + return { + gen, + keyword, + data, + schema: schema[keyword], + schemaCode, + schemaValue: schemaCode, + parentSchema: schema, + params: {}, + it, + } +} diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 20e9c9f79b..791b235961 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -1,32 +1,24 @@ import { KeywordDefinition, - KeywordErrorDefinition, - KeywordContext, - KeywordContextParams, MacroKeywordDefinition, FuncKeywordDefinition, CompilationContext, KeywordCompilationResult, } from "../../types" +import KeywordContext from "../context" import {applySubschema} from "../subschema" -import {reportError, reportExtraError, extendErrors} from "../errors" -import {callValidate, schemaRefOrVal} from "../../vocabularies/util" -import {getData} from "../util" +import {reportError, reportExtraError, extendErrors, keywordError} from "../errors" +import {callValidate} from "../../vocabularies/util" import {_, Name, Expression} from "../codegen" import N from "../names" -export const keywordError: KeywordErrorDefinition = { - message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, - params: ({keyword}) => `{keyword: "${keyword}"}`, // TODO possibly remove it as keyword is reported in the object -} - export function keywordCode( it: CompilationContext, keyword: string, def: KeywordDefinition, ruleType?: string ): void { - const cxt = _getKeywordContext(it, keyword, def) + const cxt = new KeywordContext(it, keyword, def) if ("code" in def) { def.code(cxt, ruleType) } else if (cxt.$data && "validate" in def) { @@ -38,112 +30,8 @@ export function keywordCode( } } -function _getKeywordContext( - it: CompilationContext, - keyword: string, - def: KeywordDefinition -): KeywordContext { - const schema = it.schema[keyword] - const {schemaType, $data: $defData} = def - validateKeywordSchema(it, keyword, def) - const {gen, data, opts, allErrors} = it - // TODO - // if (!code) throw new Error('"code" and "error" must be defined') - const $data = $defData && opts.$data && schema && schema.$data - const schemaValue = schemaRefOrVal(it, schema, keyword, $data) - const cxt: KeywordContext = { - gen, - ok, - pass, - fail, - errorParams, - keyword, - data, - $data, - schema, - schemaCode: $data ? gen.name("schema") : schemaValue, // reference to resolved schema value - schemaValue, // actual schema reference or value for primitive values - parentSchema: it.schema, - params: {}, - it, - } - if ($data) { - gen.const(cxt.schemaCode, `${getData($data, it)}`) - } else if (schemaType && !validSchemaType(schema, schemaType)) { - throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) - } - return cxt - - function fail(cond?: Expression, failAction?: () => void, context?: KeywordContext): void { - const action = failAction || _reportError - if (cond) { - gen.if(cond) - action() - if (allErrors) gen.endIf() - else gen.else() - } else { - action() - if (!allErrors) gen.if("false") - } - - function _reportError() { - reportError(context || cxt, def.error || keywordError) - } - } - - function pass(cond: Expression, failAction?: () => void, context?: KeywordContext): void { - cond = cond instanceof Name ? cond : `(${cond})` - fail(`!${cond}`, failAction, context) - } - - function ok(cond: Expression): void { - if (!allErrors) gen.if(cond) - } - - function errorParams(obj: KeywordContextParams, assign?: true) { - if (assign) Object.assign(cxt.params, obj) - else cxt.params = obj - } -} - -function validSchemaType(schema: any, schemaType: string | string[]): boolean { - // TODO add tests - if (Array.isArray(schemaType)) { - return schemaType.some((st) => validSchemaType(schema, st)) - } - return schemaType === "array" - ? Array.isArray(schema) - : schemaType === "object" - ? schema && typeof schema == "object" && !Array.isArray(schema) - : typeof schema == schemaType -} - -export function getKeywordContext(it: CompilationContext, keyword: string): KeywordContext { - const {gen, data, schema} = it - const schemaCode = schemaRefOrVal(it, schema, keyword) - return { - gen, - ok: exception, - pass: exception, - fail: exception, - errorParams: exception, - keyword, - data, - schema: schema[keyword], - schemaCode, - schemaValue: schemaCode, - parentSchema: schema, - params: {}, - it, - } -} - -function exception() { - throw new Error("this function can only be used in keyword") -} - function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { - const {gen, fail, keyword, schema, parentSchema, it} = cxt + const {gen, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) const schemaRef = addCustomRule(it, keyword, macroSchema) if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true) @@ -160,12 +48,11 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { }, valid ) - - fail(`!${valid}`, () => reportExtraError(cxt, keywordError)) + cxt.pass(valid, () => reportExtraError(cxt, keywordError)) } function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { - const {gen, ok, fail, keyword, schema, schemaCode, parentSchema, $data, it} = cxt + const {gen, keyword, schema, schemaCode, parentSchema, $data, it} = cxt checkAsync(it, def) const validate = "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate @@ -184,7 +71,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { assignValid() if (def.modifying) modifyData(cxt) }) - if (!def.valid) fail(`!${valid}`) + if (!def.valid) cxt.fail(`!${valid}`) } function validateRuleWithErrors(): void { @@ -246,9 +133,9 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { return case false: addKeywordErrors(cxt, ruleErrs, errsCount) - return ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? + return cxt.ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? default: - fail(`!${valid}`, () => addKeywordErrors(cxt, ruleErrs, errsCount)) + cxt.fail(`!${valid}`, () => addKeywordErrors(cxt, ruleErrs, errsCount)) } } } @@ -275,26 +162,6 @@ function checkAsync(it: CompilationContext, def: FuncKeywordDefinition) { if (def.async && !it.async) throw new Error("async keyword in sync schema") } -export function validateKeywordSchema( - it: CompilationContext, - keyword: string, - def: KeywordDefinition -): void { - const deps = def.dependencies - if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(it.schema, kwd))) { - throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) - } - - if (def.validateSchema) { - const valid = def.validateSchema(it.schema[keyword]) - if (!valid) { - const msg = "keyword schema is invalid: " + it.self.errorsText(def.validateSchema.errors) - if (it.opts.validateSchema === "log") it.logger.error(msg) - else throw new Error(msg) - } - } -} - function addCustomRule( it: CompilationContext, keyword: string, diff --git a/lib/types.ts b/lib/types.ts index a86c44aa39..e82b141e02 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -2,6 +2,7 @@ import Cache from "./cache" import CodeGen, {Name, Expression} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" +import KeywordContext from "./compile/context" export interface Options { $data?: boolean @@ -131,7 +132,6 @@ export interface CompilationContext { usePattern: (str: string) => string useDefault: (value: any) => string customRules: KeywordCompilationResult[] - validateKeywordSchema: (it: CompilationContext, keyword: string, def: KeywordDefinition) => void // TODO remove self: any // TODO RULES: ValidationRules logger: Logger // TODO ? @@ -160,7 +160,7 @@ interface _KeywordDef { } export interface CodeKeywordDefinition extends _KeywordDef { - code: (cxt: KeywordContext, ruleType?: string) => string | void + code: (cxt: KeywordContext, ruleType?: string) => void $data?: boolean } @@ -208,18 +208,14 @@ export type KeywordDefinition = | CodeKeywordDefinition export interface KeywordErrorDefinition { - message: string | ((cxt: KeywordContext) => Expression) - params?: (cxt: KeywordContext) => Expression + message: string | ((cxt: KeywordErrorContext) => Expression) + params?: (cxt: KeywordErrorContext) => Expression } export type Vocabulary = KeywordDefinition[] -export interface KeywordContext { +export interface KeywordErrorContext { gen: CodeGen - ok: (condition: Expression) => void - pass: (condition: Expression, failAction?: () => void, context?: KeywordContext) => void - fail: (condition?: Expression, failAction?: () => void, context?: KeywordContext) => void - errorParams: (obj: KeywordContextParams, assing?: true) => void keyword: string data: Name $data?: string | false @@ -231,6 +227,13 @@ export interface KeywordContext { it: CompilationContext } +// export interface KeywordContext extends KeywordErrorContext { +// ok: (condition: Expression) => void +// pass: (condition: Expression, failAction?: () => void, context?: KeywordContext) => void +// fail: (condition?: Expression, failAction?: () => void, context?: KeywordContext) => void +// errorParams: (obj: KeywordContextParams, assing?: true) => void +// } + export type KeywordContextParams = {[x: string]: Expression | number} export type FormatMode = "fast" | "full" diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 581040fe55..e2dd5c2995 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import { allSchemaProperties, schemaRefOrVal, @@ -22,8 +23,8 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: ["object", "boolean"], error, - code(cxt) { - const {gen, errorParams, schema, parentSchema, data, it} = cxt + code(cxt: KeywordContext) { + const {gen, schema, parentSchema, data, it} = cxt const { allErrors, usePattern, @@ -75,7 +76,7 @@ const def: CodeKeywordDefinition = { } if (schema === false) { - errorParams({additionalProperty: key}) + cxt.errorParams({additionalProperty: key}) reportError(cxt, error) if (!allErrors) gen.break() return diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index bc5d4fba21..c9ac0181f8 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,16 +1,18 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" const def: CodeKeywordDefinition = { keyword: "allOf", schemaType: "array", - code({gen, ok, schema, it}) { + code(cxt: KeywordContext) { + const {gen, schema, it} = cxt const valid = gen.name("valid") schema.forEach((sch: object | boolean, i: number) => { if (alwaysValidSchema(it, sch)) return applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) - ok(valid) + cxt.ok(valid) }) // TODO possibly add allOf error }, diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 7d56dcae88..015e1d9c72 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" @@ -8,7 +9,7 @@ import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", - code(cxt) { + code(cxt: KeywordContext) { const {gen, schema, it} = cxt const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) if (alwaysValid) return diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 52c1eff3c3..f6a736f230 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" @@ -9,11 +10,14 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: ["object", "boolean"], before: "uniqueItems", - code(cxt) { - const {gen, fail, schema, data, it} = cxt + code(cxt: KeywordContext) { + const {gen, schema, data, it} = cxt const errsCount = gen.const("_errs", N.errors) - if (alwaysValidSchema(it, schema)) return fail(`${data}.length === 0`) + if (alwaysValidSchema(it, schema)) { + cxt.fail(`${data}.length === 0`) + return + } const valid = gen.name("valid") const i = gen.name("i") diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 29e1501009..25e75f5994 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" @@ -15,8 +16,8 @@ const def: CodeKeywordDefinition = { keyword: "dependencies", type: "object", schemaType: "object", - code(cxt) { - const {gen, ok, errorParams, schema, data, it} = cxt + code(cxt: KeywordContext) { + const {gen, schema, data, it} = cxt const [propDeps, schDeps] = splitDependencies() const valid = gen.name("valid") validatePropertyDeps(propDeps) @@ -38,7 +39,7 @@ const def: CodeKeywordDefinition = { const deps = propertyDeps[prop] if (deps.length === 0) continue const hasProperty = propertyInData(data, prop, it.opts.ownProperties) - errorParams({ + cxt.errorParams({ property: prop, depsCount: deps.length, deps: deps.join(", "), @@ -68,7 +69,7 @@ const def: CodeKeywordDefinition = { () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), `var ${valid} = true;` // TODO refactor var ) - ok(valid) + cxt.ok(valid) } } }, diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 05154be43b..c098b8a071 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition, CompilationContext} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" @@ -15,8 +16,8 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "boolean"], // TODO // implements: ["then", "else"], - code(cxt) { - const {gen, pass, errorParams, it} = cxt + code(cxt: KeywordContext) { + const {gen, it} = cxt const hasThen = hasSchema(it, "then") const hasElse = hasSchema(it, "else") if (!hasThen && !hasElse) { @@ -33,7 +34,7 @@ const def: CodeKeywordDefinition = { if (hasThen && hasElse) { const ifClause = gen.let("ifClause") - errorParams({ifClause}) + cxt.errorParams({ifClause}) gen.if(schValid, validateClause("then", ifClause), validateClause("else", ifClause)) } else if (hasThen) { gen.if(schValid, validateClause("then")) @@ -41,7 +42,7 @@ const def: CodeKeywordDefinition = { gen.if(`!${schValid}`, validateClause("else")) } - pass(valid, () => reportExtraError(cxt, error)) + cxt.pass(valid, () => reportExtraError(cxt, error)) function validateIf(): void { applySubschema( @@ -61,7 +62,7 @@ const def: CodeKeywordDefinition = { applySubschema(it, {keyword}, schValid) gen.code(`${valid} = ${schValid};`) if (ifClause) gen.code(`${ifClause} = "${keyword}";`) - else errorParams({ifClause: keyword}) + else cxt.errorParams({ifClause: keyword}) } } }, diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 467ccbe97c..ce004c0098 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" import {_, str} from "../../compile/codegen" @@ -10,9 +11,9 @@ const def: CodeKeywordDefinition = { before: "uniqueItems", // TODO // implements: ["additionalItems"], - code(cxt) { + code(cxt: KeywordContext) { // TODO strict mode: fail or warning if "additionalItems" is present without "items" - const {gen, ok, fail, schema, parentSchema, data, it} = cxt + const {gen, schema, parentSchema, data, it} = cxt const len = gen.const("len", `${data}.length`) if (Array.isArray(schema)) { @@ -31,7 +32,7 @@ const def: CodeKeywordDefinition = { } function validateDataLength(sch: (object | boolean)[]): void { - fail(`${len} > ${sch.length}`, undefined, { + cxt.fail(`${len} > ${sch.length}`, undefined, { ...cxt, keyword: "additionalItems", schemaValue: false, @@ -54,7 +55,7 @@ const def: CodeKeywordDefinition = { valid ) ) - ok(valid) + cxt.ok(valid) }) } @@ -65,7 +66,7 @@ const def: CodeKeywordDefinition = { applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) if (!it.allErrors) gen.if(`!${valid}`, "break") }) - ok(valid) + cxt.ok(valid) } }, error: { diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 0b8e145fb6..169ab3f086 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" @@ -7,9 +8,12 @@ import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "not", schemaType: ["object", "boolean"], - code(cxt) { - const {gen, fail, schema, it} = cxt - if (alwaysValidSchema(it, schema)) return fail() + code(cxt: KeywordContext) { + const {gen, schema, it} = cxt + if (alwaysValidSchema(it, schema)) { + cxt.fail() + return + } const valid = gen.name("valid") const errsCount = gen.const("_errs", N.errors) diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 4150483700..235fe9e969 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError, resetErrorsCount} from "../../compile/errors" @@ -8,13 +9,13 @@ import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", - code(cxt) { - const {gen, errorParams, schema, it} = cxt + code(cxt: KeywordContext) { + const {gen, schema, it} = cxt const valid = gen.let("valid", false) const errsCount = gen.const("_errs", N.errors) const passing = gen.let("passing", "null") const schValid = gen.name("_valid") - errorParams({passing}) + cxt.errorParams({passing}) // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas gen.block(validateOneOf) diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 90005c2202..680773e630 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {schemaProperties, loopPropertiesCode} from "../util" import {applySubschema, Expr} from "../../compile/subschema" @@ -6,7 +7,7 @@ const def: CodeKeywordDefinition = { keyword: "patternProperties", type: "object", schemaType: "object", - code(cxt) { + code(cxt: KeywordContext) { const {gen, schema, it} = cxt const patterns = schemaProperties(it, schema) if (patterns.length === 0) return diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index e0a62f93c4..9f362e78eb 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {schemaProperties, propertyInData} from "../util" import {applySubschema, Expr} from "../../compile/subschema" @@ -6,8 +7,8 @@ const def: CodeKeywordDefinition = { keyword: "properties", type: "object", schemaType: "object", - code(cxt) { - const {gen, ok, schema, data, it} = cxt + code(cxt: KeywordContext) { + const {gen, schema, data, it} = cxt // TODO // if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { // remove all additional properties - it will fix skipped tests @@ -25,7 +26,7 @@ const def: CodeKeywordDefinition = { if (!it.allErrors) gen.else().var(valid, true) gen.endIf() } - ok(valid) + cxt.ok(valid) } function hasDefault(prop: string): boolean | undefined { diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index d37dd9e413..33476f7477 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {alwaysValidSchema, loopPropertiesCode} from "../util" import {applySubschema} from "../../compile/subschema" import {reportExtraError} from "../../compile/errors" @@ -8,13 +9,13 @@ const def: CodeKeywordDefinition = { keyword: "propertyNames", type: "object", schemaType: ["object", "boolean"], - code(cxt) { - const {gen, ok, errorParams, schema, it} = cxt + code(cxt: KeywordContext) { + const {gen, schema, it} = cxt if (alwaysValidSchema(it, schema)) return const valid = gen.name("valid") loopPropertiesCode(cxt, (key) => { - errorParams({propertyName: key}) + cxt.errorParams({propertyName: key}) applySubschema( it, {keyword: "propertyNames", data: key, propertyName: key, compositeRule: true}, @@ -26,7 +27,7 @@ const def: CodeKeywordDefinition = { }) }) - ok(valid) + cxt.ok(valid) }, error: { message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 1f98a38cb2..e1d2208596 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,4 +1,5 @@ -import {CodeKeywordDefinition, KeywordContext} from "../../types" +import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" @@ -10,7 +11,7 @@ const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", code(cxt: KeywordContext) { - const {gen, ok, fail, schema, it} = cxt + const {gen, schema, it} = cxt const {resolveRef, allErrors, baseId, isRoot, root, opts, logger} = it const ref = getRef() const passCxt = opts.passContext ? "this" : "" @@ -33,7 +34,7 @@ const def: CodeKeywordDefinition = { switch (opts.missingRefs) { case "fail": logger.error(msg) - return fail() + return cxt.fail() case "ignore": logger.warn(msg) return @@ -54,7 +55,7 @@ const def: CodeKeywordDefinition = { }, valid ) - ok(valid) + cxt.ok(valid) } function validateAsyncRef(v: Expression): void { @@ -71,11 +72,11 @@ const def: CodeKeywordDefinition = { if (!allErrors) gen.code(`${valid} = false;`) } ) - ok(valid) + cxt.ok(valid) } function validateRef(v: Expression): void { - fail(`!${callValidate(cxt, v, passCxt)}`, () => addErrorsFrom(v)) + cxt.fail(`!${callValidate(cxt, v, passCxt)}`, () => addErrorsFrom(v)) } function addErrorsFrom(source: Expression): void { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 40a991dd18..5a3c4f4897 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" +import KeywordContext from "../../compile/context" import {dataNotType} from "../util" import {getProperty} from "../../compile/util" import {_, str} from "../../compile/codegen" @@ -8,7 +9,8 @@ const def: CodeKeywordDefinition = { type: ["number", "string"], schemaType: "string", $data: true, - code({gen, pass, fail, data, $data, schema, schemaCode, it}, ruleType) { + code(cxt: KeywordContext, ruleType?: string) { + const {gen, data, $data, schema, schemaCode, it} = cxt const {formats, opts, logger, errSchemaPath} = it if (opts.format === false) return @@ -25,7 +27,7 @@ const def: CodeKeywordDefinition = { _`${fmtType} = "string"; ${format} = ${fmtDef}` ) const dnt = dataNotType(schemaCode, def.schemaType, $data) - fail(dnt + unknownFmt() + invalidFmt()) + cxt.fail(dnt + unknownFmt() + invalidFmt()) function unknownFmt(): string { if (opts.unknownFormats === "ignore") return "" @@ -51,7 +53,7 @@ const def: CodeKeywordDefinition = { return } const [fmtType, format, fmtRef] = getFormat(formatDef) - if (fmtType === ruleType) pass(validCondition()) + if (fmtType === ruleType) cxt.pass(validCondition()) function unknownFormat() { if (opts.unknownFormats === "ignore") return logger.warn(unknownMsg()) diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index c0ef9cfffc..038d5da781 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,4 +1,5 @@ -import {KeywordContext, KeywordErrorDefinition} from "../types" +import {KeywordErrorDefinition} from "../types" +import KeywordContext from "../compile/context" import {noPropertyInData, quotedString, orExpr} from "./util" import {reportError} from "../compile/errors" import {Name, _} from "../compile/codegen" @@ -8,14 +9,9 @@ export function checkReportMissingProp( prop: string, error: KeywordErrorDefinition ): void { - const { - gen, - errorParams, - data, - it: {opts}, - } = cxt - gen.if(noPropertyInData(data, prop, opts.ownProperties), () => { - errorParams({missingProperty: _`${prop}`}, true) + const {gen, data, it} = cxt + gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => { + cxt.errorParams({missingProperty: _`${prop}`}, true) reportError(cxt, error) }) } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 1df465d381..0a20f6814c 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,5 +1,6 @@ import {getProperty, schemaHasRules} from "../compile/util" -import {CompilationContext, KeywordContext} from "../types" +import {CompilationContext} from "../types" +import KeywordContext from "../compile/context" import {_, Code, Name, Expression} from "../compile/codegen" import N from "../compile/names" diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index f1966e2408..f21a1f2b87 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,9 +1,10 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" const def: CodeKeywordDefinition = { keyword: "const", $data: true, - code: ({fail, data, schemaCode}) => fail(`!equal(${data}, ${schemaCode})`), + code: (cxt: KeywordContext) => cxt.fail(`!equal(${cxt.data}, ${cxt.schemaCode})`), error: { message: "should be equal to constant", params: ({schemaCode}) => `{allowedValue: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 02f6b79485..98ed4f1aff 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {quotedString, orExpr} from "../util" import {_, Name} from "../../compile/codegen" @@ -6,23 +7,25 @@ const def: CodeKeywordDefinition = { keyword: "enum", schemaType: "array", $data: true, - code({gen, pass, data, $data, schema, schemaCode, it: {opts}}) { + code(cxt: KeywordContext) { + const {gen, data, $data, schema, schemaCode, it} = cxt + const {opts} = it if ($data) { const valid = gen.let("valid") gen.if(`${schemaCode} === undefined`, `${valid} = true;`, () => gen.code(`${valid} = false;`).if(`Array.isArray(${schemaCode})`, () => loopEnum(valid)) ) - pass(valid) + cxt.pass(valid) } else { if (schema.length === 0) throw new Error("enum must have non-empty array") if (schema.length > (opts.loopEnum as number)) { const valid = gen.let("valid", "false") loopEnum(valid) - pass(valid) + cxt.pass(valid) } else { const vSchema = gen.const("schema", schemaCode) const cond: string = orExpr(schema, (_, i) => equalCode(vSchema, i)) - pass(cond) + cxt.pass(cond) } } diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 5ab18e59df..8831634cb9 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {dataNotType} from "../util" import {_, str} from "../../compile/codegen" @@ -14,9 +15,10 @@ const def: CodeKeywordDefinition = { type: "number", schemaType: "number", $data: true, - code({fail, keyword, data, $data, schemaCode}) { + code(cxt: KeywordContext) { + const {keyword, data, $data, schemaCode} = cxt const dnt = dataNotType(schemaCode, def.schemaType, $data) - fail(dnt + data + OPS[keyword].fail + schemaCode + ` || isNaN(${data})`) + cxt.fail(dnt + data + OPS[keyword].fail + schemaCode + ` || isNaN(${data})`) }, error: { message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].ok} ${schemaCode}`, diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index ef019b9056..859128624c 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {dataNotType} from "../util" import {_, str} from "../../compile/codegen" @@ -7,10 +8,11 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: "number", $data: true, - code({fail, keyword, data, $data, schemaCode}) { + code(cxt: KeywordContext) { + const {keyword, data, $data, schemaCode} = cxt const op = keyword === "maxItems" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) - fail(dnt + `${data}.length` + op + schemaCode) + cxt.fail(dnt + `${data}.length` + op + schemaCode) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index cb0f91b083..ec607a2d55 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {dataNotType} from "../util" import {_, str} from "../../compile/codegen" @@ -7,11 +8,12 @@ const def: CodeKeywordDefinition = { type: "string", schemaType: "number", $data: true, - code({fail, keyword, data, $data, schemaCode, it: {opts}}) { + code(cxt: KeywordContext) { + const {keyword, data, $data, schemaCode, it} = cxt const op = keyword === "maxLength" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) - const len = opts.unicode === false ? `${data}.length` : `ucs2length(${data})` - fail(dnt + len + op + schemaCode) + const len = it.opts.unicode === false ? `${data}.length` : `ucs2length(${data})` + cxt.fail(dnt + len + op + schemaCode) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index ccc6c17edc..ac00a896a6 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {dataNotType} from "../util" import {_, str} from "../../compile/codegen" @@ -7,10 +8,11 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "number", $data: true, - code({fail, keyword, data, $data, schemaCode}) { + code(cxt: KeywordContext) { + const {keyword, data, $data, schemaCode} = cxt const op = keyword === "maxProperties" ? ">" : "<" const dnt = dataNotType(schemaCode, def.schemaType, $data) - fail(dnt + `Object.keys(${data}).length` + op + schemaCode) + cxt.fail(dnt + `Object.keys(${data}).length` + op + schemaCode) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index bcd1d8c0be..c03d74a9d5 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {dataNotType} from "../util" import {_, str} from "../../compile/codegen" @@ -7,14 +8,15 @@ const def: CodeKeywordDefinition = { type: "number", schemaType: "number", $data: true, - code({gen, fail, data, $data, schemaCode, it: {opts}}) { + code(cxt: KeywordContext) { + const {gen, data, $data, schemaCode, it} = cxt const dnt = dataNotType(schemaCode, def.schemaType, $data) const res = gen.let("res") - const prec = opts.multipleOfPrecision + const prec = it.opts.multipleOfPrecision const invalid = prec ? _`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : _`${res} !== parseInt(${res})` - fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) // TODO pass + cxt.fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) // TODO pass }, error: { message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 56afdbdde3..279a8003d5 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {dataNotType} from "../util" import {_, str} from "../../compile/codegen" @@ -7,10 +8,11 @@ const def: CodeKeywordDefinition = { type: "string", schemaType: "string", $data: true, - code({fail, data, $data, schema, schemaCode, it: {usePattern}}) { + code(cxt: KeywordContext) { + const {data, $data, schema, schemaCode, it} = cxt const dnt = dataNotType(schemaCode, def.schemaType, $data) - const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(schema) - fail(dnt + `!${regExp}.test(${data})`) // TODO pass? + const regExp = $data ? _`(new RegExp(${schemaCode}))` : it.usePattern(schema) + cxt.fail(dnt + `!${regExp}.test(${data})`) // TODO pass? }, error: { message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index b938e1650d..2bf78c46f3 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {reportError} from "../../compile/errors" @@ -19,8 +20,8 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: ["array"], $data: true, - code(cxt) { - const {gen, pass, errorParams, schema, schemaCode, data, $data, it} = cxt + code(cxt: KeywordContext) { + const {gen, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return const loopRequired = $data || schema.length >= it.opts.loopRequired @@ -48,7 +49,7 @@ const def: CodeKeywordDefinition = { function exitOnErrorMode(): void { const missing = gen.let("missing") - errorParams({missingProperty: missing}) + cxt.errorParams({missingProperty: missing}) if (loopRequired) { const valid = gen.let("valid", "true") @@ -65,7 +66,7 @@ const def: CodeKeywordDefinition = { loopUntilMissing(missing, valid) } - pass(valid) + cxt.pass(valid) } else { gen.if(`${checkMissingProp(cxt, schema, missing)}`) reportMissingProp(cxt, missing, error) @@ -75,7 +76,7 @@ const def: CodeKeywordDefinition = { function loopAllRequired(): void { const prop = gen.name("prop") - errorParams({missingProperty: prop}) + cxt.errorParams({missingProperty: prop}) gen.for(`const ${prop} of ${schemaCode}`, () => gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => reportError(cxt, error)) ) diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 6aff26afe0..b52653a4b0 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,4 +1,5 @@ import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" import {checkDataType, checkDataTypes} from "../../compile/util" import {_, str} from "../../compile/codegen" @@ -7,12 +8,13 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: "boolean", $data: true, - code({gen, pass, errorParams, data, $data, schema, parentSchema, schemaCode, it: {opts}}) { - if (opts.uniqueItems === false || !($data || schema)) return + code(cxt: KeywordContext) { + const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt + if (it.opts.uniqueItems === false || !($data || schema)) return const i = gen.let("i") const j = gen.let("j") const valid = gen.let("valid") - errorParams({i, j}) + cxt.errorParams({i, j}) const itemType = parentSchema.items?.type // TODO refactor to have two open blocks? same as in required @@ -24,7 +26,7 @@ const def: CodeKeywordDefinition = { validateUniqueItems() } - pass(valid) + cxt.pass(valid) function validateUniqueItems() { gen.code( @@ -45,7 +47,7 @@ const def: CodeKeywordDefinition = { const wrongType = (Array.isArray(itemType) ? checkDataTypes : checkDataType)( itemType, item, - opts.strictNumbers, + it.opts.strictNumbers, true ) const indices = gen.const("indices", "{}") From fa72bec5f49038ce643397e8f9d6cc1d3399186a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 28 Aug 2020 05:06:33 -0400 Subject: [PATCH 103/322] refactor: passing validation result from keyword "code" method --- lib/compile/codegen.ts | 5 +++ lib/compile/context.ts | 36 ++++++++++++------- lib/compile/validate/keyword.ts | 4 +-- .../applicator/additionalProperties.ts | 18 ++++------ lib/vocabularies/applicator/allOf.ts | 1 - lib/vocabularies/applicator/anyOf.ts | 21 +++++------ lib/vocabularies/applicator/contains.ts | 17 ++++----- lib/vocabularies/applicator/dependencies.ts | 35 +++++++++--------- lib/vocabularies/applicator/if.ts | 4 +-- lib/vocabularies/applicator/index.ts | 2 -- lib/vocabularies/applicator/items.ts | 28 +++++++++------ lib/vocabularies/applicator/not.ts | 19 +++++----- lib/vocabularies/applicator/oneOf.ts | 21 ++++++----- .../applicator/patternProperties.ts | 2 +- lib/vocabularies/applicator/propertyNames.ts | 13 +++---- lib/vocabularies/core/ref.ts | 9 ++--- lib/vocabularies/validation/enum.ts | 8 +++-- lib/vocabularies/validation/required.ts | 6 ++-- 18 files changed, 132 insertions(+), 117 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 3fcb5ddb74..7ed346c14b 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -133,6 +133,11 @@ export default class CodeGen { return this } + ifNot(condition: Expression, thenBody?: Block, elseBody?: Block): CodeGen { + const cond = condition instanceof Name ? condition : `(${condition})` + return this.if(`!${cond}`, thenBody, elseBody) + } + elseIf(condition: Expression): CodeGen { if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"') this.code(`}else if(${condition}){`) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index f54c98a379..27a9370ef3 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -51,25 +51,37 @@ export default class KeywordContext implements KeywordErrorContext { } } - fail(cond?: Expression, failAction?: () => void, context?: KeywordErrorContext): void { - if (cond) { - this.gen.if(cond) - failAction ? failAction() : this._reportError(context) + result(condition: Expression, successAction?: () => void, failAction?: () => void): void { + this.gen.ifNot(condition) + this._actionOrError(failAction) + if (successAction) { + this.gen.else() + successAction() if (this.allErrors) this.gen.endIf() - else this.gen.else() } else { - failAction ? failAction() : this._reportError(context) - if (!this.allErrors) this.gen.if("false") + if (this.allErrors) this.gen.endIf() + else this.gen.else() } } - _reportError(context?: KeywordErrorContext) { - reportError(context || this, this.def.error || keywordError) + pass(condition: Expression, failAction?: () => void): void { + this.result(condition, undefined, failAction) + } + + fail(condition?: Expression, failAction?: () => void): void { + if (condition) { + this.gen.if(condition) + this._actionOrError(failAction) + if (this.allErrors) this.gen.endIf() + else this.gen.else() + } else { + this._actionOrError(failAction) + if (!this.allErrors) this.gen.if("false") // TODO some other way to disable branch? + } } - pass(cond: Expression, failAction?: () => void, context?: KeywordErrorContext): void { - cond = cond instanceof Name ? cond : `(${cond})` - this.fail(`!${cond}`, failAction, context) + _actionOrError(failAction?: () => void): void { + failAction ? failAction() : reportError(this, this.def.error || keywordError) } ok(cond: Expression): void { diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 791b235961..38c1388187 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -71,7 +71,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { assignValid() if (def.modifying) modifyData(cxt) }) - if (!def.valid) cxt.fail(`!${valid}`) + if (!def.valid) cxt.pass(valid) } function validateRuleWithErrors(): void { @@ -135,7 +135,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { addKeywordErrors(cxt, ruleErrs, errsCount) return cxt.ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? default: - cxt.fail(`!${valid}`, () => addKeywordErrors(cxt, ruleErrs, errsCount)) + cxt.pass(valid, () => addKeywordErrors(cxt, ruleErrs, errsCount)) } } } diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index e2dd5c2995..29fc14fedf 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -25,15 +25,9 @@ const def: CodeKeywordDefinition = { error, code(cxt: KeywordContext) { const {gen, schema, parentSchema, data, it} = cxt - const { - allErrors, - usePattern, - opts: {removeAdditional}, - } = it + const {allErrors, usePattern, opts} = it - if ((schema === undefined || alwaysValidSchema(it, schema)) && removeAdditional !== "all") { - return - } + if (alwaysValidSchema(it, schema) && opts.removeAdditional !== "all") return const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) @@ -70,7 +64,7 @@ const def: CodeKeywordDefinition = { } function additionalPropertyCode(key: Name): void { - if (removeAdditional === "all" || (removeAdditional && schema === false)) { + if (opts.removeAdditional === "all" || (opts.removeAdditional && schema === false)) { deleteAdditional(key) return } @@ -84,15 +78,15 @@ const def: CodeKeywordDefinition = { if (typeof schema == "object" && !alwaysValidSchema(it, schema)) { const valid = gen.name("valid") - if (removeAdditional === "failing") { + if (opts.removeAdditional === "failing") { applyAdditionalSchema(key, valid, false) - gen.if(`!${valid}`, () => { + gen.ifNot(valid, () => { resetErrorsCount(gen, errsCount) deleteAdditional(key) }) } else { applyAdditionalSchema(key, valid) - if (!allErrors) gen.if(`!${valid}`, "break") + if (!allErrors) gen.ifNot(valid, "break") } } } diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index c9ac0181f8..0529055781 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -14,7 +14,6 @@ const def: CodeKeywordDefinition = { applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) cxt.ok(valid) }) - // TODO possibly add allOf error }, } diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 015e1d9c72..3068d3625f 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -6,9 +6,14 @@ import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_} from "../../compile/codegen" import N from "../../compile/names" +const error: KeywordErrorDefinition = { + message: "should match some schema in anyOf", +} + const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", + error, code(cxt: KeywordContext) { const {gen, schema, it} = cxt const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) @@ -30,19 +35,15 @@ const def: CodeKeywordDefinition = { schValid ) gen.code(_`${valid} = ${valid} || ${schValid};`) - gen.if(_`!${valid}`) + gen.ifNot(valid) }) }, schema.length) - // TODO refactor failCompoundOrReset? - gen.if(`!${valid}`) - reportExtraError(cxt, def.error as KeywordErrorDefinition) - gen.else() - resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.endIf() - }, - error: { - message: "should match some schema in anyOf", + cxt.result( + valid, + () => resetErrorsCount(gen, errsCount), + () => reportExtraError(cxt, error) + ) }, } diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index f6a736f230..54233c9280 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -2,14 +2,19 @@ import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" -import {reportError, resetErrorsCount} from "../../compile/errors" +import {resetErrorsCount} from "../../compile/errors" import N from "../../compile/names" +const error: KeywordErrorDefinition = { + message: "should contain a valid item", +} + const def: CodeKeywordDefinition = { keyword: "contains", type: "array", schemaType: ["object", "boolean"], before: "uniqueItems", + error, code(cxt: KeywordContext) { const {gen, schema, data, it} = cxt const errsCount = gen.const("_errs", N.errors) @@ -35,15 +40,7 @@ const def: CodeKeywordDefinition = { gen.if(valid, "break") }) - // TODO refactor failCompoundOrReset? It is different from anyOf though - gen.if(`!${valid}`) - reportError(cxt, def.error as KeywordErrorDefinition) - gen.else() - resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.endIf() - }, - error: { - message: "should contain a valid item", + cxt.result(valid, () => resetErrorsCount(gen, errsCount)) }, } diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 25e75f5994..3529397f2f 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -12,10 +12,23 @@ interface SchemaDependencies { [x: string]: object | boolean } +const error: KeywordErrorDefinition = { + message: ({params: {property, depsCount, deps}}) => { + const property_ies = depsCount === 1 ? "property" : "properties" + return str`should have ${property_ies} ${deps} when property ${property} is present` + }, + params: ({params: {property, depsCount, deps, missingProperty}}) => + _`{property: ${property}, + missingProperty: ${missingProperty}, + depsCount: ${depsCount}, + deps: ${deps}}`, // TODO change to reference? +} + const def: CodeKeywordDefinition = { keyword: "dependencies", type: "object", schemaType: "object", + error, code(cxt: KeywordContext) { const {gen, schema, data, it} = cxt const [propDeps, schDeps] = splitDependencies() @@ -35,6 +48,8 @@ const def: CodeKeywordDefinition = { } function validatePropertyDeps(propertyDeps: {[x: string]: string[]}): void { + if (Object.keys(propertyDeps).length === 0) return + const missing = gen.let("missing") for (const prop in propertyDeps) { const deps = propertyDeps[prop] if (deps.length === 0) continue @@ -47,15 +62,12 @@ const def: CodeKeywordDefinition = { if (it.allErrors) { gen.if(hasProperty, () => { for (const depProp of deps) { - checkReportMissingProp(cxt, depProp, def.error as KeywordErrorDefinition) + checkReportMissingProp(cxt, depProp, error) } }) } else { - // TODO refactor: maybe use one variable for all dependencies - // or not use this variable at all? - const missing = gen.let("missing") gen.if(`${hasProperty} && (${checkMissingProp(cxt, deps, missing)})`) - reportMissingProp(cxt, missing, def.error as KeywordErrorDefinition) + reportMissingProp(cxt, missing, error) gen.else() } } @@ -67,23 +79,12 @@ const def: CodeKeywordDefinition = { gen.if( propertyInData(data, prop, it.opts.ownProperties), () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), - `var ${valid} = true;` // TODO refactor var + () => gen.var(valid, true) // TODO var ) cxt.ok(valid) } } }, - error: { - message: ({params: {property, depsCount, deps}}) => { - const property_ies = depsCount === 1 ? "property" : "properties" - return str`should have ${property_ies} ${deps} when property ${property} is present` - }, - params: ({params: {property, depsCount, deps, missingProperty}}) => - _`{property: ${property}, - missingProperty: ${missingProperty}, - depsCount: ${depsCount}, - deps: ${deps}}`, // TODO change to reference? - }, } module.exports = def diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index c098b8a071..22b3aad8dc 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -16,6 +16,7 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "boolean"], // TODO // implements: ["then", "else"], + error, code(cxt: KeywordContext) { const {gen, it} = cxt const hasThen = hasSchema(it, "then") @@ -39,7 +40,7 @@ const def: CodeKeywordDefinition = { } else if (hasThen) { gen.if(schValid, validateClause("then")) } else { - gen.if(`!${schValid}`, validateClause("else")) + gen.ifNot(schValid, validateClause("else")) } cxt.pass(valid, () => reportExtraError(cxt, error)) @@ -66,7 +67,6 @@ const def: CodeKeywordDefinition = { } } }, - error, } module.exports = def diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 205f12ac62..50eae6c9c2 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -19,5 +19,3 @@ const applicator: Vocabulary = [ ] export default applicator - -module.exports = applicator diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index ce004c0098..9a3bdfb1a8 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,9 +1,15 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" +import {reportError} from "../../compile/errors" import {_, str} from "../../compile/codegen" +const additionalItemsError: KeywordErrorDefinition = { + message: ({schema}) => str`should NOT have more than ${schema.length as number} items`, + params: ({schema}) => _`{limit: ${schema.length as number}}`, +} + const def: CodeKeywordDefinition = { keyword: "items", type: "array", @@ -32,11 +38,15 @@ const def: CodeKeywordDefinition = { } function validateDataLength(sch: (object | boolean)[]): void { - cxt.fail(`${len} > ${sch.length}`, undefined, { - ...cxt, - keyword: "additionalItems", - schemaValue: false, - }) + cxt.result( + `${len} <= ${sch.length}`, + () => {}, + () => + reportError( + {...cxt, keyword: "additionalItems", schemaValue: false}, + additionalItemsError + ) + ) } function validateDefinedItems(): void { @@ -64,15 +74,11 @@ const def: CodeKeywordDefinition = { const valid = gen.name("valid") gen.for(_`let ${i}=${startFrom}; ${i}<${len}; ${i}++`, () => { applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) - if (!it.allErrors) gen.if(`!${valid}`, "break") + if (!it.allErrors) gen.ifNot(valid, "break") }) cxt.ok(valid) } }, - error: { - message: ({schema}) => str`should NOT have more than ${schema.length as number} items`, - params: ({schema}) => _`{limit: ${schema.length as number}}`, - }, } module.exports = def diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 169ab3f086..394e2231b8 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -5,6 +5,10 @@ import {applySubschema} from "../../compile/subschema" import {reportError, resetErrorsCount} from "../../compile/errors" import N from "../../compile/names" +const error: KeywordErrorDefinition = { + message: "should NOT be valid", +} + const def: CodeKeywordDefinition = { keyword: "not", schemaType: ["object", "boolean"], @@ -28,16 +32,11 @@ const def: CodeKeywordDefinition = { valid ) - // TODO refactor failCompoundOrReset? - // TODO refactor ifs - gen.if(valid) - reportError(cxt, def.error as KeywordErrorDefinition) - gen.else() - resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.endIf() - }, - error: { - message: "should NOT be valid", + cxt.result( + valid, + () => reportError(cxt, error), + () => resetErrorsCount(gen, errsCount) + ) }, } diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 235fe9e969..de3e67788c 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -6,6 +6,11 @@ import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_} from "../../compile/codegen" import N from "../../compile/names" +const error: KeywordErrorDefinition = { + message: "should match exactly one schema in oneOf", + params: ({params}) => _`{passingSchemas: ${params.passing}}`, +} + const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", @@ -20,13 +25,11 @@ const def: CodeKeywordDefinition = { gen.block(validateOneOf) - // TODO refactor failCompoundOrReset? - // TODO refactor ifs - gen.if(_`!${valid}`) - reportExtraError(cxt, def.error as KeywordErrorDefinition) - gen.else() - resetErrorsCount(gen, errsCount) - if (it.allErrors) gen.endIf() + cxt.result( + valid, + () => resetErrorsCount(gen, errsCount), + () => reportExtraError(cxt, error) + ) function validateOneOf() { schema.forEach((sch, i: number) => { @@ -58,10 +61,6 @@ const def: CodeKeywordDefinition = { }) } }, - error: { - message: "should match exactly one schema in oneOf", - params: ({params}) => _`{passingSchemas: ${params.passing}}`, - }, } module.exports = def diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 680773e630..14c9fce267 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -39,7 +39,7 @@ const def: CodeKeywordDefinition = { }, valid ) - if (!it.allErrors) gen.if(`!${valid}`, "break") + if (!it.allErrors) gen.ifNot(valid, "break") }) }) } diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 33476f7477..18f6669b03 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -5,6 +5,11 @@ import {applySubschema} from "../../compile/subschema" import {reportExtraError} from "../../compile/errors" import {_, str} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? + params: ({params}) => _`{propertyName: ${params.propertyName}}`, +} + const def: CodeKeywordDefinition = { keyword: "propertyNames", type: "object", @@ -21,18 +26,14 @@ const def: CodeKeywordDefinition = { {keyword: "propertyNames", data: key, propertyName: key, compositeRule: true}, valid ) - gen.if(`!${valid}`, () => { - reportExtraError(cxt, def.error as KeywordErrorDefinition) + gen.ifNot(valid, () => { + reportExtraError(cxt, error) if (!it.allErrors) gen.break() }) }) cxt.ok(valid) }, - error: { - message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? - params: ({params}) => _`{propertyName: ${params.propertyName}}`, - }, } module.exports = def diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index e1d2208596..54128e1018 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -34,7 +34,8 @@ const def: CodeKeywordDefinition = { switch (opts.missingRefs) { case "fail": logger.error(msg) - return cxt.fail() + cxt.fail() + return case "ignore": logger.warn(msg) return @@ -64,19 +65,19 @@ const def: CodeKeywordDefinition = { gen.try( () => { gen.code(`await ${callValidate(cxt, v, passCxt)};`) - if (!allErrors) gen.code(`${valid} = true;`) + if (!allErrors) gen.assign(valid, true) }, (e) => { gen.if(_`!(${e} instanceof ValidationError)`, `throw ${e}`) addErrorsFrom(e) - if (!allErrors) gen.code(`${valid} = false;`) + if (!allErrors) gen.assign(valid, false) } ) cxt.ok(valid) } function validateRef(v: Expression): void { - cxt.fail(`!${callValidate(cxt, v, passCxt)}`, () => addErrorsFrom(v)) + cxt.pass(callValidate(cxt, v, passCxt), () => addErrorsFrom(v)) } function addErrorsFrom(source: Expression): void { diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 98ed4f1aff..73ecda5c6a 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -12,14 +12,16 @@ const def: CodeKeywordDefinition = { const {opts} = it if ($data) { const valid = gen.let("valid") - gen.if(`${schemaCode} === undefined`, `${valid} = true;`, () => - gen.code(`${valid} = false;`).if(`Array.isArray(${schemaCode})`, () => loopEnum(valid)) + gen.if( + `${schemaCode} === undefined`, + () => gen.assign(valid, true), + () => gen.assign(valid, false).if(`Array.isArray(${schemaCode})`, () => loopEnum(valid)) ) cxt.pass(valid) } else { if (schema.length === 0) throw new Error("enum must have non-empty array") if (schema.length > (opts.loopEnum as number)) { - const valid = gen.let("valid", "false") + const valid = gen.let("valid", false) loopEnum(valid) cxt.pass(valid) } else { diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 2bf78c46f3..4a273935fe 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -52,7 +52,7 @@ const def: CodeKeywordDefinition = { cxt.errorParams({missingProperty: missing}) if (loopRequired) { - const valid = gen.let("valid", "true") + const valid = gen.let("valid", true) // TODO refactor and enable/fix test in errors.spec.js line 301 // it can be simpler once blocks are globally supported - endIf can be removed, so there will be 2 open blocks @@ -85,8 +85,8 @@ const def: CodeKeywordDefinition = { function loopUntilMissing(missing: Name, valid: Name): void { gen.for(`${missing} of ${schemaCode}`, () => gen - .code(`${valid} = ${propertyInData(data, missing, it.opts.ownProperties)};`) - .if(`!${valid}`, "break") + .assign(valid, propertyInData(data, missing, it.opts.ownProperties)) + .ifNot(valid, "break") ) } }, From 089c492c505ee129122c2b68f3700646dae10002 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 28 Aug 2020 06:14:57 -0400 Subject: [PATCH 104/322] fix: option {removeAdditional: all} without additionalProperties --- lib/compile/codegen.ts | 4 +- lib/compile/context.ts | 51 +++++++++---------- lib/compile/validate/keyword.ts | 2 +- .../applicator/additionalProperties.ts | 4 +- lib/vocabularies/applicator/items.ts | 10 +--- lib/vocabularies/applicator/properties.ts | 10 ++-- ...5_removeAdditional_custom_keywords.spec.js | 2 +- 7 files changed, 38 insertions(+), 45 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 7ed346c14b..8daa9a54d0 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -120,7 +120,7 @@ export default class CodeGen { return this } - if(condition: Expression, thenBody?: Block, elseBody?: Block): CodeGen { + if(condition: Expression | boolean, thenBody?: Block, elseBody?: Block): CodeGen { this.#blocks.push(BlockKind.If) this.code(`if(${condition}){`) if (thenBody && elseBody) { @@ -133,7 +133,7 @@ export default class CodeGen { return this } - ifNot(condition: Expression, thenBody?: Block, elseBody?: Block): CodeGen { + ifNot(condition: Expression | boolean, thenBody?: Block, elseBody?: Block): CodeGen { const cond = condition instanceof Name ? condition : `(${condition})` return this.if(`!${cond}`, thenBody, elseBody) } diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 27a9370ef3..826abef13a 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -16,38 +16,35 @@ export default class KeywordContext implements KeywordErrorContext { data: Name $data?: string | false schema: any - schemaCode: Expression | number | boolean - schemaValue: Expression | number | boolean + schemaValue: Expression | number | boolean // Code reference to keyword schema value or primitive value + schemaCode: Expression | number | boolean // Code reference to resolved schema value (different if schema is $data) parentSchema: any params: KeywordContextParams it: CompilationContext def: KeywordDefinition - constructor(it: CompilationContext, keyword: string, def: KeywordDefinition) { - const schema = it.schema[keyword] - const {schemaType, $data: $defData} = def - validateKeywordSchema(it, keyword, def) - // TODO - // if (!code) throw new Error('"code" and "error" must be defined') - const $data = $defData && it.opts.$data && schema && schema.$data - const schemaValue = schemaRefOrVal(it, schema, keyword, $data) + constructor(it: CompilationContext, def: KeywordDefinition, keyword: string) { + validateKeywordUsage(it, def, keyword) this.gen = it.gen this.allErrors = it.allErrors this.keyword = keyword this.data = it.data - this.$data = $data - this.schema = schema - this.schemaCode = $data ? it.gen.name("schema") : schemaValue // reference to resolved schema value - this.schemaValue = schemaValue // actual schema reference or value for primitive values + this.schema = it.schema[keyword] + this.$data = def.$data && it.opts.$data && this.schema && this.schema.$data + this.schemaValue = schemaRefOrVal(it, this.schema, keyword, this.$data) this.parentSchema = it.schema this.params = {} this.it = it this.def = def - if ($data) { - it.gen.const(this.schemaCode, `${getData($data, it)}`) - } else if (schemaType && !validSchemaType(schema, schemaType)) { - throw new Error(`${keyword} must be ${JSON.stringify(schemaType)}`) + if (this.$data) { + this.schemaCode = it.gen.name("schema") + it.gen.const(this.schemaCode, `${getData(this.$data, it)}`) + } else { + this.schemaCode = this.schemaValue + if (def.schemaType && !validSchemaType(this.schema, def.schemaType)) { + throw new Error(`${keyword} value must be ${JSON.stringify(def.schemaType)}`) + } } } @@ -68,15 +65,15 @@ export default class KeywordContext implements KeywordErrorContext { this.result(condition, undefined, failAction) } - fail(condition?: Expression, failAction?: () => void): void { + fail(condition?: Expression): void { if (condition) { this.gen.if(condition) - this._actionOrError(failAction) + reportError(this, this.def.error || keywordError) if (this.allErrors) this.gen.endIf() else this.gen.else() } else { - this._actionOrError(failAction) - if (!this.allErrors) this.gen.if("false") // TODO some other way to disable branch? + reportError(this, this.def.error || keywordError) + if (!this.allErrors) this.gen.if(false) // TODO some other way to disable branch? } } @@ -88,7 +85,7 @@ export default class KeywordContext implements KeywordErrorContext { if (!this.allErrors) this.gen.if(cond) } - errorParams(obj: KeywordContextParams, assign?: true) { + errorParams(obj: KeywordContextParams, assign?: true): void { if (assign) Object.assign(this.params, obj) else this.params = obj } @@ -106,10 +103,10 @@ function validSchemaType(schema: any, schemaType: string | string[]): boolean { : typeof schema == schemaType } -function validateKeywordSchema( +function validateKeywordUsage( it: CompilationContext, - keyword: string, - def: KeywordDefinition + def: KeywordDefinition, + keyword: string ): void { const deps = def.dependencies if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(it.schema, kwd))) { @@ -119,7 +116,7 @@ function validateKeywordSchema( if (def.validateSchema) { const valid = def.validateSchema(it.schema[keyword]) if (!valid) { - const msg = "keyword schema is invalid: " + it.self.errorsText(def.validateSchema.errors) + const msg = "keyword value is invalid: " + it.self.errorsText(def.validateSchema.errors) if (it.opts.validateSchema === "log") it.logger.error(msg) else throw new Error(msg) } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 38c1388187..b4892fa454 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -18,7 +18,7 @@ export function keywordCode( def: KeywordDefinition, ruleType?: string ): void { - const cxt = new KeywordContext(it, keyword, def) + const cxt = new KeywordContext(it, def, keyword) if ("code" in def) { def.code(cxt, ruleType) } else if (cxt.$data && "validate" in def) { diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 29fc14fedf..167a3ebb5a 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -21,7 +21,7 @@ const error: KeywordErrorDefinition = { const def: CodeKeywordDefinition = { keyword: "additionalProperties", type: "object", - schemaType: ["object", "boolean"], + schemaType: ["object", "boolean", "undefined"], // "undefined" is needed to support option removeAdditional: "all" error, code(cxt: KeywordContext) { const {gen, schema, parentSchema, data, it} = cxt @@ -110,3 +110,5 @@ const def: CodeKeywordDefinition = { } module.exports = def + +export default def diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 9a3bdfb1a8..5bb35eaed9 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -38,14 +38,8 @@ const def: CodeKeywordDefinition = { } function validateDataLength(sch: (object | boolean)[]): void { - cxt.result( - `${len} <= ${sch.length}`, - () => {}, - () => - reportError( - {...cxt, keyword: "additionalItems", schemaValue: false}, - additionalItemsError - ) + cxt.pass(`${len} <= ${sch.length}`, () => + reportError({...cxt, keyword: "additionalItems", schemaValue: false}, additionalItemsError) ) } diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 9f362e78eb..df9354ff4e 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -2,17 +2,17 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {schemaProperties, propertyInData} from "../util" import {applySubschema, Expr} from "../../compile/subschema" +import apDef from "./additionalProperties" const def: CodeKeywordDefinition = { keyword: "properties", type: "object", schemaType: "object", code(cxt: KeywordContext) { - const {gen, schema, data, it} = cxt - // TODO - // if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { - // remove all additional properties - it will fix skipped tests - // } + const {gen, schema, parentSchema, data, it} = cxt + if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { + apDef.code(new KeywordContext(it, apDef, "additionalProperties")) + } const properties = schemaProperties(it, schema) if (properties.length === 0) return const valid = gen.name("valid") diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js index 47823ae402..949918a7d9 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -4,7 +4,7 @@ var Ajv = require("../ajv") require("../chai").should() describe("issue #955: option removeAdditional breaks custom keywords", () => { - it.skip("should support custom keywords with option removeAdditional", () => { + it("should support custom keywords with option removeAdditional", () => { var ajv = new Ajv({removeAdditional: "all"}) ajv.addKeyword("minTrimmedLength", { From 43cec7b05cdd8a2b1f1968b614aeff2ced9a4f25 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 28 Aug 2020 08:24:37 -0400 Subject: [PATCH 105/322] test: replace old "inline" keyword with "code" keyword tests --- lib/compile/context.ts | 10 +- lib/compile/util.ts | 2 +- lib/keyword.ts | 2 - lib/vocabularies/validation/enum.ts | 2 +- spec/ajv.spec.js | 19 +-- spec/custom.spec.js | 187 ++++++++++++-------------- spec/options/removeAdditional.spec.js | 2 +- 7 files changed, 107 insertions(+), 117 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 826abef13a..86cc6a0378 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -7,7 +7,7 @@ import { import {schemaRefOrVal} from "../vocabularies/util" import {getData} from "./util" import {reportError, keywordError} from "./errors" -import CodeGen, {_, Name, Expression} from "./codegen" +import CodeGen, {Name, Expression} from "./codegen" export default class KeywordContext implements KeywordErrorContext { gen: CodeGen @@ -68,15 +68,19 @@ export default class KeywordContext implements KeywordErrorContext { fail(condition?: Expression): void { if (condition) { this.gen.if(condition) - reportError(this, this.def.error || keywordError) + this.error() if (this.allErrors) this.gen.endIf() else this.gen.else() } else { - reportError(this, this.def.error || keywordError) + this.error() if (!this.allErrors) this.gen.if(false) // TODO some other way to disable branch? } } + error(): void { + reportError(this, this.def.error || keywordError) + } + _actionOrError(failAction?: () => void): void { failAction ? failAction() : reportError(this, this.def.error || keywordError) } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 6e419e62f1..1a0a6ed633 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,4 +1,4 @@ -import {_, Code, Name, Expression} from "./codegen" +import {Code, Name, Expression} from "./codegen" import {CompilationContext} from "../types" import N from "./names" diff --git a/lib/keyword.ts b/lib/keyword.ts index a3b59328e1..cbb066e86c 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,8 +1,6 @@ import {KeywordDefinition, Vocabulary, ErrorObject, ValidateFunction} from "./types" - import {ValidationRules, Rule} from "./compile/rules" import {definitionSchema} from "./definition_schema" -import {_} from "./compile/codegen" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 73ecda5c6a..7305dbb41d 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -26,7 +26,7 @@ const def: CodeKeywordDefinition = { cxt.pass(valid) } else { const vSchema = gen.const("schema", schemaCode) - const cond: string = orExpr(schema, (_, i) => equalCode(vSchema, i)) + const cond: string = orExpr(schema, (_x, i) => equalCode(vSchema, i)) cxt.pass(cond) } } diff --git a/spec/ajv.spec.js b/spec/ajv.spec.js index efed36b43a..4cc63a89e7 100644 --- a/spec/ajv.spec.js +++ b/spec/ajv.spec.js @@ -4,6 +4,9 @@ var Ajv = require("./ajv"), should = require("./chai").should(), stableStringify = require("fast-json-stable-stringify") +const codegen = require("../dist/compile/codegen") +const {_} = codegen + describe("Ajv", () => { var ajv @@ -51,22 +54,22 @@ describe("Ajv", () => { }) }) - // TODO replace with custom "code" keyword - it.skip("should throw if compiled schema has an invalid JavaScript code", () => { - ajv.addKeyword("even", {inline: badEvenCode}) + it("should throw if compiled schema has an invalid JavaScript code", () => { + const _ajv = new Ajv({logger: false}) + _ajv.addKeyword({keyword: "even", code: badEvenCode}) var schema = {even: true} - var validate = ajv.compile(schema) + var validate = _ajv.compile(schema) validate(2).should.equal(true) validate(3).should.equal(false) schema = {even: false} should.throw(() => { - ajv.compile(schema) + _ajv.compile(schema) }) - function badEvenCode(it, keyword, _schema) { - var op = _schema ? "===" : "!===" // invalid on purpose - return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" + function badEvenCode(cxt) { + var op = cxt.schema ? _`===` : _`!===` // invalid on purpose + cxt.pass(_`${cxt.data} % 2 ${op} 0`) } }) }) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 18bf89e40b..4cc64d3720 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -4,6 +4,9 @@ var getAjvInstances = require("./ajv_instances"), should = require("./chai").should(), equal = require("../dist/compile/equal") +const codegen = require("../dist/compile/codegen") +const {_, nil} = codegen + describe("Custom keywords", () => { var ajv, instances @@ -19,7 +22,7 @@ describe("Custom keywords", () => { describe("custom rules", () => { describe('rule with "interpreted" keyword validation', () => { it("should add and validate rule", () => { - testEvenKeyword({type: "number", validate: validateEven}) + testEvenKeyword({keyword: "x-even", type: "number", validate: validateEven}) function validateEven(schema, data) { if (typeof schema != "boolean") { @@ -31,6 +34,7 @@ describe("Custom keywords", () => { it("should add, validate keyword schema and validate rule", () => { testEvenKeyword({ + keyword: "x-even", type: "number", validate: validateEven, metaSchema: {type: "boolean"}, @@ -118,7 +122,11 @@ describe("Custom keywords", () => { describe('rule with "compiled" keyword validation', () => { it("should add and validate rule", () => { - testEvenKeyword({type: "number", compile: compileEven}) + testEvenKeyword({ + keyword: "x-even", + type: "number", + compile: compileEven, + }) shouldBeInvalidSchema({"x-even": "not_boolean"}) function compileEven(schema) { @@ -138,6 +146,7 @@ describe("Custom keywords", () => { it("should add, validate keyword schema and validate rule", () => { testEvenKeyword({ + keyword: "x-even", type: "number", compile: compileEven, metaSchema: {type: "boolean"}, @@ -198,7 +207,7 @@ describe("Custom keywords", () => { describe("macro rules", () => { it('should add and validate rule with "macro" keyword', () => { - testEvenKeyword({type: "number", macro: macroEven}, 2) + testEvenKeyword({keyword: "x-even", type: "number", macro: macroEven}, 2) }) it("should add and expand macro rule", () => { @@ -497,73 +506,63 @@ describe("Custom keywords", () => { } }) - // TODO replace with custom "code" keywords - describe.skip("inline rules", () => { - it('should add and validate rule with "inline" code keyword', () => { - testEvenKeyword({type: "number", inline: inlineEven}) + describe("code rules", () => { + it('should add and validate rule with "code" keyword', () => { + testEvenKeyword({ + keyword: "x-even", + type: "number", + code(cxt) { + const {schema, data} = cxt + const op = schema ? _`===` : _`!==` + cxt.pass(_`${data} % 2 ${op} 0`) + }, + }) }) it('should pass parent schema to "inline" keyword', () => { - testRangeKeyword({type: "number", inline: inlineRange, statements: true}) - }) - - it('should define "inline" keyword as template', () => { - // var inlineRangeTemplate = customRules.range - // testRangeKeyword({ - // type: "number", - // inline: inlineRangeTemplate, - // statements: true, - // }) - }) - - it('should define "inline" keyword without errors', () => { - // var inlineRangeTemplate = customRules.range - // testRangeKeyword({ - // type: "number", - // inline: inlineRangeTemplate, - // statements: true, - // errors: false, - // }) - }) - - it("should allow defining optional errors", () => { - // var inlineRangeTemplate = customRules.rangeWithErrors - // testRangeKeyword( - // { - // type: "number", - // inline: inlineRangeTemplate, - // statements: true, - // }, - // true - // ) + testRangeKeyword({ + keyword: "x-range", + type: "number", + code(cxt) { + const { + schema: [min, max], + parentSchema, + data, + } = cxt + const eq = parentSchema.exclusiveRange ? nil : _`=` + cxt.pass(_`${data} >${eq} ${min} && ${data} <${eq} ${max}`) + }, + }) }) - it("should allow defining required errors", () => { - // var inlineRangeTemplate = customRules.rangeWithErrors - // testRangeKeyword( - // { - // type: "number", - // inline: inlineRangeTemplate, - // statements: true, - // errors: true, - // }, - // true - // ) + it("should allow defining keyword error", () => { + testRangeKeyword({ + keyword: "x-range", + type: "number", + code(cxt) { + const { + gen, + schema: [min, max], + parentSchema, + data, + } = cxt + const eq = parentSchema.exclusiveRange ? nil : _`=` + const minOk = gen.const("minOk", _`${data} >${eq} ${min}`) + const maxOk = gen.const("maxOk", _`${data} <${eq} ${max}`) + cxt.errorParams({minOk, maxOk, eq}) + cxt.pass(`${minOk} && ${maxOk}`) + }, + error: { + message: ({params: {minOk, eq}, schema: [min, max]}) => + _`${minOk} ? "should be <${eq} ${max}" : "should be >${eq} ${min}"`, + params: ({params: {minOk, eq}, schema: [min, max], parentSchema}) => _`{ + comparison: ${minOk} ? "<${eq}" : ">${eq}", + limit: ${minOk} ? ${max} : ${min}, + exclusive: ${!!parentSchema.exclusiveRange} + }`, + }, + }) }) - - function inlineEven(it, keyword, schema) { - var op = schema ? "===" : "!==" - return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" - } - - function inlineRange(it, keyword, schema, parentSchema) { - var min = schema[0], - max = schema[1], - data = "data" + (it.dataLevel || ""), - gt = parentSchema.exclusiveRange ? " > " : " >= ", - lt = parentSchema.exclusiveRange ? " < " : " <= " - return "var valid" + it.level + " = " + data + gt + min + " && " + data + lt + max + ";" - } }) describe("$data reference support with custom keywords (with $data option)", () => { @@ -581,6 +580,7 @@ describe("Custom keywords", () => { it('should validate "interpreted" rule', () => { testEvenKeyword$data({ + keyword: "x-even-$data", type: "number", $data: true, validate: validateEven, @@ -595,6 +595,7 @@ describe("Custom keywords", () => { it('should validate rule with "compile" and "validate" funcs', () => { var compileCalled testEvenKeyword$data({ + keyword: "x-even-$data", type: "number", $data: true, compile: compileEven, @@ -626,6 +627,7 @@ describe("Custom keywords", () => { it('should validate with "compile" and "validate" funcs with meta-schema', () => { var compileCalled testEvenKeyword$data({ + keyword: "x-even-$data", type: "number", $data: true, compile: compileEven, @@ -656,6 +658,7 @@ describe("Custom keywords", () => { var macroCalled testEvenKeyword$data( { + keyword: "x-even-$data", type: "number", $data: true, macro: macroEven, @@ -682,6 +685,7 @@ describe("Custom keywords", () => { var macroCalled testEvenKeyword$data( { + keyword: "x-even-$data", type: "number", $data: true, macro: macroEven, @@ -704,54 +708,35 @@ describe("Custom keywords", () => { } }) - // TODO replace with custom "code" keyword - it.skip('should validate rule with "inline" and "validate" funcs', () => { - var inlineCalled + it('should validate rule with "code" keyword', () => { testEvenKeyword$data({ + keyword: "x-even-$data", type: "number", $data: true, - inline: inlineEven, - validate: validateEven, + code(cxt) { + const {gen, schemaCode: s, data} = cxt + gen.if(_`${s} !== undefined`) + cxt.pass(_`typeof ${s} == "boolean" && (${data} % 2 ? !${s} : ${s})`) + }, }) - inlineCalled.should.equal(true) - - function validateEven(schema, data) { - if (typeof schema != "boolean") return false - return data % 2 ? !schema : schema - } - - function inlineEven(it, keyword, schema) { - inlineCalled = true - var op = schema ? "===" : "!==" - return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" - } }) - // TODO replace with custom "code" keyword - it.skip('should validate with "inline" and "validate" funcs with meta-schema', () => { - var inlineCalled + it('should validate with "code" and meta-schema', () => { testEvenKeyword$data({ + keyword: "x-even-$data", type: "number", $data: true, - inline: inlineEven, - validate: validateEven, + code(cxt) { + const {gen, schemaCode: s, data} = cxt + gen.if(_`${s} !== undefined`) + cxt.pass(_`typeof ${s} == "boolean" && (${data} % 2 ? !${s} : ${s})`) + }, metaSchema: {type: "boolean"}, }) - inlineCalled.should.equal(true) shouldBeInvalidSchema({"x-even-$data": "false"}) - - function validateEven(schema, data) { - return data % 2 ? !schema : schema - } - - function inlineEven(it, keyword, schema) { - inlineCalled = true - var op = schema ? "===" : "!==" - return "data" + (it.dataLevel || "") + " % 2 " + op + " 0" - } }) - it('should fail if keyword definition has "$data" but no "validate"', () => { + it('should fail if "macro" keyword definition has "$data" but no "validate"', () => { should.throw(() => { ajv.addKeyword("even", { type: "number", @@ -764,9 +749,9 @@ describe("Custom keywords", () => { }) }) - function testEvenKeyword(definition, numErrors) { + function testEvenKeyword(evenDefinition, numErrors) { instances.forEach((_ajv) => { - _ajv.addKeyword("x-even", definition) + _ajv.addKeyword(evenDefinition) var schema = {"x-even": true} var validate = _ajv.compile(schema) @@ -779,7 +764,7 @@ describe("Custom keywords", () => { function testEvenKeyword$data(definition, numErrors) { instances.forEach((_ajv) => { - _ajv.addKeyword("x-even-$data", definition) + _ajv.addKeyword(definition) var schema = {"x-even-$data": true} var validate = _ajv.compile(schema) diff --git a/spec/options/removeAdditional.spec.js b/spec/options/removeAdditional.spec.js index 64a6b55d4f..6e488b578b 100644 --- a/spec/options/removeAdditional.spec.js +++ b/spec/options/removeAdditional.spec.js @@ -4,7 +4,7 @@ var Ajv = require("../ajv") require("../chai").should() describe("removeAdditional option", () => { - it.skip("should remove all additional properties", () => { + it("should remove all additional properties", () => { var ajv = new Ajv({removeAdditional: "all"}) ajv.addSchema({ From 49733beaa2912ab2de8f2a0340d89dc48e002c6a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 28 Aug 2020 09:18:09 -0400 Subject: [PATCH 106/322] feat: KeywordContext methods for errors reporting and tracking --- lib/compile/context.ts | 21 +++++++---- lib/types.ts | 1 + .../applicator/additionalProperties.ts | 24 ++++++------- lib/vocabularies/applicator/anyOf.ts | 18 ++++------ lib/vocabularies/applicator/contains.ts | 16 ++++----- lib/vocabularies/applicator/dependencies.ts | 32 ++++++++--------- lib/vocabularies/applicator/if.ts | 24 ++++++------- lib/vocabularies/applicator/not.ts | 17 ++++----- lib/vocabularies/applicator/oneOf.ts | 21 +++++------ lib/vocabularies/applicator/propertyNames.ts | 16 ++++----- lib/vocabularies/missing.ts | 22 ++++-------- lib/vocabularies/validation/required.ts | 35 +++++++++---------- lib/vocabularies/validation/uniqueItems.ts | 2 +- spec/custom.spec.js | 2 +- 14 files changed, 111 insertions(+), 140 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 86cc6a0378..1bdb85516f 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -6,8 +6,9 @@ import { } from "../types" import {schemaRefOrVal} from "../vocabularies/util" import {getData} from "./util" -import {reportError, keywordError} from "./errors" +import {reportError, reportExtraError, resetErrorsCount, keywordError} from "./errors" import CodeGen, {Name, Expression} from "./codegen" +import N from "./names" export default class KeywordContext implements KeywordErrorContext { gen: CodeGen @@ -19,6 +20,7 @@ export default class KeywordContext implements KeywordErrorContext { schemaValue: Expression | number | boolean // Code reference to keyword schema value or primitive value schemaCode: Expression | number | boolean // Code reference to resolved schema value (different if schema is $data) parentSchema: any + errsCount?: Name params: KeywordContextParams it: CompilationContext def: KeywordDefinition @@ -46,11 +48,15 @@ export default class KeywordContext implements KeywordErrorContext { throw new Error(`${keyword} value must be ${JSON.stringify(def.schemaType)}`) } } + + if ("code" in def && def.trackErrors) { + this.errsCount = it.gen.const("_errs", N.errors) + } } result(condition: Expression, successAction?: () => void, failAction?: () => void): void { this.gen.ifNot(condition) - this._actionOrError(failAction) + failAction ? failAction() : this.error() if (successAction) { this.gen.else() successAction() @@ -77,19 +83,20 @@ export default class KeywordContext implements KeywordErrorContext { } } - error(): void { - reportError(this, this.def.error || keywordError) + error(append?: true): void { + ;(append ? reportExtraError : reportError)(this, this.def.error || keywordError) } - _actionOrError(failAction?: () => void): void { - failAction ? failAction() : reportError(this, this.def.error || keywordError) + reset(): void { + if (this.errsCount === undefined) throw new Error('add "trackErrors" to keyword definition') + resetErrorsCount(this.gen, this.errsCount) } ok(cond: Expression): void { if (!this.allErrors) this.gen.if(cond) } - errorParams(obj: KeywordContextParams, assign?: true): void { + setParams(obj: KeywordContextParams, assign?: true): void { if (assign) Object.assign(this.params, obj) else this.params = obj } diff --git a/lib/types.ts b/lib/types.ts index e82b141e02..58aea2b936 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -162,6 +162,7 @@ interface _KeywordDef { export interface CodeKeywordDefinition extends _KeywordDef { code: (cxt: KeywordContext, ruleType?: string) => void $data?: boolean + trackErrors?: boolean } export type MacroKeywordFunc = ( diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 167a3ebb5a..b2607ccbc0 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import { allSchemaProperties, @@ -9,22 +9,16 @@ import { orExpr, } from "../util" import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" -import {reportError, resetErrorsCount} from "../../compile/errors" import {Name} from "../../compile/codegen" import N from "../../compile/names" -const error: KeywordErrorDefinition = { - message: "should NOT have additional properties", - params: ({params}) => `{additionalProperty: ${params.additionalProperty}}`, -} - const def: CodeKeywordDefinition = { keyword: "additionalProperties", type: "object", schemaType: ["object", "boolean", "undefined"], // "undefined" is needed to support option removeAdditional: "all" - error, + trackErrors: true, code(cxt: KeywordContext) { - const {gen, schema, parentSchema, data, it} = cxt + const {gen, schema, parentSchema, data, errsCount, it} = cxt const {allErrors, usePattern, opts} = it if (alwaysValidSchema(it, schema) && opts.removeAdditional !== "all") return @@ -32,7 +26,7 @@ const def: CodeKeywordDefinition = { const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) - const errsCount = gen.const("_errs", N.errors) + // const errsCount = gen.const("_errs", N.errors) checkAdditionalProperties() if (!allErrors) gen.if(`${errsCount} === ${N.errors}`) @@ -70,8 +64,8 @@ const def: CodeKeywordDefinition = { } if (schema === false) { - cxt.errorParams({additionalProperty: key}) - reportError(cxt, error) + cxt.setParams({additionalProperty: key}) + cxt.error() if (!allErrors) gen.break() return } @@ -81,7 +75,7 @@ const def: CodeKeywordDefinition = { if (opts.removeAdditional === "failing") { applyAdditionalSchema(key, valid, false) gen.ifNot(valid, () => { - resetErrorsCount(gen, errsCount) + cxt.reset() deleteAdditional(key) }) } else { @@ -107,6 +101,10 @@ const def: CodeKeywordDefinition = { applySubschema(it, subschema, valid) } }, + error: { + message: "should NOT have additional properties", + params: ({params}) => `{additionalProperty: ${params.additionalProperty}}`, + }, } module.exports = def diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 3068d3625f..60b2840e47 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,25 +1,18 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" -import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_} from "../../compile/codegen" -import N from "../../compile/names" - -const error: KeywordErrorDefinition = { - message: "should match some schema in anyOf", -} const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", - error, + trackErrors: true, code(cxt: KeywordContext) { const {gen, schema, it} = cxt const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) if (alwaysValid) return - const errsCount = gen.const("_errs", N.errors) const valid = gen.let("valid", false) const schValid = gen.name("_valid") @@ -41,10 +34,13 @@ const def: CodeKeywordDefinition = { cxt.result( valid, - () => resetErrorsCount(gen, errsCount), - () => reportExtraError(cxt, error) + () => cxt.reset(), + () => cxt.error(true) ) }, + error: { + message: "should match some schema in anyOf", + }, } module.exports = def diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 54233c9280..c9d6c34817 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,23 +1,16 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" -import {resetErrorsCount} from "../../compile/errors" -import N from "../../compile/names" - -const error: KeywordErrorDefinition = { - message: "should contain a valid item", -} const def: CodeKeywordDefinition = { keyword: "contains", type: "array", schemaType: ["object", "boolean"], before: "uniqueItems", - error, + trackErrors: true, code(cxt: KeywordContext) { const {gen, schema, data, it} = cxt - const errsCount = gen.const("_errs", N.errors) if (alwaysValidSchema(it, schema)) { cxt.fail(`${data}.length === 0`) @@ -40,7 +33,10 @@ const def: CodeKeywordDefinition = { gen.if(valid, "break") }) - cxt.result(valid, () => resetErrorsCount(gen, errsCount)) + cxt.result(valid, () => cxt.reset()) + }, + error: { + message: "should contain a valid item", }, } diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 3529397f2f..46d85c30be 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" @@ -12,23 +12,10 @@ interface SchemaDependencies { [x: string]: object | boolean } -const error: KeywordErrorDefinition = { - message: ({params: {property, depsCount, deps}}) => { - const property_ies = depsCount === 1 ? "property" : "properties" - return str`should have ${property_ies} ${deps} when property ${property} is present` - }, - params: ({params: {property, depsCount, deps, missingProperty}}) => - _`{property: ${property}, - missingProperty: ${missingProperty}, - depsCount: ${depsCount}, - deps: ${deps}}`, // TODO change to reference? -} - const def: CodeKeywordDefinition = { keyword: "dependencies", type: "object", schemaType: "object", - error, code(cxt: KeywordContext) { const {gen, schema, data, it} = cxt const [propDeps, schDeps] = splitDependencies() @@ -54,7 +41,7 @@ const def: CodeKeywordDefinition = { const deps = propertyDeps[prop] if (deps.length === 0) continue const hasProperty = propertyInData(data, prop, it.opts.ownProperties) - cxt.errorParams({ + cxt.setParams({ property: prop, depsCount: deps.length, deps: deps.join(", "), @@ -62,12 +49,12 @@ const def: CodeKeywordDefinition = { if (it.allErrors) { gen.if(hasProperty, () => { for (const depProp of deps) { - checkReportMissingProp(cxt, depProp, error) + checkReportMissingProp(cxt, depProp) } }) } else { gen.if(`${hasProperty} && (${checkMissingProp(cxt, deps, missing)})`) - reportMissingProp(cxt, missing, error) + reportMissingProp(cxt, missing) gen.else() } } @@ -85,6 +72,17 @@ const def: CodeKeywordDefinition = { } } }, + error: { + message: ({params: {property, depsCount, deps}}) => { + const property_ies = depsCount === 1 ? "property" : "properties" + return str`should have ${property_ies} ${deps} when property ${property} is present` + }, + params: ({params: {property, depsCount, deps, missingProperty}}) => + _`{property: ${property}, + missingProperty: ${missingProperty}, + depsCount: ${depsCount}, + deps: ${deps}}`, // TODO change to reference? + }, } module.exports = def diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 22b3aad8dc..06876cef98 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,22 +1,15 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition, CompilationContext} from "../../types" +import {CodeKeywordDefinition, CompilationContext} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" -import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_, str, Name} from "../../compile/codegen" -import N from "../../compile/names" - -const error: KeywordErrorDefinition = { - message: ({params}) => str`should match "${params.ifClause}" schema`, - params: ({params}) => _`{failingKeyword: ${params.ifClause}}`, -} const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], // TODO // implements: ["then", "else"], - error, + trackErrors: true, code(cxt: KeywordContext) { const {gen, it} = cxt const hasThen = hasSchema(it, "then") @@ -27,15 +20,14 @@ const def: CodeKeywordDefinition = { } const valid = gen.let("valid", true) - const errsCount = gen.const("_errs", N.errors) const schValid = gen.name("_valid") validateIf() - resetErrorsCount(gen, errsCount) + cxt.reset() if (hasThen && hasElse) { const ifClause = gen.let("ifClause") - cxt.errorParams({ifClause}) + cxt.setParams({ifClause}) gen.if(schValid, validateClause("then", ifClause), validateClause("else", ifClause)) } else if (hasThen) { gen.if(schValid, validateClause("then")) @@ -43,7 +35,7 @@ const def: CodeKeywordDefinition = { gen.ifNot(schValid, validateClause("else")) } - cxt.pass(valid, () => reportExtraError(cxt, error)) + cxt.pass(valid, () => cxt.error(true)) function validateIf(): void { applySubschema( @@ -63,10 +55,14 @@ const def: CodeKeywordDefinition = { applySubschema(it, {keyword}, schValid) gen.code(`${valid} = ${schValid};`) if (ifClause) gen.code(`${ifClause} = "${keyword}";`) - else cxt.errorParams({ifClause: keyword}) + else cxt.setParams({ifClause: keyword}) } } }, + error: { + message: ({params}) => str`should match "${params.ifClause}" schema`, + params: ({params}) => _`{failingKeyword: ${params.ifClause}}`, + }, } module.exports = def diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 394e2231b8..184312fe45 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -1,17 +1,12 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" -import {reportError, resetErrorsCount} from "../../compile/errors" -import N from "../../compile/names" - -const error: KeywordErrorDefinition = { - message: "should NOT be valid", -} const def: CodeKeywordDefinition = { keyword: "not", schemaType: ["object", "boolean"], + trackErrors: true, code(cxt: KeywordContext) { const {gen, schema, it} = cxt if (alwaysValidSchema(it, schema)) { @@ -20,7 +15,6 @@ const def: CodeKeywordDefinition = { } const valid = gen.name("valid") - const errsCount = gen.const("_errs", N.errors) applySubschema( it, { @@ -34,10 +28,13 @@ const def: CodeKeywordDefinition = { cxt.result( valid, - () => reportError(cxt, error), - () => resetErrorsCount(gen, errsCount) + () => cxt.error(), + () => cxt.reset() ) }, + error: { + message: "should NOT be valid", + }, } module.exports = def diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index de3e67788c..43d97ff22d 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,34 +1,27 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" -import {reportExtraError, resetErrorsCount} from "../../compile/errors" import {_} from "../../compile/codegen" -import N from "../../compile/names" - -const error: KeywordErrorDefinition = { - message: "should match exactly one schema in oneOf", - params: ({params}) => _`{passingSchemas: ${params.passing}}`, -} const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", + trackErrors: true, code(cxt: KeywordContext) { const {gen, schema, it} = cxt const valid = gen.let("valid", false) - const errsCount = gen.const("_errs", N.errors) const passing = gen.let("passing", "null") const schValid = gen.name("_valid") - cxt.errorParams({passing}) + cxt.setParams({passing}) // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas gen.block(validateOneOf) cxt.result( valid, - () => resetErrorsCount(gen, errsCount), - () => reportExtraError(cxt, error) + () => cxt.reset(), + () => cxt.error(true) ) function validateOneOf() { @@ -61,6 +54,10 @@ const def: CodeKeywordDefinition = { }) } }, + error: { + message: "should match exactly one schema in oneOf", + params: ({params}) => _`{passingSchemas: ${params.passing}}`, + }, } module.exports = def diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 18f6669b03..d93345ddc4 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,15 +1,9 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema, loopPropertiesCode} from "../util" import {applySubschema} from "../../compile/subschema" -import {reportExtraError} from "../../compile/errors" import {_, str} from "../../compile/codegen" -const error: KeywordErrorDefinition = { - message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? - params: ({params}) => _`{propertyName: ${params.propertyName}}`, -} - const def: CodeKeywordDefinition = { keyword: "propertyNames", type: "object", @@ -20,20 +14,24 @@ const def: CodeKeywordDefinition = { const valid = gen.name("valid") loopPropertiesCode(cxt, (key) => { - cxt.errorParams({propertyName: key}) + cxt.setParams({propertyName: key}) applySubschema( it, {keyword: "propertyNames", data: key, propertyName: key, compositeRule: true}, valid ) gen.ifNot(valid, () => { - reportExtraError(cxt, error) + cxt.error(true) if (!it.allErrors) gen.break() }) }) cxt.ok(valid) }, + error: { + message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? + params: ({params}) => _`{propertyName: ${params.propertyName}}`, + }, } module.exports = def diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 038d5da781..35f7cb2994 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,18 +1,12 @@ -import {KeywordErrorDefinition} from "../types" import KeywordContext from "../compile/context" import {noPropertyInData, quotedString, orExpr} from "./util" -import {reportError} from "../compile/errors" import {Name, _} from "../compile/codegen" -export function checkReportMissingProp( - cxt: KeywordContext, - prop: string, - error: KeywordErrorDefinition -): void { +export function checkReportMissingProp(cxt: KeywordContext, prop: string): void { const {gen, data, it} = cxt gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => { - cxt.errorParams({missingProperty: _`${prop}`}, true) - reportError(cxt, error) + cxt.setParams({missingProperty: _`${prop}`}, true) + cxt.error() }) } @@ -27,11 +21,7 @@ export function checkMissingProp( }) } -export function reportMissingProp( - cxt: KeywordContext, - missing: Name, - error: KeywordErrorDefinition -): void { - cxt.errorParams({missingProperty: missing}, true) - reportError(cxt, error) +export function reportMissingProp(cxt: KeywordContext, missing: Name): void { + cxt.setParams({missingProperty: missing}, true) + cxt.error() } diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 4a273935fe..57aa785861 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,20 +1,9 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" -import {reportError} from "../../compile/errors" import {_, str, Name} from "../../compile/codegen" -const error: KeywordErrorDefinition = { - message: ({params: {missingProperty}}) => { - return missingProperty - ? str`should have required property '${missingProperty}'` - : str`"required" keyword value must be array` - }, - params: ({params: {missingProperty}}) => - missingProperty ? _`{missingProperty: ${missingProperty}}` : _`{}`, -} - const def: CodeKeywordDefinition = { keyword: "required", type: "object", @@ -34,7 +23,7 @@ const def: CodeKeywordDefinition = { if ($data) { gen.if( `${schemaCode} && !Array.isArray(${schemaCode})`, - () => reportError(cxt, error), + () => cxt.error(), () => gen.if(`${schemaCode} !== undefined`, loopAllRequired) ) } else { @@ -42,14 +31,14 @@ const def: CodeKeywordDefinition = { } } else { for (const prop of schema) { - checkReportMissingProp(cxt, prop, error) + checkReportMissingProp(cxt, prop) } } } function exitOnErrorMode(): void { const missing = gen.let("missing") - cxt.errorParams({missingProperty: missing}) + cxt.setParams({missingProperty: missing}) if (loopRequired) { const valid = gen.let("valid", true) @@ -69,16 +58,16 @@ const def: CodeKeywordDefinition = { cxt.pass(valid) } else { gen.if(`${checkMissingProp(cxt, schema, missing)}`) - reportMissingProp(cxt, missing, error) + reportMissingProp(cxt, missing) gen.else() } } function loopAllRequired(): void { const prop = gen.name("prop") - cxt.errorParams({missingProperty: prop}) + cxt.setParams({missingProperty: prop}) gen.for(`const ${prop} of ${schemaCode}`, () => - gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => reportError(cxt, error)) + gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => cxt.error()) ) } @@ -90,7 +79,15 @@ const def: CodeKeywordDefinition = { ) } }, - error, + error: { + message: ({params: {missingProperty}}) => { + return missingProperty + ? str`should have required property '${missingProperty}'` + : str`"required" keyword value must be array` + }, + params: ({params: {missingProperty}}) => + missingProperty ? _`{missingProperty: ${missingProperty}}` : _`{}`, + }, } module.exports = def diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index b52653a4b0..3a8b2f70eb 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -14,7 +14,7 @@ const def: CodeKeywordDefinition = { const i = gen.let("i") const j = gen.let("j") const valid = gen.let("valid") - cxt.errorParams({i, j}) + cxt.setParams({i, j}) const itemType = parentSchema.items?.type // TODO refactor to have two open blocks? same as in required diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 4cc64d3720..6a54122af5 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -549,7 +549,7 @@ describe("Custom keywords", () => { const eq = parentSchema.exclusiveRange ? nil : _`=` const minOk = gen.const("minOk", _`${data} >${eq} ${min}`) const maxOk = gen.const("maxOk", _`${data} <${eq} ${max}`) - cxt.errorParams({minOk, maxOk, eq}) + cxt.setParams({minOk, maxOk, eq}) cxt.pass(`${minOk} && ${maxOk}`) }, error: { From 5e8ddecd9dd91cfc1627eda4e8ccfb7eb83d142a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 28 Aug 2020 09:40:39 -0400 Subject: [PATCH 107/322] refactor: "compile" and "validate" keywords error tracking --- lib/compile/context.ts | 6 +++++- lib/compile/errors.ts | 13 +++++++++---- lib/compile/validate/keyword.ts | 20 ++++++++++---------- lib/keyword.ts | 1 + lib/types.ts | 8 +------- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 1bdb85516f..7296c0c026 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -49,7 +49,7 @@ export default class KeywordContext implements KeywordErrorContext { } } - if ("code" in def && def.trackErrors) { + if ("code" in def ? def.trackErrors : def.errors !== false) { this.errsCount = it.gen.const("_errs", N.errors) } } @@ -119,6 +119,10 @@ function validateKeywordUsage( def: KeywordDefinition, keyword: string ): void { + if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { + throw new Error("ajv implementation error") + } + const deps = def.dependencies if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(it.schema, kwd))) { throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 991f7eeaf3..3749bf1601 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -38,10 +38,15 @@ export function resetErrorsCount(gen: CodeGen, errsCount: Name): void { ) } -export function extendErrors( - {gen, keyword, schemaValue, data, it}: KeywordErrorContext, - errsCount: Name -): void { +export function extendErrors({ + gen, + keyword, + schemaValue, + data, + errsCount, + it, +}: KeywordErrorContext): void { + if (errsCount === undefined) throw new Error("ajv implementation error") const err = gen.name("err") gen.for(_`let i=${errsCount}; i<${N.errors}; i++`, () => { gen.const(err, _`${N.vErrors}[i]`) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index b4892fa454..b6a898bf16 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -7,7 +7,7 @@ import { } from "../../types" import KeywordContext from "../context" import {applySubschema} from "../subschema" -import {reportError, reportExtraError, extendErrors, keywordError} from "../errors" +import {extendErrors} from "../errors" import {callValidate} from "../../vocabularies/util" import {_, Name, Expression} from "../codegen" import N from "../names" @@ -48,7 +48,7 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { }, valid ) - cxt.pass(valid, () => reportExtraError(cxt, keywordError)) + cxt.pass(valid, () => cxt.error(true)) } function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { @@ -77,11 +77,11 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function validateRuleWithErrors(): void { gen.block() if ($data) check$data() - const errsCount = gen.const("_errs", N.errors) + // const errsCount = gen.const("_errs", N.errors) const ruleErrs = def.async ? validateAsyncRule() : validateSyncRule() if (def.modifying) modifyData(cxt) gen.endBlock() - reportKeywordErrors(ruleErrs, errsCount) + reportKeywordErrors(ruleErrs) } function check$data(): void { @@ -127,15 +127,15 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { gen.code(`${valid} = ${await}${callValidate(cxt, validateRef, passCxt, passSchema)};`) } - function reportKeywordErrors(ruleErrs: Expression, errsCount: Name): void { + function reportKeywordErrors(ruleErrs: Expression): void { switch (def.valid) { case true: return case false: - addKeywordErrors(cxt, ruleErrs, errsCount) + addKeywordErrors(cxt, ruleErrs) return cxt.ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? default: - cxt.pass(valid, () => addKeywordErrors(cxt, ruleErrs, errsCount)) + cxt.pass(valid, () => addKeywordErrors(cxt, ruleErrs)) } } } @@ -145,16 +145,16 @@ function modifyData(cxt: KeywordContext) { gen.if(it.parentData, () => gen.assign(data, `${it.parentData}[${it.parentDataProperty}];`)) } -function addKeywordErrors(cxt: KeywordContext, errs: Expression, errsCount: Name): void { +function addKeywordErrors(cxt: KeywordContext, errs: Expression): void { const {gen} = cxt gen.if( `Array.isArray(${errs})`, () => { gen.assign(N.vErrors, `${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged gen.assign(N.errors, _`${N.vErrors}.length;`) - extendErrors(cxt, errsCount) + extendErrors(cxt) }, - () => reportError(cxt, keywordError) + () => cxt.error() ) } diff --git a/lib/keyword.ts b/lib/keyword.ts index cbb066e86c..323952ad96 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -30,6 +30,7 @@ export function addKeyword( // this.logger.warn("this method signature is deprecated, see docs for addKeyword") definition = defOrSkip if (definition.keyword === undefined) definition.keyword = keyword + else if (definition.keyword !== keyword) throw new Error("invalid addKeyword parameters") } } else if (typeof kwdOrDef == "object" && typeof defOrSkip != "object") { definition = kwdOrDef diff --git a/lib/types.ts b/lib/types.ts index 58aea2b936..103d4ffbc6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -224,17 +224,11 @@ export interface KeywordErrorContext { parentSchema: any schemaCode: Expression | number | boolean schemaValue: Expression | number | boolean + errsCount?: Name params: KeywordContextParams it: CompilationContext } -// export interface KeywordContext extends KeywordErrorContext { -// ok: (condition: Expression) => void -// pass: (condition: Expression, failAction?: () => void, context?: KeywordContext) => void -// fail: (condition?: Expression, failAction?: () => void, context?: KeywordContext) => void -// errorParams: (obj: KeywordContextParams, assing?: true) => void -// } - export type KeywordContextParams = {[x: string]: Expression | number} export type FormatMode = "fast" | "full" From f244b06340145510dbe1d6ebc582b276c3593e04 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 28 Aug 2020 19:58:36 +0100 Subject: [PATCH 108/322] fix: validation error for $ref keyword --- lib/vocabularies/core/ref.ts | 6 +++--- package.json | 2 +- spec/errors.spec.js | 12 ++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 54128e1018..71af87500b 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -4,7 +4,7 @@ import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" import {callValidate} from "../util" -import {_, Expression} from "../../compile/codegen" +import {_, str, Expression} from "../../compile/codegen" import N from "../../compile/names" const def: CodeKeywordDefinition = { @@ -88,8 +88,8 @@ const def: CodeKeywordDefinition = { }, // TODO incorrect error message error: { - message: ({schemaCode}) => `'should match format "' + ${schemaCode} + '"'`, - params: ({schemaCode}) => `{format: ${schemaCode}}`, + message: ({schema}) => str`can't resolve reference ${schema}`, + params: ({schema}) => _`{ref: ${schema}}`, }, } diff --git a/package.json b/package.json index 9996ecb75b..9886f04f1b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "test-ts": "tsc --target ES5 --noImplicitAny --noEmit spec/typescript/index.ts", "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", "bundle-beautify": "node ./scripts/bundle.js js-beautify", - "build": "del-cli dist && tsc || true && cp -r lib/refs dist/refs", + "build": "del-cli dist && tsc && cp -r lib/refs dist/refs", "test-karma": "karma start", "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", diff --git a/spec/errors.spec.js b/spec/errors.spec.js index edce57e602..5b57fd0ef6 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -935,6 +935,18 @@ describe("Validation errors", () => { }) }) + describe("$ref errors", () => { + it("should have correct message and params", () => { + const _ajv = new Ajv({missingRefs: "fail"}) + const schema = {$ref: "#/unknown"} + const validate = _ajv.compile(schema) + shouldBeInvalid(validate, {}) + shouldBeError(validate.errors[0], "$ref", "#/$ref", "", "can't resolve reference #/unknown", { + ref: "#/unknown", + }) + }) + }) + function testSchema1(schema, schemaPathPrefix) { _testSchema1(ajv, schema, schemaPathPrefix) _testSchema1(ajvJP, schema, schemaPathPrefix) From 3ab106688bd1aa7b6b716950b6962fd82ad9b5f3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 28 Aug 2020 21:01:13 +0100 Subject: [PATCH 109/322] refactor: remove function quotedString --- lib/compile/errors.ts | 13 +++--- lib/compile/index.ts | 42 +++++++++---------- lib/compile/subschema.ts | 8 ++-- lib/compile/util.ts | 6 +-- lib/compile/validate/index.ts | 1 + lib/types.ts | 8 ++-- .../applicator/additionalProperties.ts | 9 ++-- lib/vocabularies/missing.ts | 8 ++-- lib/vocabularies/util.ts | 20 ++++----- lib/vocabularies/validation/const.ts | 3 +- lib/vocabularies/validation/enum.ts | 15 ++++--- 11 files changed, 62 insertions(+), 71 deletions(-) diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 3749bf1601..531d1251bd 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,11 +1,10 @@ import {KeywordErrorContext, KeywordErrorDefinition} from "../types" -import {quotedString} from "../vocabularies/util" -import CodeGen, {_, Name, Expression} from "./codegen" +import CodeGen, {_, str, Name, Expression} from "./codegen" import N from "./names" export const keywordError: KeywordErrorDefinition = { - message: ({keyword}) => `'should pass "${keyword}" keyword validation'`, - params: ({keyword}) => `{keyword: "${keyword}"}`, // TODO possibly remove it as keyword is reported in the object + message: ({keyword}) => str`should pass ${keyword} keyword validation`, + params: ({keyword}) => _`{keyword: ${keyword}}`, // TODO possibly remove it as keyword is reported in the object } export function reportError( @@ -93,11 +92,11 @@ function errorObjectCode(cxt: KeywordErrorContext, error: KeywordErrorDefinition let out = `{ keyword: "${keyword}", dataPath: (${N.dataPath} || "") + ${errorPath}, - schemaPath: ${quotedString(errSchemaPath + "/" + keyword)}, - params: ${params ? params(cxt) : "{}"},` + schemaPath: ${str`${errSchemaPath}/${keyword}`}, + params: ${params ? params(cxt) : _`{}`},` if (propertyName) out += `propertyName: ${propertyName},` if (opts.messages !== false) { - out += `message: ${typeof message == "string" ? quotedString(message) : message(cxt)},` + out += `message: ${typeof message == "string" ? _`${message}` : message(cxt)},` } if (opts.verbose) { // TODO trim whitespace diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 56eb18b8a9..1d22447ed4 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,4 @@ -import CodeGen, {_, nil, Expression} from "./codegen" -import {toQuotedString} from "./util" -import {quotedString} from "../vocabularies/util" +import CodeGen, {_, nil, Code, Expression} from "./codegen" import {validateFunctionCode} from "./validate" import {ErrorObject, KeywordCompilationResult} from "../types" import N from "./names" @@ -139,12 +137,11 @@ function compile(schema, root, localRefs, baseId) { self, }) - let sourceCode = - vars(refVal, refValCode) + - vars(patterns, patternCode) + - vars(defaults, defaultCode) + - vars(customRules, customRuleCode) + - gen.toString() + let sourceCode = `${vars(refVal, refValCode)} + ${vars(patterns, patternCode)} + ${vars(defaults, defaultCode)} + ${vars(customRules, customRuleCode)} + ${gen.toString()}` if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) // console.log("\n\n\n *** \n", sourceCode) @@ -268,13 +265,12 @@ function compile(schema, root, localRefs, baseId) { return "pattern" + index } - function useDefault(value: any): string { + function useDefault(value: any): Expression { switch (typeof value) { case "boolean": case "number": - return "" + value case "string": - return toQuotedString(value) + return _`${value}` case "object": if (value === null) return "null" var valueStr = stableStringify(value) @@ -341,22 +337,24 @@ function compIndex(schema, root, baseId) { return -1 } -function patternCode(i: number, patterns: string[]): string { - return `const pattern${i} = new RegExp(${quotedString(patterns[i])});` +function patternCode(i: number, patterns): Code { + return _`const pattern${i} = new RegExp(${patterns[i]});` } -function defaultCode(i: number): string { - return `const default${i} = defaults[${i}];` +function defaultCode(i: number): Code { + return _`const default${i} = defaults[${i}];` } -function refValCode(i: number, refVal): string { - return refVal[i] === undefined ? "" : `const refVal${i} = refVal[${i}];` +function refValCode(i: number, refVal): Code { + return refVal[i] === undefined ? nil : _`const refVal${i} = refVal[${i}];` } -function customRuleCode(i: number): string { - return `const customRule${i} = customRules[${i}];` +function customRuleCode(i: number): Code { + return _`const customRule${i} = customRules[${i}];` } -function vars(arr: unknown[], statement: (i: number, arr: any[]) => string) { - return arr.reduce((code: string, _, i: number) => code + statement(i, arr), "") +function vars(arr: unknown[], statement: (i: number, arr?: unknown[]) => Code): Code { + return arr + .map((_el, i, arr) => statement(i, arr)) + .reduce((res: Code, c: Code) => _`${res}${c}`, nil) } diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 08dc218ebd..29eb9499f7 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,8 +1,8 @@ import {CompilationContext} from "../types" import {subschemaCode} from "./validate" import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" -import {quotedString, accessProperty} from "../vocabularies/util" -import {Code, Name, Expression} from "./codegen" +import {accessProperty} from "../vocabularies/util" +import {_, Code, Name, Expression} from "./codegen" export interface SubschemaContext { // TODO use Optional? @@ -118,9 +118,7 @@ function extendSubschemaData( ? getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num) : getPath(errorPath, dataProp, opts.jsonPointers) - subschema.parentDataProperty = - expr === Expr.Const && typeof dataProp == "string" ? quotedString(dataProp) : dataProp - + subschema.parentDataProperty = _`${dataProp}` subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty] } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 1a0a6ed633..e8d7b979a8 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -64,7 +64,7 @@ export function toHash(arr: string[]): {[key: string]: true} { const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i const SINGLE_QUOTE = /'|\\/g export function getProperty(key: Expression | number): string { - // return key instanceof Name || (typeof key == "string" && IDENTIFIER.test(key)) + // return typeof key == "string" && IDENTIFIER.test(key) // ? _`.${key}` // : _`[${key}]` @@ -106,7 +106,7 @@ export function schemaUnknownRules(schema: object, rules: object): string | unde for (const key in schema) if (!rules[key]) return key } -export function toQuotedString(str: string): string { +function toQuotedString(str: string): string { return `'${escapeQuotes(str)}'` } @@ -184,7 +184,7 @@ export function getData( } } -export function joinPaths(a: string, b: string): string { +function joinPaths(a: string, b: string): string { if (a === '""' || a === "''") return b if (b === '""' || b === "''") return a return `${a} + ${b}`.replace(/([^\\])' \+ '/g, "$1") diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 7dbcdcdc43..7712123738 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -114,6 +114,7 @@ function initializeTop(gen: CodeGen): void { gen.let(N.vErrors, "null") gen.let(N.errors, 0) gen.if(_`${N.rootData} === undefined`, () => gen.assign(N.rootData, N.data)) + // gen.if(_`${N.dataPath} === undefined`, () => gen.assign(N.dataPath, _`""`)) // TODO maybe add it } function updateContext(it: CompilationContext): void { diff --git a/lib/types.ts b/lib/types.ts index 103d4ffbc6..bc16a252b0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import Cache from "./cache" -import CodeGen, {Name, Expression} from "./compile/codegen" +import CodeGen, {Code, Name, Expression} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" import KeywordContext from "./compile/context" @@ -130,7 +130,7 @@ export interface CompilationContext { // } compositeRule?: boolean usePattern: (str: string) => string - useDefault: (value: any) => string + useDefault: (value: any) => Expression customRules: KeywordCompilationResult[] self: any // TODO RULES: ValidationRules @@ -209,8 +209,8 @@ export type KeywordDefinition = | CodeKeywordDefinition export interface KeywordErrorDefinition { - message: string | ((cxt: KeywordErrorContext) => Expression) - params?: (cxt: KeywordErrorContext) => Expression + message: string | ((cxt: KeywordErrorContext) => Code) + params?: (cxt: KeywordErrorContext) => Code } export type Vocabulary = KeywordDefinition[] diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index b2607ccbc0..e587709f4d 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -3,13 +3,12 @@ import KeywordContext from "../../compile/context" import { allSchemaProperties, schemaRefOrVal, - quotedString, alwaysValidSchema, loopPropertiesCode, orExpr, } from "../util" import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" -import {Name} from "../../compile/codegen" +import {_, Name, Expression} from "../../compile/codegen" import N from "../../compile/names" const def: CodeKeywordDefinition = { @@ -38,13 +37,13 @@ const def: CodeKeywordDefinition = { } function isAdditional(key: Name): string { - let definedProp = "" + let definedProp: Expression = "" if (props.length > 8) { // TODO maybe an option instead of hard-coded 8? const propsSchema = schemaRefOrVal(it, parentSchema.properties, "properties") definedProp = `${propsSchema}.hasOwnProperty(${key})` } else if (props.length) { - definedProp = orExpr(props, (p) => `${key} === ${quotedString(p)}`) + definedProp = orExpr(props, (p) => _`${key} === ${p}`) } if (patProps.length) { definedProp += @@ -103,7 +102,7 @@ const def: CodeKeywordDefinition = { }, error: { message: "should NOT have additional properties", - params: ({params}) => `{additionalProperty: ${params.additionalProperty}}`, + params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`, }, } diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 35f7cb2994..25776598be 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,6 +1,6 @@ import KeywordContext from "../compile/context" -import {noPropertyInData, quotedString, orExpr} from "./util" -import {Name, _} from "../compile/codegen" +import {noPropertyInData, orExpr} from "./util" +import {_, Name, Expression} from "../compile/codegen" export function checkReportMissingProp(cxt: KeywordContext, prop: string): void { const {gen, data, it} = cxt @@ -14,10 +14,10 @@ export function checkMissingProp( {data, it: {opts}}: KeywordContext, properties: string[], missing: Name -): string { +): Expression { return orExpr(properties, (prop) => { const hasNoProp = noPropertyInData(data, prop, opts.ownProperties) - return `(${hasNoProp} && (${missing} = ${quotedString(prop)}))` + return `(${hasNoProp} && (${missing} = ${_`${prop}`}))` }) } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 0a20f6814c..8f79cda69a 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -4,12 +4,6 @@ import KeywordContext from "../compile/context" import {_, Code, Name, Expression} from "../compile/codegen" import N from "../compile/names" -export function quotedString(str: string): string { - return JSON.stringify(str) - .replace(/\u2028/g, "\\u2028") - .replace(/\u2029/g, "\\u2029") -} - export function dataNotType( schemaCode: Expression | number | boolean, schemaType: string, @@ -53,9 +47,8 @@ export function schemaProperties(it: CompilationContext, schema: object): string return allSchemaProperties(schema).filter((p) => !alwaysValidSchema(it, schema[p])) } -export function isOwnProperty(data: Name, property: Expression): Expression { - const prop = property instanceof Code ? property : quotedString(property) - return `Object.prototype.hasOwnProperty.call(${data}, ${prop})` +export function isOwnProperty(data: Name, property: Expression): Code { + return _`Object.prototype.hasOwnProperty.call(${data}, ${property})` } export function propertyInData(data: Name, property: Expression, ownProperties?: boolean): string { @@ -74,8 +67,8 @@ export function noPropertyInData( return cond } -export function accessProperty(property: Expression | number): string { - return property instanceof Code ? `[${property}]` : getProperty(property) +export function accessProperty(property: Expression | number): Expression { + return property instanceof Code ? _`[${property}]` : getProperty(property) } export function loopPropertiesCode( @@ -88,7 +81,10 @@ export function loopPropertiesCode( gen.for(`const ${key} ${iteration}`, () => loopBody(key)) } -export function orExpr(items: string[], mapCondition: (s: string, i: number) => string): string { +export function orExpr( + items: string[], + mapCondition: (s: string, i: number) => Expression +): Expression { return items.map(mapCondition).reduce((expr, cond) => `${expr} || ${cond}`) } diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index f21a1f2b87..deda6d655e 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,5 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "const", @@ -7,7 +8,7 @@ const def: CodeKeywordDefinition = { code: (cxt: KeywordContext) => cxt.fail(`!equal(${cxt.data}, ${cxt.schemaCode})`), error: { message: "should be equal to constant", - params: ({schemaCode}) => `{allowedValue: ${schemaCode}}`, + params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 7305dbb41d..cf3829df32 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {quotedString, orExpr} from "../util" -import {_, Name} from "../../compile/codegen" +import {orExpr} from "../util" +import {_, Name, Code, Expression} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "enum", @@ -26,7 +26,7 @@ const def: CodeKeywordDefinition = { cxt.pass(valid) } else { const vSchema = gen.const("schema", schemaCode) - const cond: string = orExpr(schema, (_x, i) => equalCode(vSchema, i)) + const cond: Expression = orExpr(schema, (_x, i) => equalCode(vSchema, i)) cxt.pass(cond) } } @@ -38,13 +38,12 @@ const def: CodeKeywordDefinition = { ) } - function equalCode(vSchema: Name, i: number): string { - let sch: string = schema[i] + function equalCode(vSchema: Name, i: number): Code { + const sch: string = schema[i] if (sch && typeof sch === "object") { - return `equal(${data}, ${vSchema}[${i}])` + return _`equal(${data}, ${vSchema}[${i}])` } - if (typeof sch === "string") sch = quotedString(sch) - return `${data} === ${sch}` + return _`${data} === ${sch}` } }, error: { From da9bed360c6102158edd3044bdc2d788cecaf369 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 28 Aug 2020 21:53:43 +0100 Subject: [PATCH 110/322] fix: skipped required test --- lib/vocabularies/validation/required.ts | 53 +++++++++------------- lib/vocabularies/validation/uniqueItems.ts | 2 +- spec/errors.spec.js | 2 +- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 57aa785861..d5e814f124 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -12,49 +12,25 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {gen, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return - const loopRequired = $data || schema.length >= it.opts.loopRequired - if (it.allErrors) allErrorsMode() else exitOnErrorMode() function allErrorsMode(): void { if (loopRequired) { - if ($data) { - gen.if( - `${schemaCode} && !Array.isArray(${schemaCode})`, - () => cxt.error(), - () => gen.if(`${schemaCode} !== undefined`, loopAllRequired) - ) - } else { - loopAllRequired() - } - } else { - for (const prop of schema) { - checkReportMissingProp(cxt, prop) - } + check$DataAnd(loopAllRequired) + return + } + for (const prop of schema) { + checkReportMissingProp(cxt, prop) } } function exitOnErrorMode(): void { const missing = gen.let("missing") - cxt.setParams({missingProperty: missing}) - if (loopRequired) { const valid = gen.let("valid", true) - - // TODO refactor and enable/fix test in errors.spec.js line 301 - // it can be simpler once blocks are globally supported - endIf can be removed, so there will be 2 open blocks - if ($data) { - gen.if(`${schemaCode} === undefined`, `${valid} = true`, () => - gen.if(`!Array.isArray(${schemaCode})`, `${valid} = false`, () => - loopUntilMissing(missing, valid) - ) - ) - } else { - loopUntilMissing(missing, valid) - } - + check$DataAnd(() => loopUntilMissing(missing, valid)) cxt.pass(valid) } else { gen.if(`${checkMissingProp(cxt, schema, missing)}`) @@ -63,6 +39,18 @@ const def: CodeKeywordDefinition = { } } + function check$DataAnd(loop: () => void): void { + if ($data) { + gen.if( + `${schemaCode} && !Array.isArray(${schemaCode})`, + () => cxt.error(), + () => gen.if(`${schemaCode} !== undefined`, loop) + ) + } else { + loop() + } + } + function loopAllRequired(): void { const prop = gen.name("prop") cxt.setParams({missingProperty: prop}) @@ -72,11 +60,12 @@ const def: CodeKeywordDefinition = { } function loopUntilMissing(missing: Name, valid: Name): void { - gen.for(`${missing} of ${schemaCode}`, () => + cxt.setParams({missingProperty: missing}) + gen.for(`${missing} of ${schemaCode}`, () => { gen .assign(valid, propertyInData(data, missing, it.opts.ownProperties)) .ifNot(valid, "break") - ) + }) } }, error: { diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 3a8b2f70eb..c047a795f7 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -69,7 +69,7 @@ const def: CodeKeywordDefinition = { .code(_`outer:`) .for(_`;${i}--;`, () => gen.for(_`${j} = ${i}; ${j}--;`, () => - gen.if(`equal(${data}[${i}], ${data}[${j}])`, _`${valid} = false; break outer;`) + gen.if(_`equal(${data}[${i}], ${data}[${j}])`, _`${valid} = false; break outer;`) ) ) } diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 5b57fd0ef6..ce3d483113 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -298,7 +298,7 @@ describe("Validation errors", () => { }) it("should show different error when required is $data of incorrect type", () => { - // test(new Ajv({$data: true})) + test(new Ajv({$data: true})) test(new Ajv({$data: true, allErrors: true})) function test(_ajv) { From d940126e6143ec586faf605cbc91b51a217e91f4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 28 Aug 2020 22:45:33 +0100 Subject: [PATCH 111/322] refactor: "required" and "uniqueItems" keywords --- lib/compile/codegen.ts | 4 +- lib/vocabularies/validation/required.ts | 43 +++++++------ lib/vocabularies/validation/uniqueItems.ts | 71 +++++++++++----------- 3 files changed, 62 insertions(+), 56 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 8daa9a54d0..7357824399 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -175,8 +175,8 @@ export default class CodeGen { return this } - break(): CodeGen { - this.code("break;") + break(label?: Code): CodeGen { + this.code(label ? _`break ${label};` : _`break;`) return this } diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index d5e814f124..7c0b78b7a9 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -18,7 +18,13 @@ const def: CodeKeywordDefinition = { function allErrorsMode(): void { if (loopRequired) { - check$DataAnd(loopAllRequired) + if ($data) { + gen.if(_`${schemaCode} !== undefined`, () => { + gen.if(_`!Array.isArray(${schemaCode})`, () => cxt.error(), loopAllRequired) + }) + } else { + loopAllRequired() + } return } for (const prop of schema) { @@ -30,8 +36,19 @@ const def: CodeKeywordDefinition = { const missing = gen.let("missing") if (loopRequired) { const valid = gen.let("valid", true) - check$DataAnd(() => loopUntilMissing(missing, valid)) - cxt.pass(valid) + if ($data) { + gen.if(_`${schemaCode} === undefined`) + gen.assign(valid, true) + gen.elseIf(_`!Array.isArray(${schemaCode})`) + cxt.error() + gen.assign(valid, false) + gen.else() + loopUntilMissing(missing, valid) + gen.endIf() + } else { + loopUntilMissing(missing, valid) + } + cxt.ok(valid) } else { gen.if(`${checkMissingProp(cxt, schema, missing)}`) reportMissingProp(cxt, missing) @@ -39,18 +56,6 @@ const def: CodeKeywordDefinition = { } } - function check$DataAnd(loop: () => void): void { - if ($data) { - gen.if( - `${schemaCode} && !Array.isArray(${schemaCode})`, - () => cxt.error(), - () => gen.if(`${schemaCode} !== undefined`, loop) - ) - } else { - loop() - } - } - function loopAllRequired(): void { const prop = gen.name("prop") cxt.setParams({missingProperty: prop}) @@ -62,9 +67,11 @@ const def: CodeKeywordDefinition = { function loopUntilMissing(missing: Name, valid: Name): void { cxt.setParams({missingProperty: missing}) gen.for(`${missing} of ${schemaCode}`, () => { - gen - .assign(valid, propertyInData(data, missing, it.opts.ownProperties)) - .ifNot(valid, "break") + gen.assign(valid, propertyInData(data, missing, it.opts.ownProperties)) + gen.ifNot(valid, () => { + cxt.error() + gen.break() + }) }) } }, diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index c047a795f7..8ef88ae064 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {checkDataType, checkDataTypes} from "../../compile/util" -import {_, str} from "../../compile/codegen" +import {_, str, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "uniqueItems", @@ -11,29 +11,29 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt if (it.opts.uniqueItems === false || !($data || schema)) return - const i = gen.let("i") - const j = gen.let("j") const valid = gen.let("valid") - cxt.setParams({i, j}) const itemType = parentSchema.items?.type - // TODO refactor to have two open blocks? same as in required if ($data) { - gen.if(`${schemaCode} === false || ${schemaCode} === undefined`, `${valid} = true`, () => - gen.if(`typeof ${schemaCode} != "boolean"`, `${valid} = false`, validateUniqueItems) - ) + gen.if(_`${schemaCode} === false || ${schemaCode} === undefined`) + gen.assign(valid, true) + gen.elseIf(_`typeof ${schemaCode} != "boolean"`) + cxt.error() + gen.assign(valid, false) + gen.else() + validateUniqueItems() + gen.endIf() } else { validateUniqueItems() } - - cxt.pass(valid) + cxt.ok(valid) function validateUniqueItems() { - gen.code( - `${i} = ${data}.length; - ${valid} = true;` - ) - gen.if(`${i} > 1`, canOptimize() ? loopN : loopN2) + const i = gen.let("i", _`${data}.length`) + const j = gen.let("j") + cxt.setParams({i, j}) + gen.assign(valid, true) + gen.if(`${i} > 1`, () => (canOptimize() ? loopN : loopN2)(i, j)) } function canOptimize(): boolean { @@ -42,7 +42,7 @@ const def: CodeKeywordDefinition = { : itemType && itemType !== "object" && itemType !== "array" } - function loopN(): void { + function loopN(i: Name, j: Name): void { const item = gen.name("item") const wrongType = (Array.isArray(itemType) ? checkDataTypes : checkDataType)( itemType, @@ -50,39 +50,38 @@ const def: CodeKeywordDefinition = { it.opts.strictNumbers, true ) - const indices = gen.const("indices", "{}") + const indices = gen.const("indices", _`{}`) gen.for(_`;${i}--;`, () => { gen.let(item, `${data}[${i}];`) gen.if(wrongType, "continue") if (Array.isArray(itemType)) gen.if(_`typeof ${item} == "string"`, _`${item} += "_"`) gen - .if( - _`typeof ${indices}[${item}] == "number"`, - _`${valid} = false; ${j} = ${indices}[${item}]; break;` - ) + .if(_`typeof ${indices}[${item}] == "number"`, () => { + gen.assign(j, _`${indices}[${item}]`) + cxt.error() + gen.assign(valid, false).break() + }) .code(_`${indices}[${item}] = ${i};`) }) } - function loopN2(): void { - gen - .code(_`outer:`) - .for(_`;${i}--;`, () => - gen.for(_`${j} = ${i}; ${j}--;`, () => - gen.if(_`equal(${data}[${i}], ${data}[${j}])`, _`${valid} = false; break outer;`) - ) + function loopN2(i: Name, j: Name): void { + gen.code(_`outer:`).for(_`;${i}--;`, () => + gen.for(_`${j} = ${i}; ${j}--;`, () => + gen.if(_`equal(${data}[${i}], ${data}[${j}])`, () => { + cxt.error() + gen.assign(valid, false).break(_`outer`) + }) ) + ) } }, error: { - message: ({$data, params: {i, j}}) => { - const msg = str`should NOT have duplicate items (items ## ${j} and ${i} are identical)` - return $data ? _`(${i} === undefined ? "uniqueItems must be boolean ($data)" : ${msg})` : msg - }, - params: ({$data, params: {i, j}}) => { - const obj = _`{i: ${i}, j: ${j}}` - return $data ? _`(${i} === undefined ? {} : ${obj})` : obj - }, + message: ({params: {i, j}}) => + i + ? str`should NOT have duplicate items (items ## ${j} and ${i} are identical)` + : str`uniqueItems must be boolean ($data)`, + params: ({params: {i, j}}) => (i ? _`{i: ${i}, j: ${j}}` : _`{}`), }, } From 9f6a5411d9f97799d620e67b1e9c6636657397e4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 10:13:44 +0100 Subject: [PATCH 112/322] refactor: separate "additionalItems" from "items" --- lib/compile/context.ts | 12 +++--- lib/compile/rules.ts | 1 - .../applicator/additionalItems.ts | 41 ++++++++++++++++++ .../applicator/additionalProperties.ts | 6 +-- lib/vocabularies/applicator/index.ts | 1 + lib/vocabularies/applicator/items.ts | 43 ++++--------------- 6 files changed, 58 insertions(+), 46 deletions(-) create mode 100644 lib/vocabularies/applicator/additionalItems.ts diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 7296c0c026..00e86e20ca 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -72,15 +72,15 @@ export default class KeywordContext implements KeywordErrorContext { } fail(condition?: Expression): void { - if (condition) { - this.gen.if(condition) - this.error() - if (this.allErrors) this.gen.endIf() - else this.gen.else() - } else { + if (condition === undefined) { this.error() if (!this.allErrors) this.gen.if(false) // TODO some other way to disable branch? + return } + this.gen.if(condition) + this.error() + if (this.allErrors) this.gen.endIf() + else this.gen.else() } error(append?: true): void { diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 80993ef9a3..08dbe70319 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -37,7 +37,6 @@ export default function rules(): ValidationRules { "writeOnly", "contentMediaType", "contentEncoding", - "additionalItems", "then", "else", ] diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts new file mode 100644 index 0000000000..71c88e023d --- /dev/null +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -0,0 +1,41 @@ +import {CodeKeywordDefinition} from "../../types" +import KeywordContext from "../../compile/context" +import {alwaysValidSchema} from "../util" +import {applySubschema, Expr} from "../../compile/subschema" +import {_, Name, str} from "../../compile/codegen" + +const def: CodeKeywordDefinition = { + keyword: "additionalItems", + type: "array", + schemaType: ["boolean", "object"], + before: "uniqueItems", + code(cxt: KeywordContext) { + const {gen, schema, parentSchema, data, it} = cxt + const len = gen.const("len", `${data}.length`) + const items = parentSchema.items + // TODO strict mode: fail or warning if "additionalItems" is present without "items" Array + if (!Array.isArray(items)) return + if (schema === false) { + cxt.setParams({len: items.length}) + cxt.pass(_`${len} <= ${items.length}`) + } else if (typeof schema == "object" && !alwaysValidSchema(it, schema)) { + const valid = gen.var("valid", _`${len} <= ${items.length}`) // TODO var + gen.ifNot(valid, () => validateItems(valid)) + cxt.ok(valid) + } + + function validateItems(valid: Name): void { + const i = gen.name("i") + gen.for(_`let ${i}=${items.length}; ${i}<${len}; ${i}++`, () => { + applySubschema(it, {keyword: "additionalItems", dataProp: i, expr: Expr.Num}, valid) + if (!it.allErrors) gen.ifNot(valid, "break") + }) + } + }, + error: { + message: ({params: {len}}) => str`should NOT have more than ${len} items`, + params: ({params: {len}}) => _`{limit: ${len}}`, + }, +} + +module.exports = def diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index e587709f4d..a7bf6961ca 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -14,18 +14,14 @@ import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "additionalProperties", type: "object", - schemaType: ["object", "boolean", "undefined"], // "undefined" is needed to support option removeAdditional: "all" + schemaType: ["boolean", "object", "undefined"], // "undefined" is needed to support option removeAdditional: "all" trackErrors: true, code(cxt: KeywordContext) { const {gen, schema, parentSchema, data, errsCount, it} = cxt const {allErrors, usePattern, opts} = it - if (alwaysValidSchema(it, schema) && opts.removeAdditional !== "all") return - const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) - - // const errsCount = gen.const("_errs", N.errors) checkAdditionalProperties() if (!allErrors) gen.if(`${errsCount} === ${N.errors}`) diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 50eae6c9c2..451864deef 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -2,6 +2,7 @@ import {Vocabulary} from "../../types" const applicator: Vocabulary = [ // array + require("./additionalItems"), require("./items"), require("./contains"), // object diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 5bb35eaed9..b0d35492eb 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,46 +1,21 @@ -import {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" -import {reportError} from "../../compile/errors" -import {_, str} from "../../compile/codegen" - -const additionalItemsError: KeywordErrorDefinition = { - message: ({schema}) => str`should NOT have more than ${schema.length as number} items`, - params: ({schema}) => _`{limit: ${schema.length as number}}`, -} +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "items", type: "array", schemaType: ["object", "array", "boolean"], before: "uniqueItems", - // TODO - // implements: ["additionalItems"], code(cxt: KeywordContext) { - // TODO strict mode: fail or warning if "additionalItems" is present without "items" - const {gen, schema, parentSchema, data, it} = cxt + const {gen, schema, data, it} = cxt const len = gen.const("len", `${data}.length`) - if (Array.isArray(schema)) { - validateItemsArray(schema) - } else if (!alwaysValidSchema(it, schema)) { - validateItems("items", 0) - } - - function validateItemsArray(sch: (object | boolean)[]) { - const addIts = parentSchema.additionalItems - if (addIts === false) validateDataLength(sch) validateDefinedItems() - if (typeof addIts == "object" && !alwaysValidSchema(it, addIts)) { - gen.if(_`${len} > ${sch.length}`, () => validateItems("additionalItems", sch.length)) - } - } - - function validateDataLength(sch: (object | boolean)[]): void { - cxt.pass(`${len} <= ${sch.length}`, () => - reportError({...cxt, keyword: "additionalItems", schemaValue: false}, additionalItemsError) - ) + } else if (!alwaysValidSchema(it, schema)) { + validateItems() } function validateDefinedItems(): void { @@ -63,11 +38,11 @@ const def: CodeKeywordDefinition = { }) } - function validateItems(keyword: string, startFrom: number): void { - const i = gen.name("i") + function validateItems(): void { const valid = gen.name("valid") - gen.for(_`let ${i}=${startFrom}; ${i}<${len}; ${i}++`, () => { - applySubschema(it, {keyword, dataProp: i, expr: Expr.Num}, valid) + const i = gen.name("i") + gen.for(_`let ${i}=0; ${i}<${len}; ${i}++`, () => { + applySubschema(it, {keyword: "items", dataProp: i, expr: Expr.Num}, valid) if (!it.allErrors) gen.ifNot(valid, "break") }) cxt.ok(valid) From 057cdfc7872a8d7dddfb84fbc9b46b224409292f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 11:01:05 +0100 Subject: [PATCH 113/322] refactor: $dataMetaSchema now uses $data in keyword definitions --- lib/ajv.ts | 4 ++-- lib/data.ts | 45 ++++++++++----------------------------------- lib/keyword.ts | 9 ++------- 3 files changed, 14 insertions(+), 44 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 8abf5a9970..60f65f76d5 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -36,10 +36,10 @@ Ajv.prototype.addKeyword = customKeyword.addKeyword Ajv.prototype.getKeyword = customKeyword.getKeyword Ajv.prototype.removeKeyword = customKeyword.removeKeyword Ajv.prototype.validateKeyword = customKeyword.validateKeyword +Ajv.prototype.$dataMetaSchema = $dataMetaSchema Ajv.ValidationError = ValidationError Ajv.MissingRefError = MissingRefError -Ajv.$dataMetaSchema = $dataMetaSchema var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" @@ -428,7 +428,7 @@ function addDefaultMetaSchema(self) { if (self._opts.meta === false) return var metaSchema = require("./refs/json-schema-draft-07.json") if (self._opts.$data) { - metaSchema = $dataMetaSchema(metaSchema, META_SUPPORT_DATA) + metaSchema = self.$dataMetaSchema(metaSchema, META_SUPPORT_DATA) } self.addMetaSchema(metaSchema, META_SCHEMA_ID, true) self._refs["http://json-schema.org/schema"] = META_SCHEMA_ID diff --git a/lib/data.ts b/lib/data.ts index 8594c19341..08fc1d56f1 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -1,54 +1,29 @@ -// TODO use $data in keyword definitions -const KEYWORDS = [ - "multipleOf", - "maximum", - "exclusiveMaximum", - "minimum", - "exclusiveMinimum", - "maxLength", - "minLength", - "pattern", - "additionalItems", - "maxItems", - "minItems", - "uniqueItems", - "maxProperties", - "minProperties", - "required", - "additionalProperties", - "enum", - "format", - "const", -] - export default function $dataMetaSchema( + this, metaSchema: object, keywordsJsonPointers: string[] ): object { + const rules = this.RULES.all for (const jsonPointer of keywordsJsonPointers) { metaSchema = JSON.parse(JSON.stringify(metaSchema)) const segments = jsonPointer.split("/").slice(1) // first segment is an empty string let keywords = metaSchema for (const seg of segments) keywords = keywords[seg] - for (const key of KEYWORDS) { + for (const key in rules) { + const $data = rules[key]?.definition?.$data const schema = keywords[key] - if (schema) keywords[key] = schemaOrData(schema) + if ($data && schema) keywords[key] = schemaOrData(schema) } } return metaSchema } -function schemaOrData(schema: object): object { - return { - anyOf: [ - schema, - { - $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", - }, - ], - } +const $dataRef = { + $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", } -module.exports = $dataMetaSchema +export function schemaOrData(schema: object | boolean): object { + return {anyOf: [schema, $dataRef]} +} diff --git a/lib/keyword.ts b/lib/keyword.ts index 323952ad96..cd5c08f8db 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,6 +1,7 @@ import {KeywordDefinition, Vocabulary, ErrorObject, ValidateFunction} from "./types" import {ValidationRules, Rule} from "./compile/rules" import {definitionSchema} from "./definition_schema" +import {schemaOrData} from "./data" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i @@ -99,17 +100,11 @@ function eachItem(xs: T | T[], f: (x: T) => void): void { } } -const $dataRef = { - $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", -} - function keywordMetaschema(this: any, def: KeywordDefinition): void { // TODO this Ajv let metaSchema = def.metaSchema if (metaSchema === undefined) return - if (def.$data && this._opts.$data) { - metaSchema = {anyOf: [metaSchema, $dataRef]} - } + if (def.$data && this._opts.$data) metaSchema = schemaOrData(metaSchema) def.validateSchema = this.compile(metaSchema, true) } From cd2aeb7f2a086e6d217899bf11757d902b795c06 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 12:33:46 +0100 Subject: [PATCH 114/322] feat: context data$Error method, separate error definition for $data errors --- lib/compile/context.ts | 4 ++++ lib/types.ts | 3 ++- lib/vocabularies/validation/required.ts | 17 ++++++++--------- lib/vocabularies/validation/uniqueItems.ts | 11 ++++++----- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 00e86e20ca..db008d6a64 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -87,6 +87,10 @@ export default class KeywordContext implements KeywordErrorContext { ;(append ? reportExtraError : reportError)(this, this.def.error || keywordError) } + $dataError(): void { + reportError(this, this.def.$dataError || this.def.error || keywordError) + } + reset(): void { if (this.errsCount === undefined) throw new Error('add "trackErrors" to keyword definition') resetErrorsCount(this.gen, this.errsCount) diff --git a/lib/types.ts b/lib/types.ts index bc16a252b0..3eec2d7242 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -156,7 +156,8 @@ interface _KeywordDef { metaSchema?: object validateSchema?: ValidateFunction // compiled keyword metaSchema - should not be passed dependencies?: string[] // keywords that must be present in the same schema - error?: KeywordErrorDefinition // TODO all keyword types should support error + error?: KeywordErrorDefinition + $dataError?: KeywordErrorDefinition } export interface CodeKeywordDefinition extends _KeywordDef { diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 7c0b78b7a9..718bdcfc2e 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -20,7 +20,7 @@ const def: CodeKeywordDefinition = { if (loopRequired) { if ($data) { gen.if(_`${schemaCode} !== undefined`, () => { - gen.if(_`!Array.isArray(${schemaCode})`, () => cxt.error(), loopAllRequired) + gen.if(_`Array.isArray(${schemaCode})`, loopAllRequired, () => cxt.$dataError()) }) } else { loopAllRequired() @@ -40,7 +40,7 @@ const def: CodeKeywordDefinition = { gen.if(_`${schemaCode} === undefined`) gen.assign(valid, true) gen.elseIf(_`!Array.isArray(${schemaCode})`) - cxt.error() + cxt.$dataError() gen.assign(valid, false) gen.else() loopUntilMissing(missing, valid) @@ -76,13 +76,12 @@ const def: CodeKeywordDefinition = { } }, error: { - message: ({params: {missingProperty}}) => { - return missingProperty - ? str`should have required property '${missingProperty}'` - : str`"required" keyword value must be array` - }, - params: ({params: {missingProperty}}) => - missingProperty ? _`{missingProperty: ${missingProperty}}` : _`{}`, + message: ({params: {missingProperty}}) => + str`should have required property '${missingProperty}'`, + params: ({params: {missingProperty}}) => _`{missingProperty: ${missingProperty}}`, + }, + $dataError: { + message: '"required" keyword value must be array', }, } diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 8ef88ae064..a155e2254d 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -18,7 +18,7 @@ const def: CodeKeywordDefinition = { gen.if(_`${schemaCode} === false || ${schemaCode} === undefined`) gen.assign(valid, true) gen.elseIf(_`typeof ${schemaCode} != "boolean"`) - cxt.error() + cxt.$dataError() gen.assign(valid, false) gen.else() validateUniqueItems() @@ -78,10 +78,11 @@ const def: CodeKeywordDefinition = { }, error: { message: ({params: {i, j}}) => - i - ? str`should NOT have duplicate items (items ## ${j} and ${i} are identical)` - : str`uniqueItems must be boolean ($data)`, - params: ({params: {i, j}}) => (i ? _`{i: ${i}, j: ${j}}` : _`{}`), + str`should NOT have duplicate items (items ## ${j} and ${i} are identical)`, + params: ({params: {i, j}}) => _`{i: ${i}, j: ${j}}`, + }, + $dataError: { + message: "uniqueItems must be boolean ($data)", }, } From 32df5f1f3cf128dbafc630f51257ef74d1b0220a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 12:51:16 +0100 Subject: [PATCH 115/322] feat: remove deprecated option useDefaults: shared --- README.md | 1 - lib/ajv.ts | 2 +- lib/compile/index.ts | 38 +++----------------------------- lib/compile/validate/defaults.ts | 6 ++--- lib/types.ts | 3 +-- spec/options/useDefaults.spec.js | 29 +++++++----------------- 6 files changed, 15 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 5526d4c637..38487b4583 100644 --- a/README.md +++ b/README.md @@ -1189,7 +1189,6 @@ Defaults: - `false` (default) - do not use defaults - `true` - insert defaults by value (object literal is used). - `"empty"` - in addition to missing or undefined, use defaults for properties and items that are equal to `null` or `""` (an empty string). - - `"shared"` (deprecated) - insert defaults by reference. If the default is an object, it will be shared by all instances of validated data. If you modify the inserted default in the validated data, it will be modified in the schema as well. - _coerceTypes_: change data type of data to match `type` keyword. See the example in [Coercing data types](#coercing-data-types) and [coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.md). Option values: - `false` (default) - no type coercion. - `true` - coerce scalar data types. diff --git a/lib/ajv.ts b/lib/ajv.ts index 60f65f76d5..aa860eef7c 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -68,7 +68,7 @@ export default function Ajv(opts: Options): void { this._compilations = [] this.RULES = rules() if (opts.schemaId !== undefined && opts.schemaId !== "$id") { - throw new Error("option schemaId is not supported from v7") + throw new Error("option schemaId is not supported in v7") } opts.loopRequired = opts.loopRequired || Infinity diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 1d22447ed4..4cf5c1cbf0 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -6,8 +6,7 @@ import N from "./names" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") -var resolve = require("./resolve"), - stableStringify = require("fast-json-stable-stringify") +const resolve = require("./resolve") /** * Functions below are used inside compiled validations function @@ -50,8 +49,6 @@ function compile(schema, root, localRefs, baseId) { refs = {}, patterns: string[] = [], patternsHash = {}, - defaults: any[] = [], - defaultsHash = {}, customRules: KeywordCompilationResult[] = [] root = root || {schema: schema, refVal: refVal, refs: refs} @@ -129,8 +126,7 @@ function compile(schema, root, localRefs, baseId) { RULES, // TODO refactor - it is available on the instance resolveRef, // TODO remove to imports usePattern, // TODO remove to imports - useDefault, // TODO remove to imports - customRules, // TODO add to types + customRules, opts, formats, logger: self.logger, @@ -139,7 +135,6 @@ function compile(schema, root, localRefs, baseId) { let sourceCode = `${vars(refVal, refValCode)} ${vars(patterns, patternCode)} - ${vars(defaults, defaultCode)} ${vars(customRules, customRuleCode)} ${gen.toString()}` @@ -153,7 +148,6 @@ function compile(schema, root, localRefs, baseId) { "formats", "root", "refVal", - "defaults", "customRules", "equal", "ucs2length", @@ -167,7 +161,6 @@ function compile(schema, root, localRefs, baseId) { formats, root, refVal, - defaults, customRules, equal, ucs2length, @@ -189,8 +182,7 @@ function compile(schema, root, localRefs, baseId) { if (opts.sourceCode === true) { validate.source = { code: sourceCode, - patterns: patterns, - defaults: defaults, + patterns, } } @@ -264,26 +256,6 @@ function compile(schema, root, localRefs, baseId) { } return "pattern" + index } - - function useDefault(value: any): Expression { - switch (typeof value) { - case "boolean": - case "number": - case "string": - return _`${value}` - case "object": - if (value === null) return "null" - var valueStr = stableStringify(value) - var index = defaultsHash[valueStr] - if (index === undefined) { - index = defaultsHash[valueStr] = defaults.length - defaults[index] = value - } - return "default" + index - default: - throw new Error(`unsupported default type "${typeof value}"`) - } - } } /** @@ -341,10 +313,6 @@ function patternCode(i: number, patterns): Code { return _`const pattern${i} = new RegExp(${patterns[i]});` } -function defaultCode(i: number): Code { - return _`const default${i} = defaults[${i}];` -} - function refValCode(i: number, refVal): Code { return refVal[i] === undefined ? nil : _`const refVal${i} = refVal[${i}];` } diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 2b1307350e..e5e630f4b6 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -13,7 +13,7 @@ export function assignDefaults(it: CompilationContext, ty?: string): void { } function assignDefault( - {gen, compositeRule, data, useDefault, opts, logger}: CompilationContext, + {gen, compositeRule, data, opts, logger}: CompilationContext, prop: string | number, defaultValue: any ): void { @@ -31,7 +31,5 @@ function assignDefault( const condition = `${childData} === undefined` + (opts.useDefaults === "empty" ? ` || ${childData} === null || ${childData} === ""` : "") - // TODO remove option `useDefaults === "shared"` - const defaultExpr = opts.useDefaults === "shared" ? useDefault : JSON.stringify - gen.if(condition, `${childData} = ${defaultExpr(defaultValue)}`) + gen.if(condition, `${childData} = ${JSON.stringify(defaultValue)}`) } diff --git a/lib/types.ts b/lib/types.ts index 3eec2d7242..d08e3beca0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -23,7 +23,7 @@ export interface Options { cb?: (err: Error, schema: object) => void ) => PromiseLike removeAdditional?: boolean | "all" | "failing" - useDefaults?: boolean | "empty" | "shared" + useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" strictDefaults?: boolean | "log" strictKeywords?: boolean | "log" @@ -130,7 +130,6 @@ export interface CompilationContext { // } compositeRule?: boolean usePattern: (str: string) => string - useDefault: (value: any) => Expression customRules: KeywordCompilationResult[] self: any // TODO RULES: ValidationRules diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index 3c56dd9c30..53d70bd0cc 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -118,22 +118,15 @@ describe("useDefaults option", () => { } }) - describe("useDefaults: by value / by reference", () => { - describe("using by value", () => { - it("should NOT modify underlying defaults when modifying validated data", () => { - test("value", new Ajv({useDefaults: true})) - test("value", new Ajv({useDefaults: true, allErrors: true})) - }) - }) - - describe("using by reference", () => { - it("should modify underlying defaults when modifying validated data", () => { - test("reference", new Ajv({useDefaults: "shared"})) - test("reference", new Ajv({useDefaults: "shared", allErrors: true})) - }) + describe("useDefaults: defaults are always passed by value", () => { + it("should NOT modify underlying defaults when modifying validated data", () => { + test(new Ajv({useDefaults: true})) + test(new Ajv({useDefaults: true, allErrors: true})) + test(new Ajv({useDefaults: "shared"})) + test(new Ajv({useDefaults: "shared", allErrors: true})) }) - function test(useDefaultsMode, ajv) { + function test(ajv) { var schema = { properties: { items: { @@ -155,13 +148,7 @@ describe("useDefaults option", () => { var data2 = {} validate(data2).should.equal(true) - if (useDefaultsMode === "reference") { - data2.items.should.eql(["a-default", "another-value"]) - } else if (useDefaultsMode === "value") { - data2.items.should.eql(["a-default"]) - } else { - throw new Error("unknown useDefaults mode") - } + data2.items.should.eql(["a-default"]) } }) From 3723622b6dd8b8cd982a38210d45202354c716b5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 14:07:08 +0100 Subject: [PATCH 116/322] feat: unify top level namespace management (WIP) --- lib/compile/codegen.ts | 39 +++++++++++++++++++++++++++++++++++---- lib/compile/index.ts | 9 ++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 7357824399..0f2d6a2c6f 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -38,6 +38,23 @@ const varKinds = { var: new Name("var"), } +interface NameGroup { + prefix: string + index: number + values?: Map // same key as passed in GlobalValue +} + +interface NameRec { + name: Name + value: NameValue +} + +export interface NameValue { + ref: any // this is the reference to any value that can be referred to from generated code via `globals` var in the closure + key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used + code?: Code // this is the code creating the value needed for standalone code without closure - can be a primitive value, function or import (`require`) +} + type TemplateArg = Expression | number | boolean export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { @@ -68,7 +85,7 @@ function interpolate(x: TemplateArg): TemplateArg { const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i export default class CodeGen { - #names: {[key: string]: number} = {} + #names: {[prefix: string]: NameGroup} = {} // TODO make private. Possibly stack? _out = "" #blocks: BlockKind[] = [] @@ -78,12 +95,26 @@ export default class CodeGen { return this._out } + _nameGroup(prefix: string): NameGroup { + let ng = this.#names[prefix] + if (!ng) ng = this.#names[prefix] = {prefix, index: 0} + return ng + } + + _name(ng: NameGroup): Name { + return new Name(`${ng.prefix}_${ng.index++}`) + } + name(prefix: string): Name { - if (!this.#names[prefix]) this.#names[prefix] = 0 - const num = this.#names[prefix]++ - return new Name(`${prefix}_${num}`) + const ng = this._nameGroup(prefix) + return this._name(ng) } + // global(prefix: string, value: NameValue): Name { + // const ng = this._nameGroup(prefix) + // const name = this._name(ng) + // } + _def(varKind: Name, nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) if (rhs === undefined) this.code(`${varKind} ${name};`) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 4cf5c1cbf0..85ca7c7857 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -124,9 +124,9 @@ function compile(schema, root, localRefs, baseId) { errorPath: '""', dataLevel: 0, RULES, // TODO refactor - it is available on the instance - resolveRef, // TODO remove to imports - usePattern, // TODO remove to imports - customRules, + resolveRef, // TODO move to gen.globals + usePattern, // TODO move to gen.globals + customRules, // TODO move to gen.globals opts, formats, logger: self.logger, @@ -226,6 +226,7 @@ function compile(schema, root, localRefs, baseId) { } } + // TODO gen.globals function addLocalRef(ref, v?: any): string { var refId = refVal.length refVal[refId] = v @@ -233,10 +234,12 @@ function compile(schema, root, localRefs, baseId) { return "refVal" + refId } + // TODO gen.globals remove? function removeLocalRef(ref) { delete refs[ref] } + // TODO gen.globals remove? function replaceLocalRef(ref, v) { var refId = refs[ref] refVal[refId] = v From 9dbe651d48bb6fac0e24735fb71260aa289ad2b1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 18:25:40 +0100 Subject: [PATCH 117/322] feat: managing external scope via CodeGen, refactor keywords using patterns --- lib/compile/codegen.ts | 67 +++++++++++++++++-- lib/compile/index.ts | 24 ++----- lib/compile/names.ts | 1 + lib/types.ts | 1 - .../applicator/additionalProperties.ts | 6 +- .../applicator/patternProperties.ts | 5 +- lib/vocabularies/util.ts | 10 ++- lib/vocabularies/validation/pattern.ts | 6 +- 8 files changed, 87 insertions(+), 33 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 0f2d6a2c6f..b874b87253 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -49,14 +49,28 @@ interface NameRec { value: NameValue } +type ValueReference = any // possibly make CodeGen parameterized type on this type + export interface NameValue { - ref: any // this is the reference to any value that can be referred to from generated code via `globals` var in the closure + ref: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used code?: Code // this is the code creating the value needed for standalone code without closure - can be a primitive value, function or import (`require`) } +export interface ValueStore { + [prefix: string]: ValueReference[] +} + type TemplateArg = Expression | number | boolean +export class ValueError extends Error { + value: NameValue + constructor({name, value}: NameRec) { + super(`CodeGen: code for ${name} not defined`) + this.value = value + } +} + export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { return new Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) } @@ -86,6 +100,7 @@ const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i export default class CodeGen { #names: {[prefix: string]: NameGroup} = {} + #valuePrefixes: {[prefix: string]: Name} = {} // TODO make private. Possibly stack? _out = "" #blocks: BlockKind[] = [] @@ -102,7 +117,7 @@ export default class CodeGen { } _name(ng: NameGroup): Name { - return new Name(`${ng.prefix}_${ng.index++}`) + return new Name(ng.prefix + ng.index++) } name(prefix: string): Name { @@ -110,10 +125,50 @@ export default class CodeGen { return this._name(ng) } - // global(prefix: string, value: NameValue): Name { - // const ng = this._nameGroup(prefix) - // const name = this._name(ng) - // } + value(prefix: string, value: NameValue): Name { + const {ref, key} = value + const ng = this._nameGroup(prefix) + this.#valuePrefixes[prefix] = new Name(prefix) + if (!ng.values) { + ng.values = new Map() + } else { + const rec = ng.values.get(key || ref) + if (rec) return rec.name + } + const name = this._name(ng) + ng.values.set(key || ref, {name, value}) + return name + } + + valuesClosure(valuesName: Name, store: ValueStore): Code { + return this._reduceValues(({value: {ref}}, prefix, i) => { + if (!store[prefix]) store[prefix] = [] + store[prefix][i] = ref + const prefName = this.#valuePrefixes[prefix] + return _`${valuesName}.${prefName}[${i}]` + }) + } + + valuesCode(): Code { + return this._reduceValues((rec: NameRec) => { + const c = rec.value.code + if (!c) throw new ValueError(rec) + return c + }) + } + + _reduceValues(valueCode: (n: NameRec, pref: string, index: number) => Code): Code { + let code: Code = nil + for (const prefix in this.#valuePrefixes) { + let i = 0 + const values = this.#names[prefix].values + if (!values) throw new Error("ajv implementation error") + values.forEach((rec: NameRec) => { + code = _`${code}const ${rec.name} = ${valueCode(rec, prefix, i++)};` + }) + } + return code + } _def(varKind: Name, nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 85ca7c7857..213bf8b896 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -47,10 +47,10 @@ function compile(schema, root, localRefs, baseId) { opts = this._opts, refVal = [undefined], refs = {}, - patterns: string[] = [], - patternsHash = {}, customRules: KeywordCompilationResult[] = [] + const scope = {} + root = root || {schema: schema, refVal: refVal, refs: refs} interface CallValidate { @@ -125,7 +125,6 @@ function compile(schema, root, localRefs, baseId) { dataLevel: 0, RULES, // TODO refactor - it is available on the instance resolveRef, // TODO move to gen.globals - usePattern, // TODO move to gen.globals customRules, // TODO move to gen.globals opts, formats, @@ -134,7 +133,7 @@ function compile(schema, root, localRefs, baseId) { }) let sourceCode = `${vars(refVal, refValCode)} - ${vars(patterns, patternCode)} + ${gen.valuesClosure(N.scope, scope)} ${vars(customRules, customRuleCode)} ${gen.toString()}` @@ -149,6 +148,7 @@ function compile(schema, root, localRefs, baseId) { "root", "refVal", "customRules", + "scope", "equal", "ucs2length", "ValidationError", @@ -162,6 +162,7 @@ function compile(schema, root, localRefs, baseId) { root, refVal, customRules, + scope, equal, ucs2length, ValidationError @@ -182,7 +183,7 @@ function compile(schema, root, localRefs, baseId) { if (opts.sourceCode === true) { validate.source = { code: sourceCode, - patterns, + scope, } } @@ -250,15 +251,6 @@ function compile(schema, root, localRefs, baseId) { ? {code: code, schema: refVal, inline: true} : {code: code, $async: refVal && !!refVal.$async} } - - function usePattern(regexStr) { - var index = patternsHash[regexStr] - if (index === undefined) { - index = patternsHash[regexStr] = patterns.length - patterns[index] = regexStr - } - return "pattern" + index - } } /** @@ -312,10 +304,6 @@ function compIndex(schema, root, baseId) { return -1 } -function patternCode(i: number, patterns): Code { - return _`const pattern${i} = new RegExp(${patterns[i]});` -} - function refValCode(i: number, refVal): Code { return refVal[i] === undefined ? nil : _`const refVal${i} = refVal[${i}];` } diff --git a/lib/compile/names.ts b/lib/compile/names.ts index 77efaed0d4..abb05f10d0 100644 --- a/lib/compile/names.ts +++ b/lib/compile/names.ts @@ -15,6 +15,7 @@ const names = { this: new Name("this"), // "globals" self: new Name("self"), + scope: new Name("scope"), } export default names diff --git a/lib/types.ts b/lib/types.ts index d08e3beca0..782d79b20b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -129,7 +129,6 @@ export interface CompilationContext { // [index: string]: KeywordDefinition | undefined // } compositeRule?: boolean - usePattern: (str: string) => string customRules: KeywordCompilationResult[] self: any // TODO RULES: ValidationRules diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index a7bf6961ca..84a00cac16 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -5,6 +5,7 @@ import { schemaRefOrVal, alwaysValidSchema, loopPropertiesCode, + usePattern, orExpr, } from "../util" import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" @@ -18,7 +19,7 @@ const def: CodeKeywordDefinition = { trackErrors: true, code(cxt: KeywordContext) { const {gen, schema, parentSchema, data, errsCount, it} = cxt - const {allErrors, usePattern, opts} = it + const {allErrors, opts} = it if (alwaysValidSchema(it, schema) && opts.removeAdditional !== "all") return const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) @@ -43,7 +44,8 @@ const def: CodeKeywordDefinition = { } if (patProps.length) { definedProp += - (definedProp ? " || " : "") + orExpr(patProps, (p) => `${usePattern(p)}.test(${key})`) + (definedProp ? " || " : "") + + orExpr(patProps, (p) => `${usePattern(gen, p)}.test(${key})`) } return `!(${definedProp})` } diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 14c9fce267..ce7c02587a 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,7 +1,8 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {schemaProperties, loopPropertiesCode} from "../util" +import {schemaProperties, loopPropertiesCode, usePattern} from "../util" import {applySubschema, Expr} from "../../compile/subschema" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "patternProperties", @@ -28,7 +29,7 @@ const def: CodeKeywordDefinition = { function validateProperties(pat: string) { loopPropertiesCode(cxt, (key) => { - gen.if(`${it.usePattern(pat)}.test(${key})`, () => { + gen.if(_`${usePattern(gen, pat)}.test(${key})`, () => { applySubschema( it, { diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 8f79cda69a..45b7c6722b 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,7 +1,7 @@ import {getProperty, schemaHasRules} from "../compile/util" import {CompilationContext} from "../types" import KeywordContext from "../compile/context" -import {_, Code, Name, Expression} from "../compile/codegen" +import CodeGen, {_, Code, Name, Expression} from "../compile/codegen" import N from "../compile/names" export function dataNotType( @@ -106,3 +106,11 @@ export function callValidate( const args = `${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` return context ? `${func}.call(${context}, ${args})` : `${func}(${args})` } + +export function usePattern(gen: CodeGen, pattern: string): Name { + return gen.value("pattern", { + key: pattern, + ref: new RegExp(pattern), + code: _`new RegExp(${pattern})`, + }) +} diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 279a8003d5..63d7dad532 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType} from "../util" +import {dataNotType, usePattern} from "../util" import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -9,9 +9,9 @@ const def: CodeKeywordDefinition = { schemaType: "string", $data: true, code(cxt: KeywordContext) { - const {data, $data, schema, schemaCode, it} = cxt + const {gen, data, $data, schema, schemaCode} = cxt const dnt = dataNotType(schemaCode, def.schemaType, $data) - const regExp = $data ? _`(new RegExp(${schemaCode}))` : it.usePattern(schema) + const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) cxt.fail(dnt + `!${regExp}.test(${data})`) // TODO pass? }, error: { From b8f2349dd61da89a07df459df822c40ac459a7aa Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 19:45:21 +0100 Subject: [PATCH 118/322] refactor: assign keywords functions to scope via CodeGen Scope --- lib/ajv.ts | 16 +++---- lib/compile/codegen.ts | 39 ++++++++++------- lib/compile/index.ts | 20 +++------ lib/compile/rules.ts | 75 +++++++++++++++------------------ lib/compile/validate/keyword.ts | 20 +++------ lib/keyword.ts | 2 +- lib/types.ts | 8 +--- 7 files changed, 79 insertions(+), 101 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index aa860eef7c..f2fe9fbc18 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -30,12 +30,12 @@ Ajv.prototype._addSchema = _addSchema Ajv.prototype._compile = _compile Ajv.prototype.compileAsync = require("./compile/async") -var customKeyword = require("./keyword") -Ajv.prototype.addVocabulary = customKeyword.addVocabulary -Ajv.prototype.addKeyword = customKeyword.addKeyword -Ajv.prototype.getKeyword = customKeyword.getKeyword -Ajv.prototype.removeKeyword = customKeyword.removeKeyword -Ajv.prototype.validateKeyword = customKeyword.validateKeyword +var keywordMethods = require("./keyword") +Ajv.prototype.addVocabulary = keywordMethods.addVocabulary +Ajv.prototype.addKeyword = keywordMethods.addKeyword +Ajv.prototype.getKeyword = keywordMethods.getKeyword +Ajv.prototype.removeKeyword = keywordMethods.removeKeyword +Ajv.prototype.validateKeyword = keywordMethods.validateKeyword Ajv.prototype.$dataMetaSchema = $dataMetaSchema Ajv.ValidationError = ValidationError @@ -118,7 +118,7 @@ function validate(schemaKeyRef, data) { * Create validating function for passed schema. * @this Ajv * @param {Object} schema schema object - * @param {Boolean} _meta true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. + * @param {Boolean} _meta true if schema is a meta-schema. * @return {Function} validating function */ function compile(schema, _meta) { @@ -407,7 +407,7 @@ function errorsText(errors, options) { } /** - * Add custom format + * Add format * @this Ajv * @param {String} name format name * @param {String|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index b874b87253..22f6c0b932 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -52,12 +52,12 @@ interface NameRec { type ValueReference = any // possibly make CodeGen parameterized type on this type export interface NameValue { - ref: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure + ref?: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used code?: Code // this is the code creating the value needed for standalone code without closure - can be a primitive value, function or import (`require`) } -export interface ValueStore { +export interface Scope { [prefix: string]: ValueReference[] } @@ -65,8 +65,8 @@ type TemplateArg = Expression | number | boolean export class ValueError extends Error { value: NameValue - constructor({name, value}: NameRec) { - super(`CodeGen: code for ${name} not defined`) + constructor(fields: string, {name, value}: NameRec) { + super(`CodeGen: ${fields} for ${name} not defined`) this.value = value } } @@ -126,34 +126,41 @@ export default class CodeGen { } value(prefix: string, value: NameValue): Name { - const {ref, key} = value + const {ref, key, code} = value + const valueKey = key ?? ref ?? code + if (!valueKey) throw new Error("CodeGen: ref or code must be passed in value") const ng = this._nameGroup(prefix) this.#valuePrefixes[prefix] = new Name(prefix) if (!ng.values) { ng.values = new Map() } else { - const rec = ng.values.get(key || ref) + const rec = ng.values.get(valueKey) if (rec) return rec.name } const name = this._name(ng) - ng.values.set(key || ref, {name, value}) + ng.values.set(valueKey, {name, value}) return name } - valuesClosure(valuesName: Name, store: ValueStore): Code { - return this._reduceValues(({value: {ref}}, prefix, i) => { - if (!store[prefix]) store[prefix] = [] - store[prefix][i] = ref - const prefName = this.#valuePrefixes[prefix] - return _`${valuesName}.${prefName}[${i}]` + scopeRefs(valuesName: Name, scope: Scope): Code { + return this._reduceValues((rec: NameRec, prefix: string, i: number) => { + const {value: v} = rec + if (v.ref) { + if (!scope[prefix]) scope[prefix] = [] + scope[prefix][i] = v.ref + const prefName = this.#valuePrefixes[prefix] + return _`${valuesName}.${prefName}[${i}]` + } + if (v.code) return v.code + throw new ValueError("ref and code", rec) }) } - valuesCode(): Code { + scopeCode(): Code { return this._reduceValues((rec: NameRec) => { const c = rec.value.code - if (!c) throw new ValueError(rec) - return c + if (c) return c + throw new ValueError("code", rec) }) } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 213bf8b896..9de9e9d6d5 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,6 @@ -import CodeGen, {_, nil, Code, Expression} from "./codegen" +import CodeGen, {_, nil, Code, Expression, Scope} from "./codegen" import {validateFunctionCode} from "./validate" -import {ErrorObject, KeywordCompilationResult} from "../types" +import {ErrorObject} from "../types" import N from "./names" const equal = require("fast-deep-equal") @@ -46,10 +46,9 @@ function compile(schema, root, localRefs, baseId) { var self = this, opts = this._opts, refVal = [undefined], - refs = {}, - customRules: KeywordCompilationResult[] = [] + refs = {} - const scope = {} + const scope: Scope = {} root = root || {schema: schema, refVal: refVal, refs: refs} @@ -125,7 +124,6 @@ function compile(schema, root, localRefs, baseId) { dataLevel: 0, RULES, // TODO refactor - it is available on the instance resolveRef, // TODO move to gen.globals - customRules, // TODO move to gen.globals opts, formats, logger: self.logger, @@ -133,21 +131,20 @@ function compile(schema, root, localRefs, baseId) { }) let sourceCode = `${vars(refVal, refValCode)} - ${gen.valuesClosure(N.scope, scope)} - ${vars(customRules, customRuleCode)} + ${gen.scopeRefs(N.scope, scope)} ${gen.toString()}` if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) // console.log("\n\n\n *** \n", sourceCode) var validate try { + // TODO refactor to fewer variables - maybe only self and scope var makeValidate = new Function( "self", "RULES", "formats", "root", "refVal", - "customRules", "scope", "equal", "ucs2length", @@ -161,7 +158,6 @@ function compile(schema, root, localRefs, baseId) { formats, root, refVal, - customRules, scope, equal, ucs2length, @@ -308,10 +304,6 @@ function refValCode(i: number, refVal): Code { return refVal[i] === undefined ? nil : _`const refVal${i} = refVal[${i}];` } -function customRuleCode(i: number): Code { - return _`const customRule${i} = customRules[${i}];` -} - function vars(arr: unknown[], statement: (i: number, arr?: unknown[]) => Code): Code { return arr .map((_el, i, arr) => statement(i, arr)) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 08dbe70319..d41400048f 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -6,7 +6,6 @@ export interface ValidationRules { all: {[key: string]: boolean | Rule} // rules that have to be validated keywords: {[key: string]: boolean} // all known keywords (superset of "all") types: {[key: string]: boolean | RuleGroup} - custom: {[key: string]: Rule} } export interface RuleGroup { @@ -20,48 +19,40 @@ export interface Rule { definition: KeywordDefinition } -export default function rules(): ValidationRules { - const ALL = ["type", "$comment"] - const KEYWORDS = [ - "$schema", - "$id", - "id", - "$data", - "$async", - "title", - "description", - "default", - "definitions", - "examples", - "readOnly", - "writeOnly", - "contentMediaType", - "contentEncoding", - "then", - "else", - ] - const TYPES = ["number", "integer", "string", "array", "object", "boolean", "null"] +const ALL = ["type", "$comment"] +const KEYWORDS = [ + "$schema", + "$id", + "id", + "$data", + "$async", + "title", + "description", + "default", + "definitions", + "examples", + "readOnly", + "writeOnly", + "contentMediaType", + "contentEncoding", + "then", + "else", +] - const RULES: ValidationRules = { - rules: [ - {type: "number", rules: []}, - {type: "string", rules: []}, - {type: "array", rules: []}, - {type: "object", rules: []}, - {rules: []}, - ], +export default function rules(): ValidationRules { + const types = { + number: {type: "number", rules: []}, + integer: true, + string: {type: "string", rules: []}, + array: {type: "array", rules: []}, + object: {type: "object", rules: []}, + boolean: true, + null: true, + } + return { + types, + rules: [types.number, types.string, types.array, types.object, {rules: []}], all: toHash(ALL), - keywords: {}, - types: toHash(TYPES), - custom: {}, + keywords: toHash(ALL.concat(KEYWORDS)), } - - RULES.rules.forEach((group) => { - if (group.type) RULES.types[group.type] = group - }) - - RULES.keywords = toHash(ALL.concat(KEYWORDS)) - RULES.custom = {} - - return RULES } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index b6a898bf16..1fe28bc91d 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -9,7 +9,7 @@ import KeywordContext from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" import {callValidate} from "../../vocabularies/util" -import {_, Name, Expression} from "../codegen" +import CodeGen, {_, Name, Expression} from "../codegen" import N from "../names" export function keywordCode( @@ -33,7 +33,7 @@ export function keywordCode( function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { const {gen, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) - const schemaRef = addCustomRule(it, keyword, macroSchema) + const schemaRef = useKeyword(gen, keyword, macroSchema) if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true) const valid = gen.name("valid") @@ -56,7 +56,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { checkAsync(it, def) const validate = "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate - const validateRef = addCustomRule(it, keyword, validate) + const validateRef = useKeyword(gen, keyword, validate) const valid = gen.let("valid") if (def.errors === false) { @@ -92,7 +92,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { .code(`${valid} = true;`) .else() if (def.validateSchema) { - const validateSchemaRef = addCustomRule(it, keyword, def.validateSchema) + const validateSchemaRef = useKeyword(gen, keyword, def.validateSchema) gen.code(`${valid} = ${validateSchemaRef}(${schemaCode});`) // TODO fail if schema fails validation // gen.if(`!${valid}`) @@ -162,13 +162,7 @@ function checkAsync(it: CompilationContext, def: FuncKeywordDefinition) { if (def.async && !it.async) throw new Error("async keyword in sync schema") } -function addCustomRule( - it: CompilationContext, - keyword: string, - res?: KeywordCompilationResult -): string { - if (res === undefined) throw new Error(`custom keyword "${keyword}" failed to compile`) - const idx = it.customRules.length - it.customRules[idx] = res - return `customRule${idx}` +function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name { + if (result === undefined) throw new Error(`keyword "${keyword}" failed to compile`) + return gen.value("keyword", {ref: result}) // TODO value.code } diff --git a/lib/keyword.ts b/lib/keyword.ts index cd5c08f8db..5d6f1923f9 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -152,7 +152,7 @@ export const validateKeyword: KeywordValidator = function (definition, throwErro if (v(definition)) return true validateKeyword.errors = v.errors if (throwError) { - throw new Error("custom keyword definition is invalid: " + this.errorsText(v.errors)) + throw new Error("keyword definition is invalid: " + this.errorsText(v.errors)) } return false } diff --git a/lib/types.ts b/lib/types.ts index 782d79b20b..86bdbd7bbc 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -122,14 +122,8 @@ export interface CompilationContext { baseId: string async: boolean opts: Options - formats: { - [index: string]: AddedFormat - } - // keywords: { - // [index: string]: KeywordDefinition | undefined - // } + formats: {[index: string]: AddedFormat} compositeRule?: boolean - customRules: KeywordCompilationResult[] self: any // TODO RULES: ValidationRules logger: Logger // TODO ? From 92b70e85a3626c2dd20a1fdfbcb3552e4c2bacfc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 29 Aug 2020 22:28:56 +0100 Subject: [PATCH 119/322] safer code generation --- lib/compile/codegen.ts | 36 ++++++++----- lib/compile/context.ts | 10 ++-- lib/compile/errors.ts | 2 +- lib/compile/index.ts | 18 +++---- lib/compile/subschema.ts | 21 ++++---- lib/compile/util.ts | 17 +----- lib/compile/validate/dataType.ts | 4 +- lib/compile/validate/defaults.ts | 4 +- lib/compile/validate/index.ts | 2 +- lib/compile/validate/keyword.ts | 10 ++-- lib/keyword.ts | 2 +- lib/types.ts | 28 +++++----- .../applicator/additionalProperties.ts | 2 +- lib/vocabularies/applicator/contains.ts | 3 +- lib/vocabularies/core/ref.ts | 6 +-- lib/vocabularies/format/format.ts | 38 +++++++------ lib/vocabularies/missing.ts | 6 +-- lib/vocabularies/util.ts | 54 +++++++------------ lib/vocabularies/validation/const.ts | 2 +- lib/vocabularies/validation/enum.ts | 10 ++-- lib/vocabularies/validation/limit.ts | 20 +++---- lib/vocabularies/validation/limitItems.ts | 6 +-- lib/vocabularies/validation/limitLength.ts | 8 +-- .../validation/limitProperties.ts | 6 +-- lib/vocabularies/validation/multipleOf.ts | 2 +- lib/vocabularies/validation/pattern.ts | 4 +- 26 files changed, 151 insertions(+), 170 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 22f6c0b932..6aa1ba5eed 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -12,24 +12,31 @@ export type Value = string | Name | Code | number | boolean | null export type Block = string | Name | Code | (() => void) export class Code { - _str: string + #str: string - constructor(name: string) { - this._str = name + constructor(s: string) { + this.#str = s } toString(): string { - return this._str + return this.#str } isQuoted(): boolean { - const len = this._str.length - return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"' + const len = this.#str.length + return len >= 2 && this.#str[0] === '"' && this.#str[len - 1] === '"' } } export const nil = new Code("") +export const operators = { + GT: new Code(">"), + GTE: new Code(">="), + LT: new Code("<"), + LTE: new Code("<="), +} + export class Name extends Code {} const varKinds = { @@ -61,7 +68,7 @@ export interface Scope { [prefix: string]: ValueReference[] } -type TemplateArg = Expression | number | boolean +type TemplateArg = Expression | number | boolean | null export class ValueError extends Error { value: NameValue @@ -93,7 +100,9 @@ export function str(strings: TemplateStringsArray, ...args: TemplateArg[]): Code } function interpolate(x: TemplateArg): TemplateArg { - return x instanceof Code || typeof x == "number" || typeof x == "boolean" ? x : quoteString(x) + return x instanceof Code || typeof x == "number" || typeof x == "boolean" || x === null + ? x + : quoteString(x) } const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i @@ -196,16 +205,11 @@ export default class CodeGen { return this._def(varKinds.var, nameOrPrefix, rhs) } - assign(name: Expression, rhs: Value): CodeGen { + assign(name: Code, rhs: Value): CodeGen { this.code(`${name} = ${rhs};`) return this } - prop(name: Code, key: Expression | number): Code { - name = name instanceof Name ? name : _`(${name})` - return typeof key == "string" && IDENTIFIER.test(key) ? _`${name}.${key}` : _`${name}[${key}]` - } - code(c?: Block | Value): CodeGen { // TODO optionally strip whitespace if (typeof c == "function") c() @@ -347,3 +351,7 @@ export function quoteString(s: string): string { .replace(/\u2028/g, "\\u2028") .replace(/\u2029/g, "\\u2029") } + +export function getProperty(key: Expression | number): Code { + return typeof key == "string" && IDENTIFIER.test(key) ? new Code(`.${key}`) : _`[${key}]` +} diff --git a/lib/compile/context.ts b/lib/compile/context.ts index db008d6a64..739d977645 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -7,7 +7,7 @@ import { import {schemaRefOrVal} from "../vocabularies/util" import {getData} from "./util" import {reportError, reportExtraError, resetErrorsCount, keywordError} from "./errors" -import CodeGen, {Name, Expression} from "./codegen" +import CodeGen, {Code, Name, Expression} from "./codegen" import N from "./names" export default class KeywordContext implements KeywordErrorContext { @@ -17,8 +17,8 @@ export default class KeywordContext implements KeywordErrorContext { data: Name $data?: string | false schema: any - schemaValue: Expression | number | boolean // Code reference to keyword schema value or primitive value - schemaCode: Expression | number | boolean // Code reference to resolved schema value (different if schema is $data) + schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value + schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) parentSchema: any errsCount?: Name params: KeywordContextParams @@ -71,7 +71,7 @@ export default class KeywordContext implements KeywordErrorContext { this.result(condition, undefined, failAction) } - fail(condition?: Expression): void { + fail(condition?: Code): void { if (condition === undefined) { this.error() if (!this.allErrors) this.gen.if(false) // TODO some other way to disable branch? @@ -96,7 +96,7 @@ export default class KeywordContext implements KeywordErrorContext { resetErrorsCount(this.gen, this.errsCount) } - ok(cond: Expression): void { + ok(cond: Code | boolean): void { if (!this.allErrors) this.gen.if(cond) } diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 531d1251bd..b237c9ba88 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -53,7 +53,7 @@ export function extendErrors({ _`${err}.dataPath === undefined`, `${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` ) - gen.code(_`${err}.schemaPath = ${it.errSchemaPath + "/" + keyword};`) + gen.code(_`${err}.schemaPath = ${str`${it.errSchemaPath}/${keyword}`};`) if (it.opts.verbose) { gen.code( _`${err}.schema = ${schemaValue}; diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 9de9e9d6d5..de7e61a69b 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,4 @@ -import CodeGen, {_, nil, Code, Expression, Scope} from "./codegen" +import CodeGen, {_, nil, Code, Scope} from "./codegen" import {validateFunctionCode} from "./validate" import {ErrorObject} from "../types" import N from "./names" @@ -20,13 +20,13 @@ module.exports = compile export type ResolvedRef = InlineResolvedRef | FuncResolvedRef export interface InlineResolvedRef { - code: Expression + code: Code schema: object | boolean inline: true } export interface FuncResolvedRef { - code: Expression + code: Code $async?: boolean inline?: false } @@ -111,6 +111,7 @@ function compile(schema, root, localRefs, baseId) { parentDataProperty: N.parentDataProperty, dataNames: [N.data], dataPathArr: [nil], + dataLevel: 0, topSchemaRef: _`${N.validate}.schema`, async: _schema.$async === true, schema: _schema, @@ -118,14 +119,13 @@ function compile(schema, root, localRefs, baseId) { root: _root, rootId, baseId: baseId || rootId, - schemaPath: "", + schemaPath: nil, errSchemaPath: "#", errorPath: '""', - dataLevel: 0, RULES, // TODO refactor - it is available on the instance - resolveRef, // TODO move to gen.globals - opts, formats, + opts, + resolveRef, // TODO move to gen.globals logger: self.logger, self, }) @@ -224,11 +224,11 @@ function compile(schema, root, localRefs, baseId) { } // TODO gen.globals - function addLocalRef(ref, v?: any): string { + function addLocalRef(ref, v?: any): Code { var refId = refVal.length refVal[refId] = v refs[ref] = refId - return "refVal" + refId + return _`refVal${refId}` } // TODO gen.globals remove? diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 29eb9499f7..0755f37041 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,20 +1,19 @@ import {CompilationContext} from "../types" import {subschemaCode} from "./validate" -import {getProperty, escapeFragment, getPath, getPathExpr} from "./util" -import {accessProperty} from "../vocabularies/util" -import {_, Code, Name, Expression} from "./codegen" +import {escapeFragment, getPath, getPathExpr} from "./util" +import {_, Code, Name, Expression, getProperty} from "./codegen" export interface SubschemaContext { // TODO use Optional? schema: object | boolean - schemaPath: string + schemaPath: Code errSchemaPath: string - topSchemaRef?: Expression + topSchemaRef?: Code errorPath?: string dataLevel?: number data?: Name parentData?: Name - parentDataProperty?: Expression | number + parentDataProperty?: Code | number dataNames?: Name[] dataPathArr?: (Expression | number)[] propertyName?: Name @@ -35,9 +34,9 @@ interface SubschemaApplicationParams { keyword: string schemaProp: string | number schema: object | boolean - schemaPath: string + schemaPath: Code errSchemaPath: string - topSchemaRef: Expression + topSchemaRef: Code data: Name | Code dataProp: Expression | number propertyName: Name @@ -72,12 +71,12 @@ function getSubschema( return schemaProp === undefined ? { schema: sch, - schemaPath: it.schemaPath + getProperty(keyword), + schemaPath: _`${it.schemaPath}${getProperty(keyword)}`, errSchemaPath: `${it.errSchemaPath}/${keyword}`, } : { schema: sch[schemaProp], - schemaPath: it.schemaPath + getProperty(keyword) + getProperty(schemaProp), + schemaPath: _`${it.schemaPath}${getProperty(keyword)}${getProperty(schemaProp)}`, errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + schemaProp)}`, } } @@ -110,7 +109,7 @@ function extendSubschemaData( if (dataProp !== undefined) { const {errorPath, dataPathArr, opts} = it - const nextData = gen.var("data", `${it.data}${accessProperty(dataProp)}`) // TODO var, tagged + const nextData = gen.var("data", _`${it.data}${getProperty(dataProp)}`) // TODO var dataContextProps(nextData) // TODO possibly refactor getPath and getPathExpr to one function using Expr enum subschema.errorPath = diff --git a/lib/compile/util.ts b/lib/compile/util.ts index e8d7b979a8..fae06a86a7 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,4 +1,4 @@ -import {Code, Name, Expression} from "./codegen" +import {_, Name, Expression, getProperty} from "./codegen" import {CompilationContext} from "../types" import N from "./names" @@ -61,20 +61,7 @@ export function toHash(arr: string[]): {[key: string]: true} { return hash } -const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i const SINGLE_QUOTE = /'|\\/g -export function getProperty(key: Expression | number): string { - // return typeof key == "string" && IDENTIFIER.test(key) - // ? _`.${key}` - // : _`[${key}]` - - return key instanceof Name || (typeof key == "string" && IDENTIFIER.test(key)) - ? `.${key}` - : key instanceof Code || typeof key === "number" - ? `[${key}]` - : `['${escapeQuotes(key)}']` -} - export function escapeQuotes(str: string): string { return str .replace(SINGLE_QUOTE, "\\$&") @@ -132,7 +119,7 @@ export function getPath( const path = toQuotedString( jsonPointers // false by default ? "/" + (typeof prop == "number" ? prop : escapeJsonPointer(prop)) - : getProperty(prop) + : getProperty(prop).toString() ) return joinPaths(currentPath, path) } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 6d1462ef52..1a5454eb6b 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -124,11 +124,11 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { function assignParentData( {gen, parentData, parentDataProperty}: CompilationContext, - expr: string | Name + expr: Name ): void { // TODO use gen.property gen.if(_`${parentData} !== undefined`, () => - gen.assign(`${parentData}[${parentDataProperty}]`, expr) + gen.assign(_`${parentData}[${parentDataProperty}]`, expr) ) } diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index e5e630f4b6..b5ad9ef1f8 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,5 +1,5 @@ import {CompilationContext} from "../../types" -import {getProperty} from "../util" +import {_, getProperty} from "../codegen" export function assignDefaults(it: CompilationContext, ty?: string): void { const {properties, items} = it.schema @@ -18,7 +18,7 @@ function assignDefault( defaultValue: any ): void { if (defaultValue === undefined) return - const childData = `${data}${getProperty(prop)}` // TODO tagged + const childData = _`${data}${getProperty(prop)}` if (compositeRule) { if (opts.strictDefaults) { const msg = `default is ignored for: ${childData}` diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 7712123738..194fd56ba2 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -131,7 +131,7 @@ function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: Compilat gen.code(_`console.log(${msg})`) // should it use logger? } else if (typeof $comment == "function") { const schemaPath = str`${errSchemaPath}/$comment` - gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${N.validate}.root.schema)`) // TODO chained properties? + gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${N.validate}.root.schema)`) } } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 1fe28bc91d..05b6966fe5 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -9,7 +9,7 @@ import KeywordContext from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" import {callValidate} from "../../vocabularies/util" -import CodeGen, {_, Name, Expression} from "../codegen" +import CodeGen, {_, nil, Name, Expression} from "../codegen" import N from "../names" export function keywordCode( @@ -41,7 +41,7 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { it, { schema: macroSchema, - schemaPath: "", + schemaPath: nil, errSchemaPath: `${it.errSchemaPath}/${keyword}`, topSchemaRef: schemaRef, compositeRule: true, @@ -115,8 +115,8 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { } function validateSyncRule(): Expression { - const validateErrs = `${validateRef}.errors` - gen.code(`${validateErrs} = null;`) + const validateErrs = _`${validateRef}.errors` + gen.assign(validateErrs, null) assignValid("") return validateErrs } @@ -133,7 +133,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { return case false: addKeywordErrors(cxt, ruleErrs) - return cxt.ok("false") // TODO maybe add gen.skip() to remove code till the end of the block? + return cxt.ok(false) // TODO maybe add gen.skip() to remove code till the end of the block? default: cxt.pass(valid, () => addKeywordErrors(cxt, ruleErrs)) } diff --git a/lib/keyword.ts b/lib/keyword.ts index 5d6f1923f9..df32882967 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -116,7 +116,7 @@ export function getKeyword(this, keyword: string): KeywordDefinition | boolean { /** * Remove keyword * @this Ajv - * @param {String} keyword pre-defined or custom keyword. + * @param {String} keyword keyword. * @return {Ajv} this for method chaining */ export function removeKeyword(keyword: string): object { diff --git a/lib/types.ts b/lib/types.ts index 86bdbd7bbc..ca65f4b816 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -106,31 +106,31 @@ export type KeywordCompilationResult = object | boolean | SchemaValidateFunction export interface CompilationContext { gen: CodeGen allErrors: boolean - dataLevel: number data: Name parentData: Name - parentDataProperty: Expression | number + parentDataProperty: Code | number dataNames: Name[] dataPathArr: (Expression | number)[] + dataLevel: number + topSchemaRef: Code + async: boolean schema: any isRoot: boolean - schemaPath: string + root: SchemaRoot // TODO ? + rootId: string // TODO ? + baseId: string + schemaPath: Code + errSchemaPath: string // this is actual string, should not be changed to Code errorPath: string - errSchemaPath: string propertyName?: Name - createErrors?: boolean // TODO maybe remove later - baseId: string - async: boolean - opts: Options - formats: {[index: string]: AddedFormat} compositeRule?: boolean - self: any // TODO + createErrors?: boolean // TODO maybe remove later RULES: ValidationRules - logger: Logger // TODO ? - root: SchemaRoot // TODO ? - rootId: string // TODO ? - topSchemaRef: Expression // TODO must be Code - depends on global names + formats: {[index: string]: AddedFormat} + opts: Options resolveRef: (...args: any[]) => ResolvedRef | void + logger: Logger // TODO ? + self: any // TODO } interface SchemaRoot { diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 84a00cac16..667e131a6b 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -45,7 +45,7 @@ const def: CodeKeywordDefinition = { if (patProps.length) { definedProp += (definedProp ? " || " : "") + - orExpr(patProps, (p) => `${usePattern(gen, p)}.test(${key})`) + orExpr(patProps, (p) => _`${usePattern(gen, p)}.test(${key})`) } return `!(${definedProp})` } diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index c9d6c34817..3c79ef3c34 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -2,6 +2,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Expr} from "../../compile/subschema" +import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "contains", @@ -13,7 +14,7 @@ const def: CodeKeywordDefinition = { const {gen, schema, data, it} = cxt if (alwaysValidSchema(it, schema)) { - cxt.fail(`${data}.length === 0`) + cxt.fail(_`${data}.length === 0`) return } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 71af87500b..202f023c57 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -4,7 +4,7 @@ import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" import {callValidate} from "../util" -import {_, str, Expression} from "../../compile/codegen" +import {_, str, nil, Expression} from "../../compile/codegen" import N from "../../compile/names" const def: CodeKeywordDefinition = { @@ -24,7 +24,7 @@ const def: CodeKeywordDefinition = { if (schema === "#" || schema === "#/") { return isRoot ? {code: N.validate, $async: it.async} - : {code: "root.refVal[0]", $async: root.schema.$async === true} + : {code: _`root.refVal[0]`, $async: root.schema.$async === true} } return resolveRef(baseId, schema, isRoot) } @@ -50,7 +50,7 @@ const def: CodeKeywordDefinition = { it, { schema: inlineRef.schema, - schemaPath: "", + schemaPath: nil, topSchemaRef: inlineRef.code, errSchemaPath: schema, }, diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 5a3c4f4897..6ba8341a52 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,8 +1,7 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import KeywordContext from "../../compile/context" import {dataNotType} from "../util" -import {getProperty} from "../../compile/util" -import {_, str} from "../../compile/codegen" +import {_, str, nil, Code, getProperty} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "format", @@ -27,22 +26,22 @@ const def: CodeKeywordDefinition = { _`${fmtType} = "string"; ${format} = ${fmtDef}` ) const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(dnt + unknownFmt() + invalidFmt()) + cxt.fail(_`${dnt} ${unknownFmt()} ${invalidFmt()}`) - function unknownFmt(): string { - if (opts.unknownFormats === "ignore") return "" - let unknown = `(${schemaCode} && !${format}` + function unknownFmt(): Code { + if (opts.unknownFormats === "ignore") return nil + let unknown = _`(${schemaCode} && !${format}` if (Array.isArray(opts.unknownFormats)) { - unknown += ` && !self._opts.unknownFormats.includes(${schemaCode})` + unknown = _`${unknown} && !self._opts.unknownFormats.includes(${schemaCode})` } - return unknown + ") || " + return _`${unknown}) || ` } - function invalidFmt(): string { - const fmt = `${format}(${data})` - const callFormat = it.async ? `${fmtDef}.async ? await ${fmt} : ${fmt}` : fmt - const validData = `typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` - return `(${format} && ${fmtType} === "${ruleType}" && !(${validData}))` + function invalidFmt(): Code { + const fmt = _`${format}(${data})` + const callFormat = it.async ? _`${fmtDef}.async ? await ${fmt} : ${fmt}` : fmt + const validData = _`typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` + return _`(${format} && ${fmtType} === ${ruleType} && !(${validData}))` } } @@ -65,22 +64,21 @@ const def: CodeKeywordDefinition = { } } - function getFormat(fmtDef: AddedFormat): [string, FormatValidate, string] { - const fmt = `formats${getProperty(schema)}` + function getFormat(fmtDef: AddedFormat): [string, FormatValidate, Code] { + const fmt = _`formats${getProperty(schema)}` // TODO use scope for formats? if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) { - return [fmtDef.type || "string", fmtDef.validate as FormatValidate, `${fmt}.validate`] + return [fmtDef.type || "string", fmtDef.validate as FormatValidate, _`${fmt}.validate`] } return ["string", fmtDef, fmt] } - function validCondition(): string { + function validCondition(): Code { if (typeof formatDef == "object" && !(formatDef instanceof RegExp) && formatDef.async) { if (!it.async) throw new Error("async format in sync schema") - return `await ${fmtRef}(${data})` + return _`await ${fmtRef}(${data})` } - - return fmtRef + (typeof format == "function" ? "" : ".test") + `(${data})` + return typeof format == "function" ? _`${fmtRef}(${data})` : _`${fmtRef}.test(${data})` } } }, diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 25776598be..4aaedacf16 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,6 +1,6 @@ import KeywordContext from "../compile/context" import {noPropertyInData, orExpr} from "./util" -import {_, Name, Expression} from "../compile/codegen" +import {_, Code, Name} from "../compile/codegen" export function checkReportMissingProp(cxt: KeywordContext, prop: string): void { const {gen, data, it} = cxt @@ -14,10 +14,10 @@ export function checkMissingProp( {data, it: {opts}}: KeywordContext, properties: string[], missing: Name -): Expression { +): Code { return orExpr(properties, (prop) => { const hasNoProp = noPropertyInData(data, prop, opts.ownProperties) - return `(${hasNoProp} && (${missing} = ${_`${prop}`}))` + return _`(${hasNoProp} && (${missing} = ${prop}))` }) } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 45b7c6722b..c5b7409de8 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,15 +1,15 @@ -import {getProperty, schemaHasRules} from "../compile/util" +import {schemaHasRules} from "../compile/util" import {CompilationContext} from "../types" import KeywordContext from "../compile/context" -import CodeGen, {_, Code, Name, Expression} from "../compile/codegen" +import CodeGen, {_, nil, Code, Name, Expression, getProperty} from "../compile/codegen" import N from "../compile/names" export function dataNotType( - schemaCode: Expression | number | boolean, + schemaCode: Code | number | boolean, schemaType: string, $data?: string | false -): string { - return $data ? `(${schemaCode}!==undefined && typeof ${schemaCode}!=="${schemaType}") || ` : "" +): Code { + return $data ? _`(${schemaCode}!==undefined && typeof ${schemaCode}!==${schemaType}) || ` : nil } export function schemaRefOrVal( @@ -17,7 +17,7 @@ export function schemaRefOrVal( schema: unknown, keyword: string, $data?: string | false -): Expression | number | boolean { +): Code | number | boolean { // return $data || typeof schema === "object" // ? `${topSchemaRef}${schemaPath + getProperty(keyword)}` // : _`${schema}` @@ -25,7 +25,7 @@ export function schemaRefOrVal( if (typeof schema == "number" || typeof schema == "boolean") return schema if (typeof schema == "string") return _`${schema}` } - return `${topSchemaRef}${schemaPath + getProperty(keyword)}` + return _`${topSchemaRef}${schemaPath}${getProperty(keyword)}` } export function alwaysValidSchema( @@ -51,24 +51,14 @@ export function isOwnProperty(data: Name, property: Expression): Code { return _`Object.prototype.hasOwnProperty.call(${data}, ${property})` } -export function propertyInData(data: Name, property: Expression, ownProperties?: boolean): string { - let cond = `${data}${accessProperty(property)} !== undefined` - if (ownProperties) cond += ` && ${isOwnProperty(data, property)}` - return cond -} - -export function noPropertyInData( - data: Name, - property: Expression, - ownProperties?: boolean -): string { - let cond = `${data}${accessProperty(property)} === undefined` - if (ownProperties) cond += ` || !${isOwnProperty(data, property)}` - return cond +export function propertyInData(data: Name, property: Expression, ownProperties?: boolean): Code { + const cond = _`${data}${getProperty(property)} !== undefined` + return ownProperties ? _`${cond} && ${isOwnProperty(data, property)}` : cond } -export function accessProperty(property: Expression | number): Expression { - return property instanceof Code ? _`[${property}]` : getProperty(property) +export function noPropertyInData(data: Name, property: Expression, ownProperties?: boolean): Code { + const cond = _`${data}${getProperty(property)} === undefined` + return ownProperties ? _`${cond} || !${isOwnProperty(data, property)}` : cond } export function loopPropertiesCode( @@ -77,20 +67,16 @@ export function loopPropertiesCode( ): void { // TODO maybe always iterate own properties in v7? const key = gen.name("key") - const iteration = it.opts.ownProperties ? `of Object.keys(${data})` : `in ${data}` - gen.for(`const ${key} ${iteration}`, () => loopBody(key)) + const iteration = it.opts.ownProperties ? _`of Object.keys(${data})` : _`in ${data}` + gen.for(_`const ${key} ${iteration}`, () => loopBody(key)) } -export function orExpr( - items: string[], - mapCondition: (s: string, i: number) => Expression -): Expression { - return items.map(mapCondition).reduce((expr, cond) => `${expr} || ${cond}`) +export function orExpr(items: string[], mapCondition: (s: string, i: number) => Code): Code { + return items.map(mapCondition).reduce(orCode) } -export interface ParentData { - data: Name - property: Expression | number +function orCode(x: Code, y: Code) { + return _`${x} || ${y}` } export function callValidate( @@ -100,7 +86,7 @@ export function callValidate( passSchema?: boolean ): string { const dataAndSchema = passSchema - ? `${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` + ? _`${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` : data const dataPath = `(${N.dataPath} || '')${it.errorPath === '""' ? "" : ` + ${it.errorPath}`}` // TODO joinPaths? const args = `${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index deda6d655e..163c48d287 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -5,7 +5,7 @@ import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "const", $data: true, - code: (cxt: KeywordContext) => cxt.fail(`!equal(${cxt.data}, ${cxt.schemaCode})`), + code: (cxt: KeywordContext) => cxt.fail(_`!equal(${cxt.data}, ${cxt.schemaCode})`), error: { message: "should be equal to constant", params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index cf3829df32..fdc74c3acb 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {orExpr} from "../util" -import {_, Name, Code, Expression} from "../../compile/codegen" +import {_, Name, Code} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "enum", @@ -13,9 +13,9 @@ const def: CodeKeywordDefinition = { if ($data) { const valid = gen.let("valid") gen.if( - `${schemaCode} === undefined`, + _`${schemaCode} === undefined`, () => gen.assign(valid, true), - () => gen.assign(valid, false).if(`Array.isArray(${schemaCode})`, () => loopEnum(valid)) + () => gen.assign(valid, false).if(_`Array.isArray(${schemaCode})`, () => loopEnum(valid)) ) cxt.pass(valid) } else { @@ -26,14 +26,14 @@ const def: CodeKeywordDefinition = { cxt.pass(valid) } else { const vSchema = gen.const("schema", schemaCode) - const cond: Expression = orExpr(schema, (_x, i) => equalCode(vSchema, i)) + const cond: Code = orExpr(schema, (_x, i) => equalCode(vSchema, i)) cxt.pass(cond) } } function loopEnum(valid: Name): void { const v = gen.name("v") - gen.for(`const ${v} of ${schemaCode}`, () => + gen.for(_`const ${v} of ${schemaCode}`, () => gen.if(_`equal(${data}, ${v})`, _`${valid} = true; break;`) ) } diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 8831634cb9..6db3b318a3 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,13 +1,15 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {dataNotType} from "../util" -import {_, str} from "../../compile/codegen" +import {_, str, operators, Code} from "../../compile/codegen" -const OPS: {[index: string]: {fail: string; ok: string}} = { - maximum: {fail: ">", ok: "<="}, - minimum: {fail: "<", ok: ">="}, - exclusiveMaximum: {fail: ">=", ok: "<"}, - exclusiveMinimum: {fail: "<=", ok: ">"}, +const ops = operators + +const OPS: {[index: string]: {fail: Code; ok: Code; okStr: string}} = { + maximum: {okStr: "<=", ok: ops.LTE, fail: ops.GT}, + minimum: {okStr: ">=", ok: ops.GTE, fail: ops.LT}, + exclusiveMaximum: {okStr: "<", ok: ops.LT, fail: ops.GTE}, + exclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE}, } const def: CodeKeywordDefinition = { @@ -18,11 +20,11 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode} = cxt const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(dnt + data + OPS[keyword].fail + schemaCode + ` || isNaN(${data})`) + cxt.fail(_`${dnt} ${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data})`) }, error: { - message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].ok} ${schemaCode}`, - params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].ok}, limit: ${schemaCode}}`, + message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, + params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].okStr}, limit: ${schemaCode}}`, }, } diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 859128624c..2ec6d1bd65 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {dataNotType} from "../util" -import {_, str} from "../../compile/codegen" +import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxItems", "minItems"], @@ -10,9 +10,9 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode} = cxt - const op = keyword === "maxItems" ? ">" : "<" + const op = keyword === "maxItems" ? operators.GT : operators.LT const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(dnt + `${data}.length` + op + schemaCode) + cxt.fail(_`${dnt} ${data}.length ${op} ${schemaCode}`) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index ec607a2d55..b8e9a243e1 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {dataNotType} from "../util" -import {_, str} from "../../compile/codegen" +import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxLength", "minLength"], @@ -10,10 +10,10 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode, it} = cxt - const op = keyword === "maxLength" ? ">" : "<" + const op = keyword === "maxLength" ? operators.GT : operators.LT const dnt = dataNotType(schemaCode, def.schemaType, $data) - const len = it.opts.unicode === false ? `${data}.length` : `ucs2length(${data})` - cxt.fail(dnt + len + op + schemaCode) + const len = it.opts.unicode === false ? _`${data}.length` : _`ucs2length(${data})` + cxt.fail(_`${dnt} ${len} ${op} ${schemaCode}`) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index ac00a896a6..0cd8e80a5d 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {dataNotType} from "../util" -import {_, str} from "../../compile/codegen" +import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxProperties", "minProperties"], @@ -10,9 +10,9 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode} = cxt - const op = keyword === "maxProperties" ? ">" : "<" + const op = keyword === "maxProperties" ? operators.GT : operators.LT const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(dnt + `Object.keys(${data}).length` + op + schemaCode) + cxt.fail(_`${dnt} Object.keys(${data}).length ${op} ${schemaCode}`) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index c03d74a9d5..9965ae669b 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -16,7 +16,7 @@ const def: CodeKeywordDefinition = { const invalid = prec ? _`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : _`${res} !== parseInt(${res})` - cxt.fail(dnt + `(${res} = ${data}/${schemaCode}, ${invalid})`) // TODO pass + cxt.fail(_`${dnt} (${res} = ${data}/${schemaCode}, ${invalid})`) // TODO pass }, error: { message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 63d7dad532..cad6378df6 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -11,8 +11,8 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {gen, data, $data, schema, schemaCode} = cxt const dnt = dataNotType(schemaCode, def.schemaType, $data) - const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) - cxt.fail(dnt + `!${regExp}.test(${data})`) // TODO pass? + const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch + cxt.fail(_`${dnt} !${regExp}.test(${data})`) // TODO pass? }, error: { message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, From 0e8c7d9cf85152152e936169f86dd1bd225dd0f7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Aug 2020 07:51:31 +0100 Subject: [PATCH 120/322] better OR expressions --- lib/compile/validate/keyword.ts | 2 +- lib/vocabularies/format/format.ts | 10 +++++----- lib/vocabularies/util.ts | 16 ++++++++++------ lib/vocabularies/validation/limit.ts | 6 +++--- lib/vocabularies/validation/limitItems.ts | 6 +++--- lib/vocabularies/validation/limitLength.ts | 6 +++--- lib/vocabularies/validation/limitProperties.ts | 6 +++--- lib/vocabularies/validation/multipleOf.ts | 8 ++++---- lib/vocabularies/validation/pattern.ts | 6 +++--- 9 files changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 05b6966fe5..b88638c46d 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -87,7 +87,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function check$data(): void { gen // TODO add support for schemaType in keyword definition - // .if(`${dataNotType(schemaCode, def.schemaType, $data)} false`) // TODO refactor + // .if(`${bad$DataType(schemaCode, def.schemaType, $data)} false`) // TODO refactor .if(`${schemaCode} === undefined`) .code(`${valid} = true;`) .else() diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 6ba8341a52..70e7098724 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType} from "../util" +import {bad$DataType, or} from "../util" import {_, str, nil, Code, getProperty} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -25,16 +25,16 @@ const def: CodeKeywordDefinition = { _`${fmtType} = ${fmtDef}.type || "string"; ${format} = ${fmtDef}.validate;`, _`${fmtType} = "string"; ${format} = ${fmtDef}` ) - const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(_`${dnt} ${unknownFmt()} ${invalidFmt()}`) + const bdt = bad$DataType(schemaCode, def.schemaType, $data) + cxt.fail(or(bdt, unknownFmt(), invalidFmt())) function unknownFmt(): Code { if (opts.unknownFormats === "ignore") return nil - let unknown = _`(${schemaCode} && !${format}` + let unknown = _`${schemaCode} && !${format}` if (Array.isArray(opts.unknownFormats)) { unknown = _`${unknown} && !self._opts.unknownFormats.includes(${schemaCode})` } - return _`${unknown}) || ` + return _`(${unknown})` } function invalidFmt(): Code { diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index c5b7409de8..d36e0a2d7f 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -4,12 +4,12 @@ import KeywordContext from "../compile/context" import CodeGen, {_, nil, Code, Name, Expression, getProperty} from "../compile/codegen" import N from "../compile/names" -export function dataNotType( +export function bad$DataType( schemaCode: Code | number | boolean, schemaType: string, $data?: string | false ): Code { - return $data ? _`(${schemaCode}!==undefined && typeof ${schemaCode}!==${schemaType}) || ` : nil + return $data ? _`(${schemaCode}!==undefined && typeof ${schemaCode}!==${schemaType})` : nil } export function schemaRefOrVal( @@ -71,12 +71,16 @@ export function loopPropertiesCode( gen.for(_`const ${key} ${iteration}`, () => loopBody(key)) } -export function orExpr(items: string[], mapCondition: (s: string, i: number) => Code): Code { - return items.map(mapCondition).reduce(orCode) +export function orExpr(items: string[], condition: (s: string, i: number) => Code): Code { + return items.map(condition).reduce(orCode) } -function orCode(x: Code, y: Code) { - return _`${x} || ${y}` +export function or(...args: Code[]): Code { + return args.reduce(orCode) +} + +function orCode(x: Code, y: Code): Code { + return x === nil ? y : y === nil ? x : _`${x} || ${y}` } export function callValidate( diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 6db3b318a3..34c5c8ed5a 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType} from "../util" +import {bad$DataType, or} from "../util" import {_, str, operators, Code} from "../../compile/codegen" const ops = operators @@ -19,8 +19,8 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode} = cxt - const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(_`${dnt} ${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data})`) + const bdt = bad$DataType(schemaCode, def.schemaType, $data) + cxt.fail(or(bdt, _`${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data})`)) }, error: { message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 2ec6d1bd65..09bc0f100a 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType} from "../util" +import {bad$DataType, or} from "../util" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -11,8 +11,8 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode} = cxt const op = keyword === "maxItems" ? operators.GT : operators.LT - const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(_`${dnt} ${data}.length ${op} ${schemaCode}`) + const bdt = bad$DataType(schemaCode, def.schemaType, $data) + cxt.fail(or(bdt, _`${data}.length ${op} ${schemaCode}`)) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index b8e9a243e1..9b3113ce77 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType} from "../util" +import {bad$DataType, or} from "../util" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -11,9 +11,9 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode, it} = cxt const op = keyword === "maxLength" ? operators.GT : operators.LT - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const bdt = bad$DataType(schemaCode, def.schemaType, $data) const len = it.opts.unicode === false ? _`${data}.length` : _`ucs2length(${data})` - cxt.fail(_`${dnt} ${len} ${op} ${schemaCode}`) + cxt.fail(or(bdt, _`${len} ${op} ${schemaCode}`)) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index 0cd8e80a5d..184d255d56 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType} from "../util" +import {bad$DataType, or} from "../util" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -11,8 +11,8 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {keyword, data, $data, schemaCode} = cxt const op = keyword === "maxProperties" ? operators.GT : operators.LT - const dnt = dataNotType(schemaCode, def.schemaType, $data) - cxt.fail(_`${dnt} Object.keys(${data}).length ${op} ${schemaCode}`) + const bdt = bad$DataType(schemaCode, def.schemaType, $data) + cxt.fail(or(bdt, _`Object.keys(${data}).length ${op} ${schemaCode}`)) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 9965ae669b..28adcb873e 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType} from "../util" +import {bad$DataType, or} from "../util" import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -10,13 +10,13 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {gen, data, $data, schemaCode, it} = cxt - const dnt = dataNotType(schemaCode, def.schemaType, $data) - const res = gen.let("res") + const bdt = bad$DataType(schemaCode, def.schemaType, $data) const prec = it.opts.multipleOfPrecision + const res = gen.const("res", _`${data}/${schemaCode}`) const invalid = prec ? _`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : _`${res} !== parseInt(${res})` - cxt.fail(_`${dnt} (${res} = ${data}/${schemaCode}, ${invalid})`) // TODO pass + cxt.fail(or(bdt, invalid)) }, error: { message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index cad6378df6..99d1673e88 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {dataNotType, usePattern} from "../util" +import {bad$DataType, usePattern, or} from "../util" import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -10,9 +10,9 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {gen, data, $data, schema, schemaCode} = cxt - const dnt = dataNotType(schemaCode, def.schemaType, $data) + const bdt = bad$DataType(schemaCode, def.schemaType, $data) const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch - cxt.fail(_`${dnt} !${regExp}.test(${data})`) // TODO pass? + cxt.fail(or(bdt, _`!${regExp}.test(${data})`)) }, error: { message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, From dcb9a519daad173337f1f492de23e5e7bfd7d4fc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Aug 2020 09:48:47 +0100 Subject: [PATCH 121/322] safer code generation 2 --- lib/compile/codegen.ts | 37 +++++- lib/compile/context.ts | 2 +- lib/compile/index.ts | 4 +- lib/compile/subschema.ts | 40 ++++-- lib/compile/util.ts | 121 ++++++------------ lib/compile/validate/dataType.ts | 2 +- lib/compile/validate/defaults.ts | 9 +- lib/compile/validate/index.ts | 2 +- lib/compile/validate/iterate.ts | 4 +- lib/compile/validate/keyword.ts | 32 ++--- lib/types.ts | 4 +- .../applicator/additionalItems.ts | 2 +- .../applicator/additionalProperties.ts | 22 ++-- lib/vocabularies/applicator/dependencies.ts | 2 +- lib/vocabularies/applicator/items.ts | 2 +- lib/vocabularies/applicator/oneOf.ts | 2 +- lib/vocabularies/core/ref.ts | 14 +- lib/vocabularies/format/format.ts | 6 +- lib/vocabularies/missing.ts | 13 +- lib/vocabularies/util.ts | 27 ++-- lib/vocabularies/validation/enum.ts | 5 +- lib/vocabularies/validation/limit.ts | 4 +- lib/vocabularies/validation/limitItems.ts | 4 +- lib/vocabularies/validation/limitLength.ts | 4 +- .../validation/limitProperties.ts | 4 +- lib/vocabularies/validation/multipleOf.ts | 4 +- lib/vocabularies/validation/pattern.ts | 4 +- lib/vocabularies/validation/required.ts | 2 +- lib/vocabularies/validation/uniqueItems.ts | 4 +- 29 files changed, 183 insertions(+), 199 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 6aa1ba5eed..405950c1bb 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -9,6 +9,8 @@ export type Expression = string | Name | Code export type Value = string | Name | Code | number | boolean | null +export type SafeExpr = Code | number | boolean | null + export type Block = string | Name | Code | (() => void) export class Code { @@ -35,6 +37,11 @@ export const operators = { GTE: new Code(">="), LT: new Code("<"), LTE: new Code("<="), + EQ: new Code("==="), + NEQ: new Code("!=="), + NOT: new Code("!"), + OR: new Code("||"), + AND: new Code("&&"), } export class Name extends Code {} @@ -186,22 +193,22 @@ export default class CodeGen { return code } - _def(varKind: Name, nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + _def(varKind: Name, nameOrPrefix: Name | string, rhs?: Value): Name { const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) if (rhs === undefined) this.code(`${varKind} ${name};`) else this.code(`${varKind} ${name} = ${rhs};`) return name } - const(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + const(nameOrPrefix: Name | string, rhs?: Value): Name { return this._def(varKinds.const, nameOrPrefix, rhs) } - let(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + let(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { return this._def(varKinds.let, nameOrPrefix, rhs) } - var(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name { + var(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { return this._def(varKinds.var, nameOrPrefix, rhs) } @@ -235,9 +242,9 @@ export default class CodeGen { return this.if(`!${cond}`, thenBody, elseBody) } - elseIf(condition: Expression): CodeGen { + elseIf(condition: Code): CodeGen { if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"') - this.code(`}else if(${condition}){`) + this.code(_`}else if(${condition}){`) return this } @@ -355,3 +362,21 @@ export function quoteString(s: string): string { export function getProperty(key: Expression | number): Code { return typeof key == "string" && IDENTIFIER.test(key) ? new Code(`.${key}`) : _`[${key}]` } + +const andCode = mappend(operators.AND) + +export function and(...args: Code[]): Code { + return args.reduce(andCode) +} + +const orCode = mappend(operators.OR) + +export function or(...args: Code[]): Code { + return args.reduce(orCode) +} + +type MAppend = (x: Code, y: Code) => Code + +function mappend(op: Code): MAppend { + return (x, y) => (x === nil ? y : y === nil ? x : _`${x} ${op} ${y}`) +} diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 739d977645..d879d427bc 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -41,7 +41,7 @@ export default class KeywordContext implements KeywordErrorContext { if (this.$data) { this.schemaCode = it.gen.name("schema") - it.gen.const(this.schemaCode, `${getData(this.$data, it)}`) + it.gen.const(this.schemaCode, getData(this.$data, it)) } else { this.schemaCode = this.schemaValue if (def.schemaType && !validSchemaType(this.schema, def.schemaType)) { diff --git a/lib/compile/index.ts b/lib/compile/index.ts index de7e61a69b..538f623138 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,4 @@ -import CodeGen, {_, nil, Code, Scope} from "./codegen" +import CodeGen, {_, str, nil, Code, Scope} from "./codegen" import {validateFunctionCode} from "./validate" import {ErrorObject} from "../types" import N from "./names" @@ -121,7 +121,7 @@ function compile(schema, root, localRefs, baseId) { baseId: baseId || rootId, schemaPath: nil, errSchemaPath: "#", - errorPath: '""', + errorPath: str``, RULES, // TODO refactor - it is available on the instance formats, opts, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 0755f37041..d0e9425df5 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,7 +1,7 @@ import {CompilationContext} from "../types" import {subschemaCode} from "./validate" -import {escapeFragment, getPath, getPathExpr} from "./util" -import {_, Code, Name, Expression, getProperty} from "./codegen" +import {escapeFragment, escapeJsonPointer} from "./util" +import {_, str, nil, Code, Name, getProperty} from "./codegen" export interface SubschemaContext { // TODO use Optional? @@ -9,13 +9,13 @@ export interface SubschemaContext { schemaPath: Code errSchemaPath: string topSchemaRef?: Code - errorPath?: string + errorPath?: Code dataLevel?: number data?: Name parentData?: Name parentDataProperty?: Code | number dataNames?: Name[] - dataPathArr?: (Expression | number)[] + dataPathArr?: (Code | number)[] propertyName?: Name compositeRule?: true createErrors?: boolean @@ -38,9 +38,9 @@ interface SubschemaApplicationParams { errSchemaPath: string topSchemaRef: Code data: Name | Code - dataProp: Expression | number + dataProp: Code | string | number propertyName: Name - expr: Expr + expr: Expr // TODO dataPropType compositeRule: true createErrors: boolean allErrors: boolean @@ -111,12 +111,7 @@ function extendSubschemaData( const {errorPath, dataPathArr, opts} = it const nextData = gen.var("data", _`${it.data}${getProperty(dataProp)}`) // TODO var dataContextProps(nextData) - // TODO possibly refactor getPath and getPathExpr to one function using Expr enum - subschema.errorPath = - dataProp instanceof Code - ? getPathExpr(errorPath, dataProp, opts.jsonPointers, expr === Expr.Num) - : getPath(errorPath, dataProp, opts.jsonPointers) - + subschema.errorPath = str`${errorPath}${getErrorPath(dataProp, expr, opts.jsonPointers)}` subschema.parentDataProperty = _`${dataProp}` subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty] } @@ -125,7 +120,7 @@ function extendSubschemaData( const nextData = data instanceof Name ? data : gen.var("data", data) // TODO var, replaceable if used once? dataContextProps(nextData) if (propertyName !== undefined) subschema.propertyName = propertyName - // TODO something is wrong here with not changing parentDataProperty and not appending dataPathArr + // TODO something is possibly wrong here with not changing parentDataProperty and not appending dataPathArr } function dataContextProps(_nextData: Name) { @@ -144,3 +139,22 @@ function extendSubschemaMode( if (createErrors !== undefined) subschema.createErrors = createErrors if (allErrors !== undefined) subschema.allErrors = allErrors } + +function getErrorPath( + dataProp: Code | string | number, + dataPropType?: Expr, + jsonPointers?: boolean +): Code | string { + // let path + if (dataProp instanceof Code) { + const isNumber = dataPropType === Expr.Num + return jsonPointers + ? _`"/" + ${dataProp}${isNumber ? nil : _`.replace(/~/g, "~0").replace(/\\//g, "~1")`}` // TODO maybe use global escapePointer + : isNumber + ? _`"[" + ${dataProp} + "]"` + : _`"['" + ${dataProp} + "']"` // TODO needs global string escaping + } + return jsonPointers + ? "/" + (typeof dataProp == "number" ? dataProp : escapeJsonPointer(dataProp)) + : getProperty(dataProp).toString() +} diff --git a/lib/compile/util.ts b/lib/compile/util.ts index fae06a86a7..3a6a8d0088 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,4 +1,4 @@ -import {_, Name, Expression, getProperty} from "./codegen" +import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" import {CompilationContext} from "../types" import N from "./names" @@ -7,26 +7,31 @@ export function checkDataType( data: Name, strictNumbers?: boolean, negate?: boolean -): string { - const EQ = negate ? " !== " : " === " - const OK = negate ? "!" : "" +): Code { + const EQ = negate ? operators.NEQ : operators.EQ + let cond: Code switch (dataType) { case "null": - return data + EQ + "null" + return _`${data} ${EQ} null` case "array": - return OK + `Array.isArray(${data})` + cond = _`Array.isArray(${data})` + break case "object": - return OK + `(${data} && typeof ${data} === "object" && !Array.isArray(${data}))` + cond = _`${data} && typeof ${data} === "object" && !Array.isArray(${data})` + break case "integer": - return ( - OK + - `(typeof ${data} === "number" && !(${data} % 1) && !isNaN(${data})` + - (strictNumbers ? ` && isFinite(${data}))` : ")") - ) + cond = numCond(_`!(${data} % 1) && !isNaN(${data})`) + break case "number": - return OK + `(typeof ${data} === "number"` + (strictNumbers ? `&& isFinite(${data}))` : ")") + cond = numCond() + break default: - return `typeof ${data} ${EQ} "${dataType}"` + return _`typeof ${data} ${EQ} ${dataType}` + } + return negate ? _`!(${cond})` : cond + + function numCond(cond: Code = nil): Code { + return and(_`typeof ${data} === "number"`, cond, strictNumbers ? _`isFinite(${data})` : nil) } } @@ -34,25 +39,25 @@ export function checkDataTypes( dataTypes: string[], data: Name, strictNumbers?: boolean, - negate?: true -): string { + _negate?: true +): Code { if (dataTypes.length === 1) { return checkDataType(dataTypes[0], data, strictNumbers, true) } - let code = "" + let cond: Code const types = toHash(dataTypes) if (types.array && types.object) { - code = types.null ? "(" : `(!${data} || ` - code += `typeof ${data} !== "object")` + const notObj = _`typeof ${data} !== "object"` + cond = types.null ? notObj : _`(!${data} || ${notObj})` delete types.null delete types.array delete types.object + } else { + cond = nil } if (types.number) delete types.integer - for (const t in types) { - code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, negate) - } - return code + for (const t in types) cond = and(cond, checkDataType(t, data, strictNumbers, true)) + return cond } export function toHash(arr: string[]): {[key: string]: true} { @@ -61,16 +66,6 @@ export function toHash(arr: string[]): {[key: string]: true} { return hash } -const SINGLE_QUOTE = /'|\\/g -export function escapeQuotes(str: string): string { - return str - .replace(SINGLE_QUOTE, "\\$&") - .replace(/\n/g, "\\n") - .replace(/\r/g, "\\r") - .replace(/\f/g, "\\f") - .replace(/\t/g, "\\t") -} - // TODO rules, schema? export function schemaHasRules(schema: object | boolean, rules: object): boolean | undefined { if (typeof schema == "boolean") return !schema @@ -93,65 +88,29 @@ export function schemaUnknownRules(schema: object, rules: object): string | unde for (const key in schema) if (!rules[key]) return key } -function toQuotedString(str: string): string { - return `'${escapeQuotes(str)}'` -} - -export function getPathExpr( - currentPath: string, - expr: Expression, - jsonPointers?: boolean, - isNumber?: boolean -): string { - const path = jsonPointers // false by default - ? `'/' + ${expr}` + (isNumber ? "" : ".replace(/~/g, '~0').replace(/\\//g, '~1')") - : isNumber - ? `'[' + ${expr} + ']'` - : `'[\\'' + ${expr} + '\\']'` - return joinPaths(currentPath, path) -} - -export function getPath( - currentPath: string, - prop: string | number, - jsonPointers?: boolean -): string { - const path = toQuotedString( - jsonPointers // false by default - ? "/" + (typeof prop == "number" ? prop : escapeJsonPointer(prop)) - : getProperty(prop).toString() - ) - return joinPaths(currentPath, path) -} - const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ export function getData( $data: string, {dataLevel, dataNames, dataPathArr}: CompilationContext -): Expression | number { - let jsonPointer, data +): Code | number { + let jsonPointer + let data: Code if ($data === "") return N.rootData if ($data[0] === "/") { - if (!JSON_POINTER.test($data)) { - throw new Error("Invalid JSON-pointer: " + $data) - } + if (!JSON_POINTER.test($data)) throw new Error(`Invalid JSON-pointer: ${$data}`) jsonPointer = $data data = N.rootData } else { const matches = RELATIVE_JSON_POINTER.exec($data) - if (!matches) throw new Error("Invalid JSON-pointer: " + $data) + if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`) const up: number = +matches[1] jsonPointer = matches[2] if (jsonPointer === "#") { - if (up >= dataLevel) { - throw new Error(errorMsg("property/index", up)) - } + if (up >= dataLevel) throw new Error(errorMsg("property/index", up)) return dataPathArr[dataLevel - up] } - if (up > dataLevel) throw new Error(errorMsg("data", up)) - data = dataNames[dataLevel - up] if (!jsonPointer) return data } @@ -160,8 +119,8 @@ export function getData( const segments = jsonPointer.split("/") for (const segment of segments) { if (segment) { - data += getProperty(unescapeJsonPointer(segment)) - expr += " && " + data + data = _`${data}${getProperty(unescapeJsonPointer(segment))}` + expr = _`${expr} && ${data}` } } return expr @@ -171,12 +130,6 @@ export function getData( } } -function joinPaths(a: string, b: string): string { - if (a === '""' || a === "''") return b - if (b === '""' || b === "''") return a - return `${a} + ${b}`.replace(/([^\\])' \+ '/g, "$1") -} - export function unescapeFragment(str: string): string { return unescapeJsonPointer(decodeURIComponent(str)) } @@ -189,6 +142,6 @@ export function escapeJsonPointer(str: string): string { return str.replace(/~/g, "~0").replace(/\//g, "~1") } -export function unescapeJsonPointer(str: string): string { +function unescapeJsonPointer(str: string): string { return str.replace(/~1/g, "/").replace(/~0/g, "~") } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 1a5454eb6b..5bc00f68f0 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -50,7 +50,7 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string export function coerceData(it: CompilationContext, coerceTo: string[]): void { const {gen, schema, data, opts} = it - const dataType = gen.let("dataType", `typeof ${data}`) + const dataType = gen.let("dataType", _`typeof ${data}`) const coerced = gen.let("coerced") if (opts.coerceTypes === "array") { gen.if(_`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index b5ad9ef1f8..389fc22bed 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -28,8 +28,11 @@ function assignDefault( return } - const condition = - `${childData} === undefined` + - (opts.useDefaults === "empty" ? ` || ${childData} === null || ${childData} === ""` : "") + let condition = _`${childData} === undefined` + if (opts.useDefaults === "empty") { + condition = _`${condition} || ${childData} === null || ${childData} === ""` + } + // `${childData} === undefined` + + // (opts.useDefaults === "empty" ? ` || ${childData} === null || ${childData} === ""` : "") gen.if(condition, `${childData} = ${JSON.stringify(defaultValue)}`) } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 194fd56ba2..d30faaa63b 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -111,7 +111,7 @@ function checkNoDefault({schema, opts, logger}: CompilationContext): void { } function initializeTop(gen: CodeGen): void { - gen.let(N.vErrors, "null") + gen.let(N.vErrors, null) gen.let(N.errors, 0) gen.if(_`${N.rootData} === undefined`, () => gen.assign(N.rootData, N.data)) // gen.if(_`${N.dataPath} === undefined`, () => gen.assign(N.dataPath, _`""`)) // TODO maybe add it diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index f2068862fa..6775d3566d 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -5,7 +5,7 @@ import {keywordCode} from "./keyword" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" import {Rule, RuleGroup} from "../rules" -import {Name} from "../codegen" +import {_, Name} from "../codegen" import N from "../names" export function schemaKeywords( @@ -44,7 +44,7 @@ export function schemaKeywords( iterateKeywords(it, group) } // TODO make it "ok" call? - if (!allErrors) gen.if(`${N.errors} === ${errsCount || 0}`) + if (!allErrors) gen.if(_`${N.errors} === ${errsCount || 0}`) } } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index b88638c46d..b7307efb6a 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -8,8 +8,8 @@ import { import KeywordContext from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" -import {callValidate} from "../../vocabularies/util" -import CodeGen, {_, nil, Name, Expression} from "../codegen" +import {callValidateCode} from "../../vocabularies/util" +import CodeGen, {_, nil, Code, Name} from "../codegen" import N from "../names" export function keywordCode( @@ -88,7 +88,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { gen // TODO add support for schemaType in keyword definition // .if(`${bad$DataType(schemaCode, def.schemaType, $data)} false`) // TODO refactor - .if(`${schemaCode} === undefined`) + .if(_`${schemaCode} === undefined`) .code(`${valid} = true;`) .else() if (def.validateSchema) { @@ -103,31 +103,31 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { } function validateAsyncRule(): Name { - const ruleErrs = gen.let("ruleErrs", "null") + const ruleErrs = gen.let("ruleErrs", null) gen.try( - () => assignValid("await "), + () => assignValid(_`await `), (e) => gen - .code(`${valid} = false;`) - .if(`${e} instanceof ValidationError`, `${ruleErrs} = ${e}.errors;`, `throw ${e};`) + .code(_`${valid} = false;`) + .if(_`${e} instanceof ValidationError`, _`${ruleErrs} = ${e}.errors;`, _`throw ${e};`) ) return ruleErrs } - function validateSyncRule(): Expression { + function validateSyncRule(): Code { const validateErrs = _`${validateRef}.errors` gen.assign(validateErrs, null) - assignValid("") + assignValid(nil) return validateErrs } - function assignValid(await: string = def.async ? "await " : ""): void { - const passCxt = it.opts.passContext ? "this" : "self" + function assignValid(await: Code = def.async ? _`await ` : nil): void { + const passCxt = it.opts.passContext ? N.this : N.self const passSchema = !(("compile" in def && !$data) || def.schema === false) - gen.code(`${valid} = ${await}${callValidate(cxt, validateRef, passCxt, passSchema)};`) + gen.code(`${valid} = ${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)};`) } - function reportKeywordErrors(ruleErrs: Expression): void { + function reportKeywordErrors(ruleErrs: Code): void { switch (def.valid) { case true: return @@ -145,12 +145,12 @@ function modifyData(cxt: KeywordContext) { gen.if(it.parentData, () => gen.assign(data, `${it.parentData}[${it.parentDataProperty}];`)) } -function addKeywordErrors(cxt: KeywordContext, errs: Expression): void { +function addKeywordErrors(cxt: KeywordContext, errs: Code): void { const {gen} = cxt gen.if( - `Array.isArray(${errs})`, + _`Array.isArray(${errs})`, () => { - gen.assign(N.vErrors, `${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged + gen.assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged gen.assign(N.errors, _`${N.vErrors}.length;`) extendErrors(cxt) }, diff --git a/lib/types.ts b/lib/types.ts index ca65f4b816..f9f04b283b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -110,7 +110,7 @@ export interface CompilationContext { parentData: Name parentDataProperty: Code | number dataNames: Name[] - dataPathArr: (Expression | number)[] + dataPathArr: (Code | number)[] dataLevel: number topSchemaRef: Code async: boolean @@ -121,7 +121,7 @@ export interface CompilationContext { baseId: string schemaPath: Code errSchemaPath: string // this is actual string, should not be changed to Code - errorPath: string + errorPath: Code propertyName?: Name compositeRule?: boolean createErrors?: boolean // TODO maybe remove later diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 71c88e023d..4cbd31f716 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -11,7 +11,7 @@ const def: CodeKeywordDefinition = { before: "uniqueItems", code(cxt: KeywordContext) { const {gen, schema, parentSchema, data, it} = cxt - const len = gen.const("len", `${data}.length`) + const len = gen.const("len", _`${data}.length`) const items = parentSchema.items // TODO strict mode: fail or warning if "additionalItems" is present without "items" Array if (!Array.isArray(items)) return diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 667e131a6b..75dd83de7a 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -6,10 +6,9 @@ import { alwaysValidSchema, loopPropertiesCode, usePattern, - orExpr, } from "../util" import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" -import {_, Name, Expression} from "../../compile/codegen" +import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" const def: CodeKeywordDefinition = { @@ -19,12 +18,13 @@ const def: CodeKeywordDefinition = { trackErrors: true, code(cxt: KeywordContext) { const {gen, schema, parentSchema, data, errsCount, it} = cxt + if (!errsCount) throw new Error("ajv implementation error") const {allErrors, opts} = it if (alwaysValidSchema(it, schema) && opts.removeAdditional !== "all") return const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) checkAdditionalProperties() - if (!allErrors) gen.if(`${errsCount} === ${N.errors}`) + if (!allErrors) gen.if(_`${errsCount} === ${N.errors}`) function checkAdditionalProperties(): void { loopPropertiesCode(cxt, (key: Name) => { @@ -33,21 +33,21 @@ const def: CodeKeywordDefinition = { }) } - function isAdditional(key: Name): string { - let definedProp: Expression = "" + function isAdditional(key: Name): Code { + let definedProp: Code if (props.length > 8) { // TODO maybe an option instead of hard-coded 8? const propsSchema = schemaRefOrVal(it, parentSchema.properties, "properties") - definedProp = `${propsSchema}.hasOwnProperty(${key})` + definedProp = _`${propsSchema}.hasOwnProperty(${key})` } else if (props.length) { - definedProp = orExpr(props, (p) => _`${key} === ${p}`) + definedProp = or(...props.map((p) => _`${key} === ${p}`)) + } else { + definedProp = nil } if (patProps.length) { - definedProp += - (definedProp ? " || " : "") + - orExpr(patProps, (p) => _`${usePattern(gen, p)}.test(${key})`) + definedProp = or(definedProp, ...patProps.map((p) => _`${usePattern(gen, p)}.test(${key})`)) } - return `!(${definedProp})` + return _`!(${definedProp})` } function deleteAdditional(key: Name): void { diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 46d85c30be..6c2759d53a 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -53,7 +53,7 @@ const def: CodeKeywordDefinition = { } }) } else { - gen.if(`${hasProperty} && (${checkMissingProp(cxt, deps, missing)})`) + gen.if(_`${hasProperty} && (${checkMissingProp(cxt, deps, missing)})`) reportMissingProp(cxt, missing) gen.else() } diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index b0d35492eb..f88ae3344f 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -11,7 +11,7 @@ const def: CodeKeywordDefinition = { before: "uniqueItems", code(cxt: KeywordContext) { const {gen, schema, data, it} = cxt - const len = gen.const("len", `${data}.length`) + const len = gen.const("len", _`${data}.length`) if (Array.isArray(schema)) { validateDefinedItems() } else if (!alwaysValidSchema(it, schema)) { diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 43d97ff22d..71db26d828 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -11,7 +11,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordContext) { const {gen, schema, it} = cxt const valid = gen.let("valid", false) - const passing = gen.let("passing", "null") + const passing = gen.let("passing", null) const schValid = gen.name("_valid") cxt.setParams({passing}) // TODO possibly fail straight away (with warning or exception) if there are two empty always valid schemas diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 202f023c57..6cf4eba948 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -3,8 +3,8 @@ import KeywordContext from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" -import {callValidate} from "../util" -import {_, str, nil, Expression} from "../../compile/codegen" +import {callValidateCode} from "../util" +import {_, str, nil, Code, Expression} from "../../compile/codegen" import N from "../../compile/names" const def: CodeKeywordDefinition = { @@ -14,7 +14,7 @@ const def: CodeKeywordDefinition = { const {gen, schema, it} = cxt const {resolveRef, allErrors, baseId, isRoot, root, opts, logger} = it const ref = getRef() - const passCxt = opts.passContext ? "this" : "" + const passCxt = opts.passContext ? N.this : nil if (ref === undefined) missingRef() else if (ref.inline) applyRefSchema(ref) else if (ref.$async || it.async) validateAsyncRef(ref.code) @@ -59,12 +59,12 @@ const def: CodeKeywordDefinition = { cxt.ok(valid) } - function validateAsyncRef(v: Expression): void { + function validateAsyncRef(v: Code): void { if (!it.async) throw new Error("async schema referenced by sync schema") const valid = gen.let("valid") gen.try( () => { - gen.code(`await ${callValidate(cxt, v, passCxt)};`) + gen.code(`await ${callValidateCode(cxt, v, passCxt)};`) if (!allErrors) gen.assign(valid, true) }, (e) => { @@ -76,8 +76,8 @@ const def: CodeKeywordDefinition = { cxt.ok(valid) } - function validateRef(v: Expression): void { - cxt.pass(callValidate(cxt, v, passCxt), () => addErrorsFrom(v)) + function validateRef(v: Code): void { + cxt.pass(callValidateCode(cxt, v, passCxt), () => addErrorsFrom(v)) } function addErrorsFrom(source: Expression): void { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 70e7098724..bf9ecde620 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, or} from "../util" -import {_, str, nil, Code, getProperty} from "../../compile/codegen" +import {bad$DataType} from "../util" +import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "format", @@ -17,7 +17,7 @@ const def: CodeKeywordDefinition = { else validateFormat() function validate$DataFormat() { - const fmtDef = gen.const("fmtDef", `formats[${schemaCode}]`) + const fmtDef = gen.const("fmtDef", _`formats[${schemaCode}]`) const fmtType = gen.let("fmtType") const format = gen.let("format") gen.if( diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 4aaedacf16..18544c506c 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,6 +1,6 @@ import KeywordContext from "../compile/context" -import {noPropertyInData, orExpr} from "./util" -import {_, Code, Name} from "../compile/codegen" +import {noPropertyInData} from "./util" +import {_, or, Code, Name} from "../compile/codegen" export function checkReportMissingProp(cxt: KeywordContext, prop: string): void { const {gen, data, it} = cxt @@ -15,10 +15,11 @@ export function checkMissingProp( properties: string[], missing: Name ): Code { - return orExpr(properties, (prop) => { - const hasNoProp = noPropertyInData(data, prop, opts.ownProperties) - return _`(${hasNoProp} && (${missing} = ${prop}))` - }) + return or( + ...properties.map( + (prop) => _`(${noPropertyInData(data, prop, opts.ownProperties)} && (${missing} = ${prop}))` + ) + ) } export function reportMissingProp(cxt: KeywordContext, missing: Name): void { diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index d36e0a2d7f..82d716e2fe 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -71,30 +71,19 @@ export function loopPropertiesCode( gen.for(_`const ${key} ${iteration}`, () => loopBody(key)) } -export function orExpr(items: string[], condition: (s: string, i: number) => Code): Code { - return items.map(condition).reduce(orCode) -} - -export function or(...args: Code[]): Code { - return args.reduce(orCode) -} - -function orCode(x: Code, y: Code): Code { - return x === nil ? y : y === nil ? x : _`${x} || ${y}` -} - -export function callValidate( +export function callValidateCode( {schemaCode, data, it}: KeywordContext, - func: Expression, - context?: string, + func: Code, + context: Code, passSchema?: boolean -): string { +): Expression { const dataAndSchema = passSchema ? _`${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` : data - const dataPath = `(${N.dataPath} || '')${it.errorPath === '""' ? "" : ` + ${it.errorPath}`}` // TODO joinPaths? - const args = `${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` - return context ? `${func}.call(${context}, ${args})` : `${func}(${args})` + const appendErrPath = it.errorPath.toString() === '""' ? nil : _` + ${it.errorPath}` + const dataPath = _`(${N.dataPath} || '')${appendErrPath}` // TODO concat? + const args = _`${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` + return context !== nil ? `${func}.call(${context}, ${args})` : `${func}(${args})` } export function usePattern(gen: CodeGen, pattern: string): Name { diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index fdc74c3acb..ac2a23f4a5 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,7 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {orExpr} from "../util" -import {_, Name, Code} from "../../compile/codegen" +import {_, or, Name, Code} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "enum", @@ -26,7 +25,7 @@ const def: CodeKeywordDefinition = { cxt.pass(valid) } else { const vSchema = gen.const("schema", schemaCode) - const cond: Code = orExpr(schema, (_x, i) => equalCode(vSchema, i)) + const cond: Code = or(...schema.map((_x, i) => equalCode(vSchema, i))) cxt.pass(cond) } } diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 34c5c8ed5a..a999856a32 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, or} from "../util" -import {_, str, operators, Code} from "../../compile/codegen" +import {bad$DataType} from "../util" +import {_, str, or, operators, Code} from "../../compile/codegen" const ops = operators diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 09bc0f100a..f45003e49e 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, or} from "../util" -import {_, str, operators} from "../../compile/codegen" +import {bad$DataType} from "../util" +import {_, str, or, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxItems", "minItems"], diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 9b3113ce77..9f762fc0e6 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, or} from "../util" -import {_, str, operators} from "../../compile/codegen" +import {bad$DataType} from "../util" +import {_, str, or, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxLength", "minLength"], diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index 184d255d56..ab1965ba70 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, or} from "../util" -import {_, str, operators} from "../../compile/codegen" +import {bad$DataType} from "../util" +import {_, str, or, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxProperties", "minProperties"], diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 28adcb873e..ffbba064b2 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, or} from "../util" -import {_, str} from "../../compile/codegen" +import {bad$DataType} from "../util" +import {_, str, or} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "multipleOf", diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 99d1673e88..d10d59868c 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, usePattern, or} from "../util" -import {_, str} from "../../compile/codegen" +import {bad$DataType, usePattern} from "../util" +import {_, str, or} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "pattern", diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 718bdcfc2e..c617bd2f1f 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -50,7 +50,7 @@ const def: CodeKeywordDefinition = { } cxt.ok(valid) } else { - gen.if(`${checkMissingProp(cxt, schema, missing)}`) + gen.if(checkMissingProp(cxt, schema, missing)) reportMissingProp(cxt, missing) gen.else() } diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index a155e2254d..8417a504b8 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -33,7 +33,7 @@ const def: CodeKeywordDefinition = { const j = gen.let("j") cxt.setParams({i, j}) gen.assign(valid, true) - gen.if(`${i} > 1`, () => (canOptimize() ? loopN : loopN2)(i, j)) + gen.if(_`${i} > 1`, () => (canOptimize() ? loopN : loopN2)(i, j)) } function canOptimize(): boolean { @@ -52,7 +52,7 @@ const def: CodeKeywordDefinition = { ) const indices = gen.const("indices", _`{}`) gen.for(_`;${i}--;`, () => { - gen.let(item, `${data}[${i}];`) + gen.let(item, _`${data}[${i}];`) gen.if(wrongType, "continue") if (Array.isArray(itemType)) gen.if(_`typeof ${item} == "string"`, _`${item} += "_"`) gen From 1445d212fbe4bcde01f250bc389e74d9aa2f81ff Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:17:47 -0400 Subject: [PATCH 122/322] refactor: safe code generation --- lib/compile/codegen.ts | 70 +++++++++---------- lib/compile/context.ts | 6 +- lib/compile/errors.ts | 30 ++++---- lib/compile/index.ts | 6 +- lib/compile/validate/boolSchema.ts | 2 +- lib/compile/validate/dataType.ts | 16 ++--- lib/compile/validate/defaults.ts | 4 +- lib/compile/validate/keyword.ts | 8 +-- lib/types.ts | 8 +-- .../applicator/additionalItems.ts | 2 +- .../applicator/additionalProperties.ts | 4 +- lib/vocabularies/applicator/contains.ts | 4 +- lib/vocabularies/applicator/if.ts | 4 +- lib/vocabularies/applicator/items.ts | 2 +- .../applicator/patternProperties.ts | 2 +- lib/vocabularies/core/ref.ts | 12 ++-- lib/vocabularies/util.ts | 16 +++-- lib/vocabularies/validation/required.ts | 4 +- lib/vocabularies/validation/uniqueItems.ts | 2 +- package.json | 2 +- spec/custom.spec.js | 2 +- 21 files changed, 105 insertions(+), 101 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 405950c1bb..ccd6b66ecd 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -5,13 +5,9 @@ enum BlockKind { Func, } -export type Expression = string | Name | Code - -export type Value = string | Name | Code | number | boolean | null - export type SafeExpr = Code | number | boolean | null -export type Block = string | Name | Code | (() => void) +export type Block = Code | (() => void) export class Code { #str: string @@ -75,7 +71,7 @@ export interface Scope { [prefix: string]: ValueReference[] } -type TemplateArg = Expression | number | boolean | null +type TemplateArg = SafeExpr | string export class ValueError extends Error { value: NameValue @@ -158,14 +154,14 @@ export default class CodeGen { return name } - scopeRefs(valuesName: Name, scope: Scope): Code { + scopeRefs(scopeName: Name, scope: Scope): Code { return this._reduceValues((rec: NameRec, prefix: string, i: number) => { const {value: v} = rec if (v.ref) { if (!scope[prefix]) scope[prefix] = [] scope[prefix][i] = v.ref const prefName = this.#valuePrefixes[prefix] - return _`${valuesName}.${prefName}[${i}]` + return _`${scopeName}.${prefName}[${i}]` } if (v.code) return v.code throw new ValueError("ref and code", rec) @@ -193,14 +189,14 @@ export default class CodeGen { return code } - _def(varKind: Name, nameOrPrefix: Name | string, rhs?: Value): Name { + _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) - if (rhs === undefined) this.code(`${varKind} ${name};`) - else this.code(`${varKind} ${name} = ${rhs};`) + if (rhs === undefined) this.code(_`${varKind} ${name};`) + else this.code(_`${varKind} ${name} = ${rhs};`) return name } - const(nameOrPrefix: Name | string, rhs?: Value): Name { + const(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { return this._def(varKinds.const, nameOrPrefix, rhs) } @@ -212,21 +208,21 @@ export default class CodeGen { return this._def(varKinds.var, nameOrPrefix, rhs) } - assign(name: Code, rhs: Value): CodeGen { - this.code(`${name} = ${rhs};`) + assign(name: Code, rhs: SafeExpr): CodeGen { + this.code(_`${name} = ${rhs};`) return this } - code(c?: Block | Value): CodeGen { + code(c?: Block | SafeExpr): CodeGen { // TODO optionally strip whitespace if (typeof c == "function") c() else if (c !== undefined) this._out += c + "\n" // TODO fails without line breaks return this } - if(condition: Expression | boolean, thenBody?: Block, elseBody?: Block): CodeGen { + if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen { this.#blocks.push(BlockKind.If) - this.code(`if(${condition}){`) + this.code(_`if(${condition}){`) if (thenBody && elseBody) { this.code(thenBody).else().code(elseBody).endIf() } else if (thenBody) { @@ -237,9 +233,9 @@ export default class CodeGen { return this } - ifNot(condition: Expression | boolean, thenBody?: Block, elseBody?: Block): CodeGen { - const cond = condition instanceof Name ? condition : `(${condition})` - return this.if(`!${cond}`, thenBody, elseBody) + ifNot(condition: Code, thenBody?: Block, elseBody?: Block): CodeGen { + const cond = condition instanceof Name ? condition : _`(${condition})` + return this.if(_`!${cond}`, thenBody, elseBody) } elseIf(condition: Code): CodeGen { @@ -251,7 +247,7 @@ export default class CodeGen { else(): CodeGen { if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" without "if"') this._lastBlock = BlockKind.Else - this.code(`}else{`) + this.code(_`}else{`) return this } @@ -260,13 +256,13 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" without "if"') this.#blocks.pop() - this.code(`}`) + this.code(_`}`) return this } - for(iteration: string | Code, forBody?: Block): CodeGen { + for(iteration: Code, forBody?: Block): CodeGen { this.#blocks.push(BlockKind.For) - this.code(`for(${iteration}){`) + this.code(_`for(${iteration}){`) if (forBody) this.code(forBody).endFor() return this } @@ -275,7 +271,7 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"') this.#blocks.pop() - this.code(`}`) + this.code(_`}`) return this } @@ -284,7 +280,7 @@ export default class CodeGen { return this } - return(value: Block | Value): CodeGen { + return(value: Block | SafeExpr): CodeGen { this._out += "return " this.code(value) this._out += ";" @@ -293,14 +289,14 @@ export default class CodeGen { try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen { if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') - this.code("try{").code(tryBody) + this.code(_`try{`).code(tryBody) if (catchCode) { const err = this.name("e") - this.code(`}catch(${err}){`) + this.code(_`}catch(${err}){`) catchCode(err) } - if (finallyCode) this.code("}finally{").code(finallyCode) - this.code("}") + if (finallyCode) this.code(_`}finally{`).code(finallyCode) + this.code(_`}`) return this } @@ -319,13 +315,13 @@ export default class CodeGen { throw new Error("CodeGen: block sequence already ended or incorrect number of blocks") } this.#blocks.length = len - if (toClose > 0) this.code("}".repeat(toClose)) + if (toClose > 0) this.code(new Code("}".repeat(toClose))) return this } func(name: Name, args: Code = nil, async?: boolean, funcBody?: Block): CodeGen { this.#blocks.push(BlockKind.Func) - this.code(`${async ? "async " : ""}function ${name}(${args}){`) + this.code(_`${async ? _`async ` : nil}function ${name}(${args}){`) if (funcBody) this.code(funcBody).endFunc() return this } @@ -334,7 +330,7 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" without "func"') this.#blocks.pop() - this.code(`}`) + this.code(_`}`) return this } @@ -353,13 +349,17 @@ export default class CodeGen { } } -export function quoteString(s: string): string { +function quoteString(s: string): string { return JSON.stringify(s) .replace(/\u2028/g, "\\u2028") .replace(/\u2029/g, "\\u2029") } -export function getProperty(key: Expression | number): Code { +export function stringify(s: string): Code { + return new Code(JSON.stringify(s)) +} + +export function getProperty(key: Code | string | number): Code { return typeof key == "string" && IDENTIFIER.test(key) ? new Code(`.${key}`) : _`[${key}]` } diff --git a/lib/compile/context.ts b/lib/compile/context.ts index d879d427bc..92f3b4c32c 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -7,7 +7,7 @@ import { import {schemaRefOrVal} from "../vocabularies/util" import {getData} from "./util" import {reportError, reportExtraError, resetErrorsCount, keywordError} from "./errors" -import CodeGen, {Code, Name, Expression} from "./codegen" +import CodeGen, {Code, Name} from "./codegen" import N from "./names" export default class KeywordContext implements KeywordErrorContext { @@ -54,7 +54,7 @@ export default class KeywordContext implements KeywordErrorContext { } } - result(condition: Expression, successAction?: () => void, failAction?: () => void): void { + result(condition: Code, successAction?: () => void, failAction?: () => void): void { this.gen.ifNot(condition) failAction ? failAction() : this.error() if (successAction) { @@ -67,7 +67,7 @@ export default class KeywordContext implements KeywordErrorContext { } } - pass(condition: Expression, failAction?: () => void): void { + pass(condition: Code, failAction?: () => void): void { this.result(condition, undefined, failAction) } diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index b237c9ba88..dd6fe9280e 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,5 +1,5 @@ import {KeywordErrorContext, KeywordErrorDefinition} from "../types" -import CodeGen, {_, str, Name, Expression} from "./codegen" +import CodeGen, {_, str, Code, Name} from "./codegen" import N from "./names" export const keywordError: KeywordErrorDefinition = { @@ -17,7 +17,7 @@ export function reportError( if (overrideAllErrors ?? (compositeRule || allErrors)) { addError(gen, errObj) } else { - returnErrors(gen, async, `[${errObj}]`) + returnErrors(gen, async, _`[${errObj}]`) } } @@ -51,7 +51,7 @@ export function extendErrors({ gen.const(err, _`${N.vErrors}[i]`) gen.if( _`${err}.dataPath === undefined`, - `${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` + _`${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` ) gen.code(_`${err}.schemaPath = ${str`${it.errSchemaPath}/${keyword}`};`) if (it.opts.verbose) { @@ -63,47 +63,47 @@ export function extendErrors({ }) } -function addError(gen: CodeGen, errObj: string): void { +function addError(gen: CodeGen, errObj: Code): void { const err = gen.const("err", errObj) gen.if(_`${N.vErrors} === null`, _`${N.vErrors} = [${err}]`, _`${N.vErrors}.push(${err})`) gen.code(_`${N.errors}++;`) } -function returnErrors(gen: CodeGen, async: boolean, errs: Expression): void { +function returnErrors(gen: CodeGen, async: boolean, errs: Code): void { if (async) { - gen.code(`throw new ValidationError(${errs})`) + gen.code(_`throw new ValidationError(${errs})`) } else { gen.assign(_`${N.validate}.errors`, errs) - gen.return("false") + gen.return(false) } } -function errorObjectCode(cxt: KeywordErrorContext, error: KeywordErrorDefinition): string { +function errorObjectCode(cxt: KeywordErrorContext, error: KeywordErrorDefinition): Code { const { keyword, data, schemaValue, it: {createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, } = cxt - if (createErrors === false) return "{}" + if (createErrors === false) return _`{}` if (!error) throw new Error('keyword definition must have "error" property') const {params, message} = error // TODO trim whitespace - let out = `{ - keyword: "${keyword}", + let out = _`{ + keyword: ${keyword}, dataPath: (${N.dataPath} || "") + ${errorPath}, schemaPath: ${str`${errSchemaPath}/${keyword}`}, params: ${params ? params(cxt) : _`{}`},` - if (propertyName) out += `propertyName: ${propertyName},` + if (propertyName) out = _`${out} propertyName: ${propertyName},` if (opts.messages !== false) { - out += `message: ${typeof message == "string" ? _`${message}` : message(cxt)},` + out = _`${out} message: ${typeof message == "string" ? message : message(cxt)},` } if (opts.verbose) { // TODO trim whitespace - out += ` + out = _`${out} schema: ${schemaValue}, parentSchema: ${topSchemaRef}${schemaPath}, data: ${data},` } - return out + "}" + return _`${out} }` } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 538f623138..a105f26edd 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -46,7 +46,7 @@ function compile(schema, root, localRefs, baseId) { var self = this, opts = this._opts, refVal = [undefined], - refs = {} + refs: {[ref: string]: number} = {} const scope: Scope = {} @@ -192,7 +192,7 @@ function compile(schema, root, localRefs, baseId) { var _refVal, refCode if (refIndex !== undefined) { _refVal = refVal[refIndex] - refCode = "refVal[" + refIndex + "]" + refCode = _`refVal[${refIndex}]` return resolvedRef(_refVal, refCode) } if (!isRoot && root.refs) { @@ -242,7 +242,7 @@ function compile(schema, root, localRefs, baseId) { refVal[refId] = v } - function resolvedRef(refVal, code): ResolvedRef { + function resolvedRef(refVal, code: Code): ResolvedRef { return typeof refVal == "object" || typeof refVal == "boolean" ? {code: code, schema: refVal, inline: true} : {code: code, $async: refVal && !!refVal.$async} diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 040eb7a982..d4b5701ef1 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -14,7 +14,7 @@ export function topBoolOrEmptySchema(it: CompilationContext): void { } else if (schema.$async === true) { gen.return(N.data) } else { - gen.assign(_`${N.validate}.errors`, "null") + gen.assign(_`${N.validate}.errors`, null) gen.return(true) } } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 5bc00f68f0..c99aa3a790 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -79,9 +79,9 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { case "string": gen .elseIf(_`${dataType} == "number" || ${dataType} == "boolean"`) - .code(_`${coerced} = "" + ${data}`) + .assign(coerced, _`"" + ${data}`) .elseIf(_`${data} === null`) - .code(_`${coerced} = ""`) + .assign(coerced, _`""`) return case "number": gen @@ -89,7 +89,7 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { _`${dataType} == "boolean" || ${data} === null || (${dataType} == "string" && ${data} && ${data} == +${data})` ) - .code(_`${coerced} = +${data}`) + .assign(coerced, _`+${data}`) return case "integer": gen @@ -97,18 +97,18 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { _`${dataType} === "boolean" || ${data} === null || (${dataType} === "string" && ${data} && ${data} == +${data} && !(${data} % 1))` ) - .code(`${coerced} = +${data}`) + .assign(coerced, _`+${data}`) return case "boolean": gen .elseIf(_`${data} === "false" || ${data} === 0 || ${data} === null`) - .code(_`${coerced} = false`) + .assign(coerced, false) .elseIf(_`${data} === "true" || ${data} === 1`) - .code(_`${coerced} = true`) + .assign(coerced, true) return case "null": gen.elseIf(_`${data} === "" || ${data} === 0 || ${data} === false`) - gen.code(_`${coerced} = null`) + gen.assign(coerced, null) return case "array": @@ -117,7 +117,7 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { _`${dataType} === "string" || ${dataType} === "number" || ${dataType} === "boolean" || ${data} === null` ) - .code(`${coerced} = [${data}]`) + .assign(coerced, _`[${data}]`) } } } diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 389fc22bed..cb8c073b56 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,5 +1,5 @@ import {CompilationContext} from "../../types" -import {_, getProperty} from "../codegen" +import {_, getProperty, stringify} from "../codegen" export function assignDefaults(it: CompilationContext, ty?: string): void { const {properties, items} = it.schema @@ -34,5 +34,5 @@ function assignDefault( } // `${childData} === undefined` + // (opts.useDefaults === "empty" ? ` || ${childData} === null || ${childData} === ""` : "") - gen.if(condition, `${childData} = ${JSON.stringify(defaultValue)}`) + gen.if(condition, _`${childData} = ${stringify(defaultValue)}`) } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index b7307efb6a..b4c6b82a42 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -89,11 +89,11 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { // TODO add support for schemaType in keyword definition // .if(`${bad$DataType(schemaCode, def.schemaType, $data)} false`) // TODO refactor .if(_`${schemaCode} === undefined`) - .code(`${valid} = true;`) + .assign(valid, true) .else() if (def.validateSchema) { const validateSchemaRef = useKeyword(gen, keyword, def.validateSchema) - gen.code(`${valid} = ${validateSchemaRef}(${schemaCode});`) + gen.assign(valid, _`${validateSchemaRef}(${schemaCode})`) // TODO fail if schema fails validation // gen.if(`!${valid}`) // reportError(cxt, keywordError) @@ -124,7 +124,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function assignValid(await: Code = def.async ? _`await ` : nil): void { const passCxt = it.opts.passContext ? N.this : N.self const passSchema = !(("compile" in def && !$data) || def.schema === false) - gen.code(`${valid} = ${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)};`) + gen.code(_`${valid} = ${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)};`) } function reportKeywordErrors(ruleErrs: Code): void { @@ -142,7 +142,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function modifyData(cxt: KeywordContext) { const {gen, data, it} = cxt - gen.if(it.parentData, () => gen.assign(data, `${it.parentData}[${it.parentDataProperty}];`)) + gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}];`)) } function addKeywordErrors(cxt: KeywordContext, errs: Code): void { diff --git a/lib/types.ts b/lib/types.ts index f9f04b283b..991520199f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import Cache from "./cache" -import CodeGen, {Code, Name, Expression} from "./compile/codegen" +import CodeGen, {Code, Name} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" import KeywordContext from "./compile/context" @@ -215,14 +215,14 @@ export interface KeywordErrorContext { $data?: string | false schema: any parentSchema: any - schemaCode: Expression | number | boolean - schemaValue: Expression | number | boolean + schemaCode: Code | number | boolean + schemaValue: Code | number | boolean errsCount?: Name params: KeywordContextParams it: CompilationContext } -export type KeywordContextParams = {[x: string]: Expression | number} +export type KeywordContextParams = {[x: string]: Code | string | number} export type FormatMode = "fast" | "full" diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 4cbd31f716..ae09932167 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -28,7 +28,7 @@ const def: CodeKeywordDefinition = { const i = gen.name("i") gen.for(_`let ${i}=${items.length}; ${i}<${len}; ${i}++`, () => { applySubschema(it, {keyword: "additionalItems", dataProp: i, expr: Expr.Num}, valid) - if (!it.allErrors) gen.ifNot(valid, "break") + if (!it.allErrors) gen.ifNot(valid, _`break`) }) } }, diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 75dd83de7a..27904f024d 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -51,7 +51,7 @@ const def: CodeKeywordDefinition = { } function deleteAdditional(key: Name): void { - gen.code(`delete ${data}[${key}];`) + gen.code(_`delete ${data}[${key}];`) } function additionalPropertyCode(key: Name): void { @@ -77,7 +77,7 @@ const def: CodeKeywordDefinition = { }) } else { applyAdditionalSchema(key, valid) - if (!allErrors) gen.ifNot(valid, "break") + if (!allErrors) gen.ifNot(valid, _`break`) } } } diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 3c79ef3c34..d2c1ad7b96 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -20,7 +20,7 @@ const def: CodeKeywordDefinition = { const valid = gen.name("valid") const i = gen.name("i") - gen.for(`let ${i}=0; ${i}<${data}.length; ${i}++`, () => { + gen.for(_`let ${i}=0; ${i}<${data}.length; ${i}++`, () => { applySubschema( it, { @@ -31,7 +31,7 @@ const def: CodeKeywordDefinition = { }, valid ) - gen.if(valid, "break") + gen.if(valid, _`break`) }) cxt.result(valid, () => cxt.reset()) diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 06876cef98..4402dda1ec 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -53,8 +53,8 @@ const def: CodeKeywordDefinition = { function validateClause(keyword: string, ifClause?: Name): () => void { return () => { applySubschema(it, {keyword}, schValid) - gen.code(`${valid} = ${schValid};`) - if (ifClause) gen.code(`${ifClause} = "${keyword}";`) + gen.code(_`${valid} = ${schValid};`) + if (ifClause) gen.code(_`${ifClause} = ${keyword};`) else cxt.setParams({ifClause: keyword}) } } diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index f88ae3344f..ae128e8291 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -43,7 +43,7 @@ const def: CodeKeywordDefinition = { const i = gen.name("i") gen.for(_`let ${i}=0; ${i}<${len}; ${i}++`, () => { applySubschema(it, {keyword: "items", dataProp: i, expr: Expr.Num}, valid) - if (!it.allErrors) gen.ifNot(valid, "break") + if (!it.allErrors) gen.ifNot(valid, _`break`) }) cxt.ok(valid) } diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index ce7c02587a..b762f104fd 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -40,7 +40,7 @@ const def: CodeKeywordDefinition = { }, valid ) - if (!it.allErrors) gen.ifNot(valid, "break") + if (!it.allErrors) gen.ifNot(valid, _`break`) }) }) } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 6cf4eba948..3588490f4b 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -4,7 +4,7 @@ import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" import {callValidateCode} from "../util" -import {_, str, nil, Code, Expression} from "../../compile/codegen" +import {_, str, nil, Code} from "../../compile/codegen" import N from "../../compile/names" const def: CodeKeywordDefinition = { @@ -64,11 +64,11 @@ const def: CodeKeywordDefinition = { const valid = gen.let("valid") gen.try( () => { - gen.code(`await ${callValidateCode(cxt, v, passCxt)};`) + gen.code(_`await ${callValidateCode(cxt, v, passCxt)};`) if (!allErrors) gen.assign(valid, true) }, (e) => { - gen.if(_`!(${e} instanceof ValidationError)`, `throw ${e}`) + gen.if(_`!(${e} instanceof ValidationError)`, _`throw ${e}`) addErrorsFrom(e) if (!allErrors) gen.assign(valid, false) } @@ -80,9 +80,9 @@ const def: CodeKeywordDefinition = { cxt.pass(callValidateCode(cxt, v, passCxt), () => addErrorsFrom(v)) } - function addErrorsFrom(source: Expression): void { - const errs = `${source}.errors` - gen.assign(N.vErrors, `${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged + function addErrorsFrom(source: Code): void { + const errs = _`${source}.errors` + gen.assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged gen.assign(N.errors, _`${N.vErrors}.length`) } }, diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 82d716e2fe..57a1ab5303 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,7 +1,7 @@ import {schemaHasRules} from "../compile/util" import {CompilationContext} from "../types" import KeywordContext from "../compile/context" -import CodeGen, {_, nil, Code, Name, Expression, getProperty} from "../compile/codegen" +import CodeGen, {_, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" export function bad$DataType( @@ -47,16 +47,20 @@ export function schemaProperties(it: CompilationContext, schema: object): string return allSchemaProperties(schema).filter((p) => !alwaysValidSchema(it, schema[p])) } -export function isOwnProperty(data: Name, property: Expression): Code { +function isOwnProperty(data: Name, property: Name | string): Code { return _`Object.prototype.hasOwnProperty.call(${data}, ${property})` } -export function propertyInData(data: Name, property: Expression, ownProperties?: boolean): Code { +export function propertyInData(data: Name, property: Name | string, ownProperties?: boolean): Code { const cond = _`${data}${getProperty(property)} !== undefined` return ownProperties ? _`${cond} && ${isOwnProperty(data, property)}` : cond } -export function noPropertyInData(data: Name, property: Expression, ownProperties?: boolean): Code { +export function noPropertyInData( + data: Name, + property: Name | string, + ownProperties?: boolean +): Code { const cond = _`${data}${getProperty(property)} === undefined` return ownProperties ? _`${cond} || !${isOwnProperty(data, property)}` : cond } @@ -76,14 +80,14 @@ export function callValidateCode( func: Code, context: Code, passSchema?: boolean -): Expression { +): Code { const dataAndSchema = passSchema ? _`${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` : data const appendErrPath = it.errorPath.toString() === '""' ? nil : _` + ${it.errorPath}` const dataPath = _`(${N.dataPath} || '')${appendErrPath}` // TODO concat? const args = _`${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` - return context !== nil ? `${func}.call(${context}, ${args})` : `${func}(${args})` + return context !== nil ? _`${func}.call(${context}, ${args})` : _`${func}(${args})` } export function usePattern(gen: CodeGen, pattern: string): Name { diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index c617bd2f1f..ea94590025 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -59,14 +59,14 @@ const def: CodeKeywordDefinition = { function loopAllRequired(): void { const prop = gen.name("prop") cxt.setParams({missingProperty: prop}) - gen.for(`const ${prop} of ${schemaCode}`, () => + gen.for(_`const ${prop} of ${schemaCode}`, () => gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => cxt.error()) ) } function loopUntilMissing(missing: Name, valid: Name): void { cxt.setParams({missingProperty: missing}) - gen.for(`${missing} of ${schemaCode}`, () => { + gen.for(_`${missing} of ${schemaCode}`, () => { gen.assign(valid, propertyInData(data, missing, it.opts.ownProperties)) gen.ifNot(valid, () => { cxt.error() diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 8417a504b8..a7c6aac99e 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -53,7 +53,7 @@ const def: CodeKeywordDefinition = { const indices = gen.const("indices", _`{}`) gen.for(_`;${i}--;`, () => { gen.let(item, _`${data}[${i}];`) - gen.if(wrongType, "continue") + gen.if(wrongType, _`continue`) if (Array.isArray(itemType)) gen.if(_`typeof ${item} == "string"`, _`${item} += "_"`) gen .if(_`typeof ${indices}[${item}] == "number"`, () => { diff --git a/package.json b/package.json index 9886f04f1b..c70e158fab 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", "test-spec": "mocha spec/{**/,}*.spec.js -R dot", "test-fast": "AJV_FAST_TEST=true npm run test-spec", - "test-debug": "npm run test-spec -- --inspect", + "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", "test-ts": "tsc --target ES5 --noImplicitAny --noEmit spec/typescript/index.ts", "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 6a54122af5..2ff0185c40 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -550,7 +550,7 @@ describe("Custom keywords", () => { const minOk = gen.const("minOk", _`${data} >${eq} ${min}`) const maxOk = gen.const("maxOk", _`${data} <${eq} ${max}`) cxt.setParams({minOk, maxOk, eq}) - cxt.pass(`${minOk} && ${maxOk}`) + cxt.pass(_`${minOk} && ${maxOk}`) }, error: { message: ({params: {minOk, eq}, schema: [min, max]}) => From e3dd0b92139b8b1bb40ce9a6744c0460530549a6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 30 Aug 2020 09:07:38 -0400 Subject: [PATCH 123/322] feat: Code append --- lib/compile/codegen.ts | 14 +++++++++++++- lib/compile/errors.ts | 19 +++++++++---------- lib/vocabularies/util.ts | 4 ++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index ccd6b66ecd..13b3bfd098 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -24,6 +24,10 @@ export class Code { const len = this.#str.length return len >= 2 && this.#str[0] === '"' && this.#str[len - 1] === '"' } + + append(c: Code): void { + this.#str += c.#str + } } export const nil = new Code("") @@ -40,7 +44,15 @@ export const operators = { AND: new Code("&&"), } -export class Name extends Code {} +export class Name extends Code { + isQuoted() { + return false + } + + append(_c: Code): void { + throw new Error("CodeGen: can't append Name") + } +} const varKinds = { const: new Name("const"), diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index dd6fe9280e..c9a8976cce 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -89,21 +89,20 @@ function errorObjectCode(cxt: KeywordErrorContext, error: KeywordErrorDefinition if (!error) throw new Error('keyword definition must have "error" property') const {params, message} = error // TODO trim whitespace - let out = _`{ + const out = _`{ keyword: ${keyword}, dataPath: (${N.dataPath} || "") + ${errorPath}, schemaPath: ${str`${errSchemaPath}/${keyword}`}, - params: ${params ? params(cxt) : _`{}`},` - if (propertyName) out = _`${out} propertyName: ${propertyName},` + params: ${params ? params(cxt) : _`{}`}` + if (propertyName) out.append(_`, propertyName: ${propertyName}`) if (opts.messages !== false) { - out = _`${out} message: ${typeof message == "string" ? message : message(cxt)},` + out.append(_`, message: ${typeof message == "string" ? message : message(cxt)}`) } if (opts.verbose) { - // TODO trim whitespace - out = _`${out} - schema: ${schemaValue}, - parentSchema: ${topSchemaRef}${schemaPath}, - data: ${data},` + out.append( + _`, schema: ${schemaValue}, parentSchema: ${topSchemaRef}${schemaPath}, data: ${data}` + ) } - return _`${out} }` + out.append(_`}`) + return out } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 57a1ab5303..f526f3cd3d 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -84,8 +84,8 @@ export function callValidateCode( const dataAndSchema = passSchema ? _`${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` : data - const appendErrPath = it.errorPath.toString() === '""' ? nil : _` + ${it.errorPath}` - const dataPath = _`(${N.dataPath} || '')${appendErrPath}` // TODO concat? + // const appendErrPath = it.errorPath.toString() === '""' ? nil : _` + ${it.errorPath}` + const dataPath = _`(${N.dataPath} || '') + ${it.errorPath}` // TODO refactor other places const args = _`${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` return context !== nil ? _`${func}.call(${context}, ${args})` : _`${func}(${args})` } From 694981cd899463505f1fd78675c9210de334a718 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Aug 2020 18:16:50 +0100 Subject: [PATCH 124/322] improve codegen performance --- lib/compile/codegen.ts | 81 +++++++++++-------- lib/compile/errors.ts | 9 +-- lib/compile/validate/dataType.ts | 7 +- lib/compile/validate/keyword.ts | 17 ++-- .../applicator/additionalProperties.ts | 2 +- lib/vocabularies/applicator/if.ts | 4 +- lib/vocabularies/applicator/oneOf.ts | 8 +- lib/vocabularies/core/ref.ts | 4 +- lib/vocabularies/format/format.ts | 15 ++-- lib/vocabularies/validation/enum.ts | 2 +- lib/vocabularies/validation/uniqueItems.ts | 9 ++- 11 files changed, 87 insertions(+), 71 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 13b3bfd098..6991a867ed 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -94,17 +94,18 @@ export class ValueError extends Error { } export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { + // TODO benchmark if loop is faster than reduce + // let res = strs[0] + // for (let i = 0; i < args.length; i++) { + // res += interpolate(args[i]) + strs[i + 1] + // } + // return new Code(res) return new Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) } -// TODO this is unsafe tagged template that should be removed later -export function $(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { - return new Code(strs.reduce((res, s, i) => res + args[i - 1] + s)) -} - -export function str(strings: TemplateStringsArray, ...args: TemplateArg[]): Code { +export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { return new Code( - strings.map(quoteString).reduce((res, s, i) => { + strs.map(quoteString).reduce((res, s, i) => { let aStr = interpolate(args[i - 1]) if (aStr instanceof Code && aStr.isQuoted()) aStr = aStr.toString() return typeof aStr === "string" @@ -125,13 +126,12 @@ const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i export default class CodeGen { #names: {[prefix: string]: NameGroup} = {} #valuePrefixes: {[prefix: string]: Name} = {} - // TODO make private. Possibly stack? - _out = "" + #out = "" #blocks: BlockKind[] = [] #blockStarts: number[] = [] toString(): string { - return this._out + return this.#out } _nameGroup(prefix: string): NameGroup { @@ -203,8 +203,8 @@ export default class CodeGen { _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) - if (rhs === undefined) this.code(_`${varKind} ${name};`) - else this.code(_`${varKind} ${name} = ${rhs};`) + if (rhs === undefined) this.#out += `${varKind} ${name};` + else this.#out += `${varKind} ${name} = ${rhs};` return name } @@ -221,20 +221,22 @@ export default class CodeGen { } assign(name: Code, rhs: SafeExpr): CodeGen { - this.code(_`${name} = ${rhs};`) + this.#out += `${name} = ${rhs};` return this } code(c?: Block | SafeExpr): CodeGen { // TODO optionally strip whitespace if (typeof c == "function") c() - else if (c !== undefined) this._out += c + "\n" // TODO fails without line breaks + else if (c !== undefined) this.#out += c + ";" //+ "\n" // TODO fails without line breaks return this } - if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen { + if(condition: Code | boolean, thenBody?: Block, elseBody?: Block, _negate?: true): CodeGen { this.#blocks.push(BlockKind.If) - this.code(_`if(${condition}){`) + this.#out += `if(${ + _negate ? (condition instanceof Name ? `!${condition}` : `!(${condition})`) : condition + }){` if (thenBody && elseBody) { this.code(thenBody).else().code(elseBody).endIf() } else if (thenBody) { @@ -246,20 +248,19 @@ export default class CodeGen { } ifNot(condition: Code, thenBody?: Block, elseBody?: Block): CodeGen { - const cond = condition instanceof Name ? condition : _`(${condition})` - return this.if(_`!${cond}`, thenBody, elseBody) + return this.if(condition, thenBody, elseBody, true) } elseIf(condition: Code): CodeGen { if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"') - this.code(_`}else if(${condition}){`) + this.#out += `}else if(${condition}){` return this } else(): CodeGen { if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" without "if"') this._lastBlock = BlockKind.Else - this.code(_`}else{`) + this.#out += "}else{" return this } @@ -268,13 +269,13 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" without "if"') this.#blocks.pop() - this.code(_`}`) + this.#out += "}" return this } for(iteration: Code, forBody?: Block): CodeGen { this.#blocks.push(BlockKind.For) - this.code(_`for(${iteration}){`) + this.#out += `for(${iteration}){` if (forBody) this.code(forBody).endFor() return this } @@ -283,32 +284,46 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"') this.#blocks.pop() - this.code(_`}`) + this.#out += "}" + return this + } + + label(label?: Code): CodeGen { + this.#out += label + ":" return this } break(label?: Code): CodeGen { - this.code(label ? _`break ${label};` : _`break;`) + this.#out += label ? `break ${label};` : "break;" return this } return(value: Block | SafeExpr): CodeGen { - this._out += "return " + this.#out += "return " this.code(value) - this._out += ";" + this.#out += ";" return this } try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen { if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') - this.code(_`try{`).code(tryBody) + this.#out += "try{" + this.code(tryBody) if (catchCode) { const err = this.name("e") - this.code(_`}catch(${err}){`) + this.#out += `}catch(${err}){` catchCode(err) } - if (finallyCode) this.code(_`}finally{`).code(finallyCode) - this.code(_`}`) + if (finallyCode) { + this.#out += "}finally{" + this.code(finallyCode) + } + this.#out += "}" + return this + } + + throw(err: Code): CodeGen { + this.#out += `throw ${err};` return this } @@ -327,13 +342,13 @@ export default class CodeGen { throw new Error("CodeGen: block sequence already ended or incorrect number of blocks") } this.#blocks.length = len - if (toClose > 0) this.code(new Code("}".repeat(toClose))) + if (toClose > 0) this.#out += "}".repeat(toClose) return this } func(name: Name, args: Code = nil, async?: boolean, funcBody?: Block): CodeGen { this.#blocks.push(BlockKind.Func) - this.code(_`${async ? _`async ` : nil}function ${name}(${args}){`) + this.#out += `${async ? "async " : ""}function ${name}(${args}){` if (funcBody) this.code(funcBody).endFunc() return this } @@ -342,7 +357,7 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" without "func"') this.#blocks.pop() - this.code(_`}`) + this.#out += "}" return this } diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index c9a8976cce..9e9caf0267 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -53,12 +53,9 @@ export function extendErrors({ _`${err}.dataPath === undefined`, _`${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` ) - gen.code(_`${err}.schemaPath = ${str`${it.errSchemaPath}/${keyword}`};`) + gen.code(_`${err}.schemaPath = ${str`${it.errSchemaPath}/${keyword}`}`) if (it.opts.verbose) { - gen.code( - _`${err}.schema = ${schemaValue}; - ${err}.data = ${data};` - ) + gen.code(_`${err}.schema = ${schemaValue}; ${err}.data = ${data}`) } }) } @@ -66,7 +63,7 @@ export function extendErrors({ function addError(gen: CodeGen, errObj: Code): void { const err = gen.const("err", errObj) gen.if(_`${N.vErrors} === null`, _`${N.vErrors} = [${err}]`, _`${N.vErrors}.push(${err})`) - gen.code(_`${N.errors}++;`) + gen.code(_`${N.errors}++`) } function returnErrors(gen: CodeGen, async: boolean, errs: Code): void { diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index c99aa3a790..7ab54c84e2 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -55,8 +55,9 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { if (opts.coerceTypes === "array") { gen.if(_`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen - .code(_`${data} = ${data}[0]; ${dataType} = typeof ${data};`) - .if(checkDataType(schema.type, data, opts.strictNumbers), _`${coerced} = ${data}`) + .assign(data, _`${data}[0]`) + .assign(dataType, _`typeof ${data}`) + .if(checkDataType(schema.type, data, opts.strictNumbers), () => gen.assign(coerced, data)) ) } gen.if(_`${coerced} !== undefined`) @@ -70,7 +71,7 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { gen.endIf() gen.if(_`${coerced} !== undefined`, () => { - gen.code(_`${data} = ${coerced};`) + gen.assign(data, coerced) assignParentData(it, coerced) }) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index b4c6b82a42..35feb24fa5 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -107,9 +107,11 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { gen.try( () => assignValid(_`await `), (e) => - gen - .code(_`${valid} = false;`) - .if(_`${e} instanceof ValidationError`, _`${ruleErrs} = ${e}.errors;`, _`throw ${e};`) + gen.assign(valid, false).if( + _`${e} instanceof ValidationError`, + () => gen.assign(ruleErrs, _`${e}.errors`), + () => gen.throw(e) + ) ) return ruleErrs } @@ -124,7 +126,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function assignValid(await: Code = def.async ? _`await ` : nil): void { const passCxt = it.opts.passContext ? N.this : N.self const passSchema = !(("compile" in def && !$data) || def.schema === false) - gen.code(_`${valid} = ${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)};`) + gen.assign(valid, _`${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`) } function reportKeywordErrors(ruleErrs: Code): void { @@ -142,7 +144,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { function modifyData(cxt: KeywordContext) { const {gen, data, it} = cxt - gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}];`)) + gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`)) } function addKeywordErrors(cxt: KeywordContext, errs: Code): void { @@ -150,8 +152,9 @@ function addKeywordErrors(cxt: KeywordContext, errs: Code): void { gen.if( _`Array.isArray(${errs})`, () => { - gen.assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged - gen.assign(N.errors, _`${N.vErrors}.length;`) + gen + .assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) + .assign(N.errors, _`${N.vErrors}.length`) extendErrors(cxt) }, () => cxt.error() diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 27904f024d..c2eec86494 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -51,7 +51,7 @@ const def: CodeKeywordDefinition = { } function deleteAdditional(key: Name): void { - gen.code(_`delete ${data}[${key}];`) + gen.code(_`delete ${data}[${key}]`) } function additionalPropertyCode(key: Name): void { diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 4402dda1ec..59b0011d41 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -53,8 +53,8 @@ const def: CodeKeywordDefinition = { function validateClause(keyword: string, ifClause?: Name): () => void { return () => { applySubschema(it, {keyword}, schValid) - gen.code(_`${valid} = ${schValid};`) - if (ifClause) gen.code(_`${ifClause} = ${keyword};`) + gen.assign(valid, schValid) + if (ifClause) gen.assign(ifClause, _`${keyword}`) else cxt.setParams({ifClause: keyword}) } } diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 71db26d828..fbcfdf1a53 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -43,14 +43,12 @@ const def: CodeKeywordDefinition = { if (i > 0) { gen .if(_`${schValid} && ${valid}`) - .code( - _`${valid} = false; - ${passing} = [${passing}, ${i}];` - ) + .assign(valid, false) + .assign(passing, _`[${passing}, ${i}]`) .else() } - gen.if(schValid, _`${valid} = true; ${passing} = ${i};`) + gen.if(schValid, () => gen.assign(valid, true).assign(passing, i)) }) } }, diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 3588490f4b..120fbf9ede 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -64,11 +64,11 @@ const def: CodeKeywordDefinition = { const valid = gen.let("valid") gen.try( () => { - gen.code(_`await ${callValidateCode(cxt, v, passCxt)};`) + gen.code(_`await ${callValidateCode(cxt, v, passCxt)}`) if (!allErrors) gen.assign(valid, true) }, (e) => { - gen.if(_`!(${e} instanceof ValidationError)`, _`throw ${e}`) + gen.if(_`!(${e} instanceof ValidationError)`, () => gen.throw(e)) addErrorsFrom(e) if (!allErrors) gen.assign(valid, false) } diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index bf9ecde620..6ca9813054 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -17,13 +17,14 @@ const def: CodeKeywordDefinition = { else validateFormat() function validate$DataFormat() { - const fmtDef = gen.const("fmtDef", _`formats[${schemaCode}]`) - const fmtType = gen.let("fmtType") + const fDef = gen.const("fDef", _`formats[${schemaCode}]`) + const fType = gen.let("fType") const format = gen.let("format") + // TODO simplify gen.if( - _`typeof ${fmtDef} == "object" && !(${fmtDef} instanceof RegExp)`, - _`${fmtType} = ${fmtDef}.type || "string"; ${format} = ${fmtDef}.validate;`, - _`${fmtType} = "string"; ${format} = ${fmtDef}` + _`typeof ${fDef} == "object" && !(${fDef} instanceof RegExp)`, + () => gen.assign(fType, _`${fDef}.type || "string"`).assign(format, _`${fDef}.validate`), + () => gen.assign(fType, _`"string"`).assign(format, fDef) ) const bdt = bad$DataType(schemaCode, def.schemaType, $data) cxt.fail(or(bdt, unknownFmt(), invalidFmt())) @@ -39,9 +40,9 @@ const def: CodeKeywordDefinition = { function invalidFmt(): Code { const fmt = _`${format}(${data})` - const callFormat = it.async ? _`${fmtDef}.async ? await ${fmt} : ${fmt}` : fmt + const callFormat = it.async ? _`${fDef}.async ? await ${fmt} : ${fmt}` : fmt const validData = _`typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` - return _`(${format} && ${fmtType} === ${ruleType} && !(${validData}))` + return _`(${format} && ${fType} === ${ruleType} && !(${validData}))` } } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index ac2a23f4a5..29543575a9 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -33,7 +33,7 @@ const def: CodeKeywordDefinition = { function loopEnum(valid: Name): void { const v = gen.name("v") gen.for(_`const ${v} of ${schemaCode}`, () => - gen.if(_`equal(${data}, ${v})`, _`${valid} = true; break;`) + gen.if(_`equal(${data}, ${v})`, () => gen.assign(valid, true).break()) ) } diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index a7c6aac99e..1e1e7e70e1 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -52,7 +52,7 @@ const def: CodeKeywordDefinition = { ) const indices = gen.const("indices", _`{}`) gen.for(_`;${i}--;`, () => { - gen.let(item, _`${data}[${i}];`) + gen.let(item, _`${data}[${i}]`) gen.if(wrongType, _`continue`) if (Array.isArray(itemType)) gen.if(_`typeof ${item} == "string"`, _`${item} += "_"`) gen @@ -61,16 +61,17 @@ const def: CodeKeywordDefinition = { cxt.error() gen.assign(valid, false).break() }) - .code(_`${indices}[${item}] = ${i};`) + .code(_`${indices}[${item}] = ${i}`) }) } function loopN2(i: Name, j: Name): void { - gen.code(_`outer:`).for(_`;${i}--;`, () => + const outer = gen.name("outer") + gen.label(outer).for(_`;${i}--;`, () => gen.for(_`${j} = ${i}; ${j}--;`, () => gen.if(_`equal(${data}[${i}], ${data}[${j}])`, () => { cxt.error() - gen.assign(valid, false).break(_`outer`) + gen.assign(valid, false).break(outer) }) ) ) From a6396b2214b64c65587dea409b36efdecd4221ba Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Aug 2020 21:59:04 +0100 Subject: [PATCH 125/322] refactor: data type checks --- lib/compile/codegen.ts | 6 +++--- lib/compile/errors.ts | 22 ++++++++++------------ lib/compile/rules.ts | 13 ++++++------- lib/compile/util.ts | 17 +++++++++++------ lib/compile/validate/dataType.ts | 10 +++++----- lib/vocabularies/applicator/anyOf.ts | 2 +- lib/vocabularies/validation/uniqueItems.ts | 4 ++-- 7 files changed, 38 insertions(+), 36 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 6991a867ed..f7074e0879 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -25,7 +25,7 @@ export class Code { return len >= 2 && this.#str[0] === '"' && this.#str[len - 1] === '"' } - append(c: Code): void { + add(c: Code): void { this.#str += c.#str } } @@ -49,8 +49,8 @@ export class Name extends Code { return false } - append(_c: Code): void { - throw new Error("CodeGen: can't append Name") + add(_c: Code): void { + throw new Error("CodeGen: can't add to Name") } } diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 9e9caf0267..9c319ac2ed 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -85,21 +85,19 @@ function errorObjectCode(cxt: KeywordErrorContext, error: KeywordErrorDefinition if (createErrors === false) return _`{}` if (!error) throw new Error('keyword definition must have "error" property') const {params, message} = error - // TODO trim whitespace - const out = _`{ - keyword: ${keyword}, - dataPath: (${N.dataPath} || "") + ${errorPath}, - schemaPath: ${str`${errSchemaPath}/${keyword}`}, - params: ${params ? params(cxt) : _`{}`}` - if (propertyName) out.append(_`, propertyName: ${propertyName}`) + const msg = typeof message == "string" ? message : message(cxt) + const par = params ? params(cxt) : _`{}` + const out = _`{keyword: ${keyword}, dataPath: (${N.dataPath} || "") + ${errorPath}` + out.add(_`, schemaPath: ${str`${errSchemaPath}/${keyword}`}, params: ${par}`) + if (propertyName) { + out.add(_`, propertyName: ${propertyName}`) + } if (opts.messages !== false) { - out.append(_`, message: ${typeof message == "string" ? message : message(cxt)}`) + out.add(_`, message: ${msg}`) } if (opts.verbose) { - out.append( - _`, schema: ${schemaValue}, parentSchema: ${topSchemaRef}${schemaPath}, data: ${data}` - ) + out.add(_`, schema: ${schemaValue}, parentSchema: ${topSchemaRef}${schemaPath}, data: ${data}`) } - out.append(_`}`) + out.add(_`}`) return out } diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index d41400048f..a8437119a7 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -1,11 +1,13 @@ import {toHash} from "./util" import {KeywordDefinition} from "../types" +type ValidationTypes = {[key: string]: boolean | RuleGroup} + export interface ValidationRules { rules: RuleGroup[] all: {[key: string]: boolean | Rule} // rules that have to be validated keywords: {[key: string]: boolean} // all known keywords (superset of "all") - types: {[key: string]: boolean | RuleGroup} + types: ValidationTypes } export interface RuleGroup { @@ -40,18 +42,15 @@ const KEYWORDS = [ ] export default function rules(): ValidationRules { - const types = { + const groups = { number: {type: "number", rules: []}, - integer: true, string: {type: "string", rules: []}, array: {type: "array", rules: []}, object: {type: "object", rules: []}, - boolean: true, - null: true, } return { - types, - rules: [types.number, types.string, types.array, types.object, {rules: []}], + types: {...groups, integer: true, boolean: true, null: true}, + rules: [groups.number, groups.string, groups.array, groups.object, {rules: []}], all: toHash(ALL), keywords: toHash(ALL.concat(KEYWORDS)), } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 3a6a8d0088..dc328e30fb 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -2,13 +2,18 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" import {CompilationContext} from "../types" import N from "./names" +export enum DataType { + Correct, + Wrong, +} + export function checkDataType( dataType: string, data: Name, strictNumbers?: boolean, - negate?: boolean + correct = DataType.Correct ): Code { - const EQ = negate ? operators.NEQ : operators.EQ + const EQ = correct === DataType.Correct ? operators.EQ : operators.NEQ let cond: Code switch (dataType) { case "null": @@ -28,7 +33,7 @@ export function checkDataType( default: return _`typeof ${data} ${EQ} ${dataType}` } - return negate ? _`!(${cond})` : cond + return correct === DataType.Correct ? cond : _`!(${cond})` function numCond(cond: Code = nil): Code { return and(_`typeof ${data} === "number"`, cond, strictNumbers ? _`isFinite(${data})` : nil) @@ -39,10 +44,10 @@ export function checkDataTypes( dataTypes: string[], data: Name, strictNumbers?: boolean, - _negate?: true + correct?: DataType ): Code { if (dataTypes.length === 1) { - return checkDataType(dataTypes[0], data, strictNumbers, true) + return checkDataType(dataTypes[0], data, strictNumbers, correct) } let cond: Code const types = toHash(dataTypes) @@ -56,7 +61,7 @@ export function checkDataTypes( cond = nil } if (types.number) delete types.integer - for (const t in types) cond = and(cond, checkDataType(t, data, strictNumbers, true)) + for (const t in types) cond = and(cond, checkDataType(t, data, strictNumbers, correct)) return cond } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 7ab54c84e2..19e0809030 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,11 +1,11 @@ import {CompilationContext, KeywordErrorDefinition, KeywordErrorContext} from "../../types" -import {toHash, checkDataType, checkDataTypes} from "../util" +import {toHash, checkDataType, checkDataTypes, DataType} from "../util" import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" import {_, str, Name} from "../codegen" -export function getSchemaTypes({schema, opts}: CompilationContext): string[] { +export function getSchemaTypes({schema, opts, RULES}: CompilationContext): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach(checkType) @@ -20,8 +20,8 @@ export function getSchemaTypes({schema, opts}: CompilationContext): string[] { return types function checkType(t: string): void { - // TODO check that type is allowed - if (typeof t != "string") throw new Error('"type" keyword must be string or string[]: ' + t) + if (typeof t == "string" && t in RULES.types) return + throw new Error('"type" keyword must be allowed string or string[]: ' + t) } } @@ -32,7 +32,7 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]): types.length > 0 && !(coerceTo.length === 0 && types.length === 1 && schemaHasRulesForType(it, types[0])) if (checkTypes) { - const wrongType = checkDataTypes(types, data, opts.strictNumbers, true) + const wrongType = checkDataTypes(types, data, opts.strictNumbers, DataType.Wrong) gen.if(wrongType, () => { if (coerceTo.length) coerceData(it, coerceTo) else reportTypeError(it) diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 60b2840e47..fd9931a515 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -27,7 +27,7 @@ const def: CodeKeywordDefinition = { }, schValid ) - gen.code(_`${valid} = ${valid} || ${schValid};`) + gen.assign(valid, _`${valid} || ${schValid}`) gen.ifNot(valid) }) }, schema.length) diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 1e1e7e70e1..a910ee77ce 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {checkDataType, checkDataTypes} from "../../compile/util" +import {checkDataType, checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -48,7 +48,7 @@ const def: CodeKeywordDefinition = { itemType, item, it.opts.strictNumbers, - true + DataType.Wrong ) const indices = gen.const("indices", _`{}`) gen.for(_`;${i}--;`, () => { From e7d72e98f1b542eb2d126387befbfe31d6f2e0e5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Aug 2020 22:38:20 +0100 Subject: [PATCH 126/322] make Code constructor private, check Name is valid identifier, rename Expr enum to Type --- lib/compile/codegen.ts | 51 +++++++++++-------- lib/compile/subschema.ts | 19 ++++--- .../applicator/additionalItems.ts | 4 +- .../applicator/additionalProperties.ts | 4 +- lib/vocabularies/applicator/contains.ts | 4 +- lib/vocabularies/applicator/items.ts | 5 +- .../applicator/patternProperties.ts | 4 +- lib/vocabularies/applicator/properties.ts | 3 +- 8 files changed, 49 insertions(+), 45 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index f7074e0879..193c58d5e0 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -9,7 +9,9 @@ export type SafeExpr = Code | number | boolean | null export type Block = Code | (() => void) -export class Code { +export type Code = _Code | Name + +class _Code { #str: string constructor(s: string) { @@ -25,26 +27,33 @@ export class Code { return len >= 2 && this.#str[0] === '"' && this.#str[len - 1] === '"' } - add(c: Code): void { + add(c: _Code): void { this.#str += c.#str } } -export const nil = new Code("") +export const nil = new _Code("") export const operators = { - GT: new Code(">"), - GTE: new Code(">="), - LT: new Code("<"), - LTE: new Code("<="), - EQ: new Code("==="), - NEQ: new Code("!=="), - NOT: new Code("!"), - OR: new Code("||"), - AND: new Code("&&"), + GT: new _Code(">"), + GTE: new _Code(">="), + LT: new _Code("<"), + LTE: new _Code("<="), + EQ: new _Code("==="), + NEQ: new _Code("!=="), + NOT: new _Code("!"), + OR: new _Code("||"), + AND: new _Code("&&"), } -export class Name extends Code { +const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i + +export class Name extends _Code { + constructor(s: string) { + super(s) + if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier") + } + isQuoted() { return false } @@ -99,15 +108,15 @@ export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { // for (let i = 0; i < args.length; i++) { // res += interpolate(args[i]) + strs[i + 1] // } - // return new Code(res) - return new Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) + // return new _Code(res) + return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) } export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { - return new Code( + return new _Code( strs.map(quoteString).reduce((res, s, i) => { let aStr = interpolate(args[i - 1]) - if (aStr instanceof Code && aStr.isQuoted()) aStr = aStr.toString() + if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString() return typeof aStr === "string" ? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1) : `${res} + ${aStr} + ${s}` @@ -116,13 +125,11 @@ export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { } function interpolate(x: TemplateArg): TemplateArg { - return x instanceof Code || typeof x == "number" || typeof x == "boolean" || x === null + return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null ? x : quoteString(x) } -const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i - export default class CodeGen { #names: {[prefix: string]: NameGroup} = {} #valuePrefixes: {[prefix: string]: Name} = {} @@ -383,11 +390,11 @@ function quoteString(s: string): string { } export function stringify(s: string): Code { - return new Code(JSON.stringify(s)) + return new _Code(JSON.stringify(s)) } export function getProperty(key: Code | string | number): Code { - return typeof key == "string" && IDENTIFIER.test(key) ? new Code(`.${key}`) : _`[${key}]` + return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]` } const andCode = mappend(operators.AND) diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index d0e9425df5..98128af637 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -22,8 +22,7 @@ export interface SubschemaContext { allErrors?: boolean } -export enum Expr { - Const, +export enum Type { Num, Str, } @@ -40,7 +39,7 @@ interface SubschemaApplicationParams { data: Name | Code dataProp: Code | string | number propertyName: Name - expr: Expr // TODO dataPropType + dataPropType: Type compositeRule: true createErrors: boolean allErrors: boolean @@ -99,7 +98,7 @@ function getSubschema( function extendSubschemaData( subschema: SubschemaContext, it: CompilationContext, - {dataProp, expr, data, propertyName}: SubschemaApplication + {dataProp, dataPropType: dpType, data, propertyName}: SubschemaApplication ) { if (data !== undefined && dataProp !== undefined) { throw new Error('both "data" and "dataProp" passed, only one allowed') @@ -111,7 +110,7 @@ function extendSubschemaData( const {errorPath, dataPathArr, opts} = it const nextData = gen.var("data", _`${it.data}${getProperty(dataProp)}`) // TODO var dataContextProps(nextData) - subschema.errorPath = str`${errorPath}${getErrorPath(dataProp, expr, opts.jsonPointers)}` + subschema.errorPath = str`${errorPath}${getErrorPath(dataProp, dpType, opts.jsonPointers)}` subschema.parentDataProperty = _`${dataProp}` subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty] } @@ -141,18 +140,18 @@ function extendSubschemaMode( } function getErrorPath( - dataProp: Code | string | number, - dataPropType?: Expr, + dataProp: Name | string | number, + dataPropType?: Type, jsonPointers?: boolean ): Code | string { // let path - if (dataProp instanceof Code) { - const isNumber = dataPropType === Expr.Num + if (dataProp instanceof Name) { + const isNumber = dataPropType === Type.Num return jsonPointers ? _`"/" + ${dataProp}${isNumber ? nil : _`.replace(/~/g, "~0").replace(/\\//g, "~1")`}` // TODO maybe use global escapePointer : isNumber ? _`"[" + ${dataProp} + "]"` - : _`"['" + ${dataProp} + "']"` // TODO needs global string escaping + : _`"['" + ${dataProp} + "']"` } return jsonPointers ? "/" + (typeof dataProp == "number" ? dataProp : escapeJsonPointer(dataProp)) diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index ae09932167..aa97a8d0f2 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" -import {applySubschema, Expr} from "../../compile/subschema" +import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -27,7 +27,7 @@ const def: CodeKeywordDefinition = { function validateItems(valid: Name): void { const i = gen.name("i") gen.for(_`let ${i}=${items.length}; ${i}<${len}; ${i}++`, () => { - applySubschema(it, {keyword: "additionalItems", dataProp: i, expr: Expr.Num}, valid) + applySubschema(it, {keyword: "additionalItems", dataProp: i, dataPropType: Type.Num}, valid) if (!it.allErrors) gen.ifNot(valid, _`break`) }) } diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index c2eec86494..b2d3fad290 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -7,7 +7,7 @@ import { loopPropertiesCode, usePattern, } from "../util" -import {applySubschema, SubschemaApplication, Expr} from "../../compile/subschema" +import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" @@ -86,7 +86,7 @@ const def: CodeKeywordDefinition = { const subschema: SubschemaApplication = { keyword: "additionalProperties", dataProp: key, - expr: Expr.Str, + dataPropType: Type.Str, } if (errors === false) { Object.assign(subschema, { diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index d2c1ad7b96..8e79e4246f 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" -import {applySubschema, Expr} from "../../compile/subschema" +import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -26,7 +26,7 @@ const def: CodeKeywordDefinition = { { keyword: "contains", dataProp: i, - expr: Expr.Num, + dataPropType: Type.Num, compositeRule: true, }, valid diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index ae128e8291..45527668a0 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" -import {applySubschema, Expr} from "../../compile/subschema" +import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -29,7 +29,6 @@ const def: CodeKeywordDefinition = { keyword: "items", schemaProp: i, dataProp: i, - expr: Expr.Const, }, valid ) @@ -42,7 +41,7 @@ const def: CodeKeywordDefinition = { const valid = gen.name("valid") const i = gen.name("i") gen.for(_`let ${i}=0; ${i}<${len}; ${i}++`, () => { - applySubschema(it, {keyword: "items", dataProp: i, expr: Expr.Num}, valid) + applySubschema(it, {keyword: "items", dataProp: i, dataPropType: Type.Num}, valid) if (!it.allErrors) gen.ifNot(valid, _`break`) }) cxt.ok(valid) diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index b762f104fd..f8949f31dc 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {schemaProperties, loopPropertiesCode, usePattern} from "../util" -import {applySubschema, Expr} from "../../compile/subschema" +import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -36,7 +36,7 @@ const def: CodeKeywordDefinition = { keyword: "patternProperties", schemaProp: pat, dataProp: key, - expr: Expr.Str, + dataPropType: Type.Str, }, valid ) diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index df9354ff4e..4f8ab9f281 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {schemaProperties, propertyInData} from "../util" -import {applySubschema, Expr} from "../../compile/subschema" +import {applySubschema} from "../../compile/subschema" import apDef from "./additionalProperties" const def: CodeKeywordDefinition = { @@ -40,7 +40,6 @@ const def: CodeKeywordDefinition = { keyword: "properties", schemaProp: prop, dataProp: prop, - expr: Expr.Const, }, valid ) From e5e846b0fb6f1d3c8796d0388be1e663994269f6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 30 Aug 2020 23:23:25 +0100 Subject: [PATCH 127/322] feat: codegen forOf and forIn loops --- lib/compile/codegen.ts | 42 +++++++++++++++++++ lib/compile/index.ts | 2 +- .../applicator/additionalProperties.ts | 10 +---- .../applicator/patternProperties.ts | 6 +-- lib/vocabularies/applicator/propertyNames.ts | 6 +-- lib/vocabularies/util.ts | 10 ----- lib/vocabularies/validation/enum.ts | 3 +- lib/vocabularies/validation/required.ts | 28 +++++++------ 8 files changed, 68 insertions(+), 39 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 193c58d5e0..42da849c03 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -130,12 +130,21 @@ function interpolate(x: TemplateArg): TemplateArg { : quoteString(x) } +interface CodeGenOptions { + ownProperties?: boolean +} + export default class CodeGen { #names: {[prefix: string]: NameGroup} = {} #valuePrefixes: {[prefix: string]: Name} = {} #out = "" #blocks: BlockKind[] = [] #blockStarts: number[] = [] + opts: CodeGenOptions + + constructor(opts: CodeGenOptions = {}) { + this.opts = opts + } toString(): string { return this.#out @@ -287,6 +296,39 @@ export default class CodeGen { return this } + forOf( + nameOrPrefix: Name | string, + iterable: SafeExpr, + forBody: (n: Name) => void, + varKind: Code = varKinds.const + ): CodeGen { + // TODO define enum for var kinds + const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + return this._for(new _Code(`for(${varKind} ${name} of ${iterable}){`), name, forBody) + } + + forIn( + nameOrPrefix: Name | string, + obj: SafeExpr, + forBody: (n: Name) => void, + varKind: Code = varKinds.const + ): CodeGen { + // TODO define enum for var kinds + if (this.opts.ownProperties) { + return this.forOf(nameOrPrefix, new _Code(`Object.keys(${obj})`), forBody) + } + const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) // TODO refactor with others + return this._for(new _Code(`for(${varKind} ${name} in ${obj}){`), name, forBody) + } + + _for(forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { + this.#blocks.push(BlockKind.For) + this.#out += forCode + if (forBody) forBody(name) + this.endFor() + return this + } + endFor(): CodeGen { const b = this._lastBlock if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"') diff --git a/lib/compile/index.ts b/lib/compile/index.ts index a105f26edd..867db49fb0 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -101,7 +101,7 @@ function compile(schema, root, localRefs, baseId) { var $async = _schema.$async === true const rootId = resolve.fullPath(_root.schema.$id) - const gen = new CodeGen() + const gen = new CodeGen({ownProperties: opts.ownProperties}) validateFunctionCode({ gen, diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index b2d3fad290..b805511584 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,12 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import { - allSchemaProperties, - schemaRefOrVal, - alwaysValidSchema, - loopPropertiesCode, - usePattern, -} from "../util" +import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" @@ -27,7 +21,7 @@ const def: CodeKeywordDefinition = { if (!allErrors) gen.if(_`${errsCount} === ${N.errors}`) function checkAdditionalProperties(): void { - loopPropertiesCode(cxt, (key: Name) => { + gen.forIn("key", data, (key: Name) => { if (!props.length && !patProps.length) additionalPropertyCode(key) else gen.if(isAdditional(key), () => additionalPropertyCode(key)) }) diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index f8949f31dc..e3b7b3df9c 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {schemaProperties, loopPropertiesCode, usePattern} from "../util" +import {schemaProperties, usePattern} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "object", code(cxt: KeywordContext) { - const {gen, schema, it} = cxt + const {gen, schema, data, it} = cxt const patterns = schemaProperties(it, schema) if (patterns.length === 0) return const valid = gen.name("valid") @@ -28,7 +28,7 @@ const def: CodeKeywordDefinition = { } function validateProperties(pat: string) { - loopPropertiesCode(cxt, (key) => { + gen.forIn("key", data, (key) => { gen.if(_`${usePattern(gen, pat)}.test(${key})`, () => { applySubschema( it, diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index d93345ddc4..5ed5776e86 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {alwaysValidSchema, loopPropertiesCode} from "../util" +import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" @@ -9,11 +9,11 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: ["object", "boolean"], code(cxt: KeywordContext) { - const {gen, schema, it} = cxt + const {gen, schema, data, it} = cxt if (alwaysValidSchema(it, schema)) return const valid = gen.name("valid") - loopPropertiesCode(cxt, (key) => { + gen.forIn("key", data, (key) => { cxt.setParams({propertyName: key}) applySubschema( it, diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index f526f3cd3d..975d108c75 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -65,16 +65,6 @@ export function noPropertyInData( return ownProperties ? _`${cond} || !${isOwnProperty(data, property)}` : cond } -export function loopPropertiesCode( - {gen, data, it}: KeywordContext, - loopBody: (key: Name) => void -): void { - // TODO maybe always iterate own properties in v7? - const key = gen.name("key") - const iteration = it.opts.ownProperties ? _`of Object.keys(${data})` : _`in ${data}` - gen.for(_`const ${key} ${iteration}`, () => loopBody(key)) -} - export function callValidateCode( {schemaCode, data, it}: KeywordContext, func: Code, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 29543575a9..ea6d2226a1 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -31,8 +31,7 @@ const def: CodeKeywordDefinition = { } function loopEnum(valid: Name): void { - const v = gen.name("v") - gen.for(_`const ${v} of ${schemaCode}`, () => + gen.forOf("v", schemaCode, (v) => gen.if(_`equal(${data}, ${v})`, () => gen.assign(valid, true).break()) ) } diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index ea94590025..df46ba5185 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -2,7 +2,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" -import {_, str, Name} from "../../compile/codegen" +import {_, str, nil, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "required", @@ -57,22 +57,26 @@ const def: CodeKeywordDefinition = { } function loopAllRequired(): void { - const prop = gen.name("prop") - cxt.setParams({missingProperty: prop}) - gen.for(_`const ${prop} of ${schemaCode}`, () => + gen.forOf("prop", schemaCode, (prop) => { + cxt.setParams({missingProperty: prop}) gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => cxt.error()) - ) + }) } function loopUntilMissing(missing: Name, valid: Name): void { cxt.setParams({missingProperty: missing}) - gen.for(_`${missing} of ${schemaCode}`, () => { - gen.assign(valid, propertyInData(data, missing, it.opts.ownProperties)) - gen.ifNot(valid, () => { - cxt.error() - gen.break() - }) - }) + gen.forOf( + missing, + schemaCode, + () => { + gen.assign(valid, propertyInData(data, missing, it.opts.ownProperties)) + gen.ifNot(valid, () => { + cxt.error() + gen.break() + }) + }, + nil + ) } }, error: { From d1644e6b9517178a8d5e22d4b414afa0e4cf7514 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 31 Aug 2020 08:36:45 +0100 Subject: [PATCH 128/322] feat: optional es5 code generation --- lib/compile/codegen.ts | 25 ++++++++++++++++++------- lib/compile/index.ts | 2 +- lib/compile/validate/dataType.ts | 2 +- lib/types.ts | 3 ++- spec/ajv_options.js | 2 +- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 42da849c03..3b0cc6470d 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -130,8 +130,9 @@ function interpolate(x: TemplateArg): TemplateArg { : quoteString(x) } -interface CodeGenOptions { - ownProperties?: boolean +export interface CodeGenOptions { + forInOwn?: boolean + es5?: boolean } export default class CodeGen { @@ -219,6 +220,7 @@ export default class CodeGen { _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + if (this.opts.es5) varKind = varKinds.var if (rhs === undefined) this.#out += `${varKind} ${name};` else this.#out += `${varKind} ${name} = ${rhs};` return name @@ -304,7 +306,16 @@ export default class CodeGen { ): CodeGen { // TODO define enum for var kinds const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) - return this._for(new _Code(`for(${varKind} ${name} of ${iterable}){`), name, forBody) + if (this.opts.es5) { + const i = this.name("_i") + return this._for(_`for(${varKinds.let} ${i}=0; ${i}<${iterable}.length; ${i}++){`, i, () => { + const item = _`${iterable}[${i}]` + if (nameOrPrefix instanceof Name) this.assign(name, item) + else this.const(name, item) + forBody(name) + }) + } + return this._for(_`for(${varKind} ${name} of ${iterable}){`, name, forBody) } forIn( @@ -314,17 +325,17 @@ export default class CodeGen { varKind: Code = varKinds.const ): CodeGen { // TODO define enum for var kinds - if (this.opts.ownProperties) { - return this.forOf(nameOrPrefix, new _Code(`Object.keys(${obj})`), forBody) + if (this.opts.forInOwn) { + return this.forOf(nameOrPrefix, _`Object.keys(${obj})`, forBody) } const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) // TODO refactor with others - return this._for(new _Code(`for(${varKind} ${name} in ${obj}){`), name, forBody) + return this._for(_`for(${varKind} ${name} in ${obj}){`, name, forBody) } _for(forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { this.#blocks.push(BlockKind.For) this.#out += forCode - if (forBody) forBody(name) + forBody(name) this.endFor() return this } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 867db49fb0..203a60e567 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -101,7 +101,7 @@ function compile(schema, root, localRefs, baseId) { var $async = _schema.$async === true const rootId = resolve.fullPath(_root.schema.$id) - const gen = new CodeGen({ownProperties: opts.ownProperties}) + const gen = new CodeGen({...opts.codegen, forInOwn: opts.ownProperties}) validateFunctionCode({ gen, diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 19e0809030..2fb1d0c902 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -51,7 +51,7 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string export function coerceData(it: CompilationContext, coerceTo: string[]): void { const {gen, schema, data, opts} = it const dataType = gen.let("dataType", _`typeof ${data}`) - const coerced = gen.let("coerced") + const coerced = gen.let("coerced", _`undefined`) if (opts.coerceTypes === "array") { gen.if(_`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen diff --git a/lib/types.ts b/lib/types.ts index 991520199f..0dcd8d32ac 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import Cache from "./cache" -import CodeGen, {Code, Name} from "./compile/codegen" +import CodeGen, {Code, Name, CodeGenOptions} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" import KeywordContext from "./compile/context" @@ -42,6 +42,7 @@ export interface Options { messages?: boolean sourceCode?: boolean processCode?: (code: string, schema: object) => string + codegen?: CodeGenOptions cache?: Cache logger?: Logger | false nullable?: boolean diff --git a/spec/ajv_options.js b/spec/ajv_options.js index 0131ca6a15..1754f41973 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -7,10 +7,10 @@ var options = fullTest ? { allErrors: true, verbose: true, - format: "full", extendRefs: "ignore", inlineRefs: false, jsonPointers: true, + codegen: {es5: true}, } : {allErrors: true} From b1befd98269a8cc937f6878d9811033b567ca8ff Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 31 Aug 2020 10:56:10 +0100 Subject: [PATCH 129/322] feat: optional newlines in generated code --- lib/compile/codegen.ts | 120 ++++++++++++++++++++++------------------- lib/compile/util.ts | 4 +- spec/ajv_options.js | 4 +- 3 files changed, 68 insertions(+), 60 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 3b0cc6470d..cf7bb3b7ef 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -63,7 +63,7 @@ export class Name extends _Code { } } -const varKinds = { +export const varKinds = { const: new Name("const"), let: new Name("let"), var: new Name("var"), @@ -102,7 +102,7 @@ export class ValueError extends Error { } } -export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { +export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { // TODO benchmark if loop is faster than reduce // let res = strs[0] // for (let i = 0; i < args.length; i++) { @@ -112,9 +112,9 @@ export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) } -export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { +export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { return new _Code( - strs.map(quoteString).reduce((res, s, i) => { + strs.map(safeStringify).reduce((res, s, i) => { let aStr = interpolate(args[i - 1]) if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString() return typeof aStr === "string" @@ -127,12 +127,13 @@ export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): Code { function interpolate(x: TemplateArg): TemplateArg { return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null ? x - : quoteString(x) + : safeStringify(x) } export interface CodeGenOptions { - forInOwn?: boolean es5?: boolean + lines?: boolean + forInOwn?: boolean } export default class CodeGen { @@ -141,10 +142,12 @@ export default class CodeGen { #out = "" #blocks: BlockKind[] = [] #blockStarts: number[] = [] + #n = "" opts: CodeGenOptions constructor(opts: CodeGenOptions = {}) { this.opts = opts + if (opts.lines) this.#n = "\n" } toString(): string { @@ -218,11 +221,15 @@ export default class CodeGen { return code } + _toName(nameOrPrefix: Name | string): Name { + return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + } + _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + const name = this._toName(nameOrPrefix) if (this.opts.es5) varKind = varKinds.var - if (rhs === undefined) this.#out += `${varKind} ${name};` - else this.#out += `${varKind} ${name} = ${rhs};` + if (rhs === undefined) this.#out += `${varKind} ${name};` + this.#n + else this.#out += `${varKind} ${name} = ${rhs};` + this.#n return name } @@ -239,46 +246,45 @@ export default class CodeGen { } assign(name: Code, rhs: SafeExpr): CodeGen { - this.#out += `${name} = ${rhs};` + this.#out += `${name} = ${rhs};` + this.#n return this } - code(c?: Block | SafeExpr): CodeGen { - // TODO optionally strip whitespace + code(c: Block | SafeExpr): CodeGen { if (typeof c == "function") c() - else if (c !== undefined) this.#out += c + ";" //+ "\n" // TODO fails without line breaks + else this.#out += c + ";" + this.#n + return this } - if(condition: Code | boolean, thenBody?: Block, elseBody?: Block, _negate?: true): CodeGen { + if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen { this.#blocks.push(BlockKind.If) - this.#out += `if(${ - _negate ? (condition instanceof Name ? `!${condition}` : `!(${condition})`) : condition - }){` + this.#out += `if(${condition}){` + this.#n if (thenBody && elseBody) { this.code(thenBody).else().code(elseBody).endIf() } else if (thenBody) { this.code(thenBody).endIf() } else if (elseBody) { - throw new Error("CodeGen: else body without then body") + throw new Error('CodeGen: "else" body without "then" body') } return this } ifNot(condition: Code, thenBody?: Block, elseBody?: Block): CodeGen { - return this.if(condition, thenBody, elseBody, true) + const cond = new _Code(condition instanceof Name ? `!${condition}` : `!(${condition})`) + return this.if(cond, thenBody, elseBody) } elseIf(condition: Code): CodeGen { if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"') - this.#out += `}else if(${condition}){` + this.#out += `}else if(${condition}){` + this.#n return this } else(): CodeGen { if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" without "if"') this._lastBlock = BlockKind.Else - this.#out += "}else{" + this.#out += "}else{" + this.#n return this } @@ -287,13 +293,13 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" without "if"') this.#blocks.pop() - this.#out += "}" + this.#out += "}" + this.#n return this } for(iteration: Code, forBody?: Block): CodeGen { this.#blocks.push(BlockKind.For) - this.#out += `for(${iteration}){` + this.#out += `for(${iteration}){` + this.#n if (forBody) this.code(forBody).endFor() return this } @@ -304,18 +310,20 @@ export default class CodeGen { forBody: (n: Name) => void, varKind: Code = varKinds.const ): CodeGen { - // TODO define enum for var kinds - const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + const name = this._toName(nameOrPrefix) if (this.opts.es5) { const i = this.name("_i") - return this._for(_`for(${varKinds.let} ${i}=0; ${i}<${iterable}.length; ${i}++){`, i, () => { - const item = _`${iterable}[${i}]` - if (nameOrPrefix instanceof Name) this.assign(name, item) - else this.const(name, item) - forBody(name) - }) + return this._loop( + new _Code(`for(${varKinds.let} ${i}=0; ${i}<${iterable}.length; ${i}++){`), + i, + () => { + const item = new _Code(`${iterable}[${i}]`) + this._def(varKind, name, item) + forBody(name) + } + ) } - return this._for(_`for(${varKind} ${name} of ${iterable}){`, name, forBody) + return this._loop(new _Code(`for(${varKind} ${name} of ${iterable}){`), name, forBody) } forIn( @@ -326,15 +334,15 @@ export default class CodeGen { ): CodeGen { // TODO define enum for var kinds if (this.opts.forInOwn) { - return this.forOf(nameOrPrefix, _`Object.keys(${obj})`, forBody) + return this.forOf(nameOrPrefix, new _Code(`Object.keys(${obj})`), forBody) } - const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) // TODO refactor with others - return this._for(_`for(${varKind} ${name} in ${obj}){`, name, forBody) + const name = this._toName(nameOrPrefix) + return this._loop(new _Code(`for(${varKind} ${name} in ${obj}){`), name, forBody) } - _for(forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { + _loop(forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { this.#blocks.push(BlockKind.For) - this.#out += forCode + this.#out += forCode + this.#n forBody(name) this.endFor() return this @@ -344,46 +352,46 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"') this.#blocks.pop() - this.#out += "}" + this.#out += "}" + this.#n return this } label(label?: Code): CodeGen { - this.#out += label + ":" + this.#out += label + ":" + this.#n return this } break(label?: Code): CodeGen { - this.#out += label ? `break ${label};` : "break;" + this.#out += (label ? `break ${label};` : "break;") + this.#n return this } return(value: Block | SafeExpr): CodeGen { this.#out += "return " this.code(value) - this.#out += ";" + this.#out += ";" + this.#n return this } try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen { if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') - this.#out += "try{" + this.#out += "try{" + this.#n this.code(tryBody) if (catchCode) { const err = this.name("e") - this.#out += `}catch(${err}){` + this.#out += `}catch(${err}){` + this.#n catchCode(err) } if (finallyCode) { - this.#out += "}finally{" + this.#out += "}finally{" + this.#n this.code(finallyCode) } - this.#out += "}" + this.#out += "}" + this.#n return this } throw(err: Code): CodeGen { - this.#out += `throw ${err};` + this.#out += `throw ${err};` + this.#n return this } @@ -402,13 +410,13 @@ export default class CodeGen { throw new Error("CodeGen: block sequence already ended or incorrect number of blocks") } this.#blocks.length = len - if (toClose > 0) this.#out += "}".repeat(toClose) + if (toClose > 0) this.#out += "}".repeat(toClose) + this.#n return this } func(name: Name, args: Code = nil, async?: boolean, funcBody?: Block): CodeGen { this.#blocks.push(BlockKind.Func) - this.#out += `${async ? "async " : ""}function ${name}(${args}){` + this.#out += `${async ? "async " : ""}function ${name}(${args}){` + this.#n if (funcBody) this.code(funcBody).endFunc() return this } @@ -417,7 +425,7 @@ export default class CodeGen { const b = this._lastBlock if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" without "func"') this.#blocks.pop() - this.#out += "}" + this.#out += "}" + this.#n return this } @@ -436,14 +444,14 @@ export default class CodeGen { } } -function quoteString(s: string): string { - return JSON.stringify(s) - .replace(/\u2028/g, "\\u2028") - .replace(/\u2029/g, "\\u2029") +export function stringify(x: unknown): Code { + return new _Code(safeStringify(x)) } -export function stringify(s: string): Code { - return new _Code(JSON.stringify(s)) +function safeStringify(x: unknown): string { + return JSON.stringify(x) + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029") } export function getProperty(key: Code | string | number): Code { @@ -465,5 +473,5 @@ export function or(...args: Code[]): Code { type MAppend = (x: Code, y: Code) => Code function mappend(op: Code): MAppend { - return (x, y) => (x === nil ? y : y === nil ? x : _`${x} ${op} ${y}`) + return (x, y) => (x === nil ? y : y === nil ? x : new _Code(`${x} ${op} ${y}`)) } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index dc328e30fb..dcc89ff69a 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -35,8 +35,8 @@ export function checkDataType( } return correct === DataType.Correct ? cond : _`!(${cond})` - function numCond(cond: Code = nil): Code { - return and(_`typeof ${data} === "number"`, cond, strictNumbers ? _`isFinite(${data})` : nil) + function numCond(_cond: Code = nil): Code { + return and(_`typeof ${data} === "number"`, _cond, strictNumbers ? _`isFinite(${data})` : nil) } } diff --git a/spec/ajv_options.js b/spec/ajv_options.js index 1754f41973..829c501fb9 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -10,9 +10,9 @@ var options = fullTest extendRefs: "ignore", inlineRefs: false, jsonPointers: true, - codegen: {es5: true}, + codegen: {es5: true, lines: true}, } - : {allErrors: true} + : {allErrors: true, codegen: {es5: true, lines: true}} if (fullTest && !isBrowser) { options.processCode = require("js-beautify").js_beautify From 6210e4a5acb99c242f448c3e7d02a410f51cf9c9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 31 Aug 2020 14:11:06 +0100 Subject: [PATCH 130/322] feat: support schemaType in all keywords, including $data; report $data error if $data schema validation fails --- lib/compile/codegen.ts | 9 ++- lib/compile/context.ts | 12 +++- lib/compile/errors.ts | 10 ++- lib/compile/util.ts | 6 +- lib/compile/validate/dataType.ts | 29 ++++----- lib/compile/validate/index.ts | 2 +- lib/compile/validate/iterate.ts | 3 +- lib/compile/validate/keyword.ts | 74 ++++++++++------------ lib/types.ts | 1 + lib/vocabularies/util.ts | 2 +- lib/vocabularies/validation/required.ts | 3 - lib/vocabularies/validation/uniqueItems.ts | 21 ++---- spec/custom.spec.js | 38 +++++++++++ spec/errors.spec.js | 2 +- 14 files changed, 126 insertions(+), 86 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index cf7bb3b7ef..6be22f69c7 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -112,10 +112,10 @@ export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) } -export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { +export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code { return new _Code( strs.map(safeStringify).reduce((res, s, i) => { - let aStr = interpolate(args[i - 1]) + let aStr = interpolateStr(args[i - 1]) if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString() return typeof aStr === "string" ? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1) @@ -130,6 +130,11 @@ function interpolate(x: TemplateArg): TemplateArg { : safeStringify(x) } +function interpolateStr(x: TemplateArg | string[]): TemplateArg { + if (Array.isArray(x)) x = x.join(",") + return interpolate(x) +} + export interface CodeGenOptions { es5?: boolean lines?: boolean diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 92f3b4c32c..1bb749740f 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -6,7 +6,13 @@ import { } from "../types" import {schemaRefOrVal} from "../vocabularies/util" import {getData} from "./util" -import {reportError, reportExtraError, resetErrorsCount, keywordError} from "./errors" +import { + reportError, + reportExtraError, + resetErrorsCount, + keywordError, + keyword$DataError, +} from "./errors" import CodeGen, {Code, Name} from "./codegen" import N from "./names" @@ -19,6 +25,7 @@ export default class KeywordContext implements KeywordErrorContext { schema: any schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) + schemaType?: string | string[] parentSchema: any errsCount?: Name params: KeywordContextParams @@ -34,6 +41,7 @@ export default class KeywordContext implements KeywordErrorContext { this.schema = it.schema[keyword] this.$data = def.$data && it.opts.$data && this.schema && this.schema.$data this.schemaValue = schemaRefOrVal(it, this.schema, keyword, this.$data) + this.schemaType = def.schemaType this.parentSchema = it.schema this.params = {} this.it = it @@ -88,7 +96,7 @@ export default class KeywordContext implements KeywordErrorContext { } $dataError(): void { - reportError(this, this.def.$dataError || this.def.error || keywordError) + reportError(this, this.def.$dataError || keyword$DataError) } reset(): void { diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 9c319ac2ed..06ae9acd9e 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -3,8 +3,14 @@ import CodeGen, {_, str, Code, Name} from "./codegen" import N from "./names" export const keywordError: KeywordErrorDefinition = { - message: ({keyword}) => str`should pass ${keyword} keyword validation`, - params: ({keyword}) => _`{keyword: ${keyword}}`, // TODO possibly remove it as keyword is reported in the object + message: ({keyword}) => str`should pass "${keyword}" keyword validation`, +} + +export const keyword$DataError: KeywordErrorDefinition = { + message: ({keyword, schemaType}) => + schemaType + ? str`"${keyword}" keyword must be ${schemaType} ($data)` + : str`"${keyword}" keyword is invalid ($data)`, } export function reportError( diff --git a/lib/compile/util.ts b/lib/compile/util.ts index dcc89ff69a..a648b381a0 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -22,7 +22,7 @@ export function checkDataType( cond = _`Array.isArray(${data})` break case "object": - cond = _`${data} && typeof ${data} === "object" && !Array.isArray(${data})` + cond = _`${data} && typeof ${data} == "object" && !Array.isArray(${data})` break case "integer": cond = numCond(_`!(${data} % 1) && !isNaN(${data})`) @@ -36,7 +36,7 @@ export function checkDataType( return correct === DataType.Correct ? cond : _`!(${cond})` function numCond(_cond: Code = nil): Code { - return and(_`typeof ${data} === "number"`, _cond, strictNumbers ? _`isFinite(${data})` : nil) + return and(_`typeof ${data} == "number"`, _cond, strictNumbers ? _`isFinite(${data})` : nil) } } @@ -52,7 +52,7 @@ export function checkDataTypes( let cond: Code const types = toHash(dataTypes) if (types.array && types.object) { - const notObj = _`typeof ${data} !== "object"` + const notObj = _`typeof ${data} != "object"` cond = types.null ? notObj : _`(!${data} || ${notObj})` delete types.null delete types.array diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 2fb1d0c902..a3c9357849 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,11 +1,11 @@ import {CompilationContext, KeywordErrorDefinition, KeywordErrorContext} from "../../types" -import {toHash, checkDataType, checkDataTypes, DataType} from "../util" +import {toHash, checkDataTypes, DataType} from "../util" import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" import {_, str, Name} from "../codegen" -export function getSchemaTypes({schema, opts, RULES}: CompilationContext): string[] { +export function getSchemaTypes({opts, RULES}: CompilationContext, schema): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach(checkType) @@ -34,7 +34,7 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]): if (checkTypes) { const wrongType = checkDataTypes(types, data, opts.strictNumbers, DataType.Wrong) gen.if(wrongType, () => { - if (coerceTo.length) coerceData(it, coerceTo) + if (coerceTo.length) coerceData(it, types, coerceTo) else reportTypeError(it) }) } @@ -48,8 +48,8 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string : [] } -export function coerceData(it: CompilationContext, coerceTo: string[]): void { - const {gen, schema, data, opts} = it +function coerceData(it: CompilationContext, types: string[], coerceTo: string[]): void { + const {gen, data, opts} = it const dataType = gen.let("dataType", _`typeof ${data}`) const coerced = gen.let("coerced", _`undefined`) if (opts.coerceTypes === "array") { @@ -57,7 +57,7 @@ export function coerceData(it: CompilationContext, coerceTo: string[]): void { gen .assign(data, _`${data}[0]`) .assign(dataType, _`typeof ${data}`) - .if(checkDataType(schema.type, data, opts.strictNumbers), () => gen.assign(coerced, data)) + .if(checkDataTypes(types, data, opts.strictNumbers), () => gen.assign(coerced, data)) ) } gen.if(_`${coerced} !== undefined`) @@ -134,25 +134,24 @@ function assignParentData( } const typeError: KeywordErrorDefinition = { - message: ({schema}) => - str`should be ${Array.isArray(schema) ? schema.join(",") : schema}`, - // TODO change: return type as array here - params: ({schema}) => _`{type: ${Array.isArray(schema) ? schema.join(",") : schema}}`, + message: ({schema}) => str`should be ${schema}`, + params: ({schema, schemaValue}) => + typeof schema == "string" ? _`{type: ${schema}}` : _`{type: ${schemaValue}}`, } export function reportTypeError(it: CompilationContext): void { - const cxt = getErrorContext(it, "type") + const cxt = getTypeErrorContext(it) reportError(cxt, typeError) } -function getErrorContext(it: CompilationContext, keyword: string): KeywordErrorContext { +function getTypeErrorContext(it: CompilationContext): KeywordErrorContext { const {gen, data, schema} = it - const schemaCode = schemaRefOrVal(it, schema, keyword) + const schemaCode = schemaRefOrVal(it, schema, "type") return { gen, - keyword, + keyword: "type", data, - schema: schema[keyword], + schema: schema.type, schemaCode, schemaValue: schemaCode, parentSchema: schema, diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index d30faaa63b..ca9cf285de 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -70,7 +70,7 @@ function isBoolOrEmpty({schema, RULES}: CompilationContext): boolean { } function typeAndKeywords(it: CompilationContext, errsCount?: Name): void { - const types = getSchemaTypes(it) + const types = getSchemaTypes(it, it.schema) const checkedTypes = coerceAndCheckDataType(it, types) schemaKeywords(it, types, !checkedTypes, errsCount) } diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 6775d3566d..4f3738762f 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -32,8 +32,7 @@ export function schemaKeywords( function groupKeywords(group: RuleGroup): void { if (group.type) { - const checkType = checkDataType(group.type, data, opts.strictNumbers) - gen.if(checkType) + gen.if(checkDataType(group.type, data, opts.strictNumbers)) iterateKeywords(it, group) if (types.length === 1 && types[0] === group.type && typeErrors) { gen.else() diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 35feb24fa5..15631a9859 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -8,8 +8,9 @@ import { import KeywordContext from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" +import {checkDataTypes, DataType} from "../util" import {callValidateCode} from "../../vocabularies/util" -import CodeGen, {_, nil, Code, Name} from "../codegen" +import CodeGen, {_, nil, or, Code, Name} from "../codegen" import N from "../names" export function keywordCode( @@ -52,54 +53,55 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { } function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { - const {gen, keyword, schema, schemaCode, parentSchema, $data, it} = cxt + const {gen, keyword, schema, schemaCode, schemaType, parentSchema, $data, it} = cxt checkAsync(it, def) const validate = "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate const validateRef = useKeyword(gen, keyword, validate) const valid = gen.let("valid") - if (def.errors === false) { - validateNoErrorsRule() - } else { - validateRuleWithErrors() - } + gen.block(def.errors === false ? validateNoErrorsRule : validateRuleWithErrors) + cxt.ok(def.valid ?? valid) function validateNoErrorsRule(): void { - gen.block(() => { - if ($data) check$data() - assignValid() - if (def.modifying) modifyData(cxt) - }) - if (!def.valid) cxt.pass(valid) + if ($data) check$data() + assignValid() + if (def.modifying) modifyData(cxt) + reportKeywordErrors(() => cxt.error()) } function validateRuleWithErrors(): void { - gen.block() if ($data) check$data() - // const errsCount = gen.const("_errs", N.errors) const ruleErrs = def.async ? validateAsyncRule() : validateSyncRule() if (def.modifying) modifyData(cxt) - gen.endBlock() - reportKeywordErrors(ruleErrs) + reportKeywordErrors(() => addKeywordErrors(cxt, ruleErrs)) } function check$data(): void { - gen - // TODO add support for schemaType in keyword definition - // .if(`${bad$DataType(schemaCode, def.schemaType, $data)} false`) // TODO refactor - .if(_`${schemaCode} === undefined`) - .assign(valid, true) - .else() + gen.if(_`${schemaCode} === undefined`).assign(valid, true) + if (schemaType || def.validateSchema) { + gen.elseIf(or(wrong$DataType(), invalid$DataSchema())) + cxt.$dataError() + gen.assign(valid, false) + } + gen.else() + } + + function wrong$DataType(): Code { + if (schemaType) { + if (!(schemaCode instanceof Name)) throw new Error("ajv implementation error") + const st = Array.isArray(schemaType) ? schemaType : [schemaType] + return _`(${checkDataTypes(st, schemaCode, it.opts.strictNumbers, DataType.Wrong)})` + } + return nil + } + + function invalid$DataSchema(): Code { if (def.validateSchema) { const validateSchemaRef = useKeyword(gen, keyword, def.validateSchema) - gen.assign(valid, _`${validateSchemaRef}(${schemaCode})`) - // TODO fail if schema fails validation - // gen.if(`!${valid}`) - // reportError(cxt, keywordError) - // gen.else() - gen.if(valid) + return _`!${validateSchemaRef}(${schemaCode})` } + return nil } function validateAsyncRule(): Name { @@ -129,16 +131,10 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { gen.assign(valid, _`${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`) } - function reportKeywordErrors(ruleErrs: Code): void { - switch (def.valid) { - case true: - return - case false: - addKeywordErrors(cxt, ruleErrs) - return cxt.ok(false) // TODO maybe add gen.skip() to remove code till the end of the block? - default: - cxt.pass(valid, () => addKeywordErrors(cxt, ruleErrs)) - } + // TODO maybe refactor to gen.ifNot(def.valid ?? valid, repErrs) once dead branches are removed + function reportKeywordErrors(repErrs: () => void): void { + if (def.valid === false) repErrs() + else if (def.valid !== true) gen.ifNot(valid, repErrs) } } diff --git a/lib/types.ts b/lib/types.ts index 0dcd8d32ac..e46b6b296b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -218,6 +218,7 @@ export interface KeywordErrorContext { parentSchema: any schemaCode: Code | number | boolean schemaValue: Code | number | boolean + schemaType?: string | string[] errsCount?: Name params: KeywordContextParams it: CompilationContext diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 975d108c75..e4f3f3312f 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -9,7 +9,7 @@ export function bad$DataType( schemaType: string, $data?: string | false ): Code { - return $data ? _`(${schemaCode}!==undefined && typeof ${schemaCode}!==${schemaType})` : nil + return $data ? _`(${schemaCode} !== undefined && typeof ${schemaCode} != ${schemaType})` : nil } export function schemaRefOrVal( diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index df46ba5185..7ecc1569a3 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -84,9 +84,6 @@ const def: CodeKeywordDefinition = { str`should have required property '${missingProperty}'`, params: ({params: {missingProperty}}) => _`{missingProperty: ${missingProperty}}`, }, - $dataError: { - message: '"required" keyword value must be array', - }, } module.exports = def diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index a910ee77ce..758cae2103 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,6 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {checkDataType, checkDataTypes, DataType} from "../../compile/util" +import {getSchemaTypes} from "../../compile/validate/dataType" +import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -12,7 +13,7 @@ const def: CodeKeywordDefinition = { const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt if (it.opts.uniqueItems === false || !($data || schema)) return const valid = gen.let("valid") - const itemType = parentSchema.items?.type + const itemTypes = parentSchema.items ? getSchemaTypes(it, parentSchema.items) : [] if ($data) { gen.if(_`${schemaCode} === false || ${schemaCode} === undefined`) @@ -37,24 +38,17 @@ const def: CodeKeywordDefinition = { } function canOptimize(): boolean { - return Array.isArray(itemType) - ? !itemType.some((t) => t === "object" || t === "array") - : itemType && itemType !== "object" && itemType !== "array" + return itemTypes.length > 0 && !itemTypes.some((t) => t === "object" || t === "array") } function loopN(i: Name, j: Name): void { const item = gen.name("item") - const wrongType = (Array.isArray(itemType) ? checkDataTypes : checkDataType)( - itemType, - item, - it.opts.strictNumbers, - DataType.Wrong - ) + const wrongType = checkDataTypes(itemTypes, item, it.opts.strictNumbers, DataType.Wrong) const indices = gen.const("indices", _`{}`) gen.for(_`;${i}--;`, () => { gen.let(item, _`${data}[${i}]`) gen.if(wrongType, _`continue`) - if (Array.isArray(itemType)) gen.if(_`typeof ${item} == "string"`, _`${item} += "_"`) + if (itemTypes.length > 1) gen.if(_`typeof ${item} == "string"`, _`${item} += "_"`) gen .if(_`typeof ${indices}[${item}] == "number"`, () => { gen.assign(j, _`${indices}[${item}]`) @@ -82,9 +76,6 @@ const def: CodeKeywordDefinition = { str`should NOT have duplicate items (items ## ${j} and ${i} are identical)`, params: ({params: {i, j}}) => _`{i: ${i}, j: ${j}}`, }, - $dataError: { - message: "uniqueItems must be boolean ($data)", - }, } module.exports = def diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 2ff0185c40..47f05b456f 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -118,6 +118,15 @@ describe("Custom keywords", () => { return valid } }) + + it("should support schemaType", () => { + testEvenKeyword({ + keyword: "x-even", + type: "number", + schemaType: "boolean", + validate: (schema, data) => (data % 2 ? !schema : schema), + }) + }) }) describe('rule with "compiled" keyword validation', () => { @@ -180,6 +189,21 @@ describe("Custom keywords", () => { it("should allow multiple parent schemas for the same keyword", () => { testMultipleRangeKeyword({type: "number", compile: compileRange}) }) + + it("should support schemaType", () => { + testEvenKeyword({ + keyword: "x-even", + type: "number", + schemaType: "boolean", + compile: compileEven, + }) + shouldBeInvalidSchema({"x-even": "not_boolean"}) + + function compileEven(schema) { + if (schema) return (data) => data % 2 === 0 + return (data) => data % 2 !== 0 + } + }) }) function compileConstant(schema) { @@ -747,6 +771,20 @@ describe("Custom keywords", () => { }) }) }) + + it("should support schemaType with $data", () => { + testEvenKeyword$data({ + keyword: "x-even-$data", + type: "number", + schemaType: "boolean", + $data: true, + validate: validateEven, + }) + + function validateEven(schema, data) { + return data % 2 ? !schema : schema + } + }) }) function testEvenKeyword(evenDefinition, numErrors) { diff --git a/spec/errors.spec.js b/spec/errors.spec.js index ce3d483113..7492c60795 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -330,7 +330,7 @@ describe("Validation errors", () => { "required", "#/required", "", - '"required" keyword value must be array', + '"required" keyword must be array ($data)', {} ) } From 6939f9052129a74fcb02afe8692a4062d4a9422c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 31 Aug 2020 18:46:12 +0100 Subject: [PATCH 131/322] refactor: unify $data validation for all keywords; "const" and "multipleOf" valid with undefined --- lib/compile/context.ts | 55 +++++++++++++-- lib/compile/validate/keyword.ts | 67 +++++-------------- lib/vocabularies/format/format.ts | 4 +- lib/vocabularies/util.ts | 8 --- lib/vocabularies/validation/const.ts | 2 +- lib/vocabularies/validation/enum.ts | 31 +++------ lib/vocabularies/validation/limit.ts | 10 +-- lib/vocabularies/validation/limitItems.ts | 8 +-- lib/vocabularies/validation/limitLength.ts | 8 +-- .../validation/limitProperties.ts | 8 +-- lib/vocabularies/validation/multipleOf.ts | 12 ++-- lib/vocabularies/validation/pattern.ts | 8 +-- lib/vocabularies/validation/required.ts | 35 +++------- lib/vocabularies/validation/uniqueItems.ts | 14 +--- spec/extras/$data/const.json | 7 ++ spec/extras/$data/multipleOf.json | 10 ++- 16 files changed, 132 insertions(+), 155 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 1bb749740f..abc0bef29a 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -5,7 +5,7 @@ import { CompilationContext, } from "../types" import {schemaRefOrVal} from "../vocabularies/util" -import {getData} from "./util" +import {getData, checkDataTypes, DataType} from "./util" import { reportError, reportExtraError, @@ -13,7 +13,7 @@ import { keywordError, keyword$DataError, } from "./errors" -import CodeGen, {Code, Name} from "./codegen" +import CodeGen, {_, nil, or, Code, Name} from "./codegen" import N from "./names" export default class KeywordContext implements KeywordErrorContext { @@ -48,8 +48,7 @@ export default class KeywordContext implements KeywordErrorContext { this.def = def if (this.$data) { - this.schemaCode = it.gen.name("schema") - it.gen.const(this.schemaCode, getData(this.$data, it)) + this.schemaCode = it.gen.const("schema", getData(this.$data, it)) } else { this.schemaCode = this.schemaValue if (def.schemaType && !validSchemaType(this.schema, def.schemaType)) { @@ -91,6 +90,12 @@ export default class KeywordContext implements KeywordErrorContext { else this.gen.else() } + fail$data(condition: Code): void { + if (!this.$data) return this.fail(condition) + const {schemaCode} = this + this.fail(_`${schemaCode} !== undefined && (${or(this.invalid$data(), condition)})`) + } + error(append?: true): void { ;(append ? reportExtraError : reportError)(this, this.def.error || keywordError) } @@ -112,6 +117,48 @@ export default class KeywordContext implements KeywordErrorContext { if (assign) Object.assign(this.params, obj) else this.params = obj } + + block$data(valid: Name = nil, codeBlock: () => void, $dataValid: Code = nil): void { + this.gen.block(() => { + this.check$data(valid, $dataValid) + codeBlock() + }) + } + + check$data(valid: Name = nil, $dataValid: Code = nil): void { + if (!this.$data) return + const {gen, schemaCode, schemaType, def} = this + gen.if(or(_`${schemaCode} === undefined`, $dataValid)) + if (valid !== nil) gen.assign(valid, true) + if (schemaType || def.validateSchema) { + gen.elseIf(this.invalid$data()) + this.$dataError() + if (valid !== nil) gen.assign(valid, false) + } + gen.else() + } + + invalid$data(): Code { + const {gen, schemaCode, schemaType, def, it} = this + return or(wrong$DataType(), invalid$DataSchema()) + + function wrong$DataType(): Code { + if (schemaType) { + if (!(schemaCode instanceof Name)) throw new Error("ajv implementation error") + const st = Array.isArray(schemaType) ? schemaType : [schemaType] + return _`(${checkDataTypes(st, schemaCode, it.opts.strictNumbers, DataType.Wrong)})` + } + return nil + } + + function invalid$DataSchema(): Code { + if (def.validateSchema) { + const validateSchemaRef = gen.value("validate$data", {ref: def.validateSchema}) // TODO value.code + return _`!${validateSchemaRef}(${schemaCode})` + } + return nil + } + } } function validSchemaType(schema: any, schemaType: string | string[]): boolean { diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 15631a9859..2359751d33 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -8,9 +8,8 @@ import { import KeywordContext from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" -import {checkDataTypes, DataType} from "../util" import {callValidateCode} from "../../vocabularies/util" -import CodeGen, {_, nil, or, Code, Name} from "../codegen" +import CodeGen, {_, nil, Code, Name} from "../codegen" import N from "../names" export function keywordCode( @@ -53,58 +52,28 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { } function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { - const {gen, keyword, schema, schemaCode, schemaType, parentSchema, $data, it} = cxt + const {gen, keyword, schema, parentSchema, $data, it} = cxt checkAsync(it, def) const validate = "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate const validateRef = useKeyword(gen, keyword, validate) const valid = gen.let("valid") - - gen.block(def.errors === false ? validateNoErrorsRule : validateRuleWithErrors) + cxt.block$data(valid, validateKeyword) cxt.ok(def.valid ?? valid) - function validateNoErrorsRule(): void { - if ($data) check$data() - assignValid() - if (def.modifying) modifyData(cxt) - reportKeywordErrors(() => cxt.error()) - } - - function validateRuleWithErrors(): void { - if ($data) check$data() - const ruleErrs = def.async ? validateAsyncRule() : validateSyncRule() - if (def.modifying) modifyData(cxt) - reportKeywordErrors(() => addKeywordErrors(cxt, ruleErrs)) - } - - function check$data(): void { - gen.if(_`${schemaCode} === undefined`).assign(valid, true) - if (schemaType || def.validateSchema) { - gen.elseIf(or(wrong$DataType(), invalid$DataSchema())) - cxt.$dataError() - gen.assign(valid, false) - } - gen.else() - } - - function wrong$DataType(): Code { - if (schemaType) { - if (!(schemaCode instanceof Name)) throw new Error("ajv implementation error") - const st = Array.isArray(schemaType) ? schemaType : [schemaType] - return _`(${checkDataTypes(st, schemaCode, it.opts.strictNumbers, DataType.Wrong)})` - } - return nil - } - - function invalid$DataSchema(): Code { - if (def.validateSchema) { - const validateSchemaRef = useKeyword(gen, keyword, def.validateSchema) - return _`!${validateSchemaRef}(${schemaCode})` + function validateKeyword() { + if (def.errors === false) { + assignValid() + if (def.modifying) modifyData(cxt) + reportErrs(() => cxt.error()) + } else { + const ruleErrs = def.async ? validateAsync() : validateSync() + if (def.modifying) modifyData(cxt) + reportErrs(() => addErrs(cxt, ruleErrs)) } - return nil } - function validateAsyncRule(): Name { + function validateAsync(): Name { const ruleErrs = gen.let("ruleErrs", null) gen.try( () => assignValid(_`await `), @@ -118,7 +87,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { return ruleErrs } - function validateSyncRule(): Code { + function validateSync(): Code { const validateErrs = _`${validateRef}.errors` gen.assign(validateErrs, null) assignValid(nil) @@ -132,9 +101,9 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { } // TODO maybe refactor to gen.ifNot(def.valid ?? valid, repErrs) once dead branches are removed - function reportKeywordErrors(repErrs: () => void): void { - if (def.valid === false) repErrs() - else if (def.valid !== true) gen.ifNot(valid, repErrs) + function reportErrs(errors: () => void): void { + if (def.valid === false) errors() + else if (def.valid !== true) gen.ifNot(valid, errors) } } @@ -143,7 +112,7 @@ function modifyData(cxt: KeywordContext) { gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`)) } -function addKeywordErrors(cxt: KeywordContext, errs: Code): void { +function addErrs(cxt: KeywordContext, errs: Code): void { const {gen} = cxt gen.if( _`Array.isArray(${errs})`, diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 6ca9813054..464ba4f6e2 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,6 +1,5 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType} from "../util" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -26,8 +25,7 @@ const def: CodeKeywordDefinition = { () => gen.assign(fType, _`${fDef}.type || "string"`).assign(format, _`${fDef}.validate`), () => gen.assign(fType, _`"string"`).assign(format, fDef) ) - const bdt = bad$DataType(schemaCode, def.schemaType, $data) - cxt.fail(or(bdt, unknownFmt(), invalidFmt())) + cxt.fail$data(or(unknownFmt(), invalidFmt())) // TODO this is not tested. Possibly require ajv-formats to test formats in ajv as well function unknownFmt(): Code { if (opts.unknownFormats === "ignore") return nil diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index e4f3f3312f..8166fadf6e 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -4,14 +4,6 @@ import KeywordContext from "../compile/context" import CodeGen, {_, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" -export function bad$DataType( - schemaCode: Code | number | boolean, - schemaType: string, - $data?: string | false -): Code { - return $data ? _`(${schemaCode} !== undefined && typeof ${schemaCode} != ${schemaType})` : nil -} - export function schemaRefOrVal( {topSchemaRef, schemaPath}: CompilationContext, schema: unknown, diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 163c48d287..4ea9c1d563 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -5,7 +5,7 @@ import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "const", $data: true, - code: (cxt: KeywordContext) => cxt.fail(_`!equal(${cxt.data}, ${cxt.schemaCode})`), + code: (cxt: KeywordContext) => cxt.fail$data(_`!equal(${cxt.data}, ${cxt.schemaCode})`), error: { message: "should be equal to constant", params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index ea6d2226a1..280e663da4 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -8,29 +8,20 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {gen, data, $data, schema, schemaCode, it} = cxt - const {opts} = it - if ($data) { - const valid = gen.let("valid") - gen.if( - _`${schemaCode} === undefined`, - () => gen.assign(valid, true), - () => gen.assign(valid, false).if(_`Array.isArray(${schemaCode})`, () => loopEnum(valid)) - ) - cxt.pass(valid) + if (!$data && schema.length === 0) throw new Error("enum must have non-empty array") + const useLoop = typeof it.opts.loopEnum == "number" && schema.length >= it.opts.loopEnum + let valid: Code + if (useLoop || $data) { + valid = gen.let("valid") + cxt.block$data(valid, loopEnum) } else { - if (schema.length === 0) throw new Error("enum must have non-empty array") - if (schema.length > (opts.loopEnum as number)) { - const valid = gen.let("valid", false) - loopEnum(valid) - cxt.pass(valid) - } else { - const vSchema = gen.const("schema", schemaCode) - const cond: Code = or(...schema.map((_x, i) => equalCode(vSchema, i))) - cxt.pass(cond) - } + const vSchema = gen.const("schema", schemaCode) + valid = or(...schema.map((_x, i) => equalCode(vSchema, i))) } + cxt.pass(valid) - function loopEnum(valid: Name): void { + function loopEnum(): void { + gen.assign(valid, false) gen.forOf("v", schemaCode, (v) => gen.if(_`equal(${data}, ${v})`, () => gen.assign(valid, true).break()) ) diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index a999856a32..6bbbfc7d82 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType} from "../util" -import {_, str, or, operators, Code} from "../../compile/codegen" +// import {bad$DataType} from "../util" +import {_, str, operators, Code} from "../../compile/codegen" const ops = operators @@ -18,9 +18,9 @@ const def: CodeKeywordDefinition = { schemaType: "number", $data: true, code(cxt: KeywordContext) { - const {keyword, data, $data, schemaCode} = cxt - const bdt = bad$DataType(schemaCode, def.schemaType, $data) - cxt.fail(or(bdt, _`${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data})`)) + const {keyword, data, schemaCode} = cxt + // const bdt = bad$DataType(schemaCode, def.schemaType, $data) + cxt.fail$data(_`(${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data}))`) }, error: { message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index f45003e49e..4b89f6daf0 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,7 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType} from "../util" -import {_, str, or, operators} from "../../compile/codegen" +import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxItems", "minItems"], @@ -9,10 +8,9 @@ const def: CodeKeywordDefinition = { schemaType: "number", $data: true, code(cxt: KeywordContext) { - const {keyword, data, $data, schemaCode} = cxt + const {keyword, data, schemaCode} = cxt const op = keyword === "maxItems" ? operators.GT : operators.LT - const bdt = bad$DataType(schemaCode, def.schemaType, $data) - cxt.fail(or(bdt, _`${data}.length ${op} ${schemaCode}`)) + cxt.fail$data(_`${data}.length ${op} ${schemaCode}`) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 9f762fc0e6..1d210e9b21 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,7 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType} from "../util" -import {_, str, or, operators} from "../../compile/codegen" +import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxLength", "minLength"], @@ -9,11 +8,10 @@ const def: CodeKeywordDefinition = { schemaType: "number", $data: true, code(cxt: KeywordContext) { - const {keyword, data, $data, schemaCode, it} = cxt + const {keyword, data, schemaCode, it} = cxt const op = keyword === "maxLength" ? operators.GT : operators.LT - const bdt = bad$DataType(schemaCode, def.schemaType, $data) const len = it.opts.unicode === false ? _`${data}.length` : _`ucs2length(${data})` - cxt.fail(or(bdt, _`${len} ${op} ${schemaCode}`)) + cxt.fail$data(_`${len} ${op} ${schemaCode}`) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index ab1965ba70..613bb3484d 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,7 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType} from "../util" -import {_, str, or, operators} from "../../compile/codegen" +import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: ["maxProperties", "minProperties"], @@ -9,10 +8,9 @@ const def: CodeKeywordDefinition = { schemaType: "number", $data: true, code(cxt: KeywordContext) { - const {keyword, data, $data, schemaCode} = cxt + const {keyword, data, schemaCode} = cxt const op = keyword === "maxProperties" ? operators.GT : operators.LT - const bdt = bad$DataType(schemaCode, def.schemaType, $data) - cxt.fail(or(bdt, _`Object.keys(${data}).length ${op} ${schemaCode}`)) + cxt.fail$data(_`Object.keys(${data}).length ${op} ${schemaCode}`) }, error: { message({keyword, schemaCode}) { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index ffbba064b2..4b21e88411 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType} from "../util" -import {_, str, or} from "../../compile/codegen" +// import {bad$DataType} from "../util" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "multipleOf", @@ -9,14 +9,14 @@ const def: CodeKeywordDefinition = { schemaType: "number", $data: true, code(cxt: KeywordContext) { - const {gen, data, $data, schemaCode, it} = cxt - const bdt = bad$DataType(schemaCode, def.schemaType, $data) + const {gen, data, schemaCode, it} = cxt + // const bdt = bad$DataType(schemaCode, def.schemaType, $data) const prec = it.opts.multipleOfPrecision - const res = gen.const("res", _`${data}/${schemaCode}`) + const res = gen.let("res") const invalid = prec ? _`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : _`${res} !== parseInt(${res})` - cxt.fail(or(bdt, invalid)) + cxt.fail$data(_`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid}))`) }, error: { message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index d10d59868c..fa4da72df4 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,7 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {bad$DataType, usePattern} from "../util" -import {_, str, or} from "../../compile/codegen" +import {usePattern} from "../util" +import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "pattern", @@ -10,9 +10,9 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {gen, data, $data, schema, schemaCode} = cxt - const bdt = bad$DataType(schemaCode, def.schemaType, $data) + // const bdt = bad$DataType(schemaCode, def.schemaType, $data) const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch - cxt.fail(or(bdt, _`!${regExp}.test(${data})`)) + cxt.fail$data(_`!${regExp}.test(${data})`) }, error: { message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 7ecc1569a3..10311bb066 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -7,47 +7,30 @@ import {_, str, nil, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "required", type: "object", - schemaType: ["array"], + schemaType: "array", $data: true, code(cxt: KeywordContext) { const {gen, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return - const loopRequired = $data || schema.length >= it.opts.loopRequired + const useLoop = typeof it.opts.loopRequired == "number" && schema.length >= it.opts.loopRequired if (it.allErrors) allErrorsMode() else exitOnErrorMode() function allErrorsMode(): void { - if (loopRequired) { - if ($data) { - gen.if(_`${schemaCode} !== undefined`, () => { - gen.if(_`Array.isArray(${schemaCode})`, loopAllRequired, () => cxt.$dataError()) - }) - } else { - loopAllRequired() + if (useLoop || $data) { + cxt.block$data(nil, loopAllRequired) + } else { + for (const prop of schema) { + checkReportMissingProp(cxt, prop) } - return - } - for (const prop of schema) { - checkReportMissingProp(cxt, prop) } } function exitOnErrorMode(): void { const missing = gen.let("missing") - if (loopRequired) { + if (useLoop || $data) { const valid = gen.let("valid", true) - if ($data) { - gen.if(_`${schemaCode} === undefined`) - gen.assign(valid, true) - gen.elseIf(_`!Array.isArray(${schemaCode})`) - cxt.$dataError() - gen.assign(valid, false) - gen.else() - loopUntilMissing(missing, valid) - gen.endIf() - } else { - loopUntilMissing(missing, valid) - } + cxt.block$data(valid, () => loopUntilMissing(missing, valid)) cxt.ok(valid) } else { gen.if(checkMissingProp(cxt, schema, missing)) diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 758cae2103..41fad07044 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -14,19 +14,7 @@ const def: CodeKeywordDefinition = { if (it.opts.uniqueItems === false || !($data || schema)) return const valid = gen.let("valid") const itemTypes = parentSchema.items ? getSchemaTypes(it, parentSchema.items) : [] - - if ($data) { - gen.if(_`${schemaCode} === false || ${schemaCode} === undefined`) - gen.assign(valid, true) - gen.elseIf(_`typeof ${schemaCode} != "boolean"`) - cxt.$dataError() - gen.assign(valid, false) - gen.else() - validateUniqueItems() - gen.endIf() - } else { - validateUniqueItems() - } + cxt.block$data(valid, validateUniqueItems, _`${schemaCode} === false`) cxt.ok(valid) function validateUniqueItems() { diff --git a/spec/extras/$data/const.json b/spec/extras/$data/const.json index 44c917dd88..9920464f9e 100644 --- a/spec/extras/$data/const.json +++ b/spec/extras/$data/const.json @@ -39,6 +39,13 @@ "thisOne": "5" }, "valid": false + }, + { + "description": "valid when another property ('const') not defined", + "data": { + "sameAs": 5 + }, + "valid": true } ] }, diff --git a/spec/extras/$data/multipleOf.json b/spec/extras/$data/multipleOf.json index 939a04a7b6..63c95984bd 100644 --- a/spec/extras/$data/multipleOf.json +++ b/spec/extras/$data/multipleOf.json @@ -57,10 +57,18 @@ "valid": false }, { - "description": "invalid if value of multipleOf is undefined", + "description": "valid if value of multipleOf is undefined", "data": { "multiple": 10 }, + "valid": true + }, + { + "description": "invalid if value of multipleOf is 0", + "data": { + "divider": 0, + "multiple": 10 + }, "valid": false } ] From 888029f27e8dcdb1dc4b7e203fac58fb361e31d4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 31 Aug 2020 20:54:33 +0100 Subject: [PATCH 132/322] browser bundle script (6 tests fail in browser), do not browserify code in schema tests --- .gitignore | 5 +- bower.json | 2 +- circle.yml | 31 -------- karma.conf.js | 2 +- karma.sauce.js | 122 -------------------------------- lib/compile/validate/keyword.ts | 4 +- lib/vocabularies/util.ts | 1 - package.json | 15 ++-- scripts/bundle.js | 111 ++++++++++++----------------- scripts/info | 10 --- spec/ajv_instances.js | 6 +- spec/ajv_options.js | 4 -- 12 files changed, 60 insertions(+), 253 deletions(-) delete mode 100644 circle.yml delete mode 100644 karma.sauce.js delete mode 100755 scripts/info diff --git a/.gitignore b/.gitignore index 9951605f1c..944880f903 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,10 @@ node_modules # Browserified tests .browser -# bundles +# compiled typescript dist/ +# browser bundles +bundle/ + package-lock.json diff --git a/bower.json b/bower.json index 6e8d8a893a..c840a93d43 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "ajv", "description": "Another JSON Schema Validator", - "main": "dist/ajv.min.js", + "main": "bundle/ajv.min.js", "authors": ["Evgeny Poberezkin"], "license": "MIT", "keywords": ["JSON", "schema", "validator"], diff --git a/circle.yml b/circle.yml deleted file mode 100644 index ed27bf56d2..0000000000 --- a/circle.yml +++ /dev/null @@ -1,31 +0,0 @@ -machine: - node: - version: 4 - -general: - branches: - ignore: - - gh-pages - -checkout: - post: - - git submodule sync - - git submodule update --init - -dependencies: - post: - - wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz - - tar -xzf sc-latest-linux.tar.gz - -test: - override: - - cd sc-*-linux && ./bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY --readyfile ~/sauce_is_ready: - background: true - - - while [ ! -e ~/sauce_is_ready ]; do sleep 1; done - - - scripts/prepare-tests - - karma start karma.sauce.js - - post: - - killall --wait sc # wait for Sauce Connect to close the tunnel diff --git a/karma.conf.js b/karma.conf.js index 5c2a3eb1c5..4d811aa69c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -12,7 +12,7 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ - "dist/ajv.min.js", + "bundle/ajv.min.js", "node_modules/chai/chai.js", "node_modules/ajv-async/dist/ajv-async.min.js", "node_modules/bluebird/js/browser/bluebird.core.min.js", diff --git a/karma.sauce.js b/karma.sauce.js deleted file mode 100644 index dc54aa486d..0000000000 --- a/karma.sauce.js +++ /dev/null @@ -1,122 +0,0 @@ -"use strict" - -var fs = require("fs") - -module.exports = function (config) { - // Use ENV vars on Travis and sauce.json locally to get credentials - if (!process.env.SAUCE_USERNAME) { - if (!fs.existsSync("sauce.json")) { - console.log( - "Create a sauce.json with your credentials based on the sauce-sample.json file." - ) - process.exit(1) - } else { - process.env.SAUCE_USERNAME = require("./sauce").username - process.env.SAUCE_ACCESS_KEY = require("./sauce").accessKey - } - } - - // Browsers to run on Sauce Labs - var customLaunchers = { - SL_Chrome_27: { - base: "SauceLabs", - browserName: "chrome", - version: "27", - }, - SL_Chrome: { - base: "SauceLabs", - browserName: "chrome", - }, - SL_InternetExplorer_10: { - base: "SauceLabs", - browserName: "internet explorer", - version: "10", - }, - SL_InternetExplorer: { - base: "SauceLabs", - browserName: "internet explorer", - }, - SL_MicrosoftEdge: { - base: "SauceLabs", - browserName: "MicrosoftEdge", - }, - SL_FireFox_17: { - base: "SauceLabs", - browserName: "firefox", - version: "17", - }, - SL_FireFox: { - base: "SauceLabs", - browserName: "firefox", - }, - SL_Safari_7: { - base: "SauceLabs", - browserName: "safari", - version: "7", - }, - SL_Safari: { - base: "SauceLabs", - browserName: "safari", - }, - SL_iPhone_8: { - base: "SauceLabs", - browserName: "iphone", - version: "8.4", - }, - SL_iPhone: { - base: "SauceLabs", - browserName: "iphone", - }, - SL_Android: { - base: "SauceLabs", - browserName: "android", - }, - } - - config.set({ - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: "", - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ["mocha"], - - // list of files / patterns to load in the browser - files: [ - "dist/ajv.min.js", - "node_modules/chai/chai.js", - "node_modules/ajv-async/dist/ajv-async.min.js", - "node_modules/bluebird/js/browser/bluebird.core.min.js", - ".browser/*.spec.js", - ], - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ["dots", "saucelabs"], - - // web server port - port: 9876, - - colors: true, - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - sauceLabs: { - testName: "Ajv", - idleTimeout: 900, - }, - captureTimeout: 1200000, - browserNoActivityTimeout: 600000, - browserDisconnectTimeout: 60000, - - customLaunchers: customLaunchers, - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: Object.keys(customLaunchers), - singleRun: true, - }) -} diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 2359751d33..86d23c038b 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -94,10 +94,10 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { return validateErrs } - function assignValid(await: Code = def.async ? _`await ` : nil): void { + function assignValid(_await: Code = def.async ? _`await ` : nil): void { const passCxt = it.opts.passContext ? N.this : N.self const passSchema = !(("compile" in def && !$data) || def.schema === false) - gen.assign(valid, _`${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`) + gen.assign(valid, _`${_await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`) } // TODO maybe refactor to gen.ifNot(def.valid ?? valid, repErrs) once dead branches are removed diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 8166fadf6e..a7494b408c 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -66,7 +66,6 @@ export function callValidateCode( const dataAndSchema = passSchema ? _`${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` : data - // const appendErrPath = it.errorPath.toString() === '""' ? nil : _` + ${it.errorPath}` const dataPath = _`(${N.dataPath} || '') + ${it.errorPath}` // TODO refactor other places const args = _`${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` return context !== nil ? _`${func}.call(${context}, ${args})` : _`${func}(${args})` diff --git a/package.json b/package.json index c70e158fab..334c2ded7c 100644 --- a/package.json +++ b/package.json @@ -13,21 +13,18 @@ ], "scripts": { "eslint": "eslint lib/{**/,}*.ts spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", - "lint": "npm run eslint", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", "test-spec": "mocha spec/{**/,}*.spec.js -R dot", "test-fast": "AJV_FAST_TEST=true npm run test-spec", "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", - "test-ts": "tsc --target ES5 --noImplicitAny --noEmit spec/typescript/index.ts", - "bundle": "del-cli dist && node ./scripts/bundle.js . Ajv pure_getters", - "bundle-beautify": "node ./scripts/bundle.js js-beautify", - "build": "del-cli dist && tsc && cp -r lib/refs dist/refs", + "bundle": "rm -rf bundle && node ./scripts/bundle.js", + "build": "rm -rf dist && tsc && cp -r lib/refs dist/refs", "test-karma": "karma start", - "test-browser": "del-cli .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", + "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", - "test": "npm run lint && npm run build && npm run test-cov", + "test": "npm run eslint && npm run build && npm run test-cov", "prepublish": "npm run build", "watch": "watch \"npm run build\" ./lib" }, @@ -80,7 +77,6 @@ "browserify": "^16.2.0", "chai": "^4.0.1", "coveralls": "^3.0.1", - "del-cli": "^3.0.0", "eslint": "^7.3.1", "eslint-config-prettier": "^6.11.0", "gh-pages-generator": "^0.2.3", @@ -92,14 +88,13 @@ "karma": "^5.0.0", "karma-chrome-launcher": "^3.0.0", "karma-mocha": "^2.0.0", - "karma-sauce-launcher": "^4.1.3", "lint-staged": "^10.2.11", "mocha": "^8.0.1", "nyc": "^15.0.0", "prettier": "^2.0.5", "require-globify": "^1.3.0", + "terser": "^5.2.1", "typescript": "^4.0.0", - "uglify-js": "^3.6.9", "watch": "^1.0.0" }, "collective": { diff --git a/scripts/bundle.js b/scripts/bundle.js index 06d883fd06..5b1f9a6ced 100644 --- a/scripts/bundle.js +++ b/scripts/bundle.js @@ -1,69 +1,46 @@ "use strict" -var fs = require("fs"), - path = require("path"), - browserify = require("browserify"), - uglify = require("uglify-js") - -var pkg = process.argv[2], - standalone = process.argv[3], - compress = process.argv[4] - -var packageDir = path.join(__dirname, "..") -if (pkg !== ".") packageDir = path.join(packageDir, "node_modules", pkg) - -var json = require(path.join(packageDir, "package.json")) - -var distDir = path.join(__dirname, "..", "dist") -if (!fs.existsSync(distDir)) fs.mkdirSync(distDir) - -var bOpts = {} -if (standalone) bOpts.standalone = standalone - -browserify(bOpts) - .require(path.join(packageDir, json.main), {expose: json.name}) - .bundle((err, buf) => { - if (err) { - console.error("browserify error:", err) - process.exit(1) - } - - var outputFile = path.join(distDir, json.name) - var uglifyOpts = { - warnings: true, - compress: {}, - output: { - preamble: - "/* " + - json.name + - " " + - json.version + - ": " + - json.description + - " */", - }, - } - if (compress) { - var compressOpts = compress.split(",") - for (var i = 0, il = compressOpts.length; i < il; ++i) { - var pair = compressOpts[i].split("=") - uglifyOpts.compress[pair[0]] = pair.length < 1 || pair[1] !== "false" - } - } - if (standalone) { - uglifyOpts.sourceMap = { - filename: json.name + ".min.js", - url: json.name + ".min.js.map", - } - } - - var result = uglify.minify(buf.toString(), uglifyOpts) - fs.writeFileSync(outputFile + ".min.js", result.code) - if (result.map) fs.writeFileSync(outputFile + ".min.js.map", result.map) - if (standalone) fs.writeFileSync(outputFile + ".bundle.js", buf) - if (result.warnings) { - for (var j = 0, jl = result.warnings.length; j < jl; ++j) { - console.warn("UglifyJS warning:", result.warnings[j]) - } - } - }) +const fs = require("fs") +const path = require("path") +const browserify = require("browserify") +const {minify} = require("terser") + +const json = require(path.join(__dirname, "..", "package.json")) +const bundleDir = path.join(__dirname, "..", "bundle") +if (!fs.existsSync(bundleDir)) fs.mkdirSync(bundleDir) + +browserify({standalone: "Ajv"}) + .require(path.join(__dirname, "..", json.main), {expose: json.name}) + .bundle(saveAndMinify) + +async function saveAndMinify(err, buf) { + if (err) { + console.error("browserify error:", err) + process.exit(1) + } + + const bundlePath = path.join(bundleDir, json.name) + const opts = { + ecma: 2018, + warnings: true, + compress: { + pure_getters: true, + keep_infinity: true, + unsafe_methods: true, + }, + format: { + preamble: `/* ${json.name} ${json.version}: ${json.description} */`, + }, + sourceMap: { + filename: json.name + ".min.js", + url: json.name + ".min.js.map", + }, + } + + const result = await minify(buf.toString(), opts) + + fs.writeFileSync(bundlePath + ".bundle.js", buf) + fs.writeFileSync(bundlePath + ".min.js", result.code) + fs.writeFileSync(bundlePath + ".min.js.map", result.map) + if (result.warnings) result.warnings.forEach((msg) => console.warn("terser.minify warning:", msg)) +} diff --git a/scripts/info b/scripts/info deleted file mode 100755 index 77269ab5fa..0000000000 --- a/scripts/info +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -var fs = require('fs'); -var name = process.argv[2] || '.'; -var property = process.argv[3] || 'version'; -if (name != '.') name = 'node_modules/' + name; -var json = JSON.parse(fs.readFileSync(name + '/package.json', 'utf8')); -console.log(json[property]); diff --git a/spec/ajv_instances.js b/spec/ajv_instances.js index d8711c5153..a2a07bb43e 100644 --- a/spec/ajv_instances.js +++ b/spec/ajv_instances.js @@ -11,9 +11,9 @@ function getAjvInstances(options, extraOpts) { function _getAjvInstances(opts, useOpts) { var optNames = Object.keys(opts) if (optNames.length) { - opts = {...opts} - var useOpts1 = {...useOpts}, - optName = optNames[0] + opts = Object.assign({}, opts) + var useOpts1 = Object.assign({}, useOpts) + var optName = optNames[0] useOpts1[optName] = opts[optName] delete opts[optName] var instances = _getAjvInstances(opts, useOpts), diff --git a/spec/ajv_options.js b/spec/ajv_options.js index 829c501fb9..ed8bba7f37 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -14,8 +14,4 @@ var options = fullTest } : {allErrors: true, codegen: {es5: true, lines: true}} -if (fullTest && !isBrowser) { - options.processCode = require("js-beautify").js_beautify -} - module.exports = options From 606945cf893b8835cda5e1c4d5429da2bcaf54fa Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 31 Aug 2020 22:07:15 +0100 Subject: [PATCH 133/322] feat: "implements" in keyword definition reserves keyword names (e.g., used by this definition) --- lib/compile/rules.ts | 2 -- lib/compile/validate/applicability.ts | 9 ++++---- lib/keyword.ts | 32 ++++++++++++--------------- lib/vocabularies/applicator/if.ts | 3 +-- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index a8437119a7..e96e4cd75f 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -37,8 +37,6 @@ const KEYWORDS = [ "writeOnly", "contentMediaType", "contentEncoding", - "then", - "else", ] export default function rules(): ValidationRules { diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 0cc7fe1b71..5e28602e01 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -11,9 +11,8 @@ export function shouldUseGroup(schema: object, group: RuleGroup): boolean { } export function shouldUseRule(schema: object, rule: Rule): boolean | undefined { - return schema[rule.keyword] !== undefined || ruleImplementsSomeKeyword(schema, rule) -} - -function ruleImplementsSomeKeyword(schema: object, rule: Rule): boolean | undefined { - return rule.definition.implements?.some((kwd) => schema[kwd] !== undefined) + return ( + schema[rule.keyword] !== undefined || + rule.definition.implements?.some((kwd) => schema[kwd] !== undefined) + ) } diff --git a/lib/keyword.ts b/lib/keyword.ts index df32882967..bfe58fe848 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,5 +1,5 @@ import {KeywordDefinition, Vocabulary, ErrorObject, ValidateFunction} from "./types" -import {ValidationRules, Rule} from "./compile/rules" +import {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {definitionSchema} from "./definition_schema" import {schemaOrData} from "./data" @@ -28,6 +28,7 @@ export function addKeyword( if (typeof kwdOrDef == "string") { keyword = kwdOrDef if (typeof defOrSkip == "object") { + // TODO enable once tests are updated // this.logger.warn("this method signature is deprecated, see docs for addKeyword") definition = defOrSkip if (definition.keyword === undefined) definition.keyword = keyword @@ -60,36 +61,31 @@ export function addKeyword( return this } -function _addRule(keyword: string, dataType?: string, definition?: KeywordDefinition) { +function _addRule(keyword: string, dataType?: string, definition?: KeywordDefinition): void { const RULES: ValidationRules = this.RULES let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) if (!ruleGroup) { ruleGroup = {type: dataType, rules: []} RULES.rules.push(ruleGroup) } - RULES.keywords[keyword] = true if (!definition) return - const rule: Rule = { - keyword, - definition, - } + const rule: Rule = {keyword, definition} + if (definition.before) _addBeforeRule.call(this, ruleGroup, rule, definition.before) + else ruleGroup.rules.push(rule) + RULES.all[keyword] = rule + definition.implements?.forEach((kwd) => this.addKeyword(kwd)) +} - if (definition?.before) { - const i = ruleGroup.rules.findIndex((rule) => rule.keyword === definition.before) - if (i >= 0) { - ruleGroup.rules.splice(i, 0, rule) - } else { - ruleGroup.rules.push(rule) - // TODO replace with Ajv this.logger - this.logger.log(`rule ${definition.before} is not defined`) - } +function _addBeforeRule(this, ruleGroup: RuleGroup, rule: Rule, before: string): void { + const i = ruleGroup.rules.findIndex((rule) => rule.keyword === before) + if (i >= 0) { + ruleGroup.rules.splice(i, 0, rule) } else { ruleGroup.rules.push(rule) + this.logger.warn(`rule ${before} is not defined`) } - - RULES.all[keyword] = rule } function eachItem(xs: T | T[], f: (x: T) => void): void { diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 59b0011d41..3fb485c112 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -7,8 +7,7 @@ import {_, str, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], - // TODO - // implements: ["then", "else"], + implements: ["then", "else"], trackErrors: true, code(cxt: KeywordContext) { const {gen, it} = cxt From 20ca3fffe467052229a3bcbef674e699f054b407 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 1 Sep 2020 09:22:40 +0100 Subject: [PATCH 134/322] refactor: remove keyword definition meta-schema --- lib/ajv.ts | 15 +++---- lib/compile/validate/dataType.ts | 11 ++--- lib/compile/validate/keyword.ts | 12 ++--- lib/definition_schema.ts | 37 --------------- lib/keyword.ts | 77 +++++++++++--------------------- lib/types.ts | 23 +++------- spec/custom.spec.js | 42 ++--------------- 7 files changed, 54 insertions(+), 163 deletions(-) delete mode 100644 lib/definition_schema.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index f2fe9fbc18..c7221a3b6b 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -13,6 +13,7 @@ import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" import applicatorVocabulary from "./vocabularies/applicator" import formatVocabulary from "./vocabularies/format" +import {addVocabulary, addKeyword, getKeyword, removeKeyword} from "./keyword" module.exports = Ajv @@ -25,17 +26,15 @@ Ajv.prototype.getSchema = getSchema Ajv.prototype.removeSchema = removeSchema Ajv.prototype.addFormat = addFormat Ajv.prototype.errorsText = errorsText +Ajv.prototype.addVocabulary = addVocabulary +Ajv.prototype.addKeyword = addKeyword +Ajv.prototype.getKeyword = getKeyword +Ajv.prototype.removeKeyword = removeKeyword Ajv.prototype._addSchema = _addSchema Ajv.prototype._compile = _compile Ajv.prototype.compileAsync = require("./compile/async") -var keywordMethods = require("./keyword") -Ajv.prototype.addVocabulary = keywordMethods.addVocabulary -Ajv.prototype.addKeyword = keywordMethods.addKeyword -Ajv.prototype.getKeyword = keywordMethods.getKeyword -Ajv.prototype.removeKeyword = keywordMethods.removeKeyword -Ajv.prototype.validateKeyword = keywordMethods.validateKeyword Ajv.prototype.$dataMetaSchema = $dataMetaSchema Ajv.ValidationError = ValidationError @@ -84,9 +83,7 @@ export default function Ajv(opts: Options): void { if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) - if (opts.nullable) { - this.addKeyword("nullable", {metaSchema: {type: "boolean"}}) - } + if (opts.nullable) this.addKeyword({keyword: "nullable", schemaType: "boolean"}) addInitialSchemas(this) opts.format = formatOpt } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index a3c9357849..fcb7053f1e 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -4,11 +4,12 @@ import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" import {_, str, Name} from "../codegen" +import {ValidationRules} from "../rules" export function getSchemaTypes({opts, RULES}: CompilationContext, schema): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] - types.forEach(checkType) + types.forEach((t) => checkType(t, RULES)) if (opts.nullable) { const hasNull = types.includes("null") if (hasNull && schema.nullable === false) { @@ -18,11 +19,11 @@ export function getSchemaTypes({opts, RULES}: CompilationContext, schema): strin } } return types +} - function checkType(t: string): void { - if (typeof t == "string" && t in RULES.types) return - throw new Error('"type" keyword must be allowed string or string[]: ' + t) - } +export function checkType(t: string, RULES: ValidationRules): void { + if (typeof t == "string" && t in RULES.types) return + throw new Error('"type" keyword must be allowed string or string[]: ' + t) } export function coerceAndCheckDataType(it: CompilationContext, types: string[]): boolean { diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 86d23c038b..47839403c6 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -21,16 +21,16 @@ export function keywordCode( const cxt = new KeywordContext(it, def, keyword) if ("code" in def) { def.code(cxt, ruleType) - } else if (cxt.$data && "validate" in def) { - funcKeywordCode(cxt, def as FuncKeywordDefinition) + } else if (cxt.$data && def.validate) { + funcKeywordCode(cxt, def) } else if ("macro" in def) { macroKeywordCode(cxt, def) - } else if ("compile" in def || "validate" in def) { + } else if (def.compile || def.validate) { funcKeywordCode(cxt, def) } } -function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { +function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition): void { const {gen, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) const schemaRef = useKeyword(gen, keyword, macroSchema) @@ -51,11 +51,11 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) { cxt.pass(valid, () => cxt.error(true)) } -function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) { +function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition): void { const {gen, keyword, schema, parentSchema, $data, it} = cxt checkAsync(it, def) const validate = - "compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate + !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate const validateRef = useKeyword(gen, keyword, validate) const valid = gen.let("valid") cxt.block$data(valid, validateKeyword) diff --git a/lib/definition_schema.ts b/lib/definition_schema.ts deleted file mode 100644 index 09562dce59..0000000000 --- a/lib/definition_schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -const metaSchema = require("./refs/json-schema-draft-07.json") - -export const definitionSchema: object = { - $id: "https://github.com/ajv-validator/ajv/blob/master/lib/definition_schema.js", - description: 'Keyword definition schema. "inline" keywords are replaced with "code" in Ajv v7', - definitions: { - simpleTypes: metaSchema.definitions.simpleTypes, - }, - type: "object", - dependencies: { - schema: ["validate"], - valid: {not: {required: ["macro"]}}, - $data: {anyOf: [{required: ["code"]}, {required: ["validate"]}]}, - }, - properties: { - inline: false, - validate: true, - compile: true, - macro: true, - code: true, - type: metaSchema.properties.type, - schemaType: metaSchema.properties.type, - schema: {type: "boolean"}, - dependencies: { - type: "array", - items: {type: "string"}, - }, - metaSchema: {type: "object"}, - modifying: {type: "boolean"}, - valid: {type: "boolean"}, - $data: {type: "boolean"}, - async: {type: "boolean"}, - errors: { - anyOf: [{type: "boolean"}, {const: "full"}], - }, - }, -} diff --git a/lib/keyword.ts b/lib/keyword.ts index bfe58fe848..2e5f557910 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -1,64 +1,60 @@ -import {KeywordDefinition, Vocabulary, ErrorObject, ValidateFunction} from "./types" +import {KeywordDefinition, Vocabulary, ErrorObject} from "./types" import {ValidationRules, Rule, RuleGroup} from "./compile/rules" -import {definitionSchema} from "./definition_schema" import {schemaOrData} from "./data" +import {checkType} from "./compile/validate/dataType" const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i -export function addVocabulary(this, definitions: Vocabulary, _skipValidation?: boolean): object { +export function addVocabulary(this, definitions: Vocabulary): object { // TODO return type Ajv - for (const def of definitions) { - if (!def.keyword) throw new Error('Keyword definition must have "keyword" property') - this.addKeyword(def, _skipValidation) - } + for (const def of definitions) this.addKeyword(def) return this } // TODO Ajv -export function addKeyword(this, def: KeywordDefinition, _skipValidation?: boolean): object +export function addKeyword(this, def: KeywordDefinition): object export function addKeyword(this, keyword: string): object export function addKeyword( this: any, // TODO Ajv kwdOrDef: string | KeywordDefinition, - defOrSkip?: KeywordDefinition | boolean, // deprecated - _skipValidation?: boolean // deprecated + def?: KeywordDefinition // deprecated ): object { let keyword: string | string[] - let definition: KeywordDefinition | undefined if (typeof kwdOrDef == "string") { keyword = kwdOrDef - if (typeof defOrSkip == "object") { + if (typeof def == "object") { // TODO enable once tests are updated - // this.logger.warn("this method signature is deprecated, see docs for addKeyword") - definition = defOrSkip - if (definition.keyword === undefined) definition.keyword = keyword - else if (definition.keyword !== keyword) throw new Error("invalid addKeyword parameters") + // this.logger.warn("these parameters are deprecated, see docs for addKeyword") + if (def.keyword === undefined) def.keyword = keyword + else if (def.keyword !== keyword) throw new Error("invalid addKeyword parameters") } - } else if (typeof kwdOrDef == "object" && typeof defOrSkip != "object") { - definition = kwdOrDef - keyword = definition.keyword - _skipValidation = defOrSkip + } else if (typeof kwdOrDef == "object" && def === undefined) { + def = kwdOrDef + keyword = def.keyword } else { throw new Error("invalid addKeywords parameters") } + checkKeyword.call(this, keyword, def) + if (def) keywordMetaschema.call(this, def) + eachItem(keyword, (kwd) => { + eachItem(def?.type, (t) => _addRule.call(this, kwd, t, def)) + }) + return this +} + +function checkKeyword(keyword: string | string[], def?: KeywordDefinition) { /* eslint no-shadow: 0 */ const RULES: ValidationRules = this.RULES eachItem(keyword, (kwd) => { if (RULES.keywords[kwd]) throw new Error(`Keyword ${kwd} is already defined`) if (!IDENTIFIER.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`) }) - - if (definition) { - if (!_skipValidation) this.validateKeyword(definition, true) - keywordMetaschema.call(this, definition) + if (!def) return + if (def.type) eachItem(def.type, (t) => checkType(t, RULES)) + if (def.$data && !("code" in def || "validate" in def)) { + throw new Error('$data keyword must have "code" or "validate" function') } - const types = definition?.type - eachItem(keyword, (kwd) => { - eachItem(types, (t) => _addRule.call(this, kwd, t, definition)) - }) - - return this } function _addRule(keyword: string, dataType?: string, definition?: KeywordDefinition): void { @@ -117,7 +113,6 @@ export function getKeyword(this, keyword: string): KeywordDefinition | boolean { */ export function removeKeyword(keyword: string): object { // TODO return type should be Ajv - /* jshint validthis: true */ const RULES: ValidationRules = this.RULES delete RULES.keywords[keyword] delete RULES.all[keyword] @@ -132,23 +127,3 @@ export interface KeywordValidator { (definition: KeywordDefinition, throwError: boolean): boolean errors?: ErrorObject[] | null } - -/** - * Validate keyword definition - * @this Ajv - * @param {Object} definition keyword definition object. - * @param {Boolean} throwError true to throw exception if definition is invalid - * @return {boolean} validation result - */ -export const validateKeyword: KeywordValidator = function (definition, throwError) { - validateKeyword.errors = null - const v: ValidateFunction = (this._validateKeyword = - this._validateKeyword || this.compile(definitionSchema, true)) - - if (v(definition)) return true - validateKeyword.errors = v.errors - if (throwError) { - throw new Error("keyword definition is invalid: " + this.errorsText(v.errors)) - } - return false -} diff --git a/lib/types.ts b/lib/types.ts index e46b6b296b..c5edd40c44 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -144,6 +144,7 @@ interface _KeywordDef { keyword: string | string[] type?: string | string[] schemaType?: string | string[] + $data?: boolean implements?: string[] before?: string metaSchema?: object @@ -155,7 +156,6 @@ interface _KeywordDef { export interface CodeKeywordDefinition extends _KeywordDef { code: (cxt: KeywordContext, ruleType?: string) => void - $data?: boolean trackErrors?: boolean } @@ -165,17 +165,15 @@ export type MacroKeywordFunc = ( it: CompilationContext ) => object | boolean -export type FuncKeywordDefinition = CompiledKeywordDefinition | ValidatedKeywordDefinition - export type CompileKeywordFunc = ( schema: any, parentSchema: object, it: CompilationContext ) => ValidateFunction -interface $DataKeywordDef extends _KeywordDef { +export interface FuncKeywordDefinition extends _KeywordDef { validate?: SchemaValidateFunction | ValidateFunction - $data?: boolean // requires "validate" + compile?: CompileKeywordFunc // schema: false makes validate not to expect schema (ValidateFunction) schema?: boolean // requires "validate" modifying?: boolean @@ -184,23 +182,14 @@ interface $DataKeywordDef extends _KeywordDef { errors?: boolean | "full" } -export interface MacroKeywordDefinition extends $DataKeywordDef { +export interface MacroKeywordDefinition extends FuncKeywordDefinition { macro: MacroKeywordFunc } -export interface CompiledKeywordDefinition extends $DataKeywordDef { - compile: CompileKeywordFunc -} - -export interface ValidatedKeywordDefinition extends $DataKeywordDef { - validate: SchemaValidateFunction | ValidateFunction -} - export type KeywordDefinition = - | MacroKeywordDefinition - | CompiledKeywordDefinition - | ValidatedKeywordDefinition | CodeKeywordDefinition + | FuncKeywordDefinition + | MacroKeywordDefinition export interface KeywordErrorDefinition { message: string | ((cxt: KeywordErrorContext) => Code) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 47f05b456f..f0af227917 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -760,7 +760,7 @@ describe("Custom keywords", () => { shouldBeInvalidSchema({"x-even-$data": "false"}) }) - it('should fail if "macro" keyword definition has "$data" but no "validate"', () => { + it('should fail if "macro" keyword definition has "$data" but no "code" or "validate"', () => { should.throw(() => { ajv.addKeyword("even", { type: "number", @@ -1083,15 +1083,15 @@ describe("Custom keywords", () => { it("should throw if unknown type is passed", () => { should.throw(() => { - addKeyword("custom1", "wrongtype") + addKeyword("custom1", {type: "wrongtype"}) }) should.throw(() => { - addKeyword("custom2", ["number", "wrongtype"]) + addKeyword("custom2", {type: ["number", "wrongtype"]}) }) should.throw(() => { - addKeyword("custom3", ["number", undefined]) + addKeyword("custom3", {type: ["number", undefined]}) }) }) @@ -1278,26 +1278,6 @@ describe("Custom keywords", () => { ajv.validate({pass: ""}, 1).should.equal(true) ajv.validate({fail: ""}, 1).should.equal(false) }) - - it("should throw exception if used with macro keyword", () => { - should.throw(() => { - ajv.addKeyword("pass", { - macro: () => { - return {} - }, - valid: true, - }) - }) - - should.throw(() => { - ajv.addKeyword("fail", { - macro: () => { - return {not: {}} - }, - valid: false, - }) - }) - }) }) describe('"dependencies" in keyword definition', () => { @@ -1329,19 +1309,5 @@ describe("Custom keywords", () => { v({foo: 1}).should.equal(true) v({}).should.equal(false) }) - - it("'dependencies'should be array of valid strings", () => { - ajv.addKeyword("newKeyword1", { - metaSchema: {type: "boolean"}, - dependencies: ["dep1"], - }) - - should.throw(() => { - ajv.addKeyword("newKeyword2", { - metaSchema: {type: "boolean"}, - dependencies: [1], - }) - }) - }) }) }) From e675f8a29507e807c02e9cee9374862c65898562 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 1 Sep 2020 10:25:38 +0100 Subject: [PATCH 135/322] use new addKeywords method api and keywords option in test --- lib/ajv.ts | 16 +- lib/keyword.ts | 6 +- lib/types.ts | 4 +- spec/async.spec.js | 7 +- spec/async/keyword.json | 2 +- spec/async_schemas.spec.js | 13 +- spec/async_validate.spec.js | 24 +-- spec/custom.spec.js | 183 ++++++++---------- ...1_addKeyword_and_schema_without_id.spec.js | 2 +- ...1_allErrors_custom_keyword_skipped.spec.js | 4 +- .../768_passContext_recursive_ref.spec.js | 4 +- ...5_removeAdditional_custom_keywords.spec.js | 3 +- spec/options/options_code.spec.js | 4 +- spec/options/options_validation.spec.js | 7 +- 14 files changed, 129 insertions(+), 150 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index c7221a3b6b..412953bc21 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -1,9 +1,9 @@ +import {Vocabulary, KeywordDefinition, Options} from "./types" import SchemaObject from "./compile/schema_obj" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules from "./compile/rules" import $dataMetaSchema from "./data" -import {Options} from "./types" var compileSchema = require("./compile"), resolve = require("./compile/resolve"), @@ -445,10 +445,16 @@ function addInitialFormats(self) { } } -function addInitialKeywords(self, keywords, skipValidation?: boolean) { - for (var name in keywords) { - var keyword = keywords[name] - self.addKeyword(name, keyword, skipValidation) +function addInitialKeywords(self, defs: Vocabulary | {[x: string]: KeywordDefinition}) { + if (Array.isArray(defs)) { + self.addVocabulary(defs) + return + } + self.logger.warn("keywords option as map is deprecated, pass array") + for (var keyword in defs) { + var def = defs[name] + if (!def.keyword) def.keyword = keyword + self.addKeyword(def) } } diff --git a/lib/keyword.ts b/lib/keyword.ts index 2e5f557910..328e00f428 100644 --- a/lib/keyword.ts +++ b/lib/keyword.ts @@ -23,10 +23,8 @@ export function addKeyword( if (typeof kwdOrDef == "string") { keyword = kwdOrDef if (typeof def == "object") { - // TODO enable once tests are updated - // this.logger.warn("these parameters are deprecated, see docs for addKeyword") - if (def.keyword === undefined) def.keyword = keyword - else if (def.keyword !== keyword) throw new Error("invalid addKeyword parameters") + this.logger.warn("these parameters are deprecated, see docs for addKeyword") + def.keyword = keyword } } else if (typeof kwdOrDef == "object" && def === undefined) { def = kwdOrDef diff --git a/lib/types.ts b/lib/types.ts index c5edd40c44..61537f8df8 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -13,7 +13,7 @@ export interface Options { unicode?: boolean format?: false | string formats?: object - keywords?: object + keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated unknownFormats?: true | string[] | "ignore" schemas?: object[] | object missingRefs?: true | "ignore" | "fail" @@ -196,7 +196,7 @@ export interface KeywordErrorDefinition { params?: (cxt: KeywordErrorContext) => Code } -export type Vocabulary = KeywordDefinition[] +export type Vocabulary = (KeywordDefinition | string)[] export interface KeywordErrorContext { gen: CodeGen diff --git a/spec/async.spec.js b/spec/async.spec.js index da3b83a7ee..913f4c92bd 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -193,7 +193,8 @@ describe("compileAsync method", () => { }) function test(schema, expectedLoadCallCount) { - ajv.addKeyword("myFooBar", { + ajv.addKeyword({ + keyword: "myFooBar", type: "string", validate: function (sch, data) { return sch === data @@ -326,7 +327,7 @@ describe("compileAsync method", () => { }) it("if schema compilation throws some other exception", (done) => { - ajv.addKeyword("badkeyword", {compile: badCompile}) + ajv.addKeyword({keyword: "badkeyword", compile: badCompile}) var schema = {badkeyword: true} ajv.compileAsync(schema, shouldFail(done)) @@ -390,7 +391,7 @@ describe("compileAsync method", () => { }) it("if schema compilation throws some other exception", () => { - ajv.addKeyword("badkeyword", {compile: badCompile}) + ajv.addKeyword({keyword: "badkeyword", compile: badCompile}) var schema = {badkeyword: true} return shouldReject(ajv.compileAsync(schema)) diff --git a/spec/async/keyword.json b/spec/async/keyword.json index 1b34fa00b7..76984a4572 100644 --- a/spec/async/keyword.json +++ b/spec/async/keyword.json @@ -1,6 +1,6 @@ [ { - "description": "async custom keywords (validated)", + "description": "async keywords (validated)", "schema": { "$async": true, "properties": { diff --git a/spec/async_schemas.spec.js b/spec/async_schemas.spec.js index 2869ec9820..ea60f064be 100644 --- a/spec/async_schemas.spec.js +++ b/spec/async_schemas.spec.js @@ -13,9 +13,7 @@ instances.forEach(addAsyncFormatsAndKeywords) jsonSchemaTest(instances, { description: - "asynchronous schemas tests of " + - instances.length + - " ajv instances with different options", + "asynchronous schemas tests of " + instances.length + " ajv instances with different options", suites: { "async schemas": typeof window == "object" @@ -41,21 +39,24 @@ function addAsyncFormatsAndKeywords(ajv) { validate: checkWordOnServer, }) - ajv.addKeyword("idExists", { + ajv.addKeyword({ + keyword: "idExists", async: true, type: "number", validate: checkIdExists, errors: false, }) - ajv.addKeyword("idExistsWithError", { + ajv.addKeyword({ + keyword: "idExistsWithError", async: true, type: "number", validate: checkIdExistsWithError, errors: true, }) - ajv.addKeyword("idExistsCompiled", { + ajv.addKeyword({ + keyword: "idExistsCompiled", async: true, type: "number", compile: compileCheckIdExists, diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index 83dfc61214..5b7ecdbf36 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -80,14 +80,16 @@ describe("async schemas, formats and keywords", function () { describe("async custom keywords", () => { beforeEach(() => { instances.forEach((_ajv) => { - _ajv.addKeyword("idExists", { + _ajv.addKeyword({ + keyword: "idExists", async: true, type: "number", validate: checkIdExists, errors: false, }) - _ajv.addKeyword("idExistsWithError", { + _ajv.addKeyword({ + keyword: "idExistsWithError", async: true, type: "number", validate: checkIdExistsWithError, @@ -138,12 +140,8 @@ describe("async schemas, formats and keywords", function () { var validate = _ajv.compile(schema) return Promise.all([ - shouldBeInvalid(validate({userId: 5, postId: 10}), [ - "id not found in table posts", - ]), - shouldBeInvalid(validate({userId: 9, postId: 25}), [ - "id not found in table users", - ]), + shouldBeInvalid(validate({userId: 5, postId: 10}), ["id not found in table posts"]), + shouldBeInvalid(validate({userId: 9, postId: 25}), ["id not found in table users"]), ]) }) ) @@ -378,16 +376,10 @@ describe("async schemas, formats and keywords", function () { shouldBeInvalid(validate({foo: {foo: "manana"}})), shouldBeInvalid(validate({foo: {foo: 1}})), shouldThrow(validate({foo: {foo: "today"}}), "unknown word"), - shouldBeValid( - validate((data = {foo: {foo: {foo: "tomorrow"}}})), - data - ), + shouldBeValid(validate((data = {foo: {foo: {foo: "tomorrow"}}})), data), shouldBeInvalid(validate({foo: {foo: {foo: "manana"}}})), shouldBeInvalid(validate({foo: {foo: {foo: 1}}})), - shouldThrow( - validate({foo: {foo: {foo: "today"}}}), - "unknown word" - ), + shouldThrow(validate({foo: {foo: {foo: "today"}}}), "unknown word"), ]) }) ) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index f0af227917..531e0b05a5 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -49,6 +49,7 @@ describe("Custom keywords", () => { it('should pass parent schema to "interpreted" keyword validation', () => { testRangeKeyword({ + keyword: "x-range", type: "number", validate: validateRange, }) @@ -64,6 +65,7 @@ describe("Custom keywords", () => { it('should validate meta schema and pass parent schema to "interpreted" keyword validation', () => { testRangeKeyword({ + keyword: "x-range", type: "number", validate: validateRange, metaSchema: { @@ -84,7 +86,7 @@ describe("Custom keywords", () => { }) it('should allow defining custom errors for "interpreted" keyword', () => { - testRangeKeyword({type: "number", validate: validateRange}, true) + testRangeKeyword({keyword: "x-range", type: "number", validate: validateRange}, true) function validateRange(schema, data, parentSchema) { validateRangeSchema(schema, parentSchema) @@ -175,19 +177,19 @@ describe("Custom keywords", () => { }) it("should compile keyword validating function only once per schema", () => { - testConstantKeyword({compile: compileConstant}) + testConstantKeyword({keyword: "myConstant", compile: compileConstant}) }) it("should allow multiple schemas for the same keyword", () => { - testMultipleConstantKeyword({compile: compileConstant}) + testMultipleConstantKeyword({keyword: "x-constant", compile: compileConstant}) }) it('should pass parent schema to "compiled" keyword validation', () => { - testRangeKeyword({type: "number", compile: compileRange}) + testRangeKeyword({keyword: "x-range", type: "number", compile: compileRange}) }) it("should allow multiple parent schemas for the same keyword", () => { - testMultipleRangeKeyword({type: "number", compile: compileRange}) + testMultipleRangeKeyword({keyword: "x-range", type: "number", compile: compileRange}) }) it("should support schemaType", () => { @@ -235,24 +237,25 @@ describe("Custom keywords", () => { }) it("should add and expand macro rule", () => { - testConstantKeyword({macro: macroConstant}, 2) + testConstantKeyword({keyword: "myConstant", macro: macroConstant}, 2) }) it("should allow multiple schemas for the same macro keyword", () => { - testMultipleConstantKeyword({macro: macroConstant}, 2) + testMultipleConstantKeyword({keyword: "x-constant", macro: macroConstant}, 2) }) it('should pass parent schema to "macro" keyword', () => { - testRangeKeyword({type: "number", macro: macroRange}, undefined, 2) + testRangeKeyword({keyword: "x-range", type: "number", macro: macroRange}, undefined, 2) }) it("should allow multiple parent schemas for the same macro keyword", () => { - testMultipleRangeKeyword({type: "number", macro: macroRange}, 2) + testMultipleRangeKeyword({keyword: "x-range", type: "number", macro: macroRange}, 2) }) it("should support resolving $ref without id or $id", () => { instances.forEach((_ajv) => { - _ajv.addKeyword("macroRef", { + _ajv.addKeyword({ + keyword: "macroRef", macro: function (schema, parentSchema, it) { it.baseId.should.equal("#") var ref = schema.$ref @@ -292,11 +295,12 @@ describe("Custom keywords", () => { it("should recursively expand macro keywords", () => { instances.forEach((_ajv) => { - _ajv.addKeyword("deepProperties", { + _ajv.addKeyword({ + keyword: "deepProperties", type: "object", macro: macroDeepProperties, }) - _ajv.addKeyword("range", {type: "number", macro: macroRange}) + _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) var schema = { deepProperties: { @@ -411,8 +415,8 @@ describe("Custom keywords", () => { it("should correctly expand multiple macros on the same level", () => { instances.forEach((_ajv) => { - _ajv.addKeyword("range", {type: "number", macro: macroRange}) - _ajv.addKeyword("even", {type: "number", macro: macroEven}) + _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) + _ajv.addKeyword({keyword: "even", type: "number", macro: macroEven}) var schema = { range: [4, 6], @@ -434,7 +438,7 @@ describe("Custom keywords", () => { it("should validate macro keyword when it resolves to the same keyword as exists", () => { instances.forEach((_ajv) => { - _ajv.addKeyword("range", {type: "number", macro: macroRange}) + _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) var schema = { range: [1, 4], @@ -450,7 +454,7 @@ describe("Custom keywords", () => { it("should correctly expand macros in subschemas", () => { instances.forEach((_ajv) => { - _ajv.addKeyword("range", {type: "number", macro: macroRange}) + _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) var schema = { allOf: [{range: [4, 8]}, {range: [2, 6]}], @@ -470,9 +474,9 @@ describe("Custom keywords", () => { it("should correctly expand macros in macro expansions", () => { instances.forEach((_ajv) => { - _ajv.addKeyword("range", {type: "number", macro: macroRange}) - _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) - _ajv.addKeyword("myContains", {type: "array", macro: macroContains}) + _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) + _ajv.addKeyword({keyword: "exclusiveRange", metaSchema: {type: "boolean"}}) + _ajv.addKeyword({keyword: "myContains", type: "array", macro: macroContains}) var schema = { myContains: { @@ -498,7 +502,7 @@ describe("Custom keywords", () => { }) it("should throw exception if macro expansion is an invalid schema", () => { - ajv.addKeyword("invalid", {macro: macroInvalid}) + ajv.addKeyword({keyword: "invalid", macro: macroInvalid}) var schema = {invalid: true} should.throw(() => { @@ -839,7 +843,7 @@ describe("Custom keywords", () => { function testConstantKeyword(definition, numErrors) { instances.forEach((_ajv) => { - _ajv.addKeyword("myConstant", definition) + _ajv.addKeyword(definition) var schema = {myConstant: "abc"} var validate = _ajv.compile(schema) @@ -852,7 +856,7 @@ describe("Custom keywords", () => { function testMultipleConstantKeyword(definition, numErrors) { instances.forEach((_ajv) => { - _ajv.addKeyword("x-constant", definition) + _ajv.addKeyword(definition) var schema = { properties: { @@ -879,8 +883,8 @@ describe("Custom keywords", () => { function testRangeKeyword(definition, customErrors, numErrors) { instances.forEach((_ajv) => { - _ajv.addKeyword("x-range", definition) - _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) + _ajv.addKeyword(definition) + _ajv.addKeyword({keyword: "exclusiveRange", schemaType: "boolean"}) var schema = {"x-range": [2, 4]} var validate = _ajv.compile(schema) @@ -926,8 +930,8 @@ describe("Custom keywords", () => { function testMultipleRangeKeyword(definition, numErrors) { instances.forEach((_ajv) => { - _ajv.addKeyword("x-range", definition) - _ajv.addKeyword("exclusiveRange", {metaSchema: {type: "boolean"}}) + _ajv.addKeyword(definition) + _ajv.addKeyword({keyword: "exclusiveRange", schemaType: "boolean"}) var schema = { properties: { @@ -1011,19 +1015,19 @@ describe("Custom keywords", () => { function testThrow(keywords) { TEST_TYPES.forEach((dataType, index) => { should.throw(() => { - addKeyword(keywords[index], dataType) + _addKeyword(keywords[index], dataType) }) }) } function testThrowDuplicate(keywordPrefix) { - var index = 0 + let index = 0 TEST_TYPES.forEach((dataType1) => { TEST_TYPES.forEach((dataType2) => { - var keyword = keywordPrefix + index++ - addKeyword(keyword, dataType1) + const keyword = keywordPrefix + index++ + _addKeyword(keyword, dataType1) should.throw(() => { - addKeyword(keyword, dataType2) + _addKeyword(keyword, dataType2) }) }) }) @@ -1032,71 +1036,48 @@ describe("Custom keywords", () => { it("should throw if keyword is not a valid name", () => { should.not.throw(() => { - ajv.addKeyword("mykeyword", { - validate: () => { - return true - }, - }) + ajv.addKeyword("mykeyword") }) should.not.throw(() => { - ajv.addKeyword("hyphens-are-valid", { - validate: () => { - return true - }, - }) + ajv.addKeyword("hyphens-are-valid") }) should.throw(() => { - ajv.addKeyword("3-start-with-number-not-valid`", { - validate: () => { - return true - }, - }) + ajv.addKeyword("3-start-with-number-not-valid") }) should.throw(() => { - ajv.addKeyword("-start-with-hyphen-not-valid`", { - validate: () => { - return true - }, - }) + ajv.addKeyword("-start-with-hyphen-not-valid") }) should.throw(() => { - ajv.addKeyword("spaces not valid`", { - validate: () => { - return true - }, - }) + ajv.addKeyword("spaces not valid") }) }) it("should return instance of itself", () => { - var res = ajv.addKeyword("any", { - validate: () => { - return true - }, - }) + var res = ajv.addKeyword("any") res.should.equal(ajv) }) it("should throw if unknown type is passed", () => { should.throw(() => { - addKeyword("custom1", {type: "wrongtype"}) + _addKeyword("custom1", "wrongtype") }) should.throw(() => { - addKeyword("custom2", {type: ["number", "wrongtype"]}) + _addKeyword("custom2", ["number", "wrongtype"]) }) should.throw(() => { - addKeyword("custom3", {type: ["number", undefined]}) + _addKeyword("custom3", ["number", undefined]) }) }) - function addKeyword(keyword, dataType) { - ajv.addKeyword(keyword, { + function _addKeyword(keyword, dataType) { + ajv.addKeyword({ + keyword, type: dataType, validate: () => {}, }) @@ -1113,25 +1094,23 @@ describe("Custom keywords", () => { }) // TODO change to account for pre-defined keywords with definitions - it("should return keyword definition for custom keywords", () => { + it("should return keyword definition", () => { var definition = { - validate: () => { - return true - }, + keyword: "mykeyword", + validate: () => true, } - ajv.addKeyword("mykeyword", definition) + ajv.addKeyword(definition) ajv.getKeyword("mykeyword").should.equal(definition) }) }) describe("removeKeyword", () => { it("should remove and allow redefining custom keyword", () => { - ajv.addKeyword("positive", { + ajv.addKeyword({ + keyword: "positive", type: "number", - validate: function (schema, data) { - return data > 0 - }, + validate: (_schema, data) => data > 0, }) var schema = {positive: true} @@ -1141,7 +1120,8 @@ describe("Custom keywords", () => { validate(1).should.equal(true) should.throw(() => { - ajv.addKeyword("positive", { + ajv.addKeyword({ + keyword: "positive", type: "number", validate: function (sch, data) { return data >= 0 @@ -1151,7 +1131,12 @@ describe("Custom keywords", () => { ajv.removeKeyword("positive") ajv.removeSchema(schema) - ajv.addKeyword("positive", { + validate = ajv.compile(schema) + validate(-1).should.equal(true) + ajv.removeSchema(schema) + + ajv.addKeyword({ + keyword: "positive", type: "number", validate: function (sch, data) { return data >= 0 @@ -1179,12 +1164,11 @@ describe("Custom keywords", () => { validate(1).should.equal(true) validate(2).should.equal(true) - ajv.addKeyword("minimum", { + ajv.addKeyword({ + keyword: "minimum", type: "number", // make minimum exclusive - validate: function (sch, data) { - return data > sch - }, + validate: (sch, data) => data > sch, }) ajv.removeSchema(schema) @@ -1195,13 +1179,7 @@ describe("Custom keywords", () => { }) it("should return instance of itself", () => { - var res = ajv - .addKeyword("any", { - validate: () => { - return true - }, - }) - .removeKeyword("any") + var res = ajv.addKeyword("any").removeKeyword("any") res.should.equal(ajv) }) }) @@ -1225,7 +1203,8 @@ describe("Custom keywords", () => { }, } - ajv.addKeyword("collectionFormat", { + ajv.addKeyword({ + keyword: "collectionFormat", type: "string", modifying: withOption, compile: function (schema) { @@ -1261,17 +1240,15 @@ describe("Custom keywords", () => { describe("custom keywords with predefined validation result", () => { it("should ignore result from validation function", () => { - ajv.addKeyword("pass", { - validate: () => { - return false - }, + ajv.addKeyword({ + keyword: "pass", + validate: () => false, valid: true, }) - ajv.addKeyword("fail", { - validate: () => { - return true - }, + ajv.addKeyword({ + keyword: "fail", + validate: () => true, valid: false, }) @@ -1282,15 +1259,15 @@ describe("Custom keywords", () => { describe('"dependencies" in keyword definition', () => { it("should require properties in the parent schema", () => { - ajv.addKeyword("allRequired", { - macro: function (schema, parentSchema) { - return schema ? {required: Object.keys(parentSchema.properties)} : true - }, - metaSchema: {type: "boolean"}, + ajv.addKeyword({ + keyword: "allRequired", + macro: (schema, parentSchema) => + schema ? {required: Object.keys(parentSchema.properties)} : true, + schemaType: "boolean", dependencies: ["properties"], }) - var invalidSchema = { + const invalidSchema = { allRequired: true, } diff --git a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js index 097fa41325..f5a2d67495 100644 --- a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js +++ b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js @@ -13,7 +13,7 @@ describe("issue #1001: addKeyword breaks schema without ID", () => { var ajv = new Ajv() ajv.addSchema(schema) - ajv.addKeyword("myKeyword", {}) + ajv.addKeyword("myKeyword") ajv.getSchema("#/definitions/foo").should.be.a("function") }) }) diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js index 64399c047e..01d433c7b4 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js @@ -6,6 +6,7 @@ require("../chai").should() describe("issue #181, custom keyword is not validated in allErrors mode if there were previous error", () => { it("should validate custom keyword that doesn't create errors", () => { testCustomKeywordErrors({ + keyword: "alwaysFails", type: "object", errors: true, validate: function v(/* value */) { @@ -16,6 +17,7 @@ describe("issue #181, custom keyword is not validated in allErrors mode if there it("should validate custom keyword that creates errors", () => { testCustomKeywordErrors({ + keyword: "alwaysFails", type: "object", errors: true, validate: function v(/* value */) { @@ -36,7 +38,7 @@ describe("issue #181, custom keyword is not validated in allErrors mode if there function testCustomKeywordErrors(def) { var ajv = new Ajv({allErrors: true}) - ajv.addKeyword("alwaysFails", def) + ajv.addKeyword(def) var schema = { required: ["foo"], diff --git a/spec/issues/768_passContext_recursive_ref.spec.js b/spec/issues/768_passContext_recursive_ref.spec.js index 22b47ed20e..0112b6927f 100644 --- a/spec/issues/768_passContext_recursive_ref.spec.js +++ b/spec/issues/768_passContext_recursive_ref.spec.js @@ -50,7 +50,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { function getValidate(passContext) { ajv = new Ajv({passContext: passContext}) - ajv.addKeyword("testValidate", {validate: storeContext}) + ajv.addKeyword({keyword: "testValidate", validate: storeContext}) var schema = { $id: "foo", @@ -69,7 +69,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { function getValidateFragments(passContext) { ajv = new Ajv({passContext: passContext}) - ajv.addKeyword("testValidate", {validate: storeContext}) + ajv.addKeyword({keyword: "testValidate", validate: storeContext}) ajv.addSchema({ $id: "foo", diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js index 949918a7d9..4b7c9701fd 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -7,7 +7,8 @@ describe("issue #955: option removeAdditional breaks custom keywords", () => { it("should support custom keywords with option removeAdditional", () => { var ajv = new Ajv({removeAdditional: "all"}) - ajv.addKeyword("minTrimmedLength", { + ajv.addKeyword({ + keyword: "minTrimmedLength", type: "string", compile: function (schema) { return function (str) { diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js index 09750ab0e4..a5597636be 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.js @@ -76,8 +76,8 @@ describe("code generation options", () => { function getValidate(passContext) { ajv = new Ajv({passContext: passContext, inlineRefs: false}) - ajv.addKeyword("testValidate", {validate: storeContext}) - ajv.addKeyword("testCompile", {compile: compileTestValidate}) + ajv.addKeyword({keyword: "testValidate", validate: storeContext}) + ajv.addKeyword({keyword: "testCompile", compile: compileTestValidate}) var schema = { definitions: { diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index e23bb521c6..e176244879 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -38,14 +38,15 @@ describe("validation options", () => { describe("keywords", () => { it("should add keywords from options", () => { var ajv = new Ajv({ - keywords: { - identifier: { + keywords: [ + { + keyword: "identifier", type: "string", validate: function (schema, data) { return /^[a-z_$][a-z0-9_$]*$/i.test(data) }, }, - }, + ], }) var validate = ajv.compile({identifier: true}) From d658fefbeab14ddb73d3fb7f4b98d513897f15a3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 1 Sep 2020 11:07:24 +0100 Subject: [PATCH 136/322] docs: user-defined keywords --- CUSTOM.md | 64 ++++------ FAQ.md | 6 +- README.md | 118 +++++++++--------- spec/async/format.json | 2 +- spec/async/keyword.json | 6 +- spec/async_validate.spec.js | 4 +- spec/custom.spec.js | 32 ++--- ...1_allErrors_custom_keyword_skipped.spec.js | 2 +- .../768_passContext_recursive_ref.spec.js | 8 +- ...5_removeAdditional_custom_keywords.spec.js | 4 +- spec/options/options_add_schemas.spec.js | 2 +- spec/options/options_code.spec.js | 4 +- spec/options/options_reporting.spec.js | 9 +- .../issues/27_1_recursive_raml_schema.json | 21 +--- 14 files changed, 126 insertions(+), 156 deletions(-) diff --git a/CUSTOM.md b/CUSTOM.md index 46b95887b8..81fb6a0466 100644 --- a/CUSTOM.md +++ b/CUSTOM.md @@ -1,18 +1,22 @@ -# Defining custom keywords +# User defined keywords ## Contents - Define keyword with: + - code generation function - used by all pre-defined keywords - [validation function](#define-keyword-with-validation-function) - [compilation function](#define-keyword-with-compilation-function) - [macro function](#define-keyword-with-macro-function) - - [inline compilation function](#define-keyword-with-inline-compilation-function) - [Schema compilation context](#schema-compilation-context) - [Validation time variables](#validation-time-variables) - [Ajv utilities](#ajv-utilities) -- [Reporting errors in custom keywords](#reporting-errors-in-custom-keywords) +- [Reporting errors](#reporting-errors) - [Short-circuit validation](#short-circuit-validation) +### Define keyword with code generation function + +TODO + ### Define keyword with validation function Validation function will be called during data validation and it will be passed: @@ -67,7 +71,7 @@ console.log(validate({foo: "baz"})) // false `const` keyword is already available in Ajv. -**Please note:** If the keyword does not define custom errors (see [Reporting errors in custom keywords](#reporting-errors-in-custom-keywords)) pass `errors: false` in its definition; it will make generated code more efficient. +**Please note:** If the keyword does not define errors (see [Reporting errors](#reporting-errors)) pass `errors: false` in its definition; it will make generated code more efficient. To add asynchronous keyword pass `async: true` in its definition. @@ -87,9 +91,9 @@ The function should return validation result as boolean. It can return an array In some cases it is the best approach to define keywords, but it has the performance cost of an extra function call during validation. If keyword logic can be expressed via some other JSON Schema then `macro` keyword definition is more efficient (see below). -All custom keywords types can have an optional `metaSchema` property in their definitions. It is a schema against which the value of keyword will be validated during schema compilation. +All keywords can have an optional `metaSchema` property in their definitions. It is a schema against which the value of keyword will be validated during schema compilation. -Custom keyword can also have an optional `dependencies` property in their definitions - it is a list of required keywords in a containing (parent) schema. +Keywords can also have an optional `dependencies` property in their definitions - it is a list of required keywords in a containing (parent) schema. Example. `range` and `exclusiveRange` keywords using compiled schema: @@ -127,7 +131,7 @@ console.log(validate(2)) // false console.log(validate(4)) // false ``` -See note on custom errors and asynchronous keywords in the previous section. +See note on errors and asynchronous keywords in the previous section. ### Define keyword with "macro" function @@ -191,7 +195,7 @@ console.log(validate([3, 4, 5])) // true, number 5 matches schema inside "contai `contains` keyword is already available in Ajv with option `v5: true`. -See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/ajv-validator/ajv/blob/master/spec/custom.spec.js#L151). +See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/ajv-validator/ajv/blob/master/spec/keyword.spec.js#L151). ### Define keyword with "inline" compilation function @@ -282,7 +286,7 @@ The first parameter passed to inline keyword compilation function (and the 3rd p - _baseId_ - the current schema base URI that should be used as the base for resolving URIs in references (\$ref). - _async_ - truthy if the current schema is asynchronous. - _opts_ - Ajv instance option. You should not be changing them. -- _formats_ - all formats available in Ajv instance, including the custom ones. +- _formats_ - all formats available in Ajv instance. - _compositeRule_ - boolean indicating that the current schema is inside the compound keyword where failing some rule doesn't mean validation failure (`anyOf`, `oneOf`, `not`, `if` in `switch`). This flag is used to determine whether you can return validation result immediately after any error in case the option `allErrors` is not `true. You only need to do it if you have many steps in your keywords and potentially can define multiple errors. - _validate_ - the function you need to use to compile subschemas in your keywords (see the [implementation](https://github.com/ajv-validator/ajv-keywords/blob/master/keywords/dot/switch.jst) of `switch` keyword for example). - _util_ - [Ajv utilities](#ajv-utilities) you can use in your inline compilation functions. @@ -292,20 +296,16 @@ The first parameter passed to inline keyword compilation function (and the 3rd p There is a number of variables and expressions you can use in the generated (validation-time) code of your keywords. -- `'data' + (it.dataLevel || '')` - the variable name for the data at the current level. -- `'data' + ((it.dataLevel-1)||'')` - parent data if `it.dataLevel > 0`. +TODO + - `'rootData'` - the root data. - `it.dataPathArr[it.dataLevel]` - the name of the property in the parent object that points to the current data if `it.dataLevel > 0`. -- `'validate.schema'` - top level schema of the current validation function at validation-time. -- `'validate.schema' + it.schemaPath` - current level schema available at validation time (the same schema at compile time is `it.schema`). -- `'validate.schema' + it.schemaPath + '.' + keyword` - the value of your custom keyword at validation-time. Keyword is passed as the second parameter to the inline compilation function to allow using the same function to compile multiple keywords. -- `'valid' + it.level` - the variable that you have to declare and to assign the validation result to if your keyword returns statements rather than expression (`statements: true`). -- `'errors'` - the number of encountered errors. See [Reporting errors in custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). -- `'vErrors'` - the array with errors collected so far. See [Reporting errors in custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md#reporting-errors-in-custom-keywords). +- `'errors'` - the number of encountered errors. See [Reporting errors](#reporting-errors). +- `'vErrors'` - the array with errors collected so far. See [Reporting errors](#reporting-errors). ## Ajv utilities -There are sevral useful functions you can use in your inline keywords. These functions are available as properties of `it.util` object: +There are sevral useful functions you can use in your _code_ keywords. These functions can be imported from TODO: ##### .toHash(Array arr) -> Object @@ -317,9 +317,9 @@ toHash(["a", "b", "c"]) // { a: true, b: true, c: true } ##### .equal(value1, value2) -> Boolean -Performs deep equality comparison. This function is used in keywords `enum`, `constant`, `uniqueItems` and can be used in custom keywords. +Performs deep equality comparison. This function is used in keywords `enum`, `constant`, `uniqueItems` and can be used in user-defined keywords. -##### .getProperty(String key) -> String +##### gen.getProperty(String key) -> String Converts the string that is the key/index to access the property/item to the JavaScript syntax to access the property (either "." notation or "[...]" notation). @@ -338,18 +338,6 @@ Determines whether the passed schema has rules that should be validated. This fu schemaHasRules(schema, it.RULES.all) // true or false ``` -##### .escapeQuotes(String str) -> String - -Escapes single quotes in the string, so it can be inserted in the generated code inside the string constant with the single quotes. - -##### .toQuotedString(String str) -> String - -Converts the string to the JavaScript string constant in single quotes (using the escaped string). - -```javascript -toQuotedString("a'b") // "'a\\'b'" -``` - ##### .getData(String jsonPointer, Number dataLevel, Array paths) -> String Returns the validation-time expression to safely access data based on the passed [relative json pointer](https://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (See [examples](https://gist.github.com/geraintluff/5911303)). @@ -376,15 +364,15 @@ Converts the property name to the JSON-Pointer fragment that can be used in URI. Converts the JSON-Pointer fragment from URI to the property name. -## Reporting errors in custom keywords +## Reporting errors -All custom keywords but macro keywords can optionally create custom error messages. +All keywords but _macro_ can optionally create or define error messages. -Synchronous validating and compiled keywords should define errors by assigning them to `.errors` property of the validation function. Asynchronous keywords can return promise that rejects with `new Ajv.ValidationError(errors)`, where `errors` is an array of custom validation errors (if you don't want to define custom errors in asynchronous keyword, its validation function can return the promise that resolves with `false`). +Synchronous validating and compiled keywords should define errors by assigning them to `.errors` property of the validation function. Asynchronous keywords can return promise that rejects with `new Ajv.ValidationError(errors)`, where `errors` is an array of validation errors (if you don't want to create errors in asynchronous keyword, its validation function can return the promise that resolves with `false`). TODO replace "inline" keywords with "code" keywords -Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). This can be done for both synchronous and asynchronous keywords. +Inline keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). This can be done for both synchronous and asynchronous keywords. When inline keyword performs validation Ajv checks whether it created errors by comparing errors count before and after validation. To skip this check add option `errors` (can be `"full"`, `true` or `false`) to keyword definition: @@ -393,7 +381,7 @@ ajv.addKeyword("range", { type: "number", inline: inlineRangeTemplate, statements: true, - errors: true, // keyword should create custom errors when validation fails + errors: true, // keyword should create errors when validation fails // errors: 'full' // created errors should have dataPath already set // errors: false // keyword never creates errors, Ajv will add a default error }) @@ -403,7 +391,7 @@ Each error object should at least have properties `keyword`, `message` and `para Inlined keywords can optionally define `dataPath` and `schemaPath` properties in error objects, that will be assigned by Ajv unless `errors` option of the keyword is `"full"`. -If custom keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](https://github.com/ajv-validator/ajv#validation-errors)). +If keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](https://github.com/ajv-validator/ajv#validation-errors)). ## Short-circuit validation diff --git a/FAQ.md b/FAQ.md index 902a94f7ac..18db0b8eb8 100644 --- a/FAQ.md +++ b/FAQ.md @@ -26,7 +26,7 @@ See [#65](https://github.com/ajv-validator/ajv/issues/65), [#212](https://github The reasons are history (other fast validators with the same api) and performance (returning boolean is faster). Although more code is written to process errors than to handle successful results, almost all server-side validations pass. The existing API is more efficient from the performance point of view. -Ajv also supports asynchronous validation (with custom asynchronous formats and keywords) that returns a promise that either resolves to `true` or rejects with an error. +Ajv also supports asynchronous validation (with asynchronous formats and keywords) that returns a promise that either resolves to `true` or rejects with an error. ##### Would errors get overwritten in case of "concurrent" validations? @@ -34,7 +34,7 @@ No. There is no concurrency in JavaScript - it is single-threaded. While a valid ##### Can we change / extend API to add a method that would return errors (rather than assign them to `errors` property)? -No. In many cases there is a module responsible for the validation in the application, usually to load schemas and to process errors. This module is the right place to introduce any custom API. Convenience is a subjective thing, changing or extending API purely because of convenience would either break backward compatibility (even if it's done in a new major version it still complicates migration) or bloat API (making it more difficult to maintain). +No. In many cases there is a module responsible for the validation in the application, usually to load schemas and to process errors. This module is the right place to introduce any user-defined API. Convenience is a subjective thing, changing or extending API purely because of convenience would either break backward compatibility (even if it's done in a new major version it still complicates migration) or bloat API (making it more difficult to maintain). ##### Why don't `"additionalProperties": false` errors display the property name? @@ -90,4 +90,4 @@ There were many conversations about the meaning of `$ref` in [JSON Schema GitHub There are two possible approaches: 1. Traverse schema (e.g. with json-schema-traverse) and replace every `$ref` with the referenced schema. -2. Use a specially constructed JSON Schema with a [custom keyword](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) to traverse and modify your schema. +2. Use a specially constructed JSON Schema with a [user-defined keyword](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) to traverse and modify your schema. diff --git a/README.md b/README.md index 38487b4583..7ec4a631eb 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")) - [Combining schemas with \$ref](#ref) - [\$data reference](#data-reference) - NEW: [$merge and $patch keywords](#merge-and-patch-keywords) - - [Defining custom keywords](#defining-custom-keywords) + - [User-defined keywords](#user-defined-keywords) - [Asynchronous schema compilation](#asynchronous-schema-compilation) - [Asynchronous validation](#asynchronous-validation) - [Security considerations](#security-considerations) @@ -126,22 +126,22 @@ Performance of different validators by [json-schema-benchmark](https://github.co - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) - support of circular references between schemas - correct string lengths for strings with unicode pairs (can be turned off) - - [formats](#formats) defined by JSON Schema draft-07 standard (with [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin) and custom formats (can be turned off) + - [formats](#formats) defined by JSON Schema draft-07 standard (with [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin) and additional formats (can be turned off) - [validates schemas against meta-schema](#api-validateschema) - supports [browsers](#using-in-browser) and Node.js 0.10-14.x - [asynchronous loading](#asynchronous-schema-compilation) of referenced schemas during compilation - "All errors" validation mode with [option allErrors](#options) -- [error messages with parameters](#validation-errors) describing error reasons to allow creating custom error messages +- [error messages with parameters](#validation-errors) describing error reasons to allow error message generation - i18n error messages support with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package - [filtering data](#filtering-data) from additional properties - [assigning defaults](#assigning-defaults) to missing properties and items - [coercing data](#coercing-data-types) to the types specified in `type` keywords -- [custom keywords](#defining-custom-keywords) +- [user-defined keywords](user-defined-keywords) - draft-06/07 keywords `const`, `contains`, `propertyNames` and `if/then/else` - draft-06 boolean schemas (`true`/`false` as a schema to always pass/fail). - keywords `switch`, `patternRequired`, `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum` from [JSON Schema extension proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) with [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - [\$data reference](#data-reference) to use values from the validated data as values for the schema keywords -- [asynchronous validation](#asynchronous-validation) of custom formats and keywords +- [asynchronous validation](#asynchronous-validation) of user-defined formats and keywords ## Install @@ -187,7 +187,7 @@ if (!valid) console.log(ajv.errorsText()) See [API](#api) and [Options](#options) for more details. -Ajv compiles schemas to functions and caches them in all cases (using schema serialized with [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) or a custom function as a key), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. +Ajv compiles schemas to functions and caches them in all cases (using schema serialized with [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) or another function passed via options), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. The best performance is achieved when using compiled functions returned by `compile` or `getSchema` methods (there is no additional function call). @@ -238,7 +238,7 @@ CLI is available as a separate npm package [ajv-cli](https://github.com/ajv-vali - validating data file(s) against JSON Schema - testing expected validity of data against JSON Schema - referenced schemas -- custom meta-schemas +- user-defined meta-schemas - files in JSON, JSON5, YAML, and JavaScript format - all Ajv options - reporting changes in data after validation in [JSON-patch](https://tools.ietf.org/html/rfc6902) format @@ -482,59 +482,54 @@ The properties `source` and `with` in the keywords `$merge` and `$patch` can use See the package [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) for more information. -## Defining custom keywords +## User-defined keywords -The advantages of using custom keywords are: +The advantages of defining keywords are: -- allow creating validation scenarios that cannot be expressed using JSON Schema +- allow creating validation scenarios that cannot be expressed using pre-defined keywords - simplify your schemas - help bringing a bigger part of the validation logic to your schemas - make your schemas more expressive, less verbose and closer to your application domain -- implement custom data processors that modify your data (`modifying` option MUST be used in keyword definition) and/or create side effects while the data is being validated +- implement data processors that modify your data (`modifying` option MUST be used in keyword definition) and/or create side effects while the data is being validated If a keyword is used only for side-effects and its validation result is pre-defined, use option `valid: true/false` in keyword definition to simplify both generated code (no error handling in case of `valid: true`) and your keyword functions (no need to return any validation result). -The concerns you have to be aware of when extending JSON Schema standard with custom keywords are the portability and understanding of your schemas. You will have to support these custom keywords on other platforms and to properly document these keywords so that everybody can understand them in your schemas. +The concerns you have to be aware of when extending JSON Schema standard with additional keywords are the portability and understanding of your schemas. You will have to support these keywords on other platforms and to properly document them so that everybody can understand and use your schemas. -You can define custom keywords with [addKeyword](#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords. +You can define keywords with [addKeyword](#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords. Ajv allows defining keywords with: +- code generation function (used by all pre-defined keywords) - validation function - compilation function - macro function -- inline compilation function that should return code (as string) that will be inlined in the currently compiled schema. Example. `range` and `exclusiveRange` keywords using compiled schema: ```javascript -ajv.addKeyword("range", { +ajv.addKeyword({ + keyword: "range", type: "number", - compile: function (sch, parentSchema) { - var min = sch[0] - var max = sch[1] - - return parentSchema.exclusiveRange === true - ? function (data) { - return data > min && data < max - } - : function (data) { - return data >= min && data <= max - } - }, + schemaType: "array", + implements: "exclusiveRange", + compile: ([min, max], parentSchema) => + parentSchema.exclusiveRange === true + ? (data) => data > min && data < max + : (data) => data >= min && data <= max, }) -var schema = {range: [2, 4], exclusiveRange: true} -var validate = ajv.compile(schema) +const schema = {range: [2, 4], exclusiveRange: true} +const validate = ajv.compile(schema) console.log(validate(2.01)) // true console.log(validate(3.99)) // true console.log(validate(2)) // false console.log(validate(4)) // false ``` -Several custom keywords (typeof, instanceof, range and propertyNames) are defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - they can be used for your schemas and as a starting point for your own custom keywords. +Several keywords (typeof, instanceof, range and propertyNames) are defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - they can be used for your schemas and as a starting point for your own keywords. -See [Defining custom keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) for more details. +See [User-defined keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) for more details. ## Asynchronous schema compilation @@ -564,13 +559,13 @@ function loadSchema(uri) { Example in Node.js REPL: https://tonicdev.com/esp/ajv-asynchronous-validation -You can define custom formats and keywords that perform validation asynchronously by accessing database or some other service. You should add `async: true` in the keyword or format definition (see [addFormat](#api-addformat), [addKeyword](#api-addkeyword) and [Defining custom keywords](#defining-custom-keywords)). +You can define formats and keywords that perform validation asynchronously by accessing database or some other service. You should add `async: true` in the keyword or format definition (see [addFormat](#api-addformat), [addKeyword](#api-addkeyword) and [User-defined keywords](user-defined-keywords)). If your schema uses asynchronous formats/keywords or refers to some schema that contains them it should have `"$async": true` keyword so that Ajv can compile it correctly. If asynchronous format/keyword or reference to asynchronous schema is used in the schema without `$async` keyword Ajv will throw an exception during schema compilation. **Please note**: all asynchronous subschemas that are referenced from the current or other schemas should have `"$async": true` keyword as well, otherwise the schema compilation will fail. -Validation function for an asynchronous custom format/keyword should return a promise that resolves with `true` or `false` (or rejects with `new Ajv.ValidationError(errors)` if you want to return custom errors from the keyword function). +Validation function for an asynchronous format/keyword should return a promise that resolves with `true` or `false` (or rejects with `new Ajv.ValidationError(errors)` if you want to return errors from the keyword function). Ajv compiles asynchronous schemas to [es7 async functions](http://tc39.github.io/ecmascript-asyncawait/) that can optionally be transpiled with [nodent](https://github.com/MatAtBread/nodent). Async functions are supported in Node.js 7+ and all modern browsers. You can also supply any other transpiler as a function via `processCode` option. See [Options](#options). @@ -862,9 +857,9 @@ console.log(data) // [ 1, "foo" ] - not in `properties` or `items` subschemas - in schemas inside `anyOf`, `oneOf` and `not` (see [#42](https://github.com/ajv-validator/ajv/issues/42)) - in `if` subschema of `switch` keyword -- in schemas generated by custom macro keywords +- in schemas generated by user-defined _macro_ keywords -The [`strictDefaults` option](#options) customizes Ajv's behavior for the defaults that Ajv ignores (`true` raises an error, and `"log"` outputs a warning). +The [`strictDefaults` option](#options) changes Ajv's behavior for the defaults that Ajv ignores (`true` raises an error, and `"log"` outputs a warning). ## Coercing data types @@ -1016,7 +1011,7 @@ If no parameter is passed all schemas but meta-schemas will be removed and the c ##### .addFormat(String name, String|RegExp|Function|Object format) -> Ajv -Add custom format to validate strings or numbers. It can also be used to replace pre-defined formats for Ajv instance. +Add format to validate strings or numbers. Strings are converted to RegExp. @@ -1029,11 +1024,11 @@ If object is passed it should have properties `validate`, `compare` and `async`: - _async_: an optional `true` value if `validate` is an asynchronous function; in this case it should return a promise that resolves with a value `true` or `false`. - _type_: an optional type of data that the format applies to. It can be `"string"` (default) or `"number"` (see https://github.com/ajv-validator/ajv/issues/291#issuecomment-259923858). If the type of data is different, the validation will pass. -Custom formats can be also added via `formats` option. +Formats can be also added via `formats` option. -##### .addKeyword(String keyword, Object definition) -> Ajv +##### .addKeyword(Object definition) -> Ajv -Add custom validation keyword to Ajv instance. +Add validation keyword to Ajv instance. Keyword should be different from all standard JSON Schema keywords and different from previously defined keywords. There is no way to redefine keywords or to remove keyword definition from the instance. @@ -1048,34 +1043,38 @@ Example Keywords: Keyword definition is an object with the following properties: +- _keyword_: keyword name string - _type_: optional string or array of strings with data type(s) that the keyword applies to. If not present, the keyword will apply to all types. +- _schemaType_: optional string or array of strings with the required schema type +- _code_: function to generate code, used for all pre-defined keywords - _validate_: validating function - _compile_: compiling function - _macro_: macro function -- _inline_: compiling function that returns code (as string) +- _error_: optional error definition object - _schema_: an optional `false` value used with "validate" keyword to not pass schema - _metaSchema_: an optional meta-schema for keyword schema - _dependencies_: an optional list of properties that must be present in the parent schema - it will be checked during schema compilation +- _implements_: an optional list of keyword names to reserve that this keyword implements - _modifying_: `true` MUST be passed if keyword modifies data -- _statements_: `true` can be passed in case inline keyword generates statements (as opposed to expression) - _valid_: pass `true`/`false` to pre-define validation result, the result returned from validation function will be ignored. This option cannot be used with macro keywords. -- _\$data_: an optional `true` value to support [\$data reference](#data-reference) as the value of custom keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has validating function (as the only option or in addition to compile, macro or inline function). +- _\$data_: an optional `true` value to support [\$data reference](#data-reference) as the value of keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has _code_ or _validate_ function (the latter can be used in addition to _compile_ or _macro_). +- _\$dataError_: optional error definition for invalid \$data schema - _async_: an optional `true` value if the validation function is asynchronous (whether it is compiled or passed in _validate_ property); in this case it should return a promise that resolves with a value `true` or `false`. This option is ignored in case of "macro" and "inline" keywords. - _errors_: an optional boolean or string `"full"` indicating whether keyword returns errors. If this property is not set Ajv will determine if the errors were set in case of failed validation. -_compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. _validate_ can be used separately or in addition to them to support \$data reference. +_compile_, _macro_ and _code_ are mutually exclusive, only one should be used at a time. _validate_ can be used separately or in addition to _compile_ or _macro_ to support \$data reference. **Please note**: If the keyword is validating data type that is different from the type(s) in its definition, the validation function will not be called (and expanded macro will not be used), so there is no need to check for data type inside validation function or inside schema returned by macro function (unless you want to enforce a specific type and for some reason do not want to use a separate `type` keyword for that). In the same way as standard keywords work, if the keyword does not apply to the data type being validated, the validation of this keyword will succeed. -See [Defining custom keywords](#defining-custom-keywords) for more details. +See [User defined keywords](#user-defined-keywords) for more details. ##### .getKeyword(String keyword) -> Object|Boolean -Returns custom keyword definition, `true` for pre-defined keywords and `false` if the keyword is unknown. +Returns keyword definition, `false` if the keyword is unknown. ##### .removeKeyword(String keyword) -> Ajv -Removes custom or pre-defined keyword so you can redefine them. +Removes added or pre-defined keyword so you can redefine them. While this method can be used to extend pre-defined keywords, it can also be used to completely change their meaning - it may lead to unexpected results. @@ -1155,15 +1154,15 @@ Defaults: - _format_: formats validation mode. Option values: - `true` (default) - validate added formats (see [Formats](#formats)). - `false` - ignore all format keywords. -- _formats_: an object with custom formats. Keys and values will be passed to `addFormat` method. -- _keywords_: an object with custom keywords. Keys and values will be passed to `addKeyword` method. +- _formats_: an object with format definitions. Keys and values will be passed to `addFormat` method. +- _keywords_: an array of keyword definitions or strings. Values will be passed to `addKeyword` method. - _unknownFormats_: handling of unknown formats. Option values: - `true` (default) - if an unknown format is encountered the exception is thrown during schema compilation. If `format` keyword value is [\$data reference](#data-reference) and it is unknown the validation will fail. - `[String]` - an array of unknown format names that will be ignored. This option can be used to allow usage of third party schemas with format(s) for which you don't have definitions, but still fail if another unknown format is used. If `format` keyword value is [\$data reference](#data-reference) and it is not in this array the validation will fail. - `"ignore"` - to log warning during schema compilation and always pass validation (the default behaviour in versions before 5.0.0). This option is not recommended, as it allows to mistype format name and it won't be validated without any error message. This behaviour is required by JSON Schema specification. - _schemas_: an array or object of schemas that will be added to the instance. In case you pass the array the schemas must have IDs in them. When the object is passed the method `addSchema(value, key)` will be called for each schema in this object. - _logger_: sets the logging method. Default is the global `console` object that should have methods `log`, `warn` and `error`. See [Error logging](#error-logging). Option values: - - custom logger - it should have methods `log`, `warn` and `error`. If any of these methods is missing an exception will be thrown. + - logger instance - it should have methods `log`, `warn` and `error`. If any of these methods is missing an exception will be thrown. - `false` - logging is disabled. ##### Referenced schema options @@ -1227,12 +1226,12 @@ Defaults: - `true` (default) - the referenced schemas that don't have refs in them are inlined, regardless of their size - that substantially improves performance at the cost of the bigger size of compiled schema functions. - `false` - to not inline referenced schemas (they will be compiled as separate functions). - integer number - to limit the maximum number of keywords of the schema that will be inlined. -- _passContext_: pass validation context to custom keyword functions. If this option is `true` and you pass some context to the compiled validation function with `validate.call(context, data)`, the `context` will be available as `this` in your custom keywords. By default `this` is Ajv instance. +- _passContext_: pass validation context to _compile_ and _validate_ keyword functions. If this option is `true` and you pass some context to the compiled validation function with `validate.call(context, data)`, the `context` will be available as `this` in your keywords. By default `this` is Ajv instance. - _loopRequired_: by default `required` keyword is compiled into a single expression (or a sequence of statements in `allErrors` mode). In case of a very large number of properties in this keyword it may result in a very big validation function. Pass integer to set the number of properties above which `required` keyword will be validated in a loop - smaller validation function size but also worse performance. - _loopEnum_: by default `enum` keyword is compiled into a single expression. In case of a very large number of allowed values it may result in a large validation function. Pass integer to set the number of values above which `enum` keyword will be validated in a loop. - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). -- _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when custom messages are used (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). +- _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when messages are generated outside of Ajv code (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). - _sourceCode_: add `sourceCode` property to validating function (for debugging; this code can be different from the result of toString call). - _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. Starting from version 5.0.0 this option replaced options: - `beautify` that formatted the generated function using [js-beautify](https://github.com/beautify-web/js-beautify). If you want to beautify the generated code pass a function calling `require('js-beautify').js_beautify` as `processCode: code => js_beautify(code)`. @@ -1251,7 +1250,7 @@ Each error is an object with the following properties: - _keyword_: validation keyword. - _dataPath_: the path to the part of the data that was validated. By default `dataPath` uses JavaScript property access notation (e.g., `".prop[1].subProp"`). When the option `jsonPointers` is true (see [Options](#options)) `dataPath` will be set using JSON pointer standard (e.g., `"/prop/1/subProp"`). - _schemaPath_: the path (JSON-pointer as a URI fragment) to the schema of the keyword that failed validation. -- _params_: the object with the additional information about error that can be used to create custom error messages (e.g., using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package). See below for parameters set by all keywords. +- _params_: the object with the additional information about error that can be used to generate error messages (e.g., using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package). See below for parameters set by all keywords. - _message_: the standard error message (can be excluded with option `messages` set to false). - _schema_: the schema of the keyword (added with `verbose` option). - _parentSchema_: the schema containing the keyword (added with `verbose` option) @@ -1287,17 +1286,18 @@ Properties of `params` object in errors depend on the keyword that failed valida - `enum` - property `allowedValues` pointing to the array of values (the schema of the keyword). - `$ref` - property `ref` with the referenced schema URI. - `oneOf` - property `passingSchemas` (array of indices of passing schemas, null if no schema passes). -- custom keywords (in case keyword definition doesn't create errors) - property `keyword` (the keyword name). + +User-defined keywords can define other keyword parameters. ### Error logging -Using the `logger` option when initiallizing Ajv will allow you to define custom logging. Here you can build upon the exisiting logging. The use of other logging packages is supported as long as the package or its associated wrapper exposes the required methods. If any of the required methods are missing an exception will be thrown. +A logger instance can be passed via `logger` option to Ajv constructor. The use of other logging packages is supported as long as the package or its associated wrapper exposes the required methods. If any of the required methods are missing an exception will be thrown. - **Required Methods**: `log`, `warn`, `error` ```javascript -var otherLogger = new OtherLogger() -var ajv = new Ajv({ +const otherLogger = new OtherLogger() +const ajv = new Ajv({ logger: { log: console.log.bind(console), warn: function warn() { @@ -1313,7 +1313,7 @@ var ajv = new Ajv({ ## Plugins -Ajv can be extended with plugins that add custom keywords, formats or functions to process generated code. When such plugin is published as npm package it is recommended that it follows these conventions: +Ajv can be extended with plugins that add keywords, formats or functions to process generated code. When such plugin is published as npm package it is recommended that it follows these conventions: - it exports a function - this function accepts ajv instance as the first parameter and returns the same instance to allow chaining @@ -1327,10 +1327,10 @@ If you have published a useful plugin please submit a PR to add it to the next s - [ajv-bsontype](https://github.com/BoLaMN/ajv-bsontype) - plugin to validate mongodb's bsonType formats - [ajv-cli](https://github.com/jessedc/ajv-cli) - command line interface - [ajv-formats](https://github.com/ajv-validator/ajv-formats) - formats defined in JSON Schema specification. -- [ajv-errors](https://github.com/ajv-validator/ajv-errors) - plugin for custom error messages +- [ajv-errors](https://github.com/ajv-validator/ajv-errors) - plugin for defining error messages in the schema - [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) - internationalised error messages - [ajv-istanbul](https://github.com/ajv-validator/ajv-istanbul) - plugin to instrument generated validation code to measure test coverage of your schemas -- [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) - plugin with custom validation keywords (select, typeof, etc.) +- [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) - plugin with additional validation keywords (select, typeof, etc.) - [ajv-merge-patch](https://github.com/ajv-validator/ajv-merge-patch) - plugin with keywords $merge and $patch - [ajv-pack](https://github.com/ajv-validator/ajv-pack) - produces a compact module exporting validation functions - [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) - format validators for draft2019 that aren't included in [ajv-formats](https://github.com/ajv-validator/ajv-formats) (ie. `idn-hostname`, `idn-email`, `iri`, `iri-reference` and `duration`). diff --git a/spec/async/format.json b/spec/async/format.json index 438bd05994..f699b556fd 100644 --- a/spec/async/format.json +++ b/spec/async/format.json @@ -1,6 +1,6 @@ [ { - "description": "async custom formats", + "description": "async user-defined formats", "schema": { "$async": true, "type": "string", diff --git a/spec/async/keyword.json b/spec/async/keyword.json index 76984a4572..3178dd9be2 100644 --- a/spec/async/keyword.json +++ b/spec/async/keyword.json @@ -48,7 +48,7 @@ ] }, { - "description": "async custom keywords (validated with errors)", + "description": "async user-defined keywords (validated with errors)", "schema": { "$async": true, "properties": { @@ -96,7 +96,7 @@ ] }, { - "description": "async custom keywords (compiled)", + "description": "async user-defined keywords (compiled)", "schema": { "$async": true, "properties": { @@ -134,7 +134,7 @@ ] }, { - "description": "custom keyword in async schema", + "description": "keyword in async schema", "schema": { "$async": true, "const": 5 diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index 5b7ecdbf36..813d8b530e 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -77,7 +77,7 @@ describe("async schemas, formats and keywords", function () { }) }) - describe("async custom keywords", () => { + describe("async user-defined keywords", () => { beforeEach(() => { instances.forEach((_ajv) => { _ajv.addKeyword({ @@ -119,7 +119,7 @@ describe("async schemas, formats and keywords", function () { }) }) - it("should return custom error", () => { + it("should return user-defined error", () => { return Promise.all( instances.map((_ajv) => { var schema = { diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 531e0b05a5..6b7584a0c9 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -7,7 +7,7 @@ var getAjvInstances = require("./ajv_instances"), const codegen = require("../dist/compile/codegen") const {_, nil} = codegen -describe("Custom keywords", () => { +describe("User-defined keywords", () => { var ajv, instances beforeEach(() => { @@ -19,7 +19,7 @@ describe("Custom keywords", () => { ajv = instances[0] }) - describe("custom rules", () => { + describe("user-defined rules", () => { describe('rule with "interpreted" keyword validation', () => { it("should add and validate rule", () => { testEvenKeyword({keyword: "x-even", type: "number", validate: validateEven}) @@ -85,7 +85,7 @@ describe("Custom keywords", () => { } }) - it('should allow defining custom errors for "interpreted" keyword', () => { + it('should allow defining errors for "validate" keyword', () => { testRangeKeyword({keyword: "x-range", type: "number", validate: validateRange}, true) function validateRange(schema, data, parentSchema) { @@ -593,7 +593,7 @@ describe("Custom keywords", () => { }) }) - describe("$data reference support with custom keywords (with $data option)", () => { + describe('$data reference support with "validate" keywords (with $data option)', () => { beforeEach(() => { instances = getAjvInstances( { @@ -881,7 +881,7 @@ describe("Custom keywords", () => { }) } - function testRangeKeyword(definition, customErrors, numErrors) { + function testRangeKeyword(definition, createsErrors, numErrors) { instances.forEach((_ajv) => { _ajv.addKeyword(definition) _ajv.addKeyword({keyword: "exclusiveRange", schemaType: "boolean"}) @@ -895,11 +895,11 @@ describe("Custom keywords", () => { shouldBeValid(validate, "abc") shouldBeInvalid(validate, 1.99, numErrors) - if (customErrors) { + if (createsErrors) { shouldBeRangeError(validate.errors[0], "", "#/x-range", ">=", 2) } shouldBeInvalid(validate, 4.01, numErrors) - if (customErrors) { + if (createsErrors) { shouldBeRangeError(validate.errors[0], "", "#/x-range", "<=", 4) } @@ -918,11 +918,11 @@ describe("Custom keywords", () => { shouldBeValid(validate, {foo: 3.99}) shouldBeInvalid(validate, {foo: 2}, numErrors) - if (customErrors) { + if (createsErrors) { shouldBeRangeError(validate.errors[0], ".foo", "#/properties/foo/x-range", ">", 2, true) } shouldBeInvalid(validate, {foo: 4}, numErrors) - if (customErrors) { + if (createsErrors) { shouldBeRangeError(validate.errors[0], ".foo", "#/properties/foo/x-range", "<", 4, true) } }) @@ -1010,7 +1010,7 @@ describe("Custom keywords", () => { it("should throw if defined keyword is passed", () => { testThrow(["minimum", "maximum", "multipleOf", "minLength", "maxLength"]) - testThrowDuplicate("custom") + testThrowDuplicate("user-defined") function testThrow(keywords) { TEST_TYPES.forEach((dataType, index) => { @@ -1063,15 +1063,15 @@ describe("Custom keywords", () => { it("should throw if unknown type is passed", () => { should.throw(() => { - _addKeyword("custom1", "wrongtype") + _addKeyword("user-defined1", "wrongtype") }) should.throw(() => { - _addKeyword("custom2", ["number", "wrongtype"]) + _addKeyword("user-defined2", ["number", "wrongtype"]) }) should.throw(() => { - _addKeyword("custom3", ["number", undefined]) + _addKeyword("user-defined3", ["number", undefined]) }) }) @@ -1106,7 +1106,7 @@ describe("Custom keywords", () => { }) describe("removeKeyword", () => { - it("should remove and allow redefining custom keyword", () => { + it("should remove and allow redefining keyword", () => { ajv.addKeyword({ keyword: "positive", type: "number", @@ -1184,7 +1184,7 @@ describe("Custom keywords", () => { }) }) - describe("custom keywords mutating data", () => { + describe("user-defined keywords mutating data", () => { it("should NOT update data without option modifying", () => { should.throw(() => { testModifying(false) @@ -1238,7 +1238,7 @@ describe("Custom keywords", () => { } }) - describe("custom keywords with predefined validation result", () => { + describe('"validate" keywords with predefined validation result', () => { it("should ignore result from validation function", () => { ajv.addKeyword({ keyword: "pass", diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js index 01d433c7b4..5268492d07 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js @@ -15,7 +15,7 @@ describe("issue #181, custom keyword is not validated in allErrors mode if there }) }) - it("should validate custom keyword that creates errors", () => { + it("should validate keyword that creates errors", () => { testCustomKeywordErrors({ keyword: "alwaysFails", type: "object", diff --git a/spec/issues/768_passContext_recursive_ref.spec.js b/spec/issues/768_passContext_recursive_ref.spec.js index 0112b6927f..06907219e8 100644 --- a/spec/issues/768_passContext_recursive_ref.spec.js +++ b/spec/issues/768_passContext_recursive_ref.spec.js @@ -11,7 +11,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { }) describe("passContext = true", () => { - it("should pass this value as context to custom keyword validation function", () => { + it("should pass this value as context to user-defined keyword validation function", () => { var validate = getValidate(true) var self = {} validate.call(self, {bar: "a", baz: {bar: "b"}}) @@ -21,7 +21,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { }) describe("passContext = false", () => { - it("should pass ajv instance as context to custom keyword validation function", () => { + it("should pass ajv instance as context to user-defined keyword validation function", () => { var validate = getValidate(false) validate({bar: "a", baz: {bar: "b"}}) contexts.should.have.length(2) @@ -30,7 +30,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { }) describe("ref is fragment and passContext = true", () => { - it("should pass this value as context to custom keyword validation function", () => { + it("should pass this value as context to user-defined keyword validation function", () => { var validate = getValidateFragments(true) var self = {} validate.call(self, {baz: {corge: "a", quux: {baz: {corge: "b"}}}}) @@ -40,7 +40,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { }) describe("ref is fragment and passContext = false", () => { - it("should pass ajv instance as context to custom keyword validation function", () => { + it("should pass ajv instance as context to user-defined keyword validation function", () => { var validate = getValidateFragments(false) validate({baz: {corge: "a", quux: {baz: {corge: "b"}}}}) contexts.should.have.length(2) diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js index 4b7c9701fd..669eb9712a 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -3,8 +3,8 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #955: option removeAdditional breaks custom keywords", () => { - it("should support custom keywords with option removeAdditional", () => { +describe("issue #955: option removeAdditional breaks user-defined keywords", () => { + it("should support user-defined keywords with option removeAdditional", () => { var ajv = new Ajv({removeAdditional: "all"}) ajv.addKeyword({ diff --git a/spec/options/options_add_schemas.spec.js b/spec/options/options_add_schemas.spec.js index effe461ce1..cf3b7d13e7 100644 --- a/spec/options/options_add_schemas.spec.js +++ b/spec/options/options_add_schemas.spec.js @@ -115,7 +115,7 @@ describe("options to add schemas", () => { describe("serialize", () => { var serializeCalled - it("should use custom function to serialize schema to string", () => { + it("should use user-defined function to serialize schema to string", () => { serializeCalled = undefined var ajv = new Ajv({serialize: serialize}) ajv.addSchema({type: "string"}) diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js index a5597636be..838f49e738 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.js @@ -55,7 +55,7 @@ describe("code generation options", () => { }) describe("= true", () => { - it("should pass this value as context to custom keyword validation function", () => { + it("should pass this value as context to user-defined keyword validation function", () => { var validate = getValidate(true) var self = {} validate.call(self, {}) @@ -65,7 +65,7 @@ describe("code generation options", () => { }) describe("= false", () => { - it("should pass ajv instance as context to custom keyword validation function", () => { + it("should pass ajv instance as context to user-defined keyword validation function", () => { var validate = getValidate(false) var self = {} validate.call(self, {}) diff --git a/spec/options/options_reporting.spec.js b/spec/options/options_reporting.spec.js index 450005e24a..94c2b039b4 100644 --- a/spec/options/options_reporting.spec.js +++ b/spec/options/options_reporting.spec.js @@ -86,7 +86,7 @@ describe("reporting options", () => { console.warn = origConsoleWarn }) - it("no custom logger is given - global console should be used", () => { + it("no user-defined logger is given - global console should be used", () => { var ajv = new Ajv({ meta: false, }) @@ -99,7 +99,7 @@ describe("reporting options", () => { should.equal(consoleCalled, true) }) - it("custom logger is an object - logs should only report to it", () => { + it("user-defined logger is an object - logs should only report to it", () => { var loggerCalled = false var logger = { @@ -146,10 +146,7 @@ describe("reporting options", () => { meta: false, logger: {}, }) - }).should.throw( - Error, - /logger must implement log, warn and error methods/ - ) + }).should.throw(Error, /logger must implement log, warn and error methods/) }) }) }) diff --git a/spec/tests/issues/27_1_recursive_raml_schema.json b/spec/tests/issues/27_1_recursive_raml_schema.json index 3869ddfdb0..0b2b1445bf 100644 --- a/spec/tests/issues/27_1_recursive_raml_schema.json +++ b/spec/tests/issues/27_1_recursive_raml_schema.json @@ -80,14 +80,7 @@ }, "type": { "type": "string", - "enum": [ - "string", - "number", - "integer", - "date", - "boolean", - "file" - ], + "enum": ["string", "number", "integer", "date", "boolean", "file"], "default": "string", "description": "The type attribute specifies the primitive type of the parameter's resolved value. If the type is not specified, it defaults to string. API clients MUST return/throw an error if the parameter's resolved value does not match the specified type." }, @@ -189,11 +182,7 @@ }, "settings": { "type": "object", - "required": [ - "requestTokenUri", - "authorizationUri", - "tokenCredentialsUri" - ], + "required": ["requestTokenUri", "authorizationUri", "tokenCredentialsUri"], "properties": { "requestTokenUri": { "type": "string", @@ -220,11 +209,7 @@ }, "settings": { "type": "object", - "required": [ - "authorizationUri", - "accessTokenUri", - "authorizationGrants" - ], + "required": ["authorizationUri", "accessTokenUri", "authorizationGrants"], "properties": { "authorizationUri": { "type": "string", From 0068899aa7b56984542c4b45659189f53e207bdf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 1 Sep 2020 11:29:29 +0100 Subject: [PATCH 137/322] rename test --- .../181_allErrors_custom_keyword_skipped.spec.js | 10 +++++----- spec/{custom.spec.js => keyword.spec.js} | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename spec/{custom.spec.js => keyword.spec.js} (100%) diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js index 5268492d07..438e8111c5 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js @@ -3,9 +3,9 @@ var Ajv = require("../ajv") require("../chai").should() -describe("issue #181, custom keyword is not validated in allErrors mode if there were previous error", () => { - it("should validate custom keyword that doesn't create errors", () => { - testCustomKeywordErrors({ +describe("issue #181, user-defined keyword is not validated in allErrors mode if there were previous error", () => { + it("should validate user-defined keyword that doesn't create errors", () => { + testKeywordErrors({ keyword: "alwaysFails", type: "object", errors: true, @@ -16,7 +16,7 @@ describe("issue #181, custom keyword is not validated in allErrors mode if there }) it("should validate keyword that creates errors", () => { - testCustomKeywordErrors({ + testKeywordErrors({ keyword: "alwaysFails", type: "object", errors: true, @@ -35,7 +35,7 @@ describe("issue #181, custom keyword is not validated in allErrors mode if there }) }) - function testCustomKeywordErrors(def) { + function testKeywordErrors(def) { var ajv = new Ajv({allErrors: true}) ajv.addKeyword(def) diff --git a/spec/custom.spec.js b/spec/keyword.spec.js similarity index 100% rename from spec/custom.spec.js rename to spec/keyword.spec.js From 1648b961912d6d4887f7f40d484f99f249f53379 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 07:59:50 +0100 Subject: [PATCH 138/322] remove option uniqueItems, keywords is always validated if present --- README.md | 2 -- lib/types.ts | 1 - lib/vocabularies/validation/uniqueItems.ts | 2 +- spec/options/options_validation.spec.js | 13 ------------- 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/README.md b/README.md index 7ec4a631eb..496828f126 100644 --- a/README.md +++ b/README.md @@ -1098,7 +1098,6 @@ Defaults: verbose: false, $comment: false, // NEW in Ajv version 6.0 jsonPointers: false, - uniqueItems: true, unicode: true, nullable: false, format: true, @@ -1148,7 +1147,6 @@ Defaults: - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function - _jsonPointers_: set `dataPath` property of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation. -- _uniqueItems_: validate `uniqueItems` keyword (true by default). - _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters. - _nullable_: support keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - _format_: formats validation mode. Option values: diff --git a/lib/types.ts b/lib/types.ts index 61537f8df8..a60f171c72 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -9,7 +9,6 @@ export interface Options { allErrors?: boolean verbose?: boolean jsonPointers?: boolean - uniqueItems?: boolean unicode?: boolean format?: false | string formats?: object diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 41fad07044..c1c49a50a4 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -11,7 +11,7 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordContext) { const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt - if (it.opts.uniqueItems === false || !($data || schema)) return + if (!$data && !schema) return const valid = gen.let("valid") const itemTypes = parentSchema.items ? getSchemaTypes(it, parentSchema.items) : [] cxt.block$data(valid, validateUniqueItems, _`${schemaCode} === false`) diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index e176244879..565ae1fdf4 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -58,19 +58,6 @@ describe("validation options", () => { }) }) - describe("uniqueItems", () => { - it("should not validate uniqueItems with uniqueItems option == false", () => { - testUniqueItems(new Ajv({uniqueItems: false})) - testUniqueItems(new Ajv({uniqueItems: false, allErrors: true})) - - function testUniqueItems(ajv) { - var validate = ajv.compile({uniqueItems: true}) - validate([1, 2, 3]).should.equal(true) - validate([1, 1, 1]).should.equal(true) - } - }) - }) - describe("unicode", () => { it("should use String.prototype.length with unicode option == false", () => { var ajvUnicode = new Ajv() From 168013d0d6d03d19f1898940de800292dbf027ae Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 08:12:34 +0100 Subject: [PATCH 139/322] deprecate option unicode: false --- README.md | 4 +--- lib/ajv.ts | 15 +++++++++++---- lib/types.ts | 2 +- spec/keyword.spec.js | 3 ++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 496828f126..d40d6c9dcf 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Performance of different validators by [json-schema-benchmark](https://github.co - all validation keywords (see [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md)) - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) - support of circular references between schemas - - correct string lengths for strings with unicode pairs (can be turned off) + - correct string lengths for strings with unicode pairs - [formats](#formats) defined by JSON Schema draft-07 standard (with [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin) and additional formats (can be turned off) - [validates schemas against meta-schema](#api-validateschema) - supports [browsers](#using-in-browser) and Node.js 0.10-14.x @@ -1098,7 +1098,6 @@ Defaults: verbose: false, $comment: false, // NEW in Ajv version 6.0 jsonPointers: false, - unicode: true, nullable: false, format: true, formats: {}, @@ -1147,7 +1146,6 @@ Defaults: - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function - _jsonPointers_: set `dataPath` property of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation. -- _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters. - _nullable_: support keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - _format_: formats validation mode. Option values: - `true` (default) - validate added formats (see [Formats](#formats)). diff --git a/lib/ajv.ts b/lib/ajv.ts index 412953bc21..cafe5f5299 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -66,10 +66,7 @@ export default function Ajv(opts: Options): void { this._loadingSchemas = {} this._compilations = [] this.RULES = rules() - if (opts.schemaId !== undefined && opts.schemaId !== "$id") { - throw new Error("option schemaId is not supported in v7") - } - + checkDeprecatedOptions.call(this, opts) opts.loopRequired = opts.loopRequired || Infinity opts.loopEnum = opts.loopEnum || Infinity if (opts.serialize === undefined) opts.serialize = stableStringify @@ -88,6 +85,16 @@ export default function Ajv(opts: Options): void { opts.format = formatOpt } +function checkDeprecatedOptions(this, opts: Options) { + if (opts.unicode === false) { + this.logger.warn("DEPRECATED: option unicode will be removed in the next version") + } + if (opts.schemaId !== undefined) { + if (opts.schemaId === "$id") this.logger.warn("DEPRECATED: option schemaId") + else throw new Error("Not supported in v7: option schemaId") + } +} + /** * Validate data using schema * Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize. diff --git a/lib/types.ts b/lib/types.ts index a60f171c72..751fa2ef7d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -9,7 +9,7 @@ export interface Options { allErrors?: boolean verbose?: boolean jsonPointers?: boolean - unicode?: boolean + unicode?: boolean // deprecated format?: false | string formats?: object keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated diff --git a/spec/keyword.spec.js b/spec/keyword.spec.js index 6b7584a0c9..4df7f1c55e 100644 --- a/spec/keyword.spec.js +++ b/spec/keyword.spec.js @@ -766,7 +766,8 @@ describe("User-defined keywords", () => { it('should fail if "macro" keyword definition has "$data" but no "code" or "validate"', () => { should.throw(() => { - ajv.addKeyword("even", { + ajv.addKeyword({ + keyword: "even", type: "number", $data: true, macro: () => { From f63c4e853ce2ae48154471c9c23d56c1e3c19dfd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 08:36:54 +0100 Subject: [PATCH 140/322] split removed/deprecated options to separate interface --- lib/ajv.ts | 13 ++++--------- lib/types.ts | 13 +++++++++---- spec/options/schemaId.spec.js | 11 ----------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index cafe5f5299..c8a4ff5a32 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -67,8 +67,6 @@ export default function Ajv(opts: Options): void { this._compilations = [] this.RULES = rules() checkDeprecatedOptions.call(this, opts) - opts.loopRequired = opts.loopRequired || Infinity - opts.loopEnum = opts.loopEnum || Infinity if (opts.serialize === undefined) opts.serialize = stableStringify this._metaOpts = getMetaSchemaOptions(this) @@ -86,13 +84,10 @@ export default function Ajv(opts: Options): void { } function checkDeprecatedOptions(this, opts: Options) { - if (opts.unicode === false) { - this.logger.warn("DEPRECATED: option unicode will be removed in the next version") - } - if (opts.schemaId !== undefined) { - if (opts.schemaId === "$id") this.logger.warn("DEPRECATED: option schemaId") - else throw new Error("Not supported in v7: option schemaId") - } + if (opts.errorDataPath) this.logger.error("NOT SUPPORTED: option errorDataPath") + if (opts.schemaId) this.logger.error("NOT SUPPORTED: option schemaId") + if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") + if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") } /** diff --git a/lib/types.ts b/lib/types.ts index 751fa2ef7d..2461755426 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -4,13 +4,12 @@ import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" import KeywordContext from "./compile/context" -export interface Options { +export interface CurrentOptions { $data?: boolean allErrors?: boolean verbose?: boolean jsonPointers?: boolean - unicode?: boolean // deprecated - format?: false | string + format?: false formats?: object keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated unknownFormats?: true | string[] | "ignore" @@ -47,7 +46,13 @@ export interface Options { nullable?: boolean serialize?: false | ((schema: object | boolean) => any) $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) - schemaId?: string // not supported +} + +export interface Options extends CurrentOptions { + errorDataPath?: "object" | "property" // removed + schemaId?: string // removed + unicode?: boolean // deprecated + uniqueItems?: boolean // removed } interface Logger { diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index 367dc91dae..696f087fdb 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -4,17 +4,6 @@ var Ajv = require("../ajv") var should = require("../chai").should() describe("removed schemaId option", () => { - it('should throw error if schemaId option is used and it is not equal to "$id"', () => { - new Ajv() - new Ajv({schemaId: "$id"}) - should.throw(() => { - new Ajv({schemaId: "id"}) - }) - should.throw(() => { - new Ajv({schemaId: "auto"}) - }) - }) - it("should use $id and ignore id", () => { test(new Ajv()) test(new Ajv({schemaId: "$id"})) From aee2c7241827a6aa26bae382dcbc612f780874b1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 08:54:32 +0100 Subject: [PATCH 141/322] deprecate option jsonPointers and change default to true --- README.md | 4 +--- lib/compile/subschema.ts | 16 ++++++++-------- lib/types.ts | 2 +- spec/errors.spec.js | 36 ++++++++++++++++++++---------------- spec/keyword.spec.js | 4 ++-- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index d40d6c9dcf..5c7b48110c 100644 --- a/README.md +++ b/README.md @@ -1097,7 +1097,6 @@ Defaults: allErrors: false, verbose: false, $comment: false, // NEW in Ajv version 6.0 - jsonPointers: false, nullable: false, format: true, formats: {}, @@ -1145,7 +1144,6 @@ Defaults: - `false` (default): ignore \$comment keyword. - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function -- _jsonPointers_: set `dataPath` property of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation. - _nullable_: support keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - _format_: formats validation mode. Option values: - `true` (default) - validate added formats (see [Formats](#formats)). @@ -1244,7 +1242,7 @@ In case of validation failure, Ajv assigns the array of errors to `errors` prope Each error is an object with the following properties: - _keyword_: validation keyword. -- _dataPath_: the path to the part of the data that was validated. By default `dataPath` uses JavaScript property access notation (e.g., `".prop[1].subProp"`). When the option `jsonPointers` is true (see [Options](#options)) `dataPath` will be set using JSON pointer standard (e.g., `"/prop/1/subProp"`). +- _dataPath_: JSON pointer to the part of the data that was validated (e.g., `"/prop/1/subProp"`). - _schemaPath_: the path (JSON-pointer as a URI fragment) to the schema of the keyword that failed validation. - _params_: the object with the additional information about error that can be used to generate error messages (e.g., using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package). See below for parameters set by all keywords. - _message_: the standard error message (can be excluded with option `messages` set to false). diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 98128af637..55dbdf0401 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -147,13 +147,13 @@ function getErrorPath( // let path if (dataProp instanceof Name) { const isNumber = dataPropType === Type.Num - return jsonPointers - ? _`"/" + ${dataProp}${isNumber ? nil : _`.replace(/~/g, "~0").replace(/\\//g, "~1")`}` // TODO maybe use global escapePointer - : isNumber - ? _`"[" + ${dataProp} + "]"` - : _`"['" + ${dataProp} + "']"` + return jsonPointers === false + ? isNumber + ? _`"[" + ${dataProp} + "]"` + : _`"['" + ${dataProp} + "']"` + : _`"/" + ${dataProp}${isNumber ? nil : _`.replace(/~/g, "~0").replace(/\\//g, "~1")`}` // TODO maybe use global escapePointer } - return jsonPointers - ? "/" + (typeof dataProp == "number" ? dataProp : escapeJsonPointer(dataProp)) - : getProperty(dataProp).toString() + return jsonPointers === false + ? getProperty(dataProp).toString() + : "/" + (typeof dataProp == "number" ? dataProp : escapeJsonPointer(dataProp)) } diff --git a/lib/types.ts b/lib/types.ts index 2461755426..c79232a416 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -8,7 +8,6 @@ export interface CurrentOptions { $data?: boolean allErrors?: boolean verbose?: boolean - jsonPointers?: boolean format?: false formats?: object keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated @@ -50,6 +49,7 @@ export interface CurrentOptions { export interface Options extends CurrentOptions { errorDataPath?: "object" | "property" // removed + jsonPointers?: boolean schemaId?: string // removed unicode?: boolean // deprecated uniqueItems?: boolean // removed diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 7492c60795..06af561cae 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -13,13 +13,12 @@ describe("Validation errors", () => { function createInstances() { ajv = new Ajv({loopRequired: 21}) ajvJP = new Ajv({ - jsonPointers: true, loopRequired: 21, }) fullAjv = new Ajv({ allErrors: true, verbose: true, - jsonPointers: true, + jsonPointers: false, // deprecated loopRequired: 21, }) } @@ -134,7 +133,7 @@ describe("Validation errors", () => { var validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError(validate.errors[0], "type", schPath, "['baz'].quux", "should be string", { + shouldBeError(validate.errors[0], "type", schPath, "/baz/quux", "should be string", { type: "string", }) @@ -148,10 +147,10 @@ describe("Validation errors", () => { var fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData, 2) - shouldBeError(fullValidate.errors[0], "type", schPath, "/baz/quux", "should be string", { + shouldBeError(fullValidate.errors[0], "type", schPath, "['baz'].quux", "should be string", { type: "string", }) - shouldBeError(fullValidate.errors[1], "type", schPath, "/boo/quux", "should be string", { + shouldBeError(fullValidate.errors[1], "type", schPath, "['boo'].quux", "should be string", { type: "string", }) } @@ -476,9 +475,9 @@ describe("Validation errors", () => { var validate = ajv.compile(schema1) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "[0]", "should be >= 10") + shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") shouldBeInvalid(validate, invalidData2) - shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "[1]", "should be >= 10") + shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") var validateJP = ajvJP.compile(schema1) shouldBeValid(validateJP, data) @@ -490,10 +489,10 @@ describe("Validation errors", () => { var fullValidate = fullAjv.compile(schema1) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1) - shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") + shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "[0]", "should be >= 10") shouldBeInvalid(fullValidate, invalidData2, 2) - shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") - shouldBeError(fullValidate.errors[1], "minimum", "#/items/minimum", "/3", "should be >= 10") + shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "[1]", "should be >= 10") + shouldBeError(fullValidate.errors[1], "minimum", "#/items/minimum", "[3]", "should be >= 10") var schema2 = { $id: "schema2", @@ -504,9 +503,9 @@ describe("Validation errors", () => { validate = ajv.compile(schema2) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError(validate.errors[0], "minimum", "#/items/0/minimum", "[0]", "should be >= 10") + shouldBeError(validate.errors[0], "minimum", "#/items/0/minimum", "/0", "should be >= 10") shouldBeInvalid(validate, invalidData2) - shouldBeError(validate.errors[0], "minimum", "#/items/2/minimum", "[2]", "should be >= 12") + shouldBeError(validate.errors[0], "minimum", "#/items/2/minimum", "/2", "should be >= 12") }) it("should have correct schema path for additionalItems", () => { @@ -810,7 +809,7 @@ describe("Validation errors", () => { err, "exclusiveMaximum", "#/properties/smaller/exclusiveMaximum", - _ajv._opts.jsonPointers ? "/smaller" : ".smaller", + _ajv._opts.jsonPointers === false ? ".smaller" : "/smaller", "should be < 4", {comparison: "<", limit: 4} ) @@ -924,8 +923,8 @@ describe("Validation errors", () => { var expectedErrors = _ajv._opts.allErrors ? 2 : 1 shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors) - testTypeError(0, "/1") - if (expectedErrors === 2) testTypeError(1, "/2") + testTypeError(0, _ajv._opts.jsonPointers === false ? "[1]" : "/1") + if (expectedErrors === 2) testTypeError(1, _ajv._opts.jsonPointers === false ? "[2]" : "/2") function testTypeError(i, dataPath) { var err = validate.errors[i] @@ -962,7 +961,12 @@ describe("Validation errors", () => { var validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError(validate.errors[0], "type", schPath, _ajv._opts.jsonPointers ? "/foo" : ".foo") + shouldBeError( + validate.errors[0], + "type", + schPath, + _ajv._opts.jsonPointers === false ? ".foo" : "/foo" + ) } function shouldBeValid(validate, data) { diff --git a/spec/keyword.spec.js b/spec/keyword.spec.js index 4df7f1c55e..c15fd081da 100644 --- a/spec/keyword.spec.js +++ b/spec/keyword.spec.js @@ -920,11 +920,11 @@ describe("User-defined keywords", () => { shouldBeInvalid(validate, {foo: 2}, numErrors) if (createsErrors) { - shouldBeRangeError(validate.errors[0], ".foo", "#/properties/foo/x-range", ">", 2, true) + shouldBeRangeError(validate.errors[0], "/foo", "#/properties/foo/x-range", ">", 2, true) } shouldBeInvalid(validate, {foo: 4}, numErrors) if (createsErrors) { - shouldBeRangeError(validate.errors[0], ".foo", "#/properties/foo/x-range", "<", 4, true) + shouldBeRangeError(validate.errors[0], "/foo", "#/properties/foo/x-range", "<", 4, true) } }) } From 231d1e5159a2ca9a99129f1e08e8f448416f375d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 10:21:15 +0100 Subject: [PATCH 142/322] replace options jsonPointers: false with jsPropertySyntax: true; suppress logs during tests --- README.md | 4 +- lib/ajv.ts | 13 ++--- lib/compile/subschema.ts | 14 +++--- lib/types.ts | 2 +- spec/ajv_instances.js | 4 +- spec/ajv_options.js | 1 - spec/errors.spec.js | 16 ++++--- .../533_missing_ref_error_when_ignore.spec.js | 2 +- spec/options/meta_validateSchema.spec.js | 47 +++++++++++-------- spec/options/options_refs.spec.js | 18 +++---- spec/options/options_reporting.spec.js | 8 +--- spec/options/options_validation.spec.js | 6 +-- spec/options/schemaId.spec.js | 4 +- spec/options/unknownFormats.spec.js | 2 +- spec/schema-tests.spec.js | 5 +- 15 files changed, 77 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 5c7b48110c..b271d0bceb 100644 --- a/README.md +++ b/README.md @@ -1096,7 +1096,7 @@ Defaults: $data: false, allErrors: false, verbose: false, - $comment: false, // NEW in Ajv version 6.0 + $comment: false, nullable: false, format: true, formats: {}, @@ -1132,6 +1132,7 @@ Defaults: processCode: undefined, // function (str: string, schema: object): string {} cache: new Cache, serialize: undefined + jsPropertySyntax: false, // deprecated } ``` @@ -1232,6 +1233,7 @@ Defaults: - `transpile` that transpiled asynchronous validation function. You can still use `transpile` option with [ajv-async](https://github.com/ajv-validator/ajv-async) package. See [Asynchronous validation](#asynchronous-validation) for more information. - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)`, `del(key)` and `clear()`. - _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used. +- _jsPropertySyntax_ (deprecated) - set to `true` to report `dataPath` in errors as in v6, using JavaScript property syntax (e.g., `".prop[1].subProp"`). By default `dataPath` in errors is reported as JSON pointer. This option is added for backward compatibility and is not recommended - this format is difficult to parse even in JS code. ## Validation errors diff --git a/lib/ajv.ts b/lib/ajv.ts index c8a4ff5a32..3d0edfc2a7 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -84,10 +84,11 @@ export default function Ajv(opts: Options): void { } function checkDeprecatedOptions(this, opts: Options) { - if (opts.errorDataPath) this.logger.error("NOT SUPPORTED: option errorDataPath") - if (opts.schemaId) this.logger.error("NOT SUPPORTED: option schemaId") - if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") + if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath") + if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId") if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") + if (opts.jsPropertySyntax !== undefined) this.logger.warn("DEPRECATED: option jsPropertySyntax") + if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") } /** @@ -474,10 +475,12 @@ function getMetaSchemaOptions(self) { return metaOpts } +const noLogs = {log() {}, warn() {}, error() {}} + function setLogger(self) { var logger = self._opts.logger if (logger === false) { - self.logger = {log: noop, warn: noop, error: noop} + self.logger = noLogs } else { if (logger === undefined) logger = console if (!(typeof logger == "object" && logger.log && logger.warn && logger.error)) { @@ -486,5 +489,3 @@ function setLogger(self) { self.logger = logger } } - -function noop() {} diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 55dbdf0401..6a3dee455d 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,7 +1,7 @@ import {CompilationContext} from "../types" import {subschemaCode} from "./validate" import {escapeFragment, escapeJsonPointer} from "./util" -import {_, str, nil, Code, Name, getProperty} from "./codegen" +import {_, str, Code, Name, getProperty} from "./codegen" export interface SubschemaContext { // TODO use Optional? @@ -110,7 +110,7 @@ function extendSubschemaData( const {errorPath, dataPathArr, opts} = it const nextData = gen.var("data", _`${it.data}${getProperty(dataProp)}`) // TODO var dataContextProps(nextData) - subschema.errorPath = str`${errorPath}${getErrorPath(dataProp, dpType, opts.jsonPointers)}` + subschema.errorPath = str`${errorPath}${getErrorPath(dataProp, dpType, opts.jsPropertySyntax)}` subschema.parentDataProperty = _`${dataProp}` subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty] } @@ -142,18 +142,20 @@ function extendSubschemaMode( function getErrorPath( dataProp: Name | string | number, dataPropType?: Type, - jsonPointers?: boolean + jsPropertySyntax?: boolean ): Code | string { // let path if (dataProp instanceof Name) { const isNumber = dataPropType === Type.Num - return jsonPointers === false + return jsPropertySyntax ? isNumber ? _`"[" + ${dataProp} + "]"` : _`"['" + ${dataProp} + "']"` - : _`"/" + ${dataProp}${isNumber ? nil : _`.replace(/~/g, "~0").replace(/\\//g, "~1")`}` // TODO maybe use global escapePointer + : isNumber + ? _`"/" + ${dataProp}` + : _`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")` // TODO maybe use global escapePointer } - return jsonPointers === false + return jsPropertySyntax ? getProperty(dataProp).toString() : "/" + (typeof dataProp == "number" ? dataProp : escapeJsonPointer(dataProp)) } diff --git a/lib/types.ts b/lib/types.ts index c79232a416..683426f826 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -49,7 +49,7 @@ export interface CurrentOptions { export interface Options extends CurrentOptions { errorDataPath?: "object" | "property" // removed - jsonPointers?: boolean + jsPropertySyntax?: boolean schemaId?: string // removed unicode?: boolean // deprecated uniqueItems?: boolean // removed diff --git a/spec/ajv_instances.js b/spec/ajv_instances.js index a2a07bb43e..68aedfcfcd 100644 --- a/spec/ajv_instances.js +++ b/spec/ajv_instances.js @@ -4,8 +4,8 @@ var Ajv = require("./ajv") module.exports = getAjvInstances -function getAjvInstances(options, extraOpts) { - return _getAjvInstances(options, extraOpts || {}) +function getAjvInstances(options, extraOpts = {}) { + return _getAjvInstances(options, {...extraOpts, logger: false}) } function _getAjvInstances(opts, useOpts) { diff --git a/spec/ajv_options.js b/spec/ajv_options.js index ed8bba7f37..21cbf63e35 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -9,7 +9,6 @@ var options = fullTest verbose: true, extendRefs: "ignore", inlineRefs: false, - jsonPointers: true, codegen: {es5: true, lines: true}, } : {allErrors: true, codegen: {es5: true, lines: true}} diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 06af561cae..049e6acae4 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -18,8 +18,9 @@ describe("Validation errors", () => { fullAjv = new Ajv({ allErrors: true, verbose: true, - jsonPointers: false, // deprecated + jsPropertySyntax: true, // deprecated loopRequired: 21, + logger: false, }) } @@ -790,7 +791,8 @@ describe("Validation errors", () => { $data: true, allErrors: true, verbose: true, - jsonPointers: true, + jsPropertySyntax: true, // deprecated + logger: false, }) ;[ajv, fullAjv].forEach((_ajv) => { var validate = _ajv.compile(schema) @@ -809,7 +811,7 @@ describe("Validation errors", () => { err, "exclusiveMaximum", "#/properties/smaller/exclusiveMaximum", - _ajv._opts.jsonPointers === false ? ".smaller" : "/smaller", + _ajv._opts.jsPropertySyntax ? ".smaller" : "/smaller", "should be < 4", {comparison: "<", limit: 4} ) @@ -923,8 +925,8 @@ describe("Validation errors", () => { var expectedErrors = _ajv._opts.allErrors ? 2 : 1 shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors) - testTypeError(0, _ajv._opts.jsonPointers === false ? "[1]" : "/1") - if (expectedErrors === 2) testTypeError(1, _ajv._opts.jsonPointers === false ? "[2]" : "/2") + testTypeError(0, _ajv._opts.jsPropertySyntax ? "[1]" : "/1") + if (expectedErrors === 2) testTypeError(1, _ajv._opts.jsPropertySyntax ? "[2]" : "/2") function testTypeError(i, dataPath) { var err = validate.errors[i] @@ -936,7 +938,7 @@ describe("Validation errors", () => { describe("$ref errors", () => { it("should have correct message and params", () => { - const _ajv = new Ajv({missingRefs: "fail"}) + const _ajv = new Ajv({missingRefs: "fail", logger: false}) const schema = {$ref: "#/unknown"} const validate = _ajv.compile(schema) shouldBeInvalid(validate, {}) @@ -965,7 +967,7 @@ describe("Validation errors", () => { validate.errors[0], "type", schPath, - _ajv._opts.jsonPointers === false ? ".foo" : "/foo" + _ajv._opts.jsPropertySyntax ? ".foo" : "/foo" ) } diff --git a/spec/issues/533_missing_ref_error_when_ignore.spec.js b/spec/issues/533_missing_ref_error_when_ignore.spec.js index b28277c1e0..6d2ecd1754 100644 --- a/spec/issues/533_missing_ref_error_when_ignore.spec.js +++ b/spec/issues/533_missing_ref_error_when_ignore.spec.js @@ -13,7 +13,7 @@ describe('issue #533, throwing missing ref exception with option missingRefs: "i } it("should pass validation without throwing exception", () => { - var ajv = new Ajv({missingRefs: "ignore"}) + var ajv = new Ajv({missingRefs: "ignore", logger: false}) var validate = ajv.compile(schema) validate({foo: "anything"}).should.equal(true) validate({foo: "anything", bar: "whatever"}).should.equal(true) diff --git a/spec/options/meta_validateSchema.spec.js b/spec/options/meta_validateSchema.spec.js index 5aaf4f06e3..c1c6ec070c 100644 --- a/spec/options/meta_validateSchema.spec.js +++ b/spec/options/meta_validateSchema.spec.js @@ -9,9 +9,7 @@ describe("meta and validateSchema options", () => { testOptionMeta(new Ajv({meta: true})) function testOptionMeta(ajv) { - ajv - .getSchema("http://json-schema.org/draft-07/schema") - .should.be.a("function") + ajv.getSchema("http://json-schema.org/draft-07/schema").should.be.a("function") ajv.validateSchema({type: "integer"}).should.equal(true) ajv.validateSchema({type: 123}).should.equal(false) should.not.throw(() => { @@ -24,7 +22,7 @@ describe("meta and validateSchema options", () => { }) it("should throw if meta: false and validateSchema: true", () => { - var ajv = new Ajv({meta: false}) + var ajv = new Ajv({meta: false, logger: false}) should.not.exist(ajv.getSchema("http://json-schema.org/draft-07/schema")) should.not.throw(() => { ajv.addSchema({type: "wrong_type"}, "integer") @@ -48,27 +46,36 @@ describe("meta and validateSchema options", () => { }) }) - it('should not throw on invalid schema with validateSchema: "log"', () => { - var logError = console.error - var loggedError = false - console.error = function () { - loggedError = true - logError.apply(console, arguments) + describe('validateSchema: "log"', () => { + let loggedError, loggedWarning + const logger = { + log() {}, + warn: () => (loggedWarning = true), + error: () => (loggedError = true), } - var ajv = new Ajv({validateSchema: "log"}) - should.not.throw(() => { - ajv.addSchema({type: 123}, "integer") + beforeEach(() => { + loggedError = false + loggedWarning = false }) - loggedError.should.equal(true) - loggedError = false - ajv = new Ajv({validateSchema: "log", meta: false}) - should.not.throw(() => { - ajv.addSchema({type: 123}, "integer") + it("should not throw on invalid schema", () => { + const ajv = new Ajv({validateSchema: "log", logger}) + should.not.throw(() => { + ajv.addSchema({type: 123}, "integer") + }) + loggedError.should.equal(true) + loggedWarning.should.equal(false) + }) + + it("should not throw on invalid schema with meta: false", () => { + const ajv = new Ajv({validateSchema: "log", meta: false, logger}) + should.not.throw(() => { + ajv.addSchema({type: 123}, "integer") + }) + loggedError.should.equal(false) + loggedWarning.should.equal(true) }) - loggedError.should.equal(false) - console.error = logError }) it("should validate v6 schema", () => { diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js index 48f7a16b46..4a68739106 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.js @@ -17,8 +17,8 @@ describe("referenced schema options", () => { describe('= "ignore" and default', () => { it("should ignore other keywords when $ref is used", () => { - test(new Ajv()) - test(new Ajv({extendRefs: "ignore"}), false) + test(new Ajv({logger: false})) + test(new Ajv({extendRefs: "ignore", logger: false}), false) }) it("should log warning when other keywords are used with $ref", () => { @@ -126,8 +126,8 @@ describe("referenced schema options", () => { }) it('should not throw and pass validation with missingRef == "ignore"', () => { - testMissingRefsIgnore(new Ajv({missingRefs: "ignore"})) - testMissingRefsIgnore(new Ajv({missingRefs: "ignore", allErrors: true})) + testMissingRefsIgnore(new Ajv({missingRefs: "ignore", logger: false})) + testMissingRefsIgnore(new Ajv({missingRefs: "ignore", allErrors: true, logger: false})) function testMissingRefsIgnore(ajv) { var validate = ajv.compile({$ref: "missing_reference"}) @@ -136,10 +136,12 @@ describe("referenced schema options", () => { }) it('should not throw and fail validation with missingRef == "fail" if the ref is used', () => { - testMissingRefsFail(new Ajv({missingRefs: "fail"})) - testMissingRefsFail(new Ajv({missingRefs: "fail", verbose: true})) - testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true})) - testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true, verbose: true})) + testMissingRefsFail(new Ajv({missingRefs: "fail", logger: false})) + testMissingRefsFail(new Ajv({missingRefs: "fail", verbose: true, logger: false})) + testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true, logger: false})) + testMissingRefsFail( + new Ajv({missingRefs: "fail", allErrors: true, verbose: true, logger: false}) + ) function testMissingRefsFail(ajv) { var validate = ajv.compile({ diff --git a/spec/options/options_reporting.spec.js b/spec/options/options_reporting.spec.js index 94c2b039b4..7968715e51 100644 --- a/spec/options/options_reporting.spec.js +++ b/spec/options/options_reporting.spec.js @@ -77,9 +77,7 @@ describe("reporting options", () => { beforeEach(() => { consoleCalled = false - console.warn = () => { - consoleCalled = true - } + console.warn = () => (consoleCalled = true) }) afterEach(() => { @@ -87,9 +85,7 @@ describe("reporting options", () => { }) it("no user-defined logger is given - global console should be used", () => { - var ajv = new Ajv({ - meta: false, - }) + var ajv = new Ajv({meta: false}) ajv.compile({ type: "number", diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index 565ae1fdf4..9ec583ef43 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -59,10 +59,10 @@ describe("validation options", () => { }) describe("unicode", () => { - it("should use String.prototype.length with unicode option == false", () => { + it("should use String.prototype.length with deprecated unicode option == false", () => { var ajvUnicode = new Ajv() - testUnicode(new Ajv({unicode: false})) - testUnicode(new Ajv({unicode: false, allErrors: true})) + testUnicode(new Ajv({unicode: false, logger: false})) + testUnicode(new Ajv({unicode: false, allErrors: true, logger: false})) function testUnicode(ajv) { var validateWithUnicode = ajvUnicode.compile({minLength: 2}) diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index 696f087fdb..105198828f 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -5,8 +5,8 @@ var should = require("../chai").should() describe("removed schemaId option", () => { it("should use $id and ignore id", () => { - test(new Ajv()) - test(new Ajv({schemaId: "$id"})) + test(new Ajv({logger: false})) + test(new Ajv({schemaId: "$id", logger: false})) function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.js index 2a544dd064..c6c5ba1374 100644 --- a/spec/options/unknownFormats.spec.js +++ b/spec/options/unknownFormats.spec.js @@ -42,7 +42,7 @@ describe("unknownFormats option", () => { describe('= "ignore (default before 5.0.0)"', () => { it("should pass schema compilation and be valid if unknown format is used", () => { - test(new Ajv({unknownFormats: "ignore"})) + test(new Ajv({unknownFormats: "ignore", logger: false})) function test(ajv) { var validate = ajv.compile({format: "unknown"}) diff --git a/spec/schema-tests.spec.js b/spec/schema-tests.spec.js index 3eabd831ca..00b8c1212a 100644 --- a/spec/schema-tests.spec.js +++ b/spec/schema-tests.spec.js @@ -29,10 +29,7 @@ var remoteRefsWithIds = [ instances.forEach(addRemoteRefsAndFormats) jsonSchemaTest(instances, { - description: - "Schema tests of " + - instances.length + - " ajv instances with different options", + description: "Schema tests of " + instances.length + " ajv instances with different options", suites: { "Advanced schema tests": typeof window == "object" From 7c671ffe71d081cf4fb0470bb45d15957896542f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 11:03:13 +0100 Subject: [PATCH 143/322] move keywords without implementations to vocabularies --- lib/ajv.ts | 12 ++++++++---- lib/compile/rules.ts | 18 +----------------- lib/vocabularies/core/index.ts | 4 +--- lib/vocabularies/format/index.ts | 2 -- lib/vocabularies/metadata.ts | 17 +++++++++++++++++ lib/vocabularies/validation/index.ts | 2 -- 6 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 lib/vocabularies/metadata.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 3d0edfc2a7..2814a2169f 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -13,6 +13,7 @@ import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" import applicatorVocabulary from "./vocabularies/applicator" import formatVocabulary from "./vocabularies/format" +import {metadataVocabulary, contentVocabulary} from "./vocabularies/metadata" import {addVocabulary, addKeyword, getKeyword, removeKeyword} from "./keyword" module.exports = Ajv @@ -71,10 +72,13 @@ export default function Ajv(opts: Options): void { this._metaOpts = getMetaSchemaOptions(this) if (opts.formats) addInitialFormats(this) - this.addVocabulary(coreVocabulary, true) - this.addVocabulary(validationVocabulary, true) - this.addVocabulary(applicatorVocabulary, true) - this.addVocabulary(formatVocabulary, true) + this.addVocabulary(["$async"]) + this.addVocabulary(coreVocabulary) + this.addVocabulary(validationVocabulary) + this.addVocabulary(applicatorVocabulary) + this.addVocabulary(formatVocabulary) + this.addVocabulary(metadataVocabulary) + this.addVocabulary(contentVocabulary) if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index e96e4cd75f..bc7d449c69 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -22,22 +22,6 @@ export interface Rule { } const ALL = ["type", "$comment"] -const KEYWORDS = [ - "$schema", - "$id", - "id", - "$data", - "$async", - "title", - "description", - "default", - "definitions", - "examples", - "readOnly", - "writeOnly", - "contentMediaType", - "contentEncoding", -] export default function rules(): ValidationRules { const groups = { @@ -50,6 +34,6 @@ export default function rules(): ValidationRules { types: {...groups, integer: true, boolean: true, null: true}, rules: [groups.number, groups.string, groups.array, groups.object, {rules: []}], all: toHash(ALL), - keywords: toHash(ALL.concat(KEYWORDS)), + keywords: toHash(ALL), } } diff --git a/lib/vocabularies/core/index.ts b/lib/vocabularies/core/index.ts index a6602c0bc6..5f01ad9fc9 100644 --- a/lib/vocabularies/core/index.ts +++ b/lib/vocabularies/core/index.ts @@ -1,7 +1,5 @@ import {Vocabulary} from "../../types" -const core: Vocabulary = [require("./ref")] +const core: Vocabulary = ["$schema", "$id", "$defs", "definitions", require("./ref")] export default core - -module.exports = core diff --git a/lib/vocabularies/format/index.ts b/lib/vocabularies/format/index.ts index 4a7ccc4c35..06fa12881c 100644 --- a/lib/vocabularies/format/index.ts +++ b/lib/vocabularies/format/index.ts @@ -3,5 +3,3 @@ import {Vocabulary} from "../../types" const format: Vocabulary = [require("./format")] export default format - -module.exports = format diff --git a/lib/vocabularies/metadata.ts b/lib/vocabularies/metadata.ts new file mode 100644 index 0000000000..81216f3af4 --- /dev/null +++ b/lib/vocabularies/metadata.ts @@ -0,0 +1,17 @@ +import {Vocabulary} from "../types" + +export const metadataVocabulary: Vocabulary = [ + "title", + "description", + "default", + "deprecated", + "readOnly", + "writeOnly", + "examples", +] + +export const contentVocabulary: Vocabulary = [ + "contentMediaType", + "contentEncoding", + "contentSchema", +] diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 35238ea938..5a9b535861 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -19,5 +19,3 @@ const validation: Vocabulary = [ ] export default validation - -module.exports = validation From 261cb3df506f01cd8ba440172e02d404e88b366f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 11:21:11 +0100 Subject: [PATCH 144/322] remove option "nullable", support OpenAPI keyword "nullable" by default --- README.md | 3 +- lib/ajv.ts | 1 - lib/compile/validate/dataType.ts | 14 ++-- lib/types.ts | 6 +- lib/vocabularies/validation/index.ts | 1 + spec/options/nullable.spec.js | 102 ++++++++++----------------- 6 files changed, 50 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index b271d0bceb..9250a6657e 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Performance of different validators by [json-schema-benchmark](https://github.co - Ajv implements full JSON Schema [draft-06/07](http://json-schema.org/) standards (draft-04 is supported in v6): - all validation keywords (see [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md)) + - keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) - support of circular references between schemas - correct string lengths for strings with unicode pairs @@ -1097,7 +1098,6 @@ Defaults: allErrors: false, verbose: false, $comment: false, - nullable: false, format: true, formats: {}, unknownFormats: true, @@ -1145,7 +1145,6 @@ Defaults: - `false` (default): ignore \$comment keyword. - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function -- _nullable_: support keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - _format_: formats validation mode. Option values: - `true` (default) - validate added formats (see [Formats](#formats)). - `false` - ignore all format keywords. diff --git a/lib/ajv.ts b/lib/ajv.ts index 2814a2169f..3c33d548df 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -82,7 +82,6 @@ export default function Ajv(opts: Options): void { if (opts.keywords) addInitialKeywords(this, opts.keywords) addDefaultMetaSchema(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) - if (opts.nullable) this.addKeyword({keyword: "nullable", schemaType: "boolean"}) addInitialSchemas(this) opts.format = formatOpt } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index fcb7053f1e..8797921c47 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -6,17 +6,15 @@ import {reportError} from "../errors" import {_, str, Name} from "../codegen" import {ValidationRules} from "../rules" -export function getSchemaTypes({opts, RULES}: CompilationContext, schema): string[] { +export function getSchemaTypes({RULES}: CompilationContext, schema): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach((t) => checkType(t, RULES)) - if (opts.nullable) { - const hasNull = types.includes("null") - if (hasNull && schema.nullable === false) { - throw new Error('{"type": "null"} contradicts {"nullable": "false"}') - } else if (!hasNull && schema.nullable === true) { - types.push("null") - } + const hasNull = types.includes("null") + if (hasNull && schema.nullable === false) { + throw new Error('{"type": "null"} contradicts {"nullable": "false"}') + } else if (!hasNull && schema.nullable === true) { + types.push("null") } return types } diff --git a/lib/types.ts b/lib/types.ts index 683426f826..11d9fc41fa 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -42,17 +42,17 @@ export interface CurrentOptions { codegen?: CodeGenOptions cache?: Cache logger?: Logger | false - nullable?: boolean serialize?: false | ((schema: object | boolean) => any) $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) } export interface Options extends CurrentOptions { errorDataPath?: "object" | "property" // removed - jsPropertySyntax?: boolean + nullable?: boolean // removed, "nullable" keyword is supported by default schemaId?: string // removed - unicode?: boolean // deprecated uniqueItems?: boolean // removed + jsPropertySyntax?: boolean // deprecated + unicode?: boolean // deprecated } interface Logger { diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 5a9b535861..5a30422de0 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -14,6 +14,7 @@ const validation: Vocabulary = [ require("./limitItems"), require("./uniqueItems"), // any + {keyword: "nullable", schemaType: "boolean"}, require("./const"), require("./enum"), ] diff --git a/spec/options/nullable.spec.js b/spec/options/nullable.spec.js index bbf2df735e..823b72815e 100644 --- a/spec/options/nullable.spec.js +++ b/spec/options/nullable.spec.js @@ -1,94 +1,70 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() -describe("nullable option", () => { - var ajv +describe("nullable keyword", () => { + let ajv - describe("= true", () => { - beforeEach(() => { - ajv = new Ajv({ - nullable: true, - }) + beforeEach(() => { + ajv = new Ajv() + }) + + it('should support keyword "nullable"', () => { + testNullable({ + type: "number", + nullable: true, }) - it('should add keyword "nullable"', () => { - testNullable({ - type: "number", - nullable: true, - }) + testNullable({ + type: ["number"], + nullable: true, + }) - testNullable({ - type: ["number"], - nullable: true, - }) + testNullable({ + type: ["number", "null"], + }) - testNullable({ - type: ["number", "null"], - }) + testNullable({ + type: ["number", "null"], + nullable: true, + }) - testNullable({ - type: ["number", "null"], - nullable: true, - }) + testNotNullable({type: "number"}) - testNotNullable({type: "number"}) + testNotNullable({type: ["number"]}) + }) - testNotNullable({type: ["number"]}) + it('should respect "nullable" == false', () => { + testNotNullable({ + type: "number", + nullable: false, }) - it('should respect "nullable" == false with opts.nullable == true', () => { - testNotNullable({ - type: "number", - nullable: false, - }) - - testNotNullable({ - type: ["number"], - nullable: false, - }) + testNotNullable({ + type: ["number"], + nullable: false, }) }) - describe('without option "nullable"', () => { - it("should ignore keyword nullable", () => { - ajv = new Ajv() - - testNotNullable({ - type: "number", - nullable: true, - }) - - testNotNullable({ - type: ["number"], - nullable: true, - }) - - testNullable({ - type: ["number", "null"], - }) - - testNullable({ + it("should throw if type includes null with nullable: false", () => { + should.throw(() => { + ajv.compile({ type: ["number", "null"], - nullable: true, - }) - - should.not.throw(() => { - ajv.compile({nullable: false}) + nullable: false, }) }) }) function testNullable(schema) { - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate(1).should.equal(true) validate(null).should.equal(true) validate("1").should.equal(false) } function testNotNullable(schema) { - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate(1).should.equal(true) validate(null).should.equal(false) validate("1").should.equal(false) From 8bab5d0965e64cbf2db5cab2930aab244f98da5b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:54:33 +0100 Subject: [PATCH 145/322] replace option strictKeyword with strict (true by default) --- README.md | 45 +++++++---- lib/ajv.ts | 4 +- lib/compile/validate/index.ts | 4 +- lib/types.ts | 22 +++--- .../applicator/additionalProperties.ts | 2 +- lib/vocabularies/util.ts | 6 +- spec/boolean.spec.js | 2 +- .../240_mutual_recur_frags_common_ref.spec.js | 22 +++--- .../259_validate_meta_against_itself.spec.js | 2 +- spec/json-schema.spec.js | 4 +- spec/keyword.spec.js | 5 ++ spec/options/schemaId.spec.js | 16 +++- spec/options/strictKeywords.spec.js | 74 ++++++++++--------- spec/resolve.spec.js | 51 ++++++------- spec/schema-tests.spec.js | 2 +- 15 files changed, 153 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 9250a6657e..ca72f43dfa 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")) - [Ajv and Content Security Policies (CSP)](#ajv-and-content-security-policies-csp) - [Command line interface](#command-line-interface) - Validation + - [Strict mode](#strict-mode) - [Keywords](#validation-keywords) - [Annotation keywords](#annotation-keywords) - [Formats](#formats) @@ -244,6 +245,22 @@ CLI is available as a separate npm package [ajv-cli](https://github.com/ajv-vali - all Ajv options - reporting changes in data after validation in [JSON-patch](https://tools.ietf.org/html/rfc6902) format +## Strict mode + +TODO + +- _strictDefaults_: report ignored `default` keywords in schemas. Option values: + - `false` (default) - ignored defaults are not reported + - `true` - if an ignored default is present, throw an error + - `"log"` - if an ignored default is present, log warning +- _strictKeywords_: report unknown keywords in schemas. Option values: + - `false` (default) - unknown keywords are not reported + - `true` - if an unknown keyword is present, throw an error + - `"log"` - if an unknown keyword is present, log warning +- _strictNumbers_: validate numbers strictly, failing validation for NaN and Infinity. Option values: + - `false` (default) - NaN or Infinity will pass validation for numeric types + - `true` - NaN or Infinity will not pass validation for numeric types + ## Validation keywords Ajv supports all validation keywords from draft-07 of JSON Schema standard: @@ -1093,6 +1110,8 @@ Defaults: ```javascript { + // strict mode options + strict: true, // validation and reporting options: $data: false, allErrors: false, @@ -1105,7 +1124,7 @@ Defaults: logger: undefined, // referenced schema options: missingRefs: true, - extendRefs: 'ignore', // recommended 'fail' + extendRefs: "ignore", // recommended 'fail' loadSchema: undefined, // function(uri: string): Promise {} // options to modify validated data: removeAdditional: false, @@ -1113,7 +1132,6 @@ Defaults: coerceTypes: false, // strict mode options strictDefaults: false, - strictKeywords: false, strictNumbers: false, // asynchronous validation options: transpile: undefined, // requires ajv-async package @@ -1136,6 +1154,15 @@ Defaults: } ``` +##### Strict mode + +- _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: + - `true` (default) - use strict mode and throw an exception when any strict mode restrictions is violated. + - `"log"` - log error or warning when any strict mode restriction is violated (depeding on the severity). + - `false` - ignore any strict mode restriction. + +This option replaced v6 options `strictDefaults`, `strictKeywords` and `strictNumbers` and added additional restrictions - see [Strict Mode](#strict-mode). + ##### Validation and reporting options - _\$data_: support [\$data references](#data-reference). Draft 6 meta-schema that is added by default will be extended to allow them. If you want to use another meta-schema you need to use $dataMetaSchema method to add support for $data reference. See [API](#api). @@ -1187,20 +1214,6 @@ Defaults: - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). -##### Strict mode options - -- _strictDefaults_: report ignored `default` keywords in schemas. Option values: - - `false` (default) - ignored defaults are not reported - - `true` - if an ignored default is present, throw an error - - `"log"` - if an ignored default is present, log warning -- _strictKeywords_: report unknown keywords in schemas. Option values: - - `false` (default) - unknown keywords are not reported - - `true` - if an unknown keyword is present, throw an error - - `"log"` - if an unknown keyword is present, log warning -- _strictNumbers_: validate numbers strictly, failing validation for NaN and Infinity. Option values: - - `false` (default) - NaN or Infinity will pass validation for numeric types - - `true` - NaN or Infinity will not pass validation for numeric types - ##### Asynchronous validation options - _transpile_: Requires [ajv-async](https://github.com/ajv-validator/ajv-async) package. It determines whether Ajv transpiles compiled asynchronous validation function. Option values: diff --git a/lib/ajv.ts b/lib/ajv.ts index 3c33d548df..40274c2b64 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -52,9 +52,9 @@ const META_SUPPORT_DATA = ["/properties"] * @param {Object} opts optional options * @return {Object} ajv instance */ -export default function Ajv(opts: Options): void { +export default function Ajv(opts: Options = {}): void { if (!(this instanceof Ajv)) return new Ajv(opts) - opts = this._opts = {...(opts || {})} + opts = this._opts = {strict: true, ...opts} setLogger(this) this._schemas = {} this._refs = {} diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index ca9cf285de..99dd2d0572 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -76,11 +76,11 @@ function typeAndKeywords(it: CompilationContext, errsCount?: Name): void { } function checkUnknownKeywords({schema, RULES, opts, logger}: CompilationContext): void { - if (opts.strictKeywords) { + if (opts.strict) { const unknownKeyword = schemaUnknownRules(schema, RULES.keywords) if (unknownKeyword) { const msg = `unknown keyword: "${unknownKeyword}"` - if (opts.strictKeywords === "log") logger.warn(msg) + if (opts.strict === "log") logger.error(msg) else throw new Error(msg) } } diff --git a/lib/types.ts b/lib/types.ts index 11d9fc41fa..d85078fafc 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -5,6 +5,7 @@ import {ResolvedRef} from "./compile" import KeywordContext from "./compile/context" export interface CurrentOptions { + strict?: boolean | "log" $data?: boolean allErrors?: boolean verbose?: boolean @@ -22,9 +23,6 @@ export interface CurrentOptions { removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" - strictDefaults?: boolean | "log" - strictKeywords?: boolean | "log" - strictNumbers?: boolean async?: boolean | string transpile?: string | ((code: string) => string) meta?: boolean | object @@ -47,12 +45,18 @@ export interface CurrentOptions { } export interface Options extends CurrentOptions { - errorDataPath?: "object" | "property" // removed - nullable?: boolean // removed, "nullable" keyword is supported by default - schemaId?: string // removed - uniqueItems?: boolean // removed - jsPropertySyntax?: boolean // deprecated - unicode?: boolean // deprecated + // removed: + errorDataPath?: "object" | "property" + nullable?: boolean // "nullable" keyword is supported by default + schemaId?: string + uniqueItems?: boolean + // deprecated: + jsPropertySyntax?: boolean // added instead of jsonPointers + unicode?: boolean + // replaced with option "strict": + strictDefaults?: boolean | "log" + strictKeywords?: boolean | "log" + strictNumbers?: boolean } interface Logger { diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index b805511584..10f1a85bb3 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -14,7 +14,7 @@ const def: CodeKeywordDefinition = { const {gen, schema, parentSchema, data, errsCount, it} = cxt if (!errsCount) throw new Error("ajv implementation error") const {allErrors, opts} = it - if (alwaysValidSchema(it, schema) && opts.removeAdditional !== "all") return + if (opts.removeAdditional !== "all" && alwaysValidSchema(it, schema)) return const props = allSchemaProperties(parentSchema.properties) const patProps = allSchemaProperties(parentSchema.patternProperties) checkAdditionalProperties() diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index a7494b408c..06096c8784 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -21,14 +21,12 @@ export function schemaRefOrVal( } export function alwaysValidSchema( - {RULES, opts: {strictKeywords}}: CompilationContext, + {RULES}: CompilationContext, schema: boolean | object ): boolean | void { return typeof schema == "boolean" ? schema === true - : strictKeywords - ? Object.keys(schema).length === 0 - : !schemaHasRules(schema, RULES.all) + : Object.keys(schema).length === 0 && !schemaHasRules(schema, RULES.all) } export function allSchemaProperties(schema?: object): string[] { diff --git a/spec/boolean.spec.js b/spec/boolean.spec.js index c8b030fa79..d940f38216 100644 --- a/spec/boolean.spec.js +++ b/spec/boolean.spec.js @@ -11,7 +11,7 @@ describe("boolean schemas", () => { new Ajv(), new Ajv({allErrors: true}), new Ajv({inlineRefs: false}), - new Ajv({strictKeywords: true}), + new Ajv({strict: false}), ] }) diff --git a/spec/issues/240_mutual_recur_frags_common_ref.spec.js b/spec/issues/240_mutual_recur_frags_common_ref.spec.js index f496925f76..69cf8f755c 100644 --- a/spec/issues/240_mutual_recur_frags_common_ref.spec.js +++ b/spec/issues/240_mutual_recur_frags_common_ref.spec.js @@ -7,17 +7,19 @@ describe("issue #240, mutually recursive fragment refs reference a common schema var apiSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://api.schema#", - resource: { - $id: "#resource", - properties: { - id: {type: "string"}, + $defs: { + resource: { + $id: "#resource", + properties: { + id: {type: "string"}, + }, }, - }, - resourceIdentifier: { - $id: "#resource_identifier", - properties: { - id: {type: "string"}, - type: {type: "string"}, + resourceIdentifier: { + $id: "#resource_identifier", + properties: { + id: {type: "string"}, + type: {type: "string"}, + }, }, }, } diff --git a/spec/issues/259_validate_meta_against_itself.spec.js b/spec/issues/259_validate_meta_against_itself.spec.js index 80595e1339..0ecdd6a082 100644 --- a/spec/issues/259_validate_meta_against_itself.spec.js +++ b/spec/issues/259_validate_meta_against_itself.spec.js @@ -5,7 +5,7 @@ require("../chai").should() describe("issue #259, support validating [meta-]schemas against themselves", () => { it('should add schema before validation if "id" is the same as "$schema"', () => { - var ajv = new Ajv() + var ajv = new Ajv({strict: false}) var hyperSchema = require("../remotes/hyper-schema.json") ajv.addMetaSchema(hyperSchema) }) diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 7aad65885d..2724869bb6 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -43,7 +43,7 @@ var SKIP = { } runTest( - getAjvInstances(options, {meta: false}), + getAjvInstances(options, {meta: false, strict: false}), 6, typeof window == "object" ? suite( @@ -55,7 +55,7 @@ runTest( ) runTest( - getAjvInstances(options), + getAjvInstances(options, {strict: false}), 7, typeof window == "object" ? suite( diff --git a/spec/keyword.spec.js b/spec/keyword.spec.js index c15fd081da..62477aa191 100644 --- a/spec/keyword.spec.js +++ b/spec/keyword.spec.js @@ -4,6 +4,7 @@ var getAjvInstances = require("./ajv_instances"), should = require("./chai").should(), equal = require("../dist/compile/equal") +const Ajv = require("..") const codegen = require("../dist/compile/codegen") const {_, nil} = codegen @@ -1108,6 +1109,8 @@ describe("User-defined keywords", () => { describe("removeKeyword", () => { it("should remove and allow redefining keyword", () => { + ajv = new Ajv({strict: false}) + ajv.addKeyword({ keyword: "positive", type: "number", @@ -1151,6 +1154,8 @@ describe("User-defined keywords", () => { }) it("should remove and allow redefining standard keyword", () => { + ajv = new Ajv({strict: false}) + var schema = {minimum: 1} var validate = ajv.compile(schema) validate(0).should.equal(false) diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index 105198828f..b4c275a66c 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -4,10 +4,24 @@ var Ajv = require("../ajv") var should = require("../chai").should() describe("removed schemaId option", () => { - it("should use $id and ignore id", () => { + it("should use $id and throw exception when id is used", () => { test(new Ajv({logger: false})) test(new Ajv({schemaId: "$id", logger: false})) + function test(ajv) { + ajv.addSchema({$id: "mySchema1", type: "string"}) + var validate = ajv.getSchema("mySchema1") + validate("foo").should.equal(true) + validate(1).should.equal(false) + + should.throw(() => ajv.compile({id: "mySchema2", type: "string"})) + } + }) + + it("should use $id and ignore id when strict: false", () => { + test(new Ajv({logger: false, strict: false})) + test(new Ajv({schemaId: "$id", logger: false, strict: false})) + function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) var validate = ajv.getSchema("mySchema1") diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js index 96c7ea2162..0b6e8b0085 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.js @@ -3,12 +3,12 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("strictKeywords option", () => { - describe("strictKeywords = false", () => { +describe("strict option with keywords (replaced strictKeywords)", () => { + describe("strict = false", () => { it("should NOT throw an error or log a warning given an unknown keyword", () => { var output = {} var ajv = new Ajv({ - strictKeywords: false, + strict: false, logger: getLogger(output), }) var schema = { @@ -17,28 +17,32 @@ describe("strictKeywords option", () => { } ajv.compile(schema) - should.not.exist(output.warning) + should.not.exist(output.error) }) }) - describe("strictKeywords = true", () => { - it("should throw an error given an unknown keyword in the schema root when strictKeywords is true", () => { - var ajv = new Ajv({strictKeywords: true}) - var schema = { - properties: {}, - unknownKeyword: 1, + describe("strict = true or undefined", () => { + it("should throw an error given an unknown keyword in the schema root when strict is true", () => { + test(new Ajv({strict: true})) + test(new Ajv()) + + function test(ajv) { + const schema = { + properties: {}, + unknownKeyword: 1, + } + should.throw(() => { + ajv.compile(schema) + }) } - should.throw(() => { - ajv.compile(schema) - }) }) }) - describe('strictKeywords = "log"', () => { - it('should log a warning given an unknown keyword in the schema root when strictKeywords is "log"', () => { + describe('strict = "log"', () => { + it('should log a warning given an unknown keyword in the schema root when strict is "log"', () => { var output = {} var ajv = new Ajv({ - strictKeywords: "log", + strict: "log", logger: getLogger(output), }) var schema = { @@ -46,36 +50,40 @@ describe("strictKeywords option", () => { unknownKeyword: 1, } ajv.compile(schema) - should.equal(output.warning, 'unknown keyword: "unknownKeyword"') + should.equal(output.error, 'unknown keyword: "unknownKeyword"') }) }) describe("unknown keyword inside schema that has no known keyword in compound keyword", () => { - it("should throw an error given an unknown keyword when strictKeywords is true", () => { - var ajv = new Ajv({strictKeywords: true}) - var schema = { - anyOf: [ - { - unknownKeyword: 1, - }, - ], + it("should throw an error given an unknown keyword when strict is true or undefined", () => { + test(new Ajv({strict: true})) + test(new Ajv()) + + function test(ajv) { + const schema = { + anyOf: [ + { + unknownKeyword: 1, + }, + ], + } + should.throw(() => { + ajv.compile(schema) + }) } - should.throw(() => { - ajv.compile(schema) - }) }) }) function getLogger(output) { return { - log: () => { + log() { throw new Error("log should not be called") }, - warn: function (warning) { - output.warning = warning + warn() { + throw new Error("warn should not be called") }, - error: () => { - throw new Error("error should not be called") + error(msg) { + output.error = msg }, } } diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index dc2b9de86b..e2bdeda10e 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -20,29 +20,33 @@ describe("resolve", () => { // Example from http://json-schema.org/latest/json-schema-core.html#anchor29 var schema = { $id: "http://x.y.z/rootschema.json#", - schema1: { - $id: "#foo", - description: "schema1", - type: "integer", - }, - schema2: { - $id: "otherschema.json", - description: "schema2", - nested: { - $id: "#bar", - description: "nested", - type: "string", + $defs: { + schema1: { + $id: "#foo", + description: "schema1", + type: "integer", }, - alsonested: { - $id: "t/inner.json#a", - description: "alsonested", - type: "boolean", + schema2: { + $id: "otherschema.json", + description: "schema2", + $defs: { + nested: { + $id: "#bar", + description: "nested", + type: "string", + }, + alsonested: { + $id: "t/inner.json#a", + description: "alsonested", + type: "boolean", + }, + }, + }, + schema3: { + $id: "some://where.else/completely#", + description: "schema3", + type: "null", }, - }, - schema3: { - $id: "some://where.else/completely#", - description: "schema3", - type: "null", }, properties: { foo: {$ref: "#foo"}, @@ -240,10 +244,7 @@ describe("resolve", () => { required: ["header"], properties: { header: { - allOf: [ - {$ref: "header.json"}, - {properties: {msgType: {enum: [0]}}}, - ], + allOf: [{$ref: "header.json"}, {properties: {msgType: {enum: [0]}}}], }, }, } diff --git a/spec/schema-tests.spec.js b/spec/schema-tests.spec.js index 00b8c1212a..da6cda0b7b 100644 --- a/spec/schema-tests.spec.js +++ b/spec/schema-tests.spec.js @@ -7,7 +7,7 @@ var jsonSchemaTest = require("json-schema-test"), suite = require("./browser_test_suite"), after = require("./after_test") -var instances = getAjvInstances(options, {unknownFormats: ["allowedUnknown"]}) +var instances = getAjvInstances(options, {strict: false, unknownFormats: ["allowedUnknown"]}) var remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), From 45336658b51409fd97745ace726fa9d8a808bebd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:21:50 +0100 Subject: [PATCH 146/322] replace option strictDefaults with strict (true by default) --- README.md | 10 +-- lib/ajv.ts | 2 +- lib/compile/validate/defaults.ts | 4 +- lib/compile/validate/index.ts | 4 +- spec/options/strictDefaults.spec.js | 102 ++++++++++++++++------------ 5 files changed, 71 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ca72f43dfa..c5a84a9840 100644 --- a/README.md +++ b/README.md @@ -870,14 +870,16 @@ console.log(validate(data)) // true console.log(data) // [ 1, "foo" ] ``` -`default` keywords in other cases are ignored: +With `useDefaults` option `default` keywords throws exception during schema compilation when used in: - not in `properties` or `items` subschemas - in schemas inside `anyOf`, `oneOf` and `not` (see [#42](https://github.com/ajv-validator/ajv/issues/42)) -- in `if` subschema of `switch` keyword +- in `if` schema - in schemas generated by user-defined _macro_ keywords -The [`strictDefaults` option](#options) changes Ajv's behavior for the defaults that Ajv ignores (`true` raises an error, and `"log"` outputs a warning). +The strict mode option can change the behavior for these unsupported defaults (`strict: false` to ignore them, `"log"` to log a warning). + +See [Strict mode](#strict-mode). ## Coercing data types @@ -1154,7 +1156,7 @@ Defaults: } ``` -##### Strict mode +##### Strict mode option - _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: - `true` (default) - use strict mode and throw an exception when any strict mode restrictions is violated. diff --git a/lib/ajv.ts b/lib/ajv.ts index 40274c2b64..e6967bc93b 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -43,7 +43,7 @@ Ajv.MissingRefError = MissingRefError var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" -var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes", "strictDefaults"] +var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] /** diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index cb8c073b56..c43b64835b 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -20,9 +20,9 @@ function assignDefault( if (defaultValue === undefined) return const childData = _`${data}${getProperty(prop)}` if (compositeRule) { - if (opts.strictDefaults) { + if (opts.strict) { const msg = `default is ignored for: ${childData}` - if (opts.strictDefaults === "log") logger.warn(msg) + if (opts.strict === "log") logger.warn(msg) else throw new Error(msg) } return diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 99dd2d0572..9ee72c1195 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -103,9 +103,9 @@ function checkRefsAndKeywords({ } function checkNoDefault({schema, opts, logger}: CompilationContext): void { - if (schema.default !== undefined && opts.useDefaults && opts.strictDefaults) { + if (schema.default !== undefined && opts.useDefaults && opts.strict) { const msg = "default is ignored in the schema root" - if (opts.strictDefaults === "log") logger.warn(msg) + if (opts.strict === "log") logger.warn(msg) else throw new Error(msg) } } diff --git a/spec/options/strictDefaults.spec.js b/spec/options/strictDefaults.spec.js index 6bdac7c8cf..be7d62d391 100644 --- a/spec/options/strictDefaults.spec.js +++ b/spec/options/strictDefaults.spec.js @@ -3,14 +3,14 @@ var Ajv = require("../ajv") var should = require("../chai").should() -describe("strictDefaults option", () => { +describe("strict option with defaults (replaced strictDefaults)", () => { describe("useDefaults = true", () => { - describe("strictDefaults = false", () => { + describe("strict = false", () => { it("should NOT throw an error or log a warning given an ignored default", () => { var output = {} var ajv = new Ajv({ useDefaults: true, - strictDefaults: false, + strict: false, logger: getLogger(output), }) var schema = { @@ -22,11 +22,11 @@ describe("strictDefaults option", () => { should.not.exist(output.warning) }) - it("should NOT throw an error or log a warning given an ignored default", () => { + it("should NOT throw an error or log a warning given an ignored default #2", () => { var output = {} var ajv = new Ajv({ useDefaults: true, - strictDefaults: false, + strict: false, logger: getLogger(output), }) var schema = { @@ -47,44 +47,52 @@ describe("strictDefaults option", () => { }) }) - describe("strictDefaults = true", () => { - it("should throw an error given an ignored default in the schema root when strictDefaults is true", () => { - var ajv = new Ajv({useDefaults: true, strictDefaults: true}) - var schema = { - default: 5, - properties: {}, + describe("strict = true", () => { + it("should throw an error given an ignored default in the schema root when strict is true or undefined", () => { + test(new Ajv({useDefaults: true})) + test(new Ajv({useDefaults: true, strict: true})) + + function test(ajv) { + const schema = { + default: 5, + properties: {}, + } + should.throw(() => { + ajv.compile(schema) + }) } - should.throw(() => { - ajv.compile(schema) - }) }) - it("should throw an error given an ignored default in oneOf when strictDefaults is true", () => { - var ajv = new Ajv({useDefaults: true, strictDefaults: true}) - var schema = { - oneOf: [ - {enum: ["foo", "bar"]}, - { - properties: { - foo: { - default: true, + it("should throw an error given an ignored default in oneOf when strict is true or undefined", () => { + test(new Ajv({useDefaults: true})) + test(new Ajv({useDefaults: true, strict: true})) + + function test(ajv) { + var schema = { + oneOf: [ + {enum: ["foo", "bar"]}, + { + properties: { + foo: { + default: true, + }, }, }, - }, - ], + ], + } + should.throw(() => { + ajv.compile(schema) + }) } - should.throw(() => { - ajv.compile(schema) - }) }) }) - describe('strictDefaults = "log"', () => { - it('should log a warning given an ignored default in the schema root when strictDefaults is "log"', () => { + describe('strict = "log"', () => { + it('should log a warning given an ignored default in the schema root when strict is "log"', () => { var output = {} var ajv = new Ajv({ useDefaults: true, - strictDefaults: "log", + strict: "log", logger: getLogger(output), }) var schema = { @@ -95,11 +103,11 @@ describe("strictDefaults option", () => { should.equal(output.warning, "default is ignored in the schema root") }) - it('should log a warning given an ignored default in oneOf when strictDefaults is "log"', () => { + it('should log a warning given an ignored default in oneOf when strict is "log"', () => { var output = {} var ajv = new Ajv({ useDefaults: true, - strictDefaults: "log", + strict: "log", logger: getLogger(output), }) var schema = { @@ -120,21 +128,31 @@ describe("strictDefaults option", () => { }) }) - describe("useDefaults = false", () => { - describe("strictDefaults = true", () => { - it("should NOT throw an error given an ignored default in the schema root when useDefaults is false", () => { - var ajv = new Ajv({useDefaults: false, strictDefaults: true}) - var schema = { + describe("useDefaults = false or undefined", () => { + it("should NOT throw an error given an ignored default in the schema root when useDefaults is false", () => { + test(new Ajv({useDefaults: false})) + test(new Ajv({useDefaults: false, strict: true})) + test(new Ajv()) + test(new Ajv({strict: true})) + + function test(ajv) { + const schema = { default: 5, properties: {}, } should.not.throw(() => { ajv.compile(schema) }) - }) + } + }) + + it("should NOT throw an error given an ignored default in oneOf when useDefaults is false", () => { + test(new Ajv({useDefaults: false})) + test(new Ajv({useDefaults: false, strict: true})) + test(new Ajv()) + test(new Ajv({strict: true})) - it("should NOT throw an error given an ignored default in oneOf when useDefaults is false", () => { - var ajv = new Ajv({useDefaults: false, strictDefaults: true}) + function test(ajv) { var schema = { oneOf: [ {enum: ["foo", "bar"]}, @@ -150,7 +168,7 @@ describe("strictDefaults option", () => { should.not.throw(() => { ajv.compile(schema) }) - }) + } }) }) From 8f4cf2084c84bc3176cba1b6256eaa57dc8e9bc8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:53:59 +0100 Subject: [PATCH 147/322] replace option strictNumbers with strict --- README.md | 3 -- lib/compile/context.ts | 2 +- lib/compile/util.ts | 10 +++---- lib/compile/validate/dataType.ts | 4 +-- lib/compile/validate/iterate.ts | 2 +- lib/types.ts | 4 --- lib/vocabularies/validation/uniqueItems.ts | 2 +- spec/issues/182_nan_validation.spec.js | 35 ++++++++++++++++------ spec/options/strictNumbers.spec.js | 27 ++++++++--------- 9 files changed, 48 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index c5a84a9840..f952c82faf 100644 --- a/README.md +++ b/README.md @@ -1132,9 +1132,6 @@ Defaults: removeAdditional: false, useDefaults: false, coerceTypes: false, - // strict mode options - strictDefaults: false, - strictNumbers: false, // asynchronous validation options: transpile: undefined, // requires ajv-async package // advanced options: diff --git a/lib/compile/context.ts b/lib/compile/context.ts index abc0bef29a..998e039f84 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -146,7 +146,7 @@ export default class KeywordContext implements KeywordErrorContext { if (schemaType) { if (!(schemaCode instanceof Name)) throw new Error("ajv implementation error") const st = Array.isArray(schemaType) ? schemaType : [schemaType] - return _`(${checkDataTypes(st, schemaCode, it.opts.strictNumbers, DataType.Wrong)})` + return _`(${checkDataTypes(st, schemaCode, it.opts.strict, DataType.Wrong)})` } return nil } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index a648b381a0..4feb90a6d9 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -10,7 +10,7 @@ export enum DataType { export function checkDataType( dataType: string, data: Name, - strictNumbers?: boolean, + strictNums?: boolean | "log", correct = DataType.Correct ): Code { const EQ = correct === DataType.Correct ? operators.EQ : operators.NEQ @@ -36,18 +36,18 @@ export function checkDataType( return correct === DataType.Correct ? cond : _`!(${cond})` function numCond(_cond: Code = nil): Code { - return and(_`typeof ${data} == "number"`, _cond, strictNumbers ? _`isFinite(${data})` : nil) + return and(_`typeof ${data} == "number"`, _cond, strictNums ? _`isFinite(${data})` : nil) } } export function checkDataTypes( dataTypes: string[], data: Name, - strictNumbers?: boolean, + strictNums?: boolean | "log", correct?: DataType ): Code { if (dataTypes.length === 1) { - return checkDataType(dataTypes[0], data, strictNumbers, correct) + return checkDataType(dataTypes[0], data, strictNums, correct) } let cond: Code const types = toHash(dataTypes) @@ -61,7 +61,7 @@ export function checkDataTypes( cond = nil } if (types.number) delete types.integer - for (const t in types) cond = and(cond, checkDataType(t, data, strictNumbers, correct)) + for (const t in types) cond = and(cond, checkDataType(t, data, strictNums, correct)) return cond } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 8797921c47..a909fe11b2 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -31,7 +31,7 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]): types.length > 0 && !(coerceTo.length === 0 && types.length === 1 && schemaHasRulesForType(it, types[0])) if (checkTypes) { - const wrongType = checkDataTypes(types, data, opts.strictNumbers, DataType.Wrong) + const wrongType = checkDataTypes(types, data, opts.strict, DataType.Wrong) gen.if(wrongType, () => { if (coerceTo.length) coerceData(it, types, coerceTo) else reportTypeError(it) @@ -56,7 +56,7 @@ function coerceData(it: CompilationContext, types: string[], coerceTo: string[]) gen .assign(data, _`${data}[0]`) .assign(dataType, _`typeof ${data}`) - .if(checkDataTypes(types, data, opts.strictNumbers), () => gen.assign(coerced, data)) + .if(checkDataTypes(types, data, opts.strict), () => gen.assign(coerced, data)) ) } gen.if(_`${coerced} !== undefined`) diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 4f3738762f..470b61853e 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -32,7 +32,7 @@ export function schemaKeywords( function groupKeywords(group: RuleGroup): void { if (group.type) { - gen.if(checkDataType(group.type, data, opts.strictNumbers)) + gen.if(checkDataType(group.type, data, opts.strict)) iterateKeywords(it, group) if (types.length === 1 && types[0] === group.type && typeErrors) { gen.else() diff --git a/lib/types.ts b/lib/types.ts index d85078fafc..d8a475f37f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -53,10 +53,6 @@ export interface Options extends CurrentOptions { // deprecated: jsPropertySyntax?: boolean // added instead of jsonPointers unicode?: boolean - // replaced with option "strict": - strictDefaults?: boolean | "log" - strictKeywords?: boolean | "log" - strictNumbers?: boolean } interface Logger { diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index c1c49a50a4..1d9903f329 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -31,7 +31,7 @@ const def: CodeKeywordDefinition = { function loopN(i: Name, j: Name): void { const item = gen.name("item") - const wrongType = checkDataTypes(itemTypes, item, it.opts.strictNumbers, DataType.Wrong) + const wrongType = checkDataTypes(itemTypes, item, it.opts.strict, DataType.Wrong) const indices = gen.const("indices", _`{}`) gen.for(_`;${i}--;`, () => { gen.let(item, _`${data}[${i}]`) diff --git a/spec/issues/182_nan_validation.spec.js b/spec/issues/182_nan_validation.spec.js index 9c487549e9..0b68438094 100644 --- a/spec/issues/182_nan_validation.spec.js +++ b/spec/issues/182_nan_validation.spec.js @@ -4,22 +4,39 @@ var Ajv = require("../ajv") require("../chai").should() describe("issue #182, NaN validation", () => { - it("should not pass minimum/maximum validation", () => { - testNaN({minimum: 1}, false) - testNaN({maximum: 1}, false) + const ajv = new Ajv() + + it("should pass minimum/maximum validation without type", () => { + testNaN(ajv, {minimum: 1}, true) + testNaN(ajv, {maximum: 1}, true) + }) + + it("should NOT pass minimum/maximum validation without type when strict: false", () => { + const _ajv = new Ajv({strict: false}) + testNaN(_ajv, {minimum: 1}, false) + testNaN(_ajv, {maximum: 1}, false) + }) + + it("should not pass minimum/maximum validation with type", () => { + testNaN(ajv, {type: "number", minimum: 1}, false) + testNaN(ajv, {type: "number", maximum: 1}, false) + }) + + it("should pass type: number validation when strict: false", () => { + const _ajv = new Ajv({strict: false}) + testNaN(_ajv, {type: "number"}, true) }) - it("should pass type: number validation", () => { - testNaN({type: "number"}, true) + it("should not pass type: number validation (changed in v7 - strict by default)", () => { + testNaN(ajv, {type: "number"}, false) }) it("should not pass type: integer validation", () => { - testNaN({type: "integer"}, false) + testNaN(ajv, {type: "integer"}, false) }) - function testNaN(schema, NaNisValid) { - var ajv = new Ajv() - var validate = ajv.compile(schema) + function testNaN(_ajv, schema, NaNisValid) { + const validate = _ajv.compile(schema) validate(NaN).should.equal(NaNisValid) } }) diff --git a/spec/options/strictNumbers.spec.js b/spec/options/strictNumbers.spec.js index d5569d3590..f7d587b1b5 100644 --- a/spec/options/strictNumbers.spec.js +++ b/spec/options/strictNumbers.spec.js @@ -1,19 +1,16 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") -describe("structNumbers option", () => { - var ajv - describe("strictNumbers default", testWithoutStrictNumbers(new Ajv())) - describe( - "strictNumbers = false", - testWithoutStrictNumbers(new Ajv({strictNumbers: false})) - ) - describe("strictNumbers = true", () => { - beforeEach(() => { - ajv = new Ajv({strictNumbers: true}) - }) +describe("strict option with keywords (replaced structNumbers)", () => { + describe("strict default", testStrict(new Ajv())) + describe("strict = true", testStrict(new Ajv({strict: true}))) + describe('strict = "log"', testStrict(new Ajv({strict: "log"}))) + describe("strict = false", testNotStrict(new Ajv({strict: false}))) +}) +function testStrict(ajv) { + return () => { it("should fail validation for NaN/Infinity as type number", () => { var validate = ajv.compile({type: "number"}) validate("1.1").should.equal(false) @@ -31,10 +28,10 @@ describe("structNumbers option", () => { validate(NaN).should.equal(false) validate(Infinity).should.equal(false) }) - }) -}) + } +} -function testWithoutStrictNumbers(_ajv) { +function testNotStrict(_ajv) { return () => { it("should NOT fail validation for NaN/Infinity as type number", () => { var validate = _ajv.compile({type: "number"}) From efb59a087f69dc1b981ce531c2e3990d872fc70b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 14:14:30 +0100 Subject: [PATCH 148/322] feat(strict): "additionalItems" without "items" throws exception or logs warning --- .../applicator/additionalItems.ts | 11 ++- spec/options/strict.spec.js | 86 +++++++++++++++++++ spec/options/strictKeywords.spec.js | 2 +- 3 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 spec/options/strict.spec.js diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index aa97a8d0f2..9ebe22d739 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -13,8 +13,15 @@ const def: CodeKeywordDefinition = { const {gen, schema, parentSchema, data, it} = cxt const len = gen.const("len", _`${data}.length`) const items = parentSchema.items - // TODO strict mode: fail or warning if "additionalItems" is present without "items" Array - if (!Array.isArray(items)) return + if (!Array.isArray(items)) { + const {opts, logger} = it + if (opts.strict) { + const msg = '"additionalItems" without "items" is ignored, remove it' + if (opts.strict === "log") logger.warn(msg) + else throw new Error(msg) + } + return + } if (schema === false) { cxt.setParams({len: items.length}) cxt.pass(_`${len} <= ${items.length}`) diff --git a/spec/options/strict.spec.js b/spec/options/strict.spec.js new file mode 100644 index 0000000000..e61731d7a7 --- /dev/null +++ b/spec/options/strict.spec.js @@ -0,0 +1,86 @@ +"use strict" + +const Ajv = require("../ajv") +const should = require("../chai").should() + +describe("strict option with additionalItems", () => { + describe("strict = false", () => { + it('should NOT throw an error or log a warning when "additionalItems" is used without "items"', () => { + const output = {} + const ajv = new Ajv({ + strict: false, + logger: getLogger(output), + }) + const schema = { + additionalItems: false, + } + ajv.compile(schema) + should.not.exist(output.warning) + }) + }) + + describe("strict = true or undefined", () => { + it('should throw an error when "additionalItems" is used without "items"', () => { + test(new Ajv({strict: true})) + test(new Ajv()) + + function test(ajv) { + const schema = { + additionalItems: false, + } + should.throw(() => { + ajv.compile(schema) + }) + } + }) + }) + + describe('strict = "log"', () => { + it('should log a warning when "additionalItems" is used without "items"', () => { + const output = {} + const ajv = new Ajv({ + strict: "log", + logger: getLogger(output), + }) + const schema = { + additionalItems: false, + } + ajv.compile(schema) + ;/additionalItems/.test(output.warning).should.equal(true) + }) + }) + + describe("unknown keyword inside schema that has no known keyword in compound keyword", () => { + it("should throw an error given an unknown keyword when strict is true or undefined", () => { + test(new Ajv({strict: true})) + test(new Ajv()) + + function test(ajv) { + const schema = { + anyOf: [ + { + additionalItems: false, + }, + ], + } + should.throw(() => { + ajv.compile(schema) + }) + } + }) + }) + + function getLogger(output) { + return { + log() { + throw new Error("log should not be called") + }, + warn(msg) { + output.warning = msg + }, + error() { + throw new Error("error should not be called") + }, + } + } +}) diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js index 0b6e8b0085..cd7f34156f 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.js @@ -39,7 +39,7 @@ describe("strict option with keywords (replaced strictKeywords)", () => { }) describe('strict = "log"', () => { - it('should log a warning given an unknown keyword in the schema root when strict is "log"', () => { + it("should log an error given an unknown keyword in the schema root", () => { var output = {} var ajv = new Ajv({ strict: "log", From 3990d1cadfe468f369fe0a9b2c4cfe504e351488 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 14:37:14 +0100 Subject: [PATCH 149/322] feat(strict): "if" without "then" and "else" throws or logs --- README.md | 2 +- lib/compile/validate/defaults.ts | 14 +- lib/compile/validate/index.ts | 14 +- .../applicator/additionalItems.ts | 9 +- lib/vocabularies/applicator/if.ts | 4 +- lib/vocabularies/util.ts | 8 + spec/options/strict.spec.js | 146 ++++++++++++------ spec/options/strictKeywords.spec.js | 12 +- 8 files changed, 126 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index f952c82faf..e2fd494d16 100644 --- a/README.md +++ b/README.md @@ -1157,7 +1157,7 @@ Defaults: - _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: - `true` (default) - use strict mode and throw an exception when any strict mode restrictions is violated. - - `"log"` - log error or warning when any strict mode restriction is violated (depeding on the severity). + - `"log"` - log warning when any strict mode restriction is violated. - `false` - ignore any strict mode restriction. This option replaced v6 options `strictDefaults`, `strictKeywords` and `strictNumbers` and added additional restrictions - see [Strict Mode](#strict-mode). diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index c43b64835b..2ded322dd4 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,5 +1,6 @@ import {CompilationContext} from "../../types" import {_, getProperty, stringify} from "../codegen" +import {checkStrictMode} from "../../vocabularies/util" export function assignDefaults(it: CompilationContext, ty?: string): void { const {properties, items} = it.schema @@ -12,19 +13,12 @@ export function assignDefaults(it: CompilationContext, ty?: string): void { } } -function assignDefault( - {gen, compositeRule, data, opts, logger}: CompilationContext, - prop: string | number, - defaultValue: any -): void { +function assignDefault(it: CompilationContext, prop: string | number, defaultValue: any): void { + const {gen, compositeRule, data, opts} = it if (defaultValue === undefined) return const childData = _`${data}${getProperty(prop)}` if (compositeRule) { - if (opts.strict) { - const msg = `default is ignored for: ${childData}` - if (opts.strict === "log") logger.warn(msg) - else throw new Error(msg) - } + checkStrictMode(it, `default is ignored for: ${childData}`) return } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 9ee72c1195..5ae656f35e 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,5 +1,6 @@ import {CompilationContext, Options} from "../../types" import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util" +import {checkStrictMode} from "../../vocabularies/util" import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema" import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" import {schemaKeywords} from "./iterate" @@ -75,15 +76,10 @@ function typeAndKeywords(it: CompilationContext, errsCount?: Name): void { schemaKeywords(it, types, !checkedTypes, errsCount) } -function checkUnknownKeywords({schema, RULES, opts, logger}: CompilationContext): void { - if (opts.strict) { - const unknownKeyword = schemaUnknownRules(schema, RULES.keywords) - if (unknownKeyword) { - const msg = `unknown keyword: "${unknownKeyword}"` - if (opts.strict === "log") logger.error(msg) - else throw new Error(msg) - } - } +function checkUnknownKeywords(it: CompilationContext): void { + if (!it.opts.strict) return + const unknownKeyword = schemaUnknownRules(it.schema, it.RULES.keywords) + if (unknownKeyword) checkStrictMode(it, `unknown keyword: "${unknownKeyword}"`) } function checkRefsAndKeywords({ diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 9ebe22d739..5b9cfdddc6 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {alwaysValidSchema} from "../util" +import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" @@ -14,12 +14,7 @@ const def: CodeKeywordDefinition = { const len = gen.const("len", _`${data}.length`) const items = parentSchema.items if (!Array.isArray(items)) { - const {opts, logger} = it - if (opts.strict) { - const msg = '"additionalItems" without "items" is ignored, remove it' - if (opts.strict === "log") logger.warn(msg) - else throw new Error(msg) - } + checkStrictMode(it, '"additionalItems" without "items" is ignored') return } if (schema === false) { diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 3fb485c112..5d4db70cda 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition, CompilationContext} from "../../types" import KeywordContext from "../../compile/context" -import {alwaysValidSchema} from "../util" +import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" @@ -14,7 +14,7 @@ const def: CodeKeywordDefinition = { const hasThen = hasSchema(it, "then") const hasElse = hasSchema(it, "else") if (!hasThen && !hasElse) { - // TODO strict mode: fail or warning if both "then" and "else" are not present + checkStrictMode(it, '"if" without "then" and "else" is ignored') return } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 06096c8784..da004fdc3f 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -76,3 +76,11 @@ export function usePattern(gen: CodeGen, pattern: string): Name { code: _`new RegExp(${pattern})`, }) } + +export function checkStrictMode(it: CompilationContext, msg: string): void { + const {opts, logger} = it + if (opts.strict) { + if (opts.strict === "log") logger.warn(msg) + else throw new Error(msg) + } +} diff --git a/spec/options/strict.spec.js b/spec/options/strict.spec.js index e61731d7a7..8e5e4a3888 100644 --- a/spec/options/strict.spec.js +++ b/spec/options/strict.spec.js @@ -3,70 +3,120 @@ const Ajv = require("../ajv") const should = require("../chai").should() -describe("strict option with additionalItems", () => { - describe("strict = false", () => { - it('should NOT throw an error or log a warning when "additionalItems" is used without "items"', () => { - const output = {} - const ajv = new Ajv({ - strict: false, - logger: getLogger(output), +describe("strict mode", () => { + describe('"additionalItems" is used without "items"', () => { + describe("strict = false", () => { + it("should NOT throw an error or log a warning", () => { + const output = {} + const ajv = new Ajv({ + strict: false, + logger: getLogger(output), + }) + const schema = { + additionalItems: false, + } + ajv.compile(schema) + should.not.exist(output.warning) }) - const schema = { - additionalItems: false, - } - ajv.compile(schema) - should.not.exist(output.warning) }) - }) - describe("strict = true or undefined", () => { - it('should throw an error when "additionalItems" is used without "items"', () => { - test(new Ajv({strict: true})) - test(new Ajv()) + describe("strict = true or undefined", () => { + it("should throw an error", () => { + test(new Ajv({strict: true})) + test(new Ajv()) + + function test(ajv) { + const schema = { + additionalItems: false, + } + should.throw(() => { + ajv.compile(schema) + }) + } + }) + }) - function test(ajv) { + describe('strict = "log"', () => { + it("should log a warning", () => { + const output = {} + const ajv = new Ajv({ + strict: "log", + logger: getLogger(output), + }) const schema = { additionalItems: false, } - should.throw(() => { - ajv.compile(schema) - }) - } + ajv.compile(schema) + ;/additionalItems/.test(output.warning).should.equal(true) + }) }) - }) - describe('strict = "log"', () => { - it('should log a warning when "additionalItems" is used without "items"', () => { - const output = {} - const ajv = new Ajv({ - strict: "log", - logger: getLogger(output), + describe("inside schema that has no known keyword in compound keyword", () => { + it("should throw an error when strict is true or undefined", () => { + test(new Ajv({strict: true})) + test(new Ajv()) + + function test(ajv) { + const schema = { + anyOf: [ + { + additionalItems: false, + }, + ], + } + should.throw(() => { + ajv.compile(schema) + }) + } }) - const schema = { - additionalItems: false, - } - ajv.compile(schema) - ;/additionalItems/.test(output.warning).should.equal(true) }) }) - describe("unknown keyword inside schema that has no known keyword in compound keyword", () => { - it("should throw an error given an unknown keyword when strict is true or undefined", () => { - test(new Ajv({strict: true})) - test(new Ajv()) - - function test(ajv) { + describe('"if" without "then" and "else"', () => { + describe("strict = false", () => { + it("should NOT throw an error or log a warning", () => { + const output = {} + const ajv = new Ajv({ + strict: false, + logger: getLogger(output), + }) const schema = { - anyOf: [ - { - additionalItems: false, - }, - ], + if: true, + } + ajv.compile(schema) + should.not.exist(output.warning) + }) + }) + + describe("strict = true or undefined", () => { + it("should throw an error", () => { + test(new Ajv({strict: true})) + test(new Ajv()) + + function test(ajv) { + const schema = { + if: true, + } + should.throw(() => { + ajv.compile(schema) + }) } - should.throw(() => { - ajv.compile(schema) + }) + }) + + describe('strict = "log"', () => { + it("should log a warning", () => { + const output = {} + const ajv = new Ajv({ + strict: "log", + logger: getLogger(output), }) - } + const schema = { + if: true, + } + ajv.compile(schema) + ;/if/.test(output.warning).should.equal(true) + }) }) }) diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js index cd7f34156f..51c8f2d1a2 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.js @@ -17,7 +17,7 @@ describe("strict option with keywords (replaced strictKeywords)", () => { } ajv.compile(schema) - should.not.exist(output.error) + should.not.exist(output.warning) }) }) @@ -50,7 +50,7 @@ describe("strict option with keywords (replaced strictKeywords)", () => { unknownKeyword: 1, } ajv.compile(schema) - should.equal(output.error, 'unknown keyword: "unknownKeyword"') + should.equal(output.warning, 'unknown keyword: "unknownKeyword"') }) }) @@ -79,11 +79,11 @@ describe("strict option with keywords (replaced strictKeywords)", () => { log() { throw new Error("log should not be called") }, - warn() { - throw new Error("warn should not be called") + warn(msg) { + output.warning = msg }, - error(msg) { - output.error = msg + error() { + throw new Error("warn should not be called") }, } } From 3fc3d6f145b0ab1fee5a77bd8c5d273c14c81b7c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 16:02:55 +0100 Subject: [PATCH 150/322] feat(strict): patterns in "patternProperties" matching "properties" are only allowed with allowMatchingProperties option --- lib/types.ts | 1 + .../applicator/patternProperties.ts | 18 ++- spec/options/strict.spec.js | 131 ++++++------------ 3 files changed, 62 insertions(+), 88 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index d8a475f37f..fd1a733a0b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -42,6 +42,7 @@ export interface CurrentOptions { logger?: Logger | false serialize?: false | ((schema: object | boolean) => any) $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) + allowMatchingProperties?: boolean // exclusion to strict mode } export interface Options extends CurrentOptions { diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index e3b7b3df9c..e35a00adae 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,6 +1,6 @@ import {CodeKeywordDefinition} from "../../types" import KeywordContext from "../../compile/context" -import {schemaProperties, usePattern} from "../util" +import {schemaProperties, usePattern, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -9,14 +9,17 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "object", code(cxt: KeywordContext) { - const {gen, schema, data, it} = cxt + const {gen, schema, data, parentSchema, it} = cxt const patterns = schemaProperties(it, schema) if (patterns.length === 0) return + const checkProperties = + it.opts.strict && !it.opts.allowMatchingProperties && parentSchema.properties const valid = gen.name("valid") validatePatternProperties() function validatePatternProperties() { for (const pat of patterns) { + if (checkProperties) assertNoProperty(pat) if (it.allErrors) { validateProperties(pat) } else { @@ -27,6 +30,17 @@ const def: CodeKeywordDefinition = { } } + function assertNoProperty(pat: string): void { + for (const prop in checkProperties) { + if (new RegExp(pat).test(prop)) { + checkStrictMode( + it, + `property ${prop} matches pattern ${pat} (use allowMatchingProperties)` + ) + } + } + } + function validateProperties(pat: string) { gen.forIn("key", data, (key) => { gen.if(_`${usePattern(gen, pat)}.test(${key})`, () => { diff --git a/spec/options/strict.spec.js b/spec/options/strict.spec.js index 8e5e4a3888..15ccd750bc 100644 --- a/spec/options/strict.spec.js +++ b/spec/options/strict.spec.js @@ -4,75 +4,43 @@ const Ajv = require("../ajv") const should = require("../chai").should() describe("strict mode", () => { - describe('"additionalItems" is used without "items"', () => { - describe("strict = false", () => { - it("should NOT throw an error or log a warning", () => { - const output = {} - const ajv = new Ajv({ - strict: false, - logger: getLogger(output), - }) - const schema = { - additionalItems: false, - } - ajv.compile(schema) - should.not.exist(output.warning) - }) - }) + describe( + '"additionalItems" without "items"', + testStrictMode({type: "array", additionalItems: false}, /additionalItems/) + ) - describe("strict = true or undefined", () => { - it("should throw an error", () => { - test(new Ajv({strict: true})) - test(new Ajv()) + describe('"if" without "then" and "else"', testStrictMode({if: true}, /if/)) - function test(ajv) { - const schema = { - additionalItems: false, - } - should.throw(() => { - ajv.compile(schema) - }) - } - }) - }) - - describe('strict = "log"', () => { - it("should log a warning", () => { - const output = {} - const ajv = new Ajv({ - strict: "log", - logger: getLogger(output), - }) - const schema = { - additionalItems: false, - } - ajv.compile(schema) - ;/additionalItems/.test(output.warning).should.equal(true) - }) - }) - - describe("inside schema that has no known keyword in compound keyword", () => { - it("should throw an error when strict is true or undefined", () => { - test(new Ajv({strict: true})) - test(new Ajv()) + describe( + '"properties" matching "patternProperties"', + testStrictMode( + { + properties: {foo: false}, + patternProperties: {foo: false}, + }, + /property.*pattern/ + ) + ) - function test(ajv) { - const schema = { - anyOf: [ - { - additionalItems: false, - }, - ], - } - should.throw(() => { - ajv.compile(schema) - }) - } + describe('option allowMatchingProperties to allow "properties" matching "patternProperties"', () => { + it("should NOT throw an error or log a warning", () => { + const output = {} + const ajv = new Ajv({ + allowMatchingProperties: true, + logger: getLogger(output), }) + const schema = { + properties: {foo: false}, + patternProperties: {foo: false}, + } + ajv.compile(schema) + should.not.exist(output.warning) }) }) +}) - describe('"if" without "then" and "else"', () => { +function testStrictMode(schema, logPattern) { + return () => { describe("strict = false", () => { it("should NOT throw an error or log a warning", () => { const output = {} @@ -80,9 +48,6 @@ describe("strict mode", () => { strict: false, logger: getLogger(output), }) - const schema = { - if: true, - } ajv.compile(schema) should.not.exist(output.warning) }) @@ -94,9 +59,6 @@ describe("strict mode", () => { test(new Ajv()) function test(ajv) { - const schema = { - if: true, - } should.throw(() => { ajv.compile(schema) }) @@ -111,26 +73,23 @@ describe("strict mode", () => { strict: "log", logger: getLogger(output), }) - const schema = { - if: true, - } ajv.compile(schema) - ;/if/.test(output.warning).should.equal(true) + logPattern.test(output.warning).should.equal(true) }) }) - }) + } +} - function getLogger(output) { - return { - log() { - throw new Error("log should not be called") - }, - warn(msg) { - output.warning = msg - }, - error() { - throw new Error("error should not be called") - }, - } +function getLogger(output) { + return { + log() { + throw new Error("log should not be called") + }, + warn(msg) { + output.warning = msg + }, + error() { + throw new Error("error should not be called") + }, } -}) +} From a8a658cac4ae7aa2acc47c368b34b57939cb394f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 16:22:54 +0100 Subject: [PATCH 151/322] feat(strict): "if" without "then" and "else", "then" or "else" without "if" --- lib/vocabularies/applicator/if.ts | 12 +++++------- lib/vocabularies/applicator/index.ts | 1 + lib/vocabularies/applicator/thenElse.ts | 12 ++++++++++++ spec/options/strict.spec.js | 6 +++++- 4 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 lib/vocabularies/applicator/thenElse.ts diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 5d4db70cda..d58d583ec3 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -7,20 +7,18 @@ import {_, str, Name} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], - implements: ["then", "else"], trackErrors: true, code(cxt: KeywordContext) { - const {gen, it} = cxt - const hasThen = hasSchema(it, "then") - const hasElse = hasSchema(it, "else") - if (!hasThen && !hasElse) { + const {gen, parentSchema, it} = cxt + if (parentSchema.then === undefined && parentSchema.else === undefined) { checkStrictMode(it, '"if" without "then" and "else" is ignored') - return } + const hasThen = hasSchema(it, "then") + const hasElse = hasSchema(it, "else") + if (!hasThen && !hasElse) return const valid = gen.let("valid", true) const schValid = gen.name("_valid") - validateIf() cxt.reset() diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 451864deef..6abc401b18 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -17,6 +17,7 @@ const applicator: Vocabulary = [ require("./oneOf"), require("./allOf"), require("./if"), + require("./thenElse"), ] export default applicator diff --git a/lib/vocabularies/applicator/thenElse.ts b/lib/vocabularies/applicator/thenElse.ts new file mode 100644 index 0000000000..5e9d1e58e0 --- /dev/null +++ b/lib/vocabularies/applicator/thenElse.ts @@ -0,0 +1,12 @@ +import {CodeKeywordDefinition} from "../../types" +import {checkStrictMode} from "../util" + +const def: CodeKeywordDefinition = { + keyword: ["then", "else"], + schemaType: ["object", "boolean"], + code({keyword, parentSchema, it}) { + if (parentSchema.if === undefined) checkStrictMode(it, `"${keyword}" without "if" is ignored`) + }, +} + +module.exports = def diff --git a/spec/options/strict.spec.js b/spec/options/strict.spec.js index 15ccd750bc..a170ed5895 100644 --- a/spec/options/strict.spec.js +++ b/spec/options/strict.spec.js @@ -9,7 +9,11 @@ describe("strict mode", () => { testStrictMode({type: "array", additionalItems: false}, /additionalItems/) ) - describe('"if" without "then" and "else"', testStrictMode({if: true}, /if/)) + describe('"if" without "then" and "else"', testStrictMode({if: true}, /if.*then.*else/)) + + describe('"then" without "if"', testStrictMode({then: true}, /then.*if/)) + + describe('"else" without "if"', testStrictMode({else: true}, /else.*if/)) describe( '"properties" matching "patternProperties"', From 4960c63f03a031a56a2bc84a80a5605a5290a3c6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 19:29:21 +0100 Subject: [PATCH 152/322] docs: strict mode, closes #1258 --- README.md | 69 ++++++++++++++++++++++++++++++++++++------- lib/types.ts | 2 +- spec/security.spec.js | 4 +-- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e2fd494d16..3ac33b0d9c 100644 --- a/README.md +++ b/README.md @@ -247,19 +247,66 @@ CLI is available as a separate npm package [ajv-cli](https://github.com/ajv-vali ## Strict mode +Strict mode intends to prevent any unexpected behaviours or silently ignored mistakes in user schemas. It does not change any validation results compared with JSON Schema specification, but it makes some schemas invalid and throws exception or logs warning (with `strict: "log"` option) in case any restriction is violated. + +The strict mode restrictions are below. To disable these restrictions use option `strict: false`. + +##### Prohibit unknown keywords + +JSON Schema [section 6.5](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-6.5) requires to ignore unknown keywords. The motivation is to increase cross-platform portability of schemas, so that implementations that do not support certain keywords can still do partial validation. + +The problems with this approach are: + +- Different validation results with the same schema and data, leading to bugs and inconsistent behaviours. +- Typos in keywords resulting in keywords being quietly ignored, requiring extensive test coverage of schemas to avoid these mistakes. + +By default Ajv fails schema compilation when unknown keywords are used. Users can explicitly define the keywords that should be allowed and ignored: + +```javascript +ajv.addKeyword("allowedKeyword") +``` + +or + +```javascript +ajv.addVocabulary(["allowed1", "allowed2"]) +``` + +#### Prohibit ignored "additionalItems" keyword + +JSON Schema section [9.3.1.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2) requires to ignore "additionalItems" keyword if "items" keyword is absent. This is inconsistent with the interaction of "additionalProperties" and "properties", and may cause unexpected results. + +By default Ajv fails schema compilation when "additionalItems" is used without "items. + +#### Prohibit ignored "if", "then", "else" keywords + +JSON Schema section [9.2.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2) requires to ignore "if" (only annotations are collected) if both "then" and "else" are absent, and ignore "then"/"else" if "if" is absent. + +By default Ajv fails schema compilation in these cases. + +#### Prohibit overlap between "properties" and "patternProperties" keywords + +The expectation of users (see #196, #286) is that "patternProperties" only apply to properties not already defined in "properties" keyword, but JSON Schema section [9.3.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2) defines these two keywords as independent. It means that to some properties two subschemas can be applied - one defined in "properties" keyword and another defined in "patternProperties" for the pattern matching this property. + +By default Ajv fails schema compilation if a pattern in "patternProperties" matches a property in "properties" in the same schema. + +In addition to allowing such patterns by using option `strict: false`, there is an option `allowMatchingProperties: true` to only allow this case without disabling other strict mode restrictions - there are some rare cases when this is necessary. + +To reiterate, neither this nor other strict mode restrictions change the validation results - they only restrict which schemas are valid. + +#### Prohibit unknown formats + TODO -- _strictDefaults_: report ignored `default` keywords in schemas. Option values: - - `false` (default) - ignored defaults are not reported - - `true` - if an ignored default is present, throw an error - - `"log"` - if an ignored default is present, log warning -- _strictKeywords_: report unknown keywords in schemas. Option values: - - `false` (default) - unknown keywords are not reported - - `true` - if an unknown keyword is present, throw an error - - `"log"` - if an unknown keyword is present, log warning -- _strictNumbers_: validate numbers strictly, failing validation for NaN and Infinity. Option values: - - `false` (default) - NaN or Infinity will pass validation for numeric types - - `true` - NaN or Infinity will not pass validation for numeric types +This will supercede unknownFormats option. + +#### Prohibit ignored defaults + +With `useDefaults` option Ajv modifies validated data by assigning defaults from the schema, but there are different limitations when the defaults can be ignored (see [Assigning defaults](#assigning-defaults)). In strict mode Ajv fails schema compilation if such defaults are used in the schema. + +#### Number validation + +Strict mode also affects number validation. By default Ajv fails `{"type": "number"}` (or `"integer"`) validation for `Infinity` and `NaN`. ## Validation keywords diff --git a/lib/types.ts b/lib/types.ts index fd1a733a0b..5eeeb09357 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -42,7 +42,7 @@ export interface CurrentOptions { logger?: Logger | false serialize?: false | ((schema: object | boolean) => any) $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) - allowMatchingProperties?: boolean // exclusion to strict mode + allowMatchingProperties?: boolean // disables a strict mode restriction } export interface Options extends CurrentOptions { diff --git a/spec/security.spec.js b/spec/security.spec.js index 73a09e5118..febb6dfc81 100644 --- a/spec/security.spec.js +++ b/spec/security.spec.js @@ -12,9 +12,7 @@ var instances = getAjvInstances(options, { jsonSchemaTest(instances, { description: - "Secure schemas tests of " + - instances.length + - " ajv instances with different options", + "Secure schemas tests of " + instances.length + " ajv instances with different options", suites: { security: typeof window == "object" From 58951d6acc13c190018e66eb77a1e09aff7432f1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 19:54:15 +0100 Subject: [PATCH 153/322] remove transpile and async options, remove ajv-async dev dependency --- README.md | 29 ++++++---------------- lib/types.ts | 2 -- package.json | 1 - spec/ajv_async_instances.js | 48 ++++++++++--------------------------- 4 files changed, 20 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 3ac33b0d9c..05c67d3ac9 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ npm install ajv ## Getting started -Try it in the Node.js REPL: https://tonicdev.com/npm/ajv +Try it in the Node.js REPL: https://runkit.com/npm/ajv The fastest validation call: @@ -622,7 +622,7 @@ function loadSchema(uri) { ## Asynchronous validation -Example in Node.js REPL: https://tonicdev.com/esp/ajv-asynchronous-validation +Example in Node.js REPL: https://runkit.com/esp/ajv-asynchronous-validation You can define formats and keywords that perform validation asynchronously by accessing database or some other service. You should add `async: true` in the keyword or format definition (see [addFormat](#api-addformat), [addKeyword](#api-addkeyword) and [User-defined keywords](user-defined-keywords)). @@ -632,7 +632,7 @@ If your schema uses asynchronous formats/keywords or refers to some schema that Validation function for an asynchronous format/keyword should return a promise that resolves with `true` or `false` (or rejects with `new Ajv.ValidationError(errors)` if you want to return errors from the keyword function). -Ajv compiles asynchronous schemas to [es7 async functions](http://tc39.github.io/ecmascript-asyncawait/) that can optionally be transpiled with [nodent](https://github.com/MatAtBread/nodent). Async functions are supported in Node.js 7+ and all modern browsers. You can also supply any other transpiler as a function via `processCode` option. See [Options](#options). +Ajv compiles asynchronous schemas to [async functions](http://tc39.github.io/ecmascript-asyncawait/). Async functions are supported in Node.js 7+ and all modern browsers. You can supply a transpiler as a function via `processCode` option. See [Options](#options). The compiled validation function has `$async: true` property (if the schema is asynchronous), so you can differentiate these functions if you are using both synchronous and asynchronous schemas. @@ -641,10 +641,10 @@ Validation result will be a promise that resolves with validated data or rejects Example: ```javascript -var ajv = new Ajv() -// require('ajv-async')(ajv); +const ajv = new Ajv() -ajv.addKeyword("idExists", { +ajv.addKeyword({ + keyword: "idExists" async: true, type: "number", validate: checkIdExists, @@ -686,22 +686,7 @@ validate({userId: 1, postId: 19}) }) ``` -### Using transpilers with asynchronous validation functions. - -[ajv-async](https://github.com/ajv-validator/ajv-async) uses [nodent](https://github.com/MatAtBread/nodent) to transpile async functions. To use another transpiler you should separately install it (or load its bundle in the browser). - -#### Using nodent - -```javascript -var ajv = new Ajv() -require("ajv-async")(ajv) -// in the browser if you want to load ajv-async bundle separately you can: -// window.ajvAsync(ajv); -var validate = ajv.compile(schema) // transpiled es7 async function -validate(data).then(successFunc).catch(errorFunc) -``` - -#### Using other transpilers +#### Using transpilers ```javascript var ajv = new Ajv({processCode: transpileFunc}) diff --git a/lib/types.ts b/lib/types.ts index 5eeeb09357..b6d5ac713f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -23,8 +23,6 @@ export interface CurrentOptions { removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" - async?: boolean | string - transpile?: string | ((code: string) => string) meta?: boolean | object validateSchema?: boolean | "log" addUsedSchema?: boolean diff --git a/package.json b/package.json index 334c2ded7c..c4a9795698 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "@types/node": "^14.0.27", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", - "ajv-async": "^1.0.0", "ajv-formats": "^0.1.0", "bluebird": "^3.5.3", "brfs": "^2.0.0", diff --git a/spec/ajv_async_instances.js b/spec/ajv_async_instances.js index a35f50d8db..387eb6c196 100644 --- a/spec/ajv_async_instances.js +++ b/spec/ajv_async_instances.js @@ -1,38 +1,16 @@ "use strict" -var Ajv = require("./ajv"), - setupAsync = require("./ajv-async") - -module.exports = getAjvInstances - -var firstTime = true - -function getAjvInstances(opts) { - opts = opts || {} - var instances = [] - var options = [ - {}, - {transpile: true}, - {allErrors: true}, - {transpile: true, allErrors: true}, - ] - - options.forEach((_opts) => { - Object.assign(_opts, opts) - var ajv = getAjv(_opts) - if (ajv) instances.push(ajv) - }) - - if (firstTime) { - console.log("Testing", instances.length, "ajv instances:") - firstTime = false - } - - return instances -} - -function getAjv(opts) { - try { - return setupAsync(new Ajv(opts)) - } catch (e) {} +const getAjvInstances = require("./ajv_instances") + +module.exports = getAjvSyncInstances + +function getAjvSyncInstances(extraOpts) { + return getAjvInstances( + { + strict: false, + allErrors: true, + codegen: {lines: true}, + }, + extraOpts + ) } From efc63161945e8ad734a44c162f40ac50b0a6a6b2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 20:00:12 +0100 Subject: [PATCH 154/322] docs: update options --- README.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 05c67d3ac9..af77c176dd 100644 --- a/README.md +++ b/README.md @@ -1146,6 +1146,7 @@ Defaults: { // strict mode options strict: true, + allowMatchingProperties: false, // validation and reporting options: $data: false, allErrors: false, @@ -1164,8 +1165,6 @@ Defaults: removeAdditional: false, useDefaults: false, coerceTypes: false, - // asynchronous validation options: - transpile: undefined, // requires ajv-async package // advanced options: meta: true, validateSchema: true, @@ -1185,14 +1184,13 @@ Defaults: } ``` -##### Strict mode option +##### Strict mode options - _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: - `true` (default) - use strict mode and throw an exception when any strict mode restrictions is violated. - `"log"` - log warning when any strict mode restriction is violated. - `false` - ignore any strict mode restriction. - -This option replaced v6 options `strictDefaults`, `strictKeywords` and `strictNumbers` and added additional restrictions - see [Strict Mode](#strict-mode). +- _allowMatchingProperties_: pass true to allow overlap between "properties" and "patternProperties". See [Strict Mode](#strict-mode). ##### Validation and reporting options @@ -1245,13 +1243,6 @@ This option replaced v6 options `strictDefaults`, `strictKeywords` and `strictNu - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). -##### Asynchronous validation options - -- _transpile_: Requires [ajv-async](https://github.com/ajv-validator/ajv-async) package. It determines whether Ajv transpiles compiled asynchronous validation function. Option values: - - `undefined` (default) - transpile with [nodent](https://github.com/MatAtBread/nodent) if async functions are not supported. - - `true` - always transpile with nodent. - - `false` - do not transpile; if async functions are not supported an exception will be thrown. - ##### Advanced options - _meta_: add [meta-schema](http://json-schema.org/documentation.html) so it can be used by other schemas (true by default). If an object is passed, it will be used as the default meta-schema for schemas that have no `$schema` keyword. This default meta-schema MUST have `$schema` keyword. @@ -1271,9 +1262,7 @@ This option replaced v6 options `strictDefaults`, `strictKeywords` and `strictNu - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). - _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when messages are generated outside of Ajv code (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). - _sourceCode_: add `sourceCode` property to validating function (for debugging; this code can be different from the result of toString call). -- _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. Starting from version 5.0.0 this option replaced options: - - `beautify` that formatted the generated function using [js-beautify](https://github.com/beautify-web/js-beautify). If you want to beautify the generated code pass a function calling `require('js-beautify').js_beautify` as `processCode: code => js_beautify(code)`. - - `transpile` that transpiled asynchronous validation function. You can still use `transpile` option with [ajv-async](https://github.com/ajv-validator/ajv-async) package. See [Asynchronous validation](#asynchronous-validation) for more information. +- _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)`, `del(key)` and `clear()`. - _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used. - _jsPropertySyntax_ (deprecated) - set to `true` to report `dataPath` in errors as in v6, using JavaScript property syntax (e.g., `".prop[1].subProp"`). By default `dataPath` in errors is reported as JSON pointer. This option is added for backward compatibility and is not recommended - this format is difficult to parse even in JS code. From ee65b8496894111af8cd816981d984c8141a5ae3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 3 Sep 2020 20:09:13 +0100 Subject: [PATCH 155/322] refactor: use checkStrictMode --- lib/compile/validate/index.ts | 7 +++---- lib/vocabularies/applicator/patternProperties.ts | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 5ae656f35e..b1bbc3068a 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -98,11 +98,10 @@ function checkRefsAndKeywords({ } } -function checkNoDefault({schema, opts, logger}: CompilationContext): void { +function checkNoDefault(it: CompilationContext): void { + const {schema, opts} = it if (schema.default !== undefined && opts.useDefaults && opts.strict) { - const msg = "default is ignored in the schema root" - if (opts.strict === "log") logger.warn(msg) - else throw new Error(msg) + checkStrictMode(it, "default is ignored in the schema root") } } diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index e35a00adae..dabbdd2117 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -19,7 +19,7 @@ const def: CodeKeywordDefinition = { function validatePatternProperties() { for (const pat of patterns) { - if (checkProperties) assertNoProperty(pat) + if (checkProperties) checkMatchingProperties(pat) if (it.allErrors) { validateProperties(pat) } else { @@ -30,7 +30,7 @@ const def: CodeKeywordDefinition = { } } - function assertNoProperty(pat: string): void { + function checkMatchingProperties(pat: string): void { for (const prop in checkProperties) { if (new RegExp(pat).test(prop)) { checkStrictMode( From 1187c6ec1e03bd891ad7983ce43039e693c4f7d7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 4 Sep 2020 20:37:23 +0100 Subject: [PATCH 156/322] refactor: convert Ajv to class --- .eslintrc.yml | 1 + lib/ajv.ts | 917 ++++++++++++++++++------------- lib/cache.ts | 11 +- lib/compile/async.ts | 86 --- lib/compile/codegen.ts | 4 +- lib/compile/index.ts | 14 +- lib/compile/resolve.ts | 10 +- lib/compile/schema_obj.ts | 8 - lib/compile/stored_schema.ts | 38 ++ lib/compile/validate/dataType.ts | 9 +- lib/data.ts | 29 - lib/keyword.ts | 127 ----- lib/types.ts | 46 +- 13 files changed, 653 insertions(+), 647 deletions(-) delete mode 100644 lib/compile/async.ts delete mode 100644 lib/compile/schema_obj.ts create mode 100644 lib/compile/stored_schema.ts delete mode 100644 lib/data.ts delete mode 100644 lib/keyword.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index b8152071b5..dacbb32c16 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -34,6 +34,7 @@ overrides: "@typescript-eslint/no-empty-function": off "@typescript-eslint/no-this-alias": off "@typescript-eslint/no-implied-eval": off + "@typescript-eslint/no-floating-promises": off no-invalid-this: off rules: block-scoped-var: error diff --git a/lib/ajv.ts b/lib/ajv.ts index e6967bc93b..a5e6fd822d 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -1,11 +1,25 @@ -import {Vocabulary, KeywordDefinition, Options} from "./types" -import SchemaObject from "./compile/schema_obj" +import { + Schema, + SchemaObject, + Vocabulary, + KeywordDefinition, + Options, + ValidateFunction, + CacheInterface, + Logger, + ErrorObject, + Format, + AddedFormat, + LoadSchemaFunction, +} from "./types" +import StoredSchema from "./compile/stored_schema" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" -import rules from "./compile/rules" -import $dataMetaSchema from "./data" +import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" +import {checkType} from "./compile/validate/dataType" +import {SchemaRoot, Compilation} from "./compile" -var compileSchema = require("./compile"), +const compileSchema = require("./compile"), resolve = require("./compile/resolve"), stableStringify = require("fast-json-stable-stringify") @@ -14,300 +28,449 @@ import validationVocabulary from "./vocabularies/validation" import applicatorVocabulary from "./vocabularies/applicator" import formatVocabulary from "./vocabularies/format" import {metadataVocabulary, contentVocabulary} from "./vocabularies/metadata" -import {addVocabulary, addKeyword, getKeyword, removeKeyword} from "./keyword" -module.exports = Ajv +const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" -Ajv.prototype.validate = validate -Ajv.prototype.compile = compile -Ajv.prototype.addSchema = addSchema -Ajv.prototype.addMetaSchema = addMetaSchema -Ajv.prototype.validateSchema = validateSchema -Ajv.prototype.getSchema = getSchema -Ajv.prototype.removeSchema = removeSchema -Ajv.prototype.addFormat = addFormat -Ajv.prototype.errorsText = errorsText -Ajv.prototype.addVocabulary = addVocabulary -Ajv.prototype.addKeyword = addKeyword -Ajv.prototype.getKeyword = getKeyword -Ajv.prototype.removeKeyword = removeKeyword +const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] +const META_SUPPORT_DATA = ["/properties"] -Ajv.prototype._addSchema = _addSchema -Ajv.prototype._compile = _compile +type CompileAsyncCallback = (err: Error | null, validate?: ValidateFunction) => void + +export default class Ajv { + _opts: Options + _cache: CacheInterface + _schemas: {[key: string]: StoredSchema} = {} + _refs: {[ref: string]: StoredSchema | string} = {} + _fragments: {[key: string]: StoredSchema} = {} + _formats: {[name: string]: AddedFormat} = {} + _compilations: Compilation[] = [] + _loadingSchemas: {[ref: string]: Promise} = {} + _metaOpts: Options + RULES: ValidationRules + logger: Logger + errors?: ErrorObject[] | null // errors from the last validation + + static ValidationError = ValidationError + static MissingRefError = MissingRefError + + constructor(opts: Options = {}) { + opts = this._opts = {strict: true, ...opts} + this.logger = getLogger(opts.logger) + const formatOpt = opts.format + opts.format = false + + this._cache = opts.cache || new Cache() + this.RULES = rules() + checkDeprecatedOptions.call(this, opts) + if (opts.serialize === undefined) opts.serialize = stableStringify + this._metaOpts = getMetaSchemaOptions.call(this) + + if (opts.formats) addInitialFormats.call(this) + this.addVocabulary(["$async"]) + this.addVocabulary(coreVocabulary) + this.addVocabulary(validationVocabulary) + this.addVocabulary(applicatorVocabulary) + this.addVocabulary(formatVocabulary) + this.addVocabulary(metadataVocabulary) + this.addVocabulary(contentVocabulary) + if (opts.keywords) addInitialKeywords.call(this, opts.keywords) + addDefaultMetaSchema.call(this) + if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) + addInitialSchemas.call(this) + opts.format = formatOpt + } -Ajv.prototype.compileAsync = require("./compile/async") -Ajv.prototype.$dataMetaSchema = $dataMetaSchema + // Validate data using schema + // Schema will be compiled and cached using as a key JSON serialized with + // [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) + validate( + schemaKeyRef: Schema | string, // key, ref or schema object + data: unknown // to be validated + ): boolean | Promise { + let v: ValidateFunction | undefined + if (typeof schemaKeyRef == "string") { + v = this.getSchema(schemaKeyRef) + if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"') + } else { + const schemaObj = _addSchema.call(this, schemaKeyRef) + v = schemaObj.validate || this._compile(schemaObj) + } -Ajv.ValidationError = ValidationError -Ajv.MissingRefError = MissingRefError + const valid = v(data) + if (v.$async !== true) this.errors = v.errors + return valid + } -var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" + // Create validation function for passed schema + compile( + schema: Schema, + _meta?: boolean // true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. + ): ValidateFunction { + const schemaObj = _addSchema.call(this, schema, undefined, _meta) + return schemaObj.validate || this._compile(schemaObj) + } -var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] -const META_SUPPORT_DATA = ["/properties"] + _compile(this: Ajv, schemaObj: StoredSchema, root?: SchemaRoot): ValidateFunction { + if (schemaObj.compiling) return _makeValidate(schemaObj, root) + schemaObj.compiling = true + const v = _tryCompile.call(this, schemaObj, root) + schemaObj.validate = v + schemaObj.refs = v.refs + schemaObj.refVal = v.refVal + schemaObj.root = v.root + return v + } -/** - * Creates validator instance. - * Usage: `Ajv(opts)` - * @param {Object} opts optional options - * @return {Object} ajv instance - */ -export default function Ajv(opts: Options = {}): void { - if (!(this instanceof Ajv)) return new Ajv(opts) - opts = this._opts = {strict: true, ...opts} - setLogger(this) - this._schemas = {} - this._refs = {} - this._fragments = {} - this._formats = {} - var formatOpt = opts.format - opts.format = false - - this._cache = opts.cache || new Cache() - this._loadingSchemas = {} - this._compilations = [] - this.RULES = rules() - checkDeprecatedOptions.call(this, opts) - if (opts.serialize === undefined) opts.serialize = stableStringify - this._metaOpts = getMetaSchemaOptions(this) - - if (opts.formats) addInitialFormats(this) - this.addVocabulary(["$async"]) - this.addVocabulary(coreVocabulary) - this.addVocabulary(validationVocabulary) - this.addVocabulary(applicatorVocabulary) - this.addVocabulary(formatVocabulary) - this.addVocabulary(metadataVocabulary) - this.addVocabulary(contentVocabulary) - if (opts.keywords) addInitialKeywords(this, opts.keywords) - addDefaultMetaSchema(this) - if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) - addInitialSchemas(this) - opts.format = formatOpt -} + // Creates validating function for passed schema with asynchronous loading of missing schemas. + // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. + compileAsync( + schema: SchemaObject, + metaOrCallback?: boolean | CompileAsyncCallback, // optional true to compile meta-schema; this parameter can be skipped + callback?: CompileAsyncCallback + ): Promise { + /* eslint no-shadow: 0 */ + const self = this + if (typeof this._opts.loadSchema != "function") { + throw new Error("options.loadSchema should be a function") + } + const loadSchema: LoadSchemaFunction = this._opts.loadSchema + let meta: boolean | undefined + if (typeof metaOrCallback == "function") callback = metaOrCallback + else meta = metaOrCallback + + return runCompileAsync(schema, meta, callback) + + function runCompileAsync(sch: SchemaObject, _meta?: boolean, cb?: CompileAsyncCallback) { + const p = loadMetaSchemaOf(sch).then(() => { + const schemaObj = _addSchema.call(self, sch, undefined, _meta) + return schemaObj.validate || _compileAsync(schemaObj) + }) + if (cb) p.then((v) => cb(null, v), cb) + return p + } -function checkDeprecatedOptions(this, opts: Options) { - if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath") - if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId") - if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") - if (opts.jsPropertySyntax !== undefined) this.logger.warn("DEPRECATED: option jsPropertySyntax") - if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") -} + function loadMetaSchemaOf(sch: SchemaObject): Promise { + const $schema = sch.$schema + return $schema && !self.getSchema($schema) + ? runCompileAsync({$ref: $schema}, true) + : Promise.resolve() + } -/** - * Validate data using schema - * Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize. - * @this Ajv - * @param {String|Object} schemaKeyRef key, ref or schema object - * @param {Any} data to be validated - * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). - */ -function validate(schemaKeyRef, data) { - var v - if (typeof schemaKeyRef == "string") { - v = this.getSchema(schemaKeyRef) - if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"') - } else { - var schemaObj = this._addSchema(schemaKeyRef) - v = schemaObj.validate || this._compile(schemaObj) + function _compileAsync(schemaObj: StoredSchema): ValidateFunction | Promise { + try { + return self._compile(schemaObj) + } catch (e) { + if (e instanceof MissingRefError) return loadMissingSchema(schemaObj, e) + throw e + } + } + + async function loadMissingSchema( + schemaObj: StoredSchema, + e: MissingRefError + ): Promise { + const ref = e.missingSchema + if (added(ref)) { + throw new Error(`Schema ${ref} is loaded but ${e.missingRef} cannot be resolved`) + } + let schPromise = self._loadingSchemas[ref] + if (schPromise === undefined) { + schPromise = self._loadingSchemas[ref] = loadSchema(ref) + schPromise.then(removePromise, removePromise) + } + + const sch = await schPromise + if (!added(ref)) { + await loadMetaSchemaOf(sch) + if (!added(ref)) self.addSchema(sch, ref, undefined, meta) + } + return _compileAsync(schemaObj) + + function removePromise(): void { + delete self._loadingSchemas[ref] + } + } + + function added(ref: string): string | StoredSchema { + return self._refs[ref] || self._schemas[ref] + } } - var valid = v(data) - if (v.$async !== true) this.errors = v.errors - return valid -} + // Adds schema to the instance + addSchema( + schema: Schema | Schema[], // If array is passed, `key` will be ignored + key?: string, // Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. + _skipValidation?: boolean, //true to skip schema validation. Used internally, option validateSchema should be used instead. + _meta?: boolean // true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. + ): Ajv { + if (Array.isArray(schema)) { + for (const sch of schema) this.addSchema(sch, undefined, _skipValidation, _meta) + return this + } + let id: string | undefined + if (typeof schema === "object") { + id = schema.$id + if (id !== undefined && typeof id != "string") throw new Error("schema id must be string") + } + key = resolve.normalizeId(key || id) + checkUnique.call(this, key) + this._schemas[key] = _addSchema.call(this, schema, _skipValidation, _meta, true) + return this + } -/** - * Create validating function for passed schema. - * @this Ajv - * @param {Object} schema schema object - * @param {Boolean} _meta true if schema is a meta-schema. - * @return {Function} validating function - */ -function compile(schema, _meta) { - var schemaObj = this._addSchema(schema, undefined, _meta) - return schemaObj.validate || this._compile(schemaObj) -} + // Add schema that will be used to validate other schemas + // options in META_IGNORE_OPTIONS are alway set to false + addMetaSchema( + schema: SchemaObject, + key?: string, // schema key + skipValidation?: boolean // true to skip schema validation, can be used to override validateSchema option for meta-schema + ): Ajv { + this.addSchema(schema, key, skipValidation, true) + return this + } -/** - * Adds schema to the instance. - * @this Ajv - * @param {Object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. - * @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. - * @param {Boolean} _skipValidation true to skip schema validation. Used internally, option validateSchema should be used instead. - * @param {Boolean} _meta true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. - * @return {Ajv} this for method chaining - */ -function addSchema(schema, key, _skipValidation, _meta) { - if (Array.isArray(schema)) { - for (var i = 0; i < schema.length; i++) { - this.addSchema(schema[i], undefined, _skipValidation, _meta) + // Validate schema against its meta-schema + validateSchema(schema: Schema, throwOrLogError?: boolean): boolean | Promise { + if (typeof schema == "boolean") return true + let $schema: string | SchemaObject | undefined = schema.$schema + if ($schema !== undefined && typeof $schema != "string") { + throw new Error("$schema must be a string") + } + $schema = $schema || this._opts.defaultMeta || defaultMeta.call(this) + if (!$schema) { + this.logger.warn("meta-schema not available") + this.errors = null + return true + } + const valid = this.validate($schema, schema) + if (!valid && throwOrLogError) { + const message = "schema is invalid: " + this.errorsText() + if (this._opts.validateSchema === "log") this.logger.error(message) + else throw new Error(message) } + return valid + } + + // Get compiled schema by `key` or `ref`. + getSchema( + keyRef: string // `key` that was passed to `addSchema` or full schema reference (`schema.$id` or resolved id). + ): ValidateFunction | undefined { + const schemaObj = _getSchemaObj.call(this, keyRef) + switch (typeof schemaObj) { + case "object": + return schemaObj.validate || this._compile(schemaObj) + case "string": + return this.getSchema(schemaObj) + case "undefined": + return _getSchemaFragment.call(this, keyRef) + } + } + + // Remove cached schema(s). + // If no parameter is passed all schemas but meta-schemas are removed. + // If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. + // Even if schema is referenced by other schemas it still can be removed as other schemas have local references. + removeSchema(schemaKeyRef: Schema | string | RegExp): Ajv { + if (schemaKeyRef instanceof RegExp) { + _removeAllSchemas.call(this, this._schemas, schemaKeyRef) + _removeAllSchemas.call(this, this._refs, schemaKeyRef) + return this + } + switch (typeof schemaKeyRef) { + case "undefined": + _removeAllSchemas.call(this, this._schemas) + _removeAllSchemas.call(this, this._refs) + this._cache.clear() + return this + case "string": { + const schemaObj = _getSchemaObj.call(this, schemaKeyRef) + if (schemaObj) this._cache.del(schemaObj.cacheKey) + delete this._schemas[schemaKeyRef] + delete this._refs[schemaKeyRef] + return this + } + case "object": { + const serialize = this._opts.serialize + const cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef + this._cache.del(cacheKey) + let id = schemaKeyRef.$id + if (id) { + id = resolve.normalizeId(id) + delete this._schemas[id] + delete this._refs[id] + } + } + } + return this + } + + // add "vocabulary" - a collection of keywords + addVocabulary(definitions: Vocabulary): Ajv { + for (const def of definitions) this.addKeyword(def) + return this + } + + addKeyword(kwdOrDef: string | KeywordDefinition): Ajv + addKeyword( + kwdOrDef: string | KeywordDefinition, + def?: KeywordDefinition // deprecated + ): Ajv { + let keyword: string | string[] + if (typeof kwdOrDef == "string") { + keyword = kwdOrDef + if (typeof def == "object") { + this.logger.warn("these parameters are deprecated, see docs for addKeyword") + def.keyword = keyword + } + } else if (typeof kwdOrDef == "object" && def === undefined) { + def = kwdOrDef + keyword = def.keyword + } else { + throw new Error("invalid addKeywords parameters") + } + + checkKeyword.call(this, keyword, def) + if (def) keywordMetaschema.call(this, def) + eachItem(keyword, (kwd) => { + eachItem(def?.type, (t) => _addRule.call(this, kwd, t, def)) + }) return this } - var id = schema.$id - if (id !== undefined && typeof id != "string") { - throw new Error("schema id must be string") + + getKeyword(keyword: string): KeywordDefinition | boolean { + const rule = this.RULES.all[keyword] + return typeof rule == "object" ? rule.definition : !!rule + } + + // Remove keyword + removeKeyword(keyword: string): Ajv { + // TODO return type should be Ajv + const RULES: ValidationRules = this.RULES + delete RULES.keywords[keyword] + delete RULES.all[keyword] + for (const group of RULES.rules) { + const i = group.rules.findIndex((rule) => rule.keyword === keyword) + if (i >= 0) group.rules.splice(i, 1) + } + return this + } + + // Add format + addFormat(name: string, format: Format): Ajv { + if (typeof format == "string") format = new RegExp(format) + this._formats[name] = format + return this + } + + errorsText( + errors: ErrorObject[] | null | undefined = this.errors, // optional array of validation errors + {separator = ", ", dataVar = "data"}: ErrorsTextOptions = {} // optional options with properties `separator` and `dataVar` + ): string { + if (!errors || errors.length === 0) return "No errors" + return errors + .map((e) => dataVar + e.dataPath + " " + e.message) + .reduce((text, msg) => text + msg + separator) + } + + $dataMetaSchema(metaSchema: SchemaObject, keywordsJsonPointers: string[]): SchemaObject { + const rules = this.RULES.all + for (const jsonPointer of keywordsJsonPointers) { + metaSchema = JSON.parse(JSON.stringify(metaSchema)) + const segments = jsonPointer.split("/").slice(1) // first segment is an empty string + let keywords = metaSchema + for (const seg of segments) keywords = keywords[seg] + + for (const key in rules) { + const rule = rules[key] + if (typeof rule != "object") continue + const $data = rule.definition?.$data + const schema = keywords[key] + if ($data && schema) keywords[key] = schemaOrData(schema) + } + } + + return metaSchema } - key = resolve.normalizeId(key || id) - checkUnique(this, key) - this._schemas[key] = this._addSchema(schema, _skipValidation, _meta, true) - return this } -/** - * Add schema that will be used to validate other schemas - * options in META_IGNORE_OPTIONS are alway set to false - * @this Ajv - * @param {Object} schema schema object - * @param {String} key optional schema key - * @param {Boolean} skipValidation true to skip schema validation, can be used to override validateSchema option for meta-schema - * @return {Ajv} this for method chaining - */ -function addMetaSchema(schema, key, skipValidation) { - this.addSchema(schema, key, skipValidation, true) - return this +export interface ErrorsTextOptions { + separator?: string + dataVar?: string } -/** - * Validate schema - * @this Ajv - * @param {Object} schema schema to validate - * @param {Boolean} throwOrLogError pass true to throw (or log) an error if invalid - * @return {Boolean} true if schema is valid - */ -function validateSchema(schema, throwOrLogError) { - var $schema = schema.$schema - if ($schema !== undefined && typeof $schema != "string") { - throw new Error("$schema must be a string") - } - $schema = $schema || this._opts.defaultMeta || defaultMeta(this) - if (!$schema) { - this.logger.warn("meta-schema not available") - this.errors = null - return true - } - var valid = this.validate($schema, schema) - if (!valid && throwOrLogError) { - var message = "schema is invalid: " + this.errorsText() - if (this._opts.validateSchema === "log") this.logger.error(message) - else throw new Error(message) - } - return valid +function checkDeprecatedOptions(this: Ajv, opts: Options) { + if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath") + if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId") + if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") + if (opts.jsPropertySyntax !== undefined) this.logger.warn("DEPRECATED: option jsPropertySyntax") + if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") } -function defaultMeta(self) { - var meta = self._opts.meta - self._opts.defaultMeta = +function defaultMeta(this: Ajv): string | SchemaObject | undefined { + const meta = this._opts.meta + this._opts.defaultMeta = typeof meta == "object" ? meta.$id || meta - : self.getSchema(META_SCHEMA_ID) + : this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined - return self._opts.defaultMeta + return this._opts.defaultMeta } -/** - * Get compiled schema from the instance by `key` or `ref`. - * @this Ajv - * @param {String} keyRef `key` that was passed to `addSchema` or full schema reference (`schema.id` or resolved id). - * @return {Function} schema validating function (with property `schema`). - */ -function getSchema(keyRef) { - var schemaObj = _getSchemaObj(this, keyRef) - switch (typeof schemaObj) { - case "object": - return schemaObj.validate || this._compile(schemaObj) - case "string": - return this.getSchema(schemaObj) - case "undefined": - return _getSchemaFragment(this, keyRef) - } +function _getSchemaFragment(this: Ajv, ref: string): ValidateFunction | undefined { + const res = resolve.schema.call(this, {schema: {}}, ref) + if (!res) return + const schema = res.schema + const root = res.root + const baseId = res.baseId + const validate = compileSchema.call(this, schema, root, undefined, baseId) + this._fragments[ref] = new StoredSchema({ + ref, + fragment: true, + schema, + root, + baseId, + validate, + }) + return validate } -function _getSchemaFragment(self, ref) { - var res = resolve.schema.call(self, {schema: {}}, ref) - if (res) { - var schema = res.schema, - root = res.root, - baseId = res.baseId - var v = compileSchema.call(self, schema, root, undefined, baseId) - self._fragments[ref] = new SchemaObject({ - ref: ref, - fragment: true, - schema: schema, - root: root, - baseId: baseId, - validate: v, - }) - return v - } -} - -function _getSchemaObj(self, keyRef) { +function _getSchemaObj(this: Ajv, keyRef: string): StoredSchema | undefined { keyRef = resolve.normalizeId(keyRef) - return self._schemas[keyRef] || self._refs[keyRef] || self._fragments[keyRef] + return this._schemas[keyRef] || this._refs[keyRef] || this._fragments[keyRef] } -/** - * Remove cached schema(s). - * If no parameter is passed all schemas but meta-schemas are removed. - * If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. - * Even if schema is referenced by other schemas it still can be removed as other schemas have local references. - * @this Ajv - * @param {String|Object|RegExp} schemaKeyRef key, ref, pattern to match key/ref or schema object - * @return {Ajv} this for method chaining - */ -function removeSchema(schemaKeyRef) { - if (schemaKeyRef instanceof RegExp) { - _removeAllSchemas(this, this._schemas, schemaKeyRef) - _removeAllSchemas(this, this._refs, schemaKeyRef) - return this - } - switch (typeof schemaKeyRef) { - case "undefined": - _removeAllSchemas(this, this._schemas) - _removeAllSchemas(this, this._refs) - this._cache.clear() - return this - case "string": - var schemaObj = _getSchemaObj(this, schemaKeyRef) - if (schemaObj) this._cache.del(schemaObj.cacheKey) - delete this._schemas[schemaKeyRef] - delete this._refs[schemaKeyRef] - return this - case "object": - var serialize = this._opts.serialize - var cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef - this._cache.del(cacheKey) - var id = schemaKeyRef.$id - if (id) { - id = resolve.normalizeId(id) - delete this._schemas[id] - delete this._refs[id] +function _removeAllSchemas( + this: Ajv, + schemas: {[ref: string]: StoredSchema | string}, + regex?: RegExp +) { + for (const keyRef in schemas) { + const schemaObj = schemas[keyRef] + if (!regex || regex.test(keyRef)) { + if (typeof schemaObj == "string") { + delete schemas[keyRef] + } else if (!schemaObj.meta) { + this._cache.del(schemaObj.cacheKey) + delete schemas[keyRef] } - } - return this -} - -function _removeAllSchemas(self, schemas, regex?: RegExp) { - for (var keyRef in schemas) { - var schemaObj = schemas[keyRef] - if (!schemaObj.meta && (!regex || regex.test(keyRef))) { - self._cache.del(schemaObj.cacheKey) - delete schemas[keyRef] } } } -/* @this Ajv */ -function _addSchema(schema: {[x: string]: any} | boolean, skipValidation, meta, shouldAddSchema) { +function _addSchema( + this: Ajv, + schema: Schema, + skipValidation?: boolean, + meta?: boolean, + shouldAddSchema?: boolean +) { if (typeof schema != "object" && typeof schema != "boolean") { - throw new Error("schema should be object or boolean") + throw new Error("schema must be object or boolean") } - var serialize = this._opts.serialize - var cacheKey = serialize ? serialize(schema) : schema - var cached = this._cache.get(cacheKey) + const serialize = this._opts.serialize + const cacheKey = serialize ? serialize(schema) : schema + const cached = this._cache.get(cacheKey) if (cached) return cached shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false @@ -317,18 +480,18 @@ function _addSchema(schema: {[x: string]: any} | boolean, skipValidation, meta, $id = schema.$id $schema = schema.$schema } - var id = resolve.normalizeId($id) - if (id && shouldAddSchema) checkUnique(this, id) + const id = resolve.normalizeId($id) + if (id && shouldAddSchema) checkUnique.call(this, id) - var willValidate = this._opts.validateSchema !== false && !skipValidation - var recursiveMeta + const willValidate = this._opts.validateSchema !== false && !skipValidation + let recursiveMeta if (willValidate && !(recursiveMeta = id && id === resolve.normalizeId($schema))) { this.validateSchema(schema, true) } - var localRefs = resolve.ids.call(this, schema) + const localRefs = resolve.ids.call(this, schema) - var schemaObj = new SchemaObject({id, schema, localRefs, cacheKey, meta}) + const schemaObj = new StoredSchema({id, schema, localRefs, cacheKey, meta}) if (id[0] !== "#" && shouldAddSchema) this._refs[id] = schemaObj this._cache.put(cacheKey, schemaObj) @@ -338,25 +501,9 @@ function _addSchema(schema: {[x: string]: any} | boolean, skipValidation, meta, return schemaObj } -/* @this Ajv */ -function _compile(schemaObj, root) { - if (schemaObj.compiling) return _makeValidate(schemaObj, root) - schemaObj.compiling = true - const v = _tryCompile.call(this, schemaObj, root) - schemaObj.validate = v - schemaObj.refs = v.refs - schemaObj.refVal = v.refVal - schemaObj.root = v.root - return v -} - -/* @this Ajv */ -function _tryCompile(schemaObj, root) { - var currentOpts - if (schemaObj.meta) { - currentOpts = this._opts - this._opts = this._metaOpts - } +function _tryCompile(this: Ajv, schemaObj: StoredSchema, root?: SchemaRoot) { + const currentOpts = this._opts + if (schemaObj.meta) this._opts = this._metaOpts try { return compileSchema.call(this, schemaObj.schema, root, schemaObj.localRefs) @@ -369,126 +516,158 @@ function _tryCompile(schemaObj, root) { } } -function _makeValidate(schemaObj, root) { +function _makeValidate(schemaObj: StoredSchema, root?: SchemaRoot): ValidateFunction { + const callValidate: ValidateFunction = function (this: Ajv | any, ...args) { + if (schemaObj.validate === undefined) throw new Error("ajv implementation error") + const _validate = schemaObj.validate + const result = _validate.apply(this, args) + callValidate.errors = _validate.errors + return result + } + const sch = schemaObj.schema schemaObj.validate = callValidate - callValidate.schema = schemaObj.schema + callValidate.schema = sch callValidate.errors = null callValidate.root = root ? root : callValidate - if (schemaObj.schema.$async === true) callValidate.$async = true + if (typeof sch == "object" && sch.$async === true) callValidate.$async = true return callValidate - - /* @this {*} - custom context, see passContext option */ - function callValidate(...args) { - /* jshint validthis: true */ - var _validate = schemaObj.validate - var result = _validate.apply(this, args) - callValidate.errors = _validate.errors - return result - } -} - -/** - * Convert array of error message objects to string - * @this Ajv - * @param {Array} errors optional array of validation errors, if not passed errors from the instance are used. - * @param {Object} options optional options with properties `separator` and `dataVar`. - * @return {String} human readable string with all errors descriptions - */ -function errorsText(errors, options) { - errors = errors || this.errors - if (!errors) return "No errors" - options = options || {} - var separator = options.separator === undefined ? ", " : options.separator - var dataVar = options.dataVar === undefined ? "data" : options.dataVar - - var text = "" - for (var i = 0; i < errors.length; i++) { - var e = errors[i] - if (e) text += dataVar + e.dataPath + " " + e.message + separator - } - return text.slice(0, -separator.length) } -/** - * Add format - * @this Ajv - * @param {String} name format name - * @param {String|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) - * @return {Ajv} this for method chaining - */ -function addFormat(name, format) { - if (typeof format == "string") format = new RegExp(format) - this._formats[name] = format - return this -} - -function addDefaultMetaSchema(self) { - var $dataSchema - if (self._opts.$data) { +function addDefaultMetaSchema(this: Ajv): void { + let $dataSchema + if (this._opts.$data) { $dataSchema = require("./refs/data.json") - self.addMetaSchema($dataSchema, $dataSchema.$id, true) + this.addMetaSchema($dataSchema, $dataSchema.$id, true) } - if (self._opts.meta === false) return - var metaSchema = require("./refs/json-schema-draft-07.json") - if (self._opts.$data) { - metaSchema = self.$dataMetaSchema(metaSchema, META_SUPPORT_DATA) + if (this._opts.meta === false) return + let metaSchema = require("./refs/json-schema-draft-07.json") + if (this._opts.$data) { + metaSchema = this.$dataMetaSchema(metaSchema, META_SUPPORT_DATA) } - self.addMetaSchema(metaSchema, META_SCHEMA_ID, true) - self._refs["http://json-schema.org/schema"] = META_SCHEMA_ID + this.addMetaSchema(metaSchema, META_SCHEMA_ID, true) + this._refs["http://json-schema.org/schema"] = META_SCHEMA_ID } -function addInitialSchemas(self) { - var optsSchemas = self._opts.schemas +function addInitialSchemas(this: Ajv): void { + const optsSchemas = this._opts.schemas if (!optsSchemas) return - if (Array.isArray(optsSchemas)) self.addSchema(optsSchemas) - else for (var key in optsSchemas) self.addSchema(optsSchemas[key], key) + if (Array.isArray(optsSchemas)) this.addSchema(optsSchemas) + else for (const key in optsSchemas) this.addSchema(optsSchemas[key], key) } -function addInitialFormats(self) { - for (var name in self._opts.formats) { - var format = self._opts.formats[name] - self.addFormat(name, format) +function addInitialFormats(this: Ajv): void { + for (const name in this._opts.formats) { + const format = this._opts.formats[name] + this.addFormat(name, format) } } -function addInitialKeywords(self, defs: Vocabulary | {[x: string]: KeywordDefinition}) { +function addInitialKeywords(this: Ajv, defs: Vocabulary | {[x: string]: KeywordDefinition}) { if (Array.isArray(defs)) { - self.addVocabulary(defs) + this.addVocabulary(defs) return } - self.logger.warn("keywords option as map is deprecated, pass array") - for (var keyword in defs) { - var def = defs[name] + this.logger.warn("keywords option as map is deprecated, pass array") + for (const keyword in defs) { + const def = defs[name] if (!def.keyword) def.keyword = keyword - self.addKeyword(def) + this.addKeyword(def) } } -function checkUnique(self, id) { - if (self._schemas[id] || self._refs[id]) { +function checkUnique(this: Ajv, id: string) { + if (this._schemas[id] || this._refs[id]) { throw new Error('schema with key or id "' + id + '" already exists') } } -function getMetaSchemaOptions(self) { - var metaOpts = {...self._opts} - for (var i = 0; i < META_IGNORE_OPTIONS.length; i++) { - delete metaOpts[META_IGNORE_OPTIONS[i]] - } +function getMetaSchemaOptions(this: Ajv) { + const metaOpts = {...this._opts} + for (const opt of META_IGNORE_OPTIONS) delete metaOpts[opt] return metaOpts } const noLogs = {log() {}, warn() {}, error() {}} -function setLogger(self) { - var logger = self._opts.logger - if (logger === false) { - self.logger = noLogs +function getLogger(logger?: Logger | false): Logger { + if (logger === false) return noLogs + if (logger === undefined) return console + if (!(typeof logger == "object" && logger.log && logger.warn && logger.error)) { + throw new Error("logger must implement log, warn and error methods") + } + return logger +} + +const KEYWORD_NAME = /^[a-z_$][a-z0-9_$-]*$/i + +function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefinition) { + /* eslint no-shadow: 0 */ + const RULES: ValidationRules = this.RULES + eachItem(keyword, (kwd) => { + if (RULES.keywords[kwd]) throw new Error(`Keyword ${kwd} is already defined`) + if (!KEYWORD_NAME.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`) + }) + if (!def) return + if (def.type) eachItem(def.type, (t) => checkType(t, RULES)) + if (def.$data && !("code" in def || "validate" in def)) { + throw new Error('$data keyword must have "code" or "validate" function') + } +} + +function _addRule( + this: Ajv, + keyword: string, + dataType?: string, + definition?: KeywordDefinition +): void { + const RULES: ValidationRules = this.RULES + let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) + if (!ruleGroup) { + ruleGroup = {type: dataType, rules: []} + RULES.rules.push(ruleGroup) + } + RULES.keywords[keyword] = true + if (!definition) return + + const rule: Rule = {keyword, definition} + if (definition.before) _addBeforeRule.call(this, ruleGroup, rule, definition.before) + else ruleGroup.rules.push(rule) + RULES.all[keyword] = rule + definition.implements?.forEach((kwd) => this.addKeyword(kwd)) +} + +function _addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: string): void { + const i = ruleGroup.rules.findIndex((rule) => rule.keyword === before) + if (i >= 0) { + ruleGroup.rules.splice(i, 0, rule) } else { - if (logger === undefined) logger = console - if (!(typeof logger == "object" && logger.log && logger.warn && logger.error)) { - throw new Error("logger must implement log, warn and error methods") - } - self.logger = logger + ruleGroup.rules.push(rule) + this.logger.warn(`rule ${before} is not defined`) + } +} + +function eachItem(xs: T | T[], f: (x: T) => void): void { + if (Array.isArray(xs)) { + for (const x of xs) f(x) + } else { + f(xs) } } + +function keywordMetaschema(this: any, def: KeywordDefinition): void { + // TODO this Ajv + let metaSchema = def.metaSchema + if (metaSchema === undefined) return + if (def.$data && this._opts.$data) metaSchema = schemaOrData(metaSchema) + def.validateSchema = this.compile(metaSchema, true) +} + +const $dataRef = { + $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", +} + +function schemaOrData(schema: object | boolean): object { + return {anyOf: [schema, $dataRef]} +} + +module.exports = Ajv diff --git a/lib/cache.ts b/lib/cache.ts index 1aebbb5404..c165fcc2b4 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,17 +1,18 @@ -import SchemaObject from "./compile/schema_obj" +import StoredSchema from "./compile/stored_schema" +import {CacheInterface} from "./types" -export default class Cache { - _cache: {[key: string]: SchemaObject} +export default class Cache implements CacheInterface { + _cache: {[key: string]: StoredSchema} constructor() { this._cache = {} } - put(key: string, value: SchemaObject): void { + put(key: string, value: StoredSchema): void { this._cache[key] = value } - get(key: string): SchemaObject { + get(key: string): StoredSchema { return this._cache[key] } diff --git a/lib/compile/async.ts b/lib/compile/async.ts deleted file mode 100644 index ba61bae7f8..0000000000 --- a/lib/compile/async.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {MissingRefError} from "./error_classes" - -module.exports = compileAsync - -type Callback = (...args: any[]) => void - -/** - * Creates validating function for passed schema with asynchronous loading of missing schemas. - * `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. - * @this Ajv - * @param {Object} schema schema object - * @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped - * @param {Function} callback an optional node-style callback, it is called with 2 parameters: error (or null) and validating function. - * @return {Promise} promise that resolves with a validating function. - */ -function compileAsync(schema, meta?: boolean | Callback, callback?: Callback) { - /* eslint no-shadow: 0 */ - /* jshint validthis: true */ - var self = this - if (typeof this._opts.loadSchema != "function") { - throw new Error("options.loadSchema should be a function") - } - - if (typeof meta == "function") { - callback = meta - meta = undefined - } - - var p = loadMetaSchemaOf(schema).then(() => { - var schemaObj = this._addSchema(schema, undefined, meta) - return schemaObj.validate || _compileAsync(schemaObj) - }) - - if (callback) { - p.then((v) => (callback)(null, v), callback) - } - - return p - - function loadMetaSchemaOf(sch) { - var $schema = sch.$schema - return $schema && !self.getSchema($schema) - ? compileAsync.call(self, {$ref: $schema}, true) - : Promise.resolve() - } - - function _compileAsync(schemaObj) { - try { - return self._compile(schemaObj) - } catch (e) { - if (e instanceof MissingRefError) return loadMissingSchema(e) - throw e - } - - function loadMissingSchema(e) { - var ref = e.missingSchema - if (added(ref)) { - throw new Error("Schema " + ref + " is loaded but " + e.missingRef + " cannot be resolved") - } - - var schemaPromise = self._loadingSchemas[ref] - if (!schemaPromise) { - schemaPromise = self._loadingSchemas[ref] = self._opts.loadSchema(ref) - schemaPromise.then(removePromise, removePromise) - } - - return schemaPromise - .then((sch) => { - if (!added(ref)) { - return loadMetaSchemaOf(sch).then(() => { - if (!added(ref)) self.addSchema(sch, ref, undefined, meta) - }) - } - }) - .then(() => _compileAsync(schemaObj)) - - function removePromise() { - delete self._loadingSchemas[ref] - } - - function added(ref) { - return self._refs[ref] || self._schemas[ref] - } - } - } -} diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 6be22f69c7..321ead5e69 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -54,11 +54,11 @@ export class Name extends _Code { if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier") } - isQuoted() { + isQuoted(): boolean { return false } - add(_c: Code): void { + add(_c: _Code): void { throw new Error("CodeGen: can't add to Name") } } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 203a60e567..1a6956d759 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,6 @@ import CodeGen, {_, str, nil, Code, Scope} from "./codegen" import {validateFunctionCode} from "./validate" -import {ErrorObject} from "../types" +import {ErrorObject, Schema} from "../types" import N from "./names" const equal = require("fast-deep-equal") @@ -31,6 +31,18 @@ export interface FuncResolvedRef { inline?: false } +export interface SchemaRoot { + schema: Schema + refVal: unknown[] + refs: {[ref: string]: number} +} + +export interface Compilation { + schema: Schema + root: SchemaRoot + baseId: string +} + /** * Compiles schema to validation function * @this Ajv diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 5e56153ee6..8d555cd9e8 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,4 +1,4 @@ -import SchemaObject from "./schema_obj" +import StoredSchema from "./stored_schema" import {toHash, escapeFragment, unescapeFragment} from "./util" var URI = require("uri-js"), @@ -29,7 +29,7 @@ function resolve(compile, root, ref) { } refVal = refVal || this._schemas[ref] - if (refVal instanceof SchemaObject) { + if (refVal instanceof StoredSchema) { return inlineRef(refVal.schema, this._opts.inlineRefs) ? refVal.schema : refVal.validate || this._compile(refVal) @@ -43,7 +43,7 @@ function resolve(compile, root, ref) { baseId = res.baseId } - if (schema instanceof SchemaObject) { + if (schema instanceof StoredSchema) { v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId) } else if (schema !== undefined) { v = inlineRef(schema, this._opts.inlineRefs) @@ -71,12 +71,12 @@ function resolveSchema(root, ref) { var refVal = this._refs[id] if (typeof refVal == "string") { return resolveRecursive.call(this, root, refVal, p) - } else if (refVal instanceof SchemaObject) { + } else if (refVal instanceof StoredSchema) { if (!refVal.validate) this._compile(refVal) root = refVal } else { refVal = this._schemas[id] - if (refVal instanceof SchemaObject) { + if (refVal instanceof StoredSchema) { if (!refVal.validate) this._compile(refVal) if (id === normalizeId(ref)) { return {schema: refVal, root: root, baseId: baseId} diff --git a/lib/compile/schema_obj.ts b/lib/compile/schema_obj.ts deleted file mode 100644 index 7219362ce8..0000000000 --- a/lib/compile/schema_obj.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default class SchemaObject { - schema?: any - validate?: () => any - - constructor(obj: object) { - Object.assign(this, obj) - } -} diff --git a/lib/compile/stored_schema.ts b/lib/compile/stored_schema.ts new file mode 100644 index 0000000000..98f9d77827 --- /dev/null +++ b/lib/compile/stored_schema.ts @@ -0,0 +1,38 @@ +import {Schema, ValidateFunction} from "../types" +import {SchemaRoot} from "./index" + +export interface _StoredSchema { + id?: string + ref?: string + cacheKey?: unknown + schema?: Schema + fragment?: true + meta?: boolean + root?: SchemaRoot + refs?: {[ref: string]: number} + refVal?: unknown[] + localRefs?: unknown + baseId?: string + validate?: ValidateFunction + compiling?: boolean +} + +export default class StoredSchema implements _StoredSchema { + id?: string + ref?: string + cacheKey?: unknown + schema?: Schema + fragment?: true + meta?: boolean + root?: SchemaRoot + refs?: {[ref: string]: number} + refVal?: unknown[] + localRefs?: unknown + baseId?: string + validate?: ValidateFunction + compiling?: boolean + + constructor(obj: _StoredSchema) { + Object.assign(this, obj) + } +} diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index a909fe11b2..f854488904 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,4 +1,9 @@ -import {CompilationContext, KeywordErrorDefinition, KeywordErrorContext} from "../../types" +import { + CompilationContext, + KeywordErrorDefinition, + KeywordErrorContext, + SchemaObject, +} from "../../types" import {toHash, checkDataTypes, DataType} from "../util" import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" @@ -6,7 +11,7 @@ import {reportError} from "../errors" import {_, str, Name} from "../codegen" import {ValidationRules} from "../rules" -export function getSchemaTypes({RULES}: CompilationContext, schema): string[] { +export function getSchemaTypes({RULES}: CompilationContext, schema: SchemaObject): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach((t) => checkType(t, RULES)) diff --git a/lib/data.ts b/lib/data.ts deleted file mode 100644 index 08fc1d56f1..0000000000 --- a/lib/data.ts +++ /dev/null @@ -1,29 +0,0 @@ -export default function $dataMetaSchema( - this, - metaSchema: object, - keywordsJsonPointers: string[] -): object { - const rules = this.RULES.all - for (const jsonPointer of keywordsJsonPointers) { - metaSchema = JSON.parse(JSON.stringify(metaSchema)) - const segments = jsonPointer.split("/").slice(1) // first segment is an empty string - let keywords = metaSchema - for (const seg of segments) keywords = keywords[seg] - - for (const key in rules) { - const $data = rules[key]?.definition?.$data - const schema = keywords[key] - if ($data && schema) keywords[key] = schemaOrData(schema) - } - } - - return metaSchema -} - -const $dataRef = { - $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", -} - -export function schemaOrData(schema: object | boolean): object { - return {anyOf: [schema, $dataRef]} -} diff --git a/lib/keyword.ts b/lib/keyword.ts deleted file mode 100644 index 328e00f428..0000000000 --- a/lib/keyword.ts +++ /dev/null @@ -1,127 +0,0 @@ -import {KeywordDefinition, Vocabulary, ErrorObject} from "./types" -import {ValidationRules, Rule, RuleGroup} from "./compile/rules" -import {schemaOrData} from "./data" -import {checkType} from "./compile/validate/dataType" - -const IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i - -export function addVocabulary(this, definitions: Vocabulary): object { - // TODO return type Ajv - for (const def of definitions) this.addKeyword(def) - return this -} - -// TODO Ajv -export function addKeyword(this, def: KeywordDefinition): object -export function addKeyword(this, keyword: string): object -export function addKeyword( - this: any, // TODO Ajv - kwdOrDef: string | KeywordDefinition, - def?: KeywordDefinition // deprecated -): object { - let keyword: string | string[] - if (typeof kwdOrDef == "string") { - keyword = kwdOrDef - if (typeof def == "object") { - this.logger.warn("these parameters are deprecated, see docs for addKeyword") - def.keyword = keyword - } - } else if (typeof kwdOrDef == "object" && def === undefined) { - def = kwdOrDef - keyword = def.keyword - } else { - throw new Error("invalid addKeywords parameters") - } - - checkKeyword.call(this, keyword, def) - if (def) keywordMetaschema.call(this, def) - eachItem(keyword, (kwd) => { - eachItem(def?.type, (t) => _addRule.call(this, kwd, t, def)) - }) - return this -} - -function checkKeyword(keyword: string | string[], def?: KeywordDefinition) { - /* eslint no-shadow: 0 */ - const RULES: ValidationRules = this.RULES - eachItem(keyword, (kwd) => { - if (RULES.keywords[kwd]) throw new Error(`Keyword ${kwd} is already defined`) - if (!IDENTIFIER.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`) - }) - if (!def) return - if (def.type) eachItem(def.type, (t) => checkType(t, RULES)) - if (def.$data && !("code" in def || "validate" in def)) { - throw new Error('$data keyword must have "code" or "validate" function') - } -} - -function _addRule(keyword: string, dataType?: string, definition?: KeywordDefinition): void { - const RULES: ValidationRules = this.RULES - let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) - if (!ruleGroup) { - ruleGroup = {type: dataType, rules: []} - RULES.rules.push(ruleGroup) - } - RULES.keywords[keyword] = true - if (!definition) return - - const rule: Rule = {keyword, definition} - if (definition.before) _addBeforeRule.call(this, ruleGroup, rule, definition.before) - else ruleGroup.rules.push(rule) - RULES.all[keyword] = rule - definition.implements?.forEach((kwd) => this.addKeyword(kwd)) -} - -function _addBeforeRule(this, ruleGroup: RuleGroup, rule: Rule, before: string): void { - const i = ruleGroup.rules.findIndex((rule) => rule.keyword === before) - if (i >= 0) { - ruleGroup.rules.splice(i, 0, rule) - } else { - ruleGroup.rules.push(rule) - this.logger.warn(`rule ${before} is not defined`) - } -} - -function eachItem(xs: T | T[], f: (x: T) => void): void { - if (Array.isArray(xs)) { - for (const x of xs) f(x) - } else { - f(xs) - } -} - -function keywordMetaschema(this: any, def: KeywordDefinition): void { - // TODO this Ajv - let metaSchema = def.metaSchema - if (metaSchema === undefined) return - if (def.$data && this._opts.$data) metaSchema = schemaOrData(metaSchema) - def.validateSchema = this.compile(metaSchema, true) -} - -export function getKeyword(this, keyword: string): KeywordDefinition | boolean { - const rule = this.RULES.all[keyword] - return rule?.definition || rule || false -} - -/** - * Remove keyword - * @this Ajv - * @param {String} keyword keyword. - * @return {Ajv} this for method chaining - */ -export function removeKeyword(keyword: string): object { - // TODO return type should be Ajv - const RULES: ValidationRules = this.RULES - delete RULES.keywords[keyword] - delete RULES.all[keyword] - for (const group of RULES.rules) { - const i = group.rules.findIndex((rule) => rule.keyword === keyword) - if (i >= 0) group.rules.splice(i, 1) - } - return this -} - -export interface KeywordValidator { - (definition: KeywordDefinition, throwError: boolean): boolean - errors?: ErrorObject[] | null -} diff --git a/lib/types.ts b/lib/types.ts index b6d5ac713f..8f08c18c92 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,8 +1,22 @@ -import Cache from "./cache" import CodeGen, {Code, Name, CodeGenOptions} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {ResolvedRef} from "./compile" import KeywordContext from "./compile/context" +import StoredSchema from "./compile/stored_schema" +import Ajv from "./ajv" + +export interface SchemaObject { + $id?: string + $schema?: string + [x: string]: any +} + +export type Schema = SchemaObject | boolean + +export type LoadSchemaFunction = ( + uri: string, + cb?: (err: Error | null, schema?: SchemaObject) => void +) => Promise export interface CurrentOptions { strict?: boolean | "log" @@ -10,20 +24,18 @@ export interface CurrentOptions { allErrors?: boolean verbose?: boolean format?: false - formats?: object + formats?: {[name: string]: Format} keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated unknownFormats?: true | string[] | "ignore" - schemas?: object[] | object + schemas?: Schema[] | {[key: string]: Schema} missingRefs?: true | "ignore" | "fail" extendRefs?: true | "ignore" | "fail" - loadSchema?: ( - uri: string, - cb?: (err: Error, schema: object) => void - ) => PromiseLike + loadSchema?: LoadSchemaFunction removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" - meta?: boolean | object + meta?: SchemaObject | false + defaultMeta?: string | SchemaObject validateSchema?: boolean | "log" addUsedSchema?: boolean inlineRefs?: boolean | number @@ -36,7 +48,7 @@ export interface CurrentOptions { sourceCode?: boolean processCode?: (code: string, schema: object) => string codegen?: CodeGenOptions - cache?: Cache + cache?: CacheInterface logger?: Logger | false serialize?: false | ((schema: object | boolean) => any) $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) @@ -54,20 +66,28 @@ export interface Options extends CurrentOptions { unicode?: boolean } -interface Logger { +export interface Logger { log(...args: any[]): any warn(...args: any[]): any error(...args: any[]): any } +export interface CacheInterface { + put: (key: any, value: StoredSchema) => void + get: (key: any) => StoredSchema + del(key: any): void + clear(): void +} + export interface ValidateFunction { ( + this: Ajv | any, data: any, dataPath?: string, parentData?: object | any[], parentDataProperty?: string | number, rootData?: object | any[] - ): boolean | PromiseLike + ): boolean | Promise schema?: object | boolean errors?: null | ErrorObject[] refs?: object @@ -86,7 +106,7 @@ export interface SchemaValidateFunction { parentData?: object | any[], parentDataProperty?: string | number, rootData?: object | any[] - ): boolean | PromiseLike + ): boolean | Promise errors?: ErrorObject[] } @@ -226,7 +246,7 @@ export type FormatValidator = (data: T) => boolean export type FormatCompare = (data1: T, data2: T) => boolean -export type AsyncFormatValidator = (data: T) => PromiseLike +export type AsyncFormatValidator = (data: T) => Promise export interface FormatDefinition { type: T extends string ? "string" : "number" From 9041881cfef2f64d7f3d74155f335ce92c6a19e0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 5 Sep 2020 09:19:34 +0100 Subject: [PATCH 157/322] enable noImplicitThis --- lib/compile/index.ts | 91 ++++++++++++++---------------------------- lib/compile/resolve.ts | 67 +++++++++++++++---------------- tsconfig.json | 1 - 3 files changed, 63 insertions(+), 96 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 1a6956d759..9d5d68c82d 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,7 @@ +import Ajv from "../ajv" import CodeGen, {_, str, nil, Code, Scope} from "./codegen" import {validateFunctionCode} from "./validate" -import {ErrorObject, Schema} from "../types" +import {Schema, SchemaObject, ValidateFunction} from "../types" import N from "./names" const equal = require("fast-deep-equal") @@ -32,7 +33,7 @@ export interface FuncResolvedRef { } export interface SchemaRoot { - schema: Schema + schema: SchemaObject refVal: unknown[] refs: {[ref: string]: number} } @@ -41,19 +42,18 @@ export interface Compilation { schema: Schema root: SchemaRoot baseId: string + validate?: ValidateFunction + callValidate?: ValidateFunction } -/** - * Compiles schema to validation function - * @this Ajv - * @param {Object} schema schema object - * @param {Object} root object with information about the root schema for this schema - * @param {Object} localRefs the hash of local references inside the schema (created by resolve.id), used for inline resolution - * @param {String} baseId base ID for IDs in the schema - * @return {Function} validation function - */ -function compile(schema, root, localRefs, baseId) { - /* jshint validthis: true, evil: true */ +// Compiles schema to validation function +function compile( + this: Ajv, + schema: Schema, // TODO or SchemaObject? + root: SchemaRoot, // object with information about the root schema for this schema + localRefs, // the hash of local references inside the schema (created by resolve.id), used for inline resolution + baseId: string // base ID for IDs in the schema +) { /* eslint no-shadow: 0 */ var self = this, opts = this._opts, @@ -64,17 +64,11 @@ function compile(schema, root, localRefs, baseId) { root = root || {schema: schema, refVal: refVal, refs: refs} - interface CallValidate { - (): any - errors?: null | ErrorObject[] - } - var c = checkCompiling.call(this, schema, root, baseId) const compilation = this._compilations[c.index] - /* @this {*} - custom context, see passContext option */ - const callValidate: CallValidate = function (...args) { - var validate = compilation.validate + const callValidate: ValidateFunction = function (...args) { + var validate = compilation.validate /* eslint-disable no-invalid-this */ var result = validate.apply(this, args) callValidate.errors = validate.errors @@ -261,55 +255,32 @@ function compile(schema, root, localRefs, baseId) { } } -/** - * Checks if the schema is currently compiled - * @this Ajv - * @param {Object} schema schema to compile - * @param {Object} root root object - * @param {String} baseId base schema ID - * @return {Object} object with properties "index" (compilation index) and "compiling" (boolean) - */ -function checkCompiling(schema, root, baseId) { +// Checks if the schema is currently compiled +function checkCompiling( + this: Ajv, + schema: Schema, // TODO or SchemaObject? + root: SchemaRoot, + baseId: string +): {index: number; compiling: boolean} { /* jshint validthis: true */ var index = compIndex.call(this, schema, root, baseId) if (index >= 0) return {index: index, compiling: true} index = this._compilations.length - this._compilations[index] = { - schema: schema, - root: root, - baseId: baseId, - } - return {index: index, compiling: false} + this._compilations[index] = {schema, root, baseId} + return {index, compiling: false} } -/** - * Removes the schema from the currently compiled list - * @this Ajv - * @param {Object} schema schema to compile - * @param {Object} root root object - * @param {String} baseId base schema ID - */ -function endCompiling(schema, root, baseId) { - /* jshint validthis: true */ +// Removes the schema from the currently compiled list +function endCompiling(this: Ajv, schema: Schema, root: SchemaRoot, baseId: string) { var i = compIndex.call(this, schema, root, baseId) if (i >= 0) this._compilations.splice(i, 1) } -/** - * Index of schema compilation in the currently compiled list - * @this Ajv - * @param {Object} schema schema to compile - * @param {Object} root root object - * @param {String} baseId base schema ID - * @return {Integer} compilation index - */ -function compIndex(schema, root, baseId) { - /* jshint validthis: true */ - for (var i = 0; i < this._compilations.length; i++) { - var c = this._compilations[i] - if (c.schema === schema && c.root === root && c.baseId === baseId) return i - } - return -1 +// Index of schema compilation in the currently compiled list +function compIndex(this: Ajv, schema: Schema, root: SchemaRoot, baseId: string) { + return this._compilations.findIndex( + (c) => c.schema === schema && c.root === root && c.baseId === baseId + ) } function refValCode(i: number, refVal): Code { diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 8d555cd9e8..13879ce9a6 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,5 +1,8 @@ import StoredSchema from "./stored_schema" import {toHash, escapeFragment, unescapeFragment} from "./util" +import Ajv from "../ajv" +import {SchemaRoot} from "./index" +import {ValidateFunction, Schema} from "../types" var URI = require("uri-js"), equal = require("fast-deep-equal"), @@ -12,16 +15,14 @@ resolve.ids = resolveIds resolve.inlineRef = inlineRef resolve.schema = resolveSchema -/** - * [resolve and compile the references ($ref)] - * @this Ajv - * @param {Function} compile reference to schema compilation funciton (localCompile) - * @param {Object} root object with information about the root schema for the current schema - * @param {String} ref reference to resolve - * @return {Object|Function} schema object (if the schema can be inlined) or validation function - */ -function resolve(compile, root, ref) { - /* jshint validthis: true */ +// resolve and compile the references ($ref) +// TODO returns SchemaObject (if the schema can be inlined) or validation function +function resolve( + this: Ajv, + compile, // reference to schema compilation funciton (localCompile) + root: SchemaRoot, // information about the root schema for the current schema + ref: string // reference to resolve +): Schema | ValidateFunction | undefined { var refVal = this._refs[ref] if (typeof refVal == "string") { if (this._refs[refVal]) refVal = this._refs[refVal] @@ -54,18 +55,16 @@ function resolve(compile, root, ref) { return v } -/** - * Resolve schema, its root and baseId - * @this Ajv - * @param {Object} root root object with properties schema, refVal, refs - * @param {String} ref reference to resolve - * @return {Object} object with properties schema, root, baseId - */ -function resolveSchema(root, ref) { - /* jshint validthis: true */ - var p = URI.parse(ref), - refPath = _getFullPath(p), - baseId = getFullPath(root.schema.$id) +// Resolve schema, its root and baseId +// TODO returns object with properties schema, root, baseId +function resolveSchema( + this: Ajv, + root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it + ref: string // reference to resolve +) { + const p = URI.parse(ref) + const refPath = _getFullPath(p) + let baseId = getFullPath(root.schema.$id) if (Object.keys(root.schema).length === 0 || refPath !== baseId) { var id = normalizeId(refPath) var refVal = this._refs[id] @@ -73,15 +72,15 @@ function resolveSchema(root, ref) { return resolveRecursive.call(this, root, refVal, p) } else if (refVal instanceof StoredSchema) { if (!refVal.validate) this._compile(refVal) - root = refVal + root = refVal } else { refVal = this._schemas[id] if (refVal instanceof StoredSchema) { if (!refVal.validate) this._compile(refVal) if (id === normalizeId(ref)) { - return {schema: refVal, root: root, baseId: baseId} + return {schema: refVal, root, baseId} } - root = refVal + root = refVal } else { return } @@ -92,9 +91,7 @@ function resolveSchema(root, ref) { return getJsonPointer.call(this, p, baseId, root.schema, root) } -/* @this Ajv */ -function resolveRecursive(root, ref, parsedRef) { - /* jshint validthis: true */ +function resolveRecursive(this: Ajv, root, ref, parsedRef) { var res = resolveSchema.call(this, root, ref) if (res) { var schema = res.schema @@ -113,8 +110,8 @@ var PREVENT_SCOPE_CHANGE = toHash([ "dependencies", "definitions", ]) -/* @this Ajv */ -function getJsonPointer(parsedRef, baseId, schema, root) { + +function getJsonPointer(this: Ajv, parsedRef, baseId, schema, root) { /* jshint validthis: true */ parsedRef.fragment = parsedRef.fragment || "" if (parsedRef.fragment.slice(0, 1) !== "/") return @@ -143,7 +140,7 @@ function getJsonPointer(parsedRef, baseId, schema, root) { } } if (schema !== undefined && schema !== root.schema) { - return {schema: schema, root: root, baseId: baseId} + return {schema, root, baseId} } } @@ -211,7 +208,7 @@ function countKeys(schema) { return count } -export function getFullPath(id: string, normalize?: boolean): string { +export function getFullPath(id: string | undefined, normalize?: boolean): string { if (normalize !== false) id = normalizeId(id) var p = URI.parse(id) return _getFullPath(p) @@ -222,7 +219,7 @@ function _getFullPath(p) { } var TRAILING_SLASH_HASH = /#\/?$/ -export function normalizeId(id: string): string { +export function normalizeId(id: string | undefined): string { return id ? id.replace(TRAILING_SLASH_HASH, "") : "" } @@ -232,7 +229,7 @@ export function resolveUrl(baseId: string, id: string): string { } /* @this Ajv */ -function resolveIds(schema) { +function resolveIds(this: Ajv, schema) { var schemaId = normalizeId(schema.$id) var baseIds = {"": schemaId} var fullPaths = {"": getFullPath(schemaId, false)} @@ -256,7 +253,7 @@ function resolveIds(schema) { var refVal = self._refs[id] if (typeof refVal == "string") refVal = self._refs[refVal] - if (refVal && refVal.schema) { + if (typeof refVal == "object" && refVal.schema) { if (!equal(sch, refVal.schema)) { throw new Error('id "' + id + '" resolves to more than one schema') } diff --git a/tsconfig.json b/tsconfig.json index 6361ceab7a..d9d2f99ce8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "lib": ["ES2018", "DOM"], "types": ["node"], "noImplicitAny": false, - "noImplicitThis": false, "noImplicitReturns": false, "allowJs": true, "target": "ES2018" From 4dd9c549581bcf1f3bf1701edcfb1a6269f0c87a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 5 Sep 2020 10:47:20 +0100 Subject: [PATCH 158/322] refactor: tracking in-progress compilations in Set --- lib/ajv.ts | 2 +- lib/compile/index.ts | 82 +++++++++++++++++++++----------------------- lib/types.ts | 8 +---- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index a5e6fd822d..906daa0545 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -43,7 +43,7 @@ export default class Ajv { _refs: {[ref: string]: StoredSchema | string} = {} _fragments: {[key: string]: StoredSchema} = {} _formats: {[name: string]: AddedFormat} = {} - _compilations: Compilation[] = [] + _compilations: Set = new Set() _loadingSchemas: {[ref: string]: Promise} = {} _metaOpts: Options RULES: ValidationRules diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 9d5d68c82d..1a9f6c2578 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -34,14 +34,26 @@ export interface FuncResolvedRef { export interface SchemaRoot { schema: SchemaObject - refVal: unknown[] + refVal: (string | undefined)[] refs: {[ref: string]: number} } -export interface Compilation { +export class SchemaEnv { schema: Schema root: SchemaRoot baseId: string + constructor(schema: Schema, root: SchemaRoot, baseId: string) { + this.schema = schema + this.root = root + this.baseId = baseId + } + + equal(env: SchemaEnv): boolean { + return this.schema === env.schema && this.root === env.root && this.baseId === env.baseId + } +} + +export interface Compilation extends SchemaEnv { validate?: ValidateFunction callValidate?: ValidateFunction } @@ -49,7 +61,7 @@ export interface Compilation { // Compiles schema to validation function function compile( this: Ajv, - schema: Schema, // TODO or SchemaObject? + schema: SchemaObject, // TODO or SchemaObject? root: SchemaRoot, // object with information about the root schema for this schema localRefs, // the hash of local references inside the schema (created by resolve.id), used for inline resolution baseId: string // base ID for IDs in the schema @@ -62,20 +74,27 @@ function compile( const scope: Scope = {} - root = root || {schema: schema, refVal: refVal, refs: refs} + root = root || {schema, refVal, refs} - var c = checkCompiling.call(this, schema, root, baseId) - const compilation = this._compilations[c.index] + const env = new SchemaEnv(schema, root, baseId) + let compilation = getCompilation.call(this, env) - const callValidate: ValidateFunction = function (...args) { - var validate = compilation.validate - /* eslint-disable no-invalid-this */ - var result = validate.apply(this, args) - callValidate.errors = validate.errors - return result + if (compilation) { + const c: Compilation = compilation + if (!c.callValidate) { + const validate: ValidateFunction = function (this: Ajv | any, ...args) { + const v = c.validate + const valid = v.apply(this, args) + validate.errors = v.errors + return valid + } + c.callValidate = validate + } + return c.callValidate } - if (c.compiling) return (compilation.callValidate = callValidate) + compilation = env + this._compilations.add(compilation) var formats = this._formats var RULES = this.RULES @@ -95,10 +114,10 @@ function compile( } return v } finally { - endCompiling.call(this, schema, root, baseId) + this._compilations.delete(compilation) } - function localCompile(_schema, _root, localRefs, baseId) { + function localCompile(_schema: SchemaObject, _root: SchemaRoot, localRefs, baseId: string) { var isRoot = !_root || (_root && _root.schema === _schema) if (_root.schema !== root.schema) { return compile.call(self, _schema, _root, localRefs, baseId) @@ -230,7 +249,7 @@ function compile( } // TODO gen.globals - function addLocalRef(ref, v?: any): Code { + function addLocalRef(ref: string, v?: any): Code { var refId = refVal.length refVal[refId] = v refs[ref] = refId @@ -238,7 +257,7 @@ function compile( } // TODO gen.globals remove? - function removeLocalRef(ref) { + function removeLocalRef(ref: string) { delete refs[ref] } @@ -255,32 +274,11 @@ function compile( } } -// Checks if the schema is currently compiled -function checkCompiling( - this: Ajv, - schema: Schema, // TODO or SchemaObject? - root: SchemaRoot, - baseId: string -): {index: number; compiling: boolean} { - /* jshint validthis: true */ - var index = compIndex.call(this, schema, root, baseId) - if (index >= 0) return {index: index, compiling: true} - index = this._compilations.length - this._compilations[index] = {schema, root, baseId} - return {index, compiling: false} -} - -// Removes the schema from the currently compiled list -function endCompiling(this: Ajv, schema: Schema, root: SchemaRoot, baseId: string) { - var i = compIndex.call(this, schema, root, baseId) - if (i >= 0) this._compilations.splice(i, 1) -} - // Index of schema compilation in the currently compiled list -function compIndex(this: Ajv, schema: Schema, root: SchemaRoot, baseId: string) { - return this._compilations.findIndex( - (c) => c.schema === schema && c.root === root && c.baseId === baseId - ) +function getCompilation(this: Ajv, env: SchemaEnv): Compilation | void { + for (const c of this._compilations) { + if (c.equal(env)) return c + } } function refValCode(i: number, refVal): Code { diff --git a/lib/types.ts b/lib/types.ts index 8f08c18c92..945bd800c0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,6 @@ import CodeGen, {Code, Name, CodeGenOptions} from "./compile/codegen" import {ValidationRules} from "./compile/rules" -import {ResolvedRef} from "./compile" +import {ResolvedRef, SchemaRoot} from "./compile" import KeywordContext from "./compile/context" import StoredSchema from "./compile/stored_schema" import Ajv from "./ajv" @@ -157,12 +157,6 @@ export interface CompilationContext { self: any // TODO } -interface SchemaRoot { - schema: any - refVal: (string | undefined)[] // TODO - refs: {[key: string]: any} // TODO -} - interface _KeywordDef { keyword: string | string[] type?: string | string[] From f57679e6ccb1059402954c4997f31ca53b317cdb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 5 Sep 2020 12:27:28 +0100 Subject: [PATCH 159/322] refactor: resolve --- lib/ajv.ts | 33 +++--- lib/compile/error_classes.ts | 8 +- lib/compile/index.ts | 106 +++++++++--------- lib/compile/resolve.ts | 195 ++++++++++++++++------------------ lib/compile/stored_schema.ts | 18 ++-- lib/compile/util.ts | 12 ++- lib/compile/validate/index.ts | 15 ++- lib/types.ts | 8 +- spec/resolve.spec.js | 8 +- 9 files changed, 197 insertions(+), 206 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 906daa0545..46596d6f76 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -17,17 +17,15 @@ import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" -import {SchemaRoot, Compilation} from "./compile" - -const compileSchema = require("./compile"), - resolve = require("./compile/resolve"), - stableStringify = require("fast-json-stable-stringify") - +import {compileSchema, SchemaRoot, Compilation} from "./compile" +import {resolveSchema, normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" import applicatorVocabulary from "./vocabularies/applicator" import formatVocabulary from "./vocabularies/format" import {metadataVocabulary, contentVocabulary} from "./vocabularies/metadata" +import stableStringify from "fast-json-stable-stringify" +import {eachItem} from "./compile/util" const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" @@ -212,7 +210,7 @@ export default class Ajv { id = schema.$id if (id !== undefined && typeof id != "string") throw new Error("schema id must be string") } - key = resolve.normalizeId(key || id) + key = normalizeId(key || id) checkUnique.call(this, key) this._schemas[key] = _addSchema.call(this, schema, _skipValidation, _meta, true) return this @@ -295,7 +293,7 @@ export default class Ajv { this._cache.del(cacheKey) let id = schemaKeyRef.$id if (id) { - id = resolve.normalizeId(id) + id = normalizeId(id) delete this._schemas[id] delete this._refs[id] } @@ -418,7 +416,8 @@ function defaultMeta(this: Ajv): string | SchemaObject | undefined { } function _getSchemaFragment(this: Ajv, ref: string): ValidateFunction | undefined { - const res = resolve.schema.call(this, {schema: {}}, ref) + const _root: SchemaRoot = {schema: {}, refVal: [undefined], refs: {}} + const res = resolveSchema.call(this, _root, ref) if (!res) return const schema = res.schema const root = res.root @@ -436,7 +435,7 @@ function _getSchemaFragment(this: Ajv, ref: string): ValidateFunction | undefine } function _getSchemaObj(this: Ajv, keyRef: string): StoredSchema | undefined { - keyRef = resolve.normalizeId(keyRef) + keyRef = normalizeId(keyRef) return this._schemas[keyRef] || this._refs[keyRef] || this._fragments[keyRef] } @@ -480,16 +479,16 @@ function _addSchema( $id = schema.$id $schema = schema.$schema } - const id = resolve.normalizeId($id) + const id = normalizeId($id) if (id && shouldAddSchema) checkUnique.call(this, id) const willValidate = this._opts.validateSchema !== false && !skipValidation let recursiveMeta - if (willValidate && !(recursiveMeta = id && id === resolve.normalizeId($schema))) { + if (willValidate && !(recursiveMeta = id && id === normalizeId($schema))) { this.validateSchema(schema, true) } - const localRefs = resolve.ids.call(this, schema) + const localRefs = getSchemaRefs.call(this, schema) const schemaObj = new StoredSchema({id, schema, localRefs, cacheKey, meta}) @@ -646,14 +645,6 @@ function _addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: str } } -function eachItem(xs: T | T[], f: (x: T) => void): void { - if (Array.isArray(xs)) { - for (const x of xs) f(x) - } else { - f(xs) - } -} - function keywordMetaschema(this: any, def: KeywordDefinition): void { // TODO this Ajv let metaSchema = def.metaSchema diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts index 7bda2f7e16..6c0e752ab6 100644 --- a/lib/compile/error_classes.ts +++ b/lib/compile/error_classes.ts @@ -1,6 +1,6 @@ import {ErrorObject} from "../types" - -const resolve = require("./resolve") +import {resolveUrl, normalizeId, getFullPath} from "./resolve" +// const resolve = require("./resolve") export class ValidationError extends Error { errors: ErrorObject[] @@ -24,8 +24,8 @@ export class MissingRefError extends Error { constructor(baseId: string, ref: string, message?: string) { super(message || MissingRefError.message(baseId, ref)) - this.missingRef = resolve.url(baseId, ref) - this.missingSchema = resolve.normalizeId(resolve.fullPath(this.missingRef)) + this.missingRef = resolveUrl(baseId, ref) + this.missingSchema = normalizeId(getFullPath(this.missingRef)) } } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 1a9f6c2578..260c920963 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,14 +1,13 @@ -import Ajv from "../ajv" -import CodeGen, {_, str, nil, Code, Scope} from "./codegen" -import {validateFunctionCode} from "./validate" import {Schema, SchemaObject, ValidateFunction} from "../types" +import CodeGen, {_, nil, str, Code, Scope} from "./codegen" import N from "./names" +import {LocalRefs, getFullPath, inlineRef, resolve, resolveUrl} from "./resolve" +import {validateFunctionCode} from "./validate" +import Ajv from "../ajv" const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") -const resolve = require("./resolve") - /** * Functions below are used inside compiled validations function */ @@ -16,8 +15,6 @@ const resolve = require("./resolve") // this error is thrown by async schemas to return validation errors via exception const ValidationError = require("./error_classes").ValidationError -module.exports = compile - export type ResolvedRef = InlineResolvedRef | FuncResolvedRef export interface InlineResolvedRef { @@ -38,19 +35,10 @@ export interface SchemaRoot { refs: {[ref: string]: number} } -export class SchemaEnv { +export interface SchemaEnv { schema: Schema root: SchemaRoot - baseId: string - constructor(schema: Schema, root: SchemaRoot, baseId: string) { - this.schema = schema - this.root = root - this.baseId = baseId - } - - equal(env: SchemaEnv): boolean { - return this.schema === env.schema && this.root === env.root && this.baseId === env.baseId - } + baseId?: string } export interface Compilation extends SchemaEnv { @@ -59,14 +47,13 @@ export interface Compilation extends SchemaEnv { } // Compiles schema to validation function -function compile( +export function compileSchema( this: Ajv, - schema: SchemaObject, // TODO or SchemaObject? - root: SchemaRoot, // object with information about the root schema for this schema - localRefs, // the hash of local references inside the schema (created by resolve.id), used for inline resolution - baseId: string // base ID for IDs in the schema -) { - /* eslint no-shadow: 0 */ + schema: Schema, + passedRoot?: SchemaRoot, // object with information about the root schema for this schema + localRefs?: LocalRefs, // the hash of local references inside the schema (created by resolve.id), used for inline resolution + baseId?: string // base ID for IDs in the schema +): ValidateFunction { var self = this, opts = this._opts, refVal = [undefined], @@ -74,9 +61,13 @@ function compile( const scope: Scope = {} - root = root || {schema, refVal, refs} + const root: SchemaRoot = passedRoot || { + schema: typeof schema == "boolean" ? {} : schema, + refVal, + refs, + } - const env = new SchemaEnv(schema, root, baseId) + const env = {schema, root, baseId} let compilation = getCompilation.call(this, env) if (compilation) { @@ -117,14 +108,19 @@ function compile( this._compilations.delete(compilation) } - function localCompile(_schema: SchemaObject, _root: SchemaRoot, localRefs, baseId: string) { + function localCompile( + _schema: Schema, + _root: SchemaRoot, + _localRefs?: LocalRefs, + _baseId?: string + ): ValidateFunction { var isRoot = !_root || (_root && _root.schema === _schema) if (_root.schema !== root.schema) { - return compile.call(self, _schema, _root, localRefs, baseId) + return compileSchema.call(self, _schema, _root, _localRefs, _baseId) } - var $async = _schema.$async === true - const rootId = resolve.fullPath(_root.schema.$id) + var $async = typeof _schema == "object" && _schema.$async === true + const rootId = getFullPath(_root.schema.$id) const gen = new CodeGen({...opts.codegen, forInOwn: opts.ownProperties}) @@ -138,12 +134,12 @@ function compile( dataPathArr: [nil], dataLevel: 0, topSchemaRef: _`${N.validate}.schema`, - async: _schema.$async === true, + async: $async, schema: _schema, isRoot, root: _root, rootId, - baseId: baseId || rootId, + baseId: _baseId || rootId, schemaPath: nil, errSchemaPath: "#", errorPath: str``, @@ -211,8 +207,8 @@ function compile( return validate } - function resolveRef(baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { - ref = resolve.url(baseId, ref) + function resolveRef(_baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { + ref = resolveUrl(_baseId, ref) var refIndex = refs[ref] var _refVal, refCode if (refIndex !== undefined) { @@ -230,28 +226,28 @@ function compile( } refCode = addLocalRef(ref) - var v = resolve.call(self, localCompile, root, ref) - if (v === undefined) { + let _v = resolve.call(self, localCompile, root, ref) + if (_v === undefined) { var localSchema = localRefs && localRefs[ref] if (localSchema) { - v = resolve.inlineRef(localSchema, opts.inlineRefs) + _v = inlineRef(localSchema, opts.inlineRefs) ? localSchema - : compile.call(self, localSchema, root, localRefs, baseId) + : compileSchema.call(self, localSchema, root, localRefs, _baseId) } } - if (v === undefined) { + if (_v === undefined) { removeLocalRef(ref) } else { - replaceLocalRef(ref, v) - return resolvedRef(v, refCode) + replaceLocalRef(ref, _v) + return resolvedRef(_v, refCode) } } // TODO gen.globals - function addLocalRef(ref: string, v?: any): Code { + function addLocalRef(ref: string, _v?): Code { var refId = refVal.length - refVal[refId] = v + refVal[refId] = _v refs[ref] = refId return _`refVal${refId}` } @@ -262,31 +258,33 @@ function compile( } // TODO gen.globals remove? - function replaceLocalRef(ref, v) { + function replaceLocalRef(ref: string, _v) { var refId = refs[ref] - refVal[refId] = v + refVal[refId] = _v } - function resolvedRef(refVal, code: Code): ResolvedRef { - return typeof refVal == "object" || typeof refVal == "boolean" - ? {code: code, schema: refVal, inline: true} - : {code: code, $async: refVal && !!refVal.$async} + function resolvedRef(_refVal, code: Code): ResolvedRef { + return typeof _refVal == "object" || typeof _refVal == "boolean" + ? {code: code, schema: _refVal, inline: true} + : {code: code, $async: _refVal && !!_refVal.$async} } } // Index of schema compilation in the currently compiled list function getCompilation(this: Ajv, env: SchemaEnv): Compilation | void { for (const c of this._compilations) { - if (c.equal(env)) return c + if (equalEnv(c, env)) return c } } +function equalEnv(e1: SchemaEnv, e2: SchemaEnv): boolean { + return e1.schema === e2.schema && e1.root === e2.root && e1.baseId === e2.baseId +} + function refValCode(i: number, refVal): Code { return refVal[i] === undefined ? nil : _`const refVal${i} = refVal[${i}];` } function vars(arr: unknown[], statement: (i: number, arr?: unknown[]) => Code): Code { - return arr - .map((_el, i, arr) => statement(i, arr)) - .reduce((res: Code, c: Code) => _`${res}${c}`, nil) + return arr.map((_el, i) => statement(i, arr)).reduce((res: Code, c: Code) => _`${res}${c}`, nil) } diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 13879ce9a6..5bed82cf5e 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,32 +1,33 @@ +import {Schema, ValidateFunction, SchemaObject} from "../types" +import {SchemaEnv, SchemaRoot} from "./index" import StoredSchema from "./stored_schema" -import {toHash, escapeFragment, unescapeFragment} from "./util" +import {eachItem, toHash, schemaHasRulesExcept, escapeFragment, unescapeFragment} from "./util" import Ajv from "../ajv" -import {SchemaRoot} from "./index" -import {ValidateFunction, Schema} from "../types" +import equal from "fast-deep-equal" +import traverse = require("json-schema-traverse") +import URI = require("uri-js") -var URI = require("uri-js"), - equal = require("fast-deep-equal"), - traverse = require("json-schema-traverse") - -resolve.normalizeId = normalizeId -resolve.fullPath = getFullPath -resolve.url = resolveUrl -resolve.ids = resolveIds -resolve.inlineRef = inlineRef -resolve.schema = resolveSchema +export interface LocalRefs { + [ref: string]: SchemaObject +} // resolve and compile the references ($ref) // TODO returns SchemaObject (if the schema can be inlined) or validation function -function resolve( +export function resolve( this: Ajv, - compile, // reference to schema compilation funciton (localCompile) + localCompile: ( + _schema: Schema, + _root: SchemaRoot, + localRefs?: LocalRefs, + baseId?: string + ) => ValidateFunction, // reference to schema compilation function (localCompile) root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve ): Schema | ValidateFunction | undefined { var refVal = this._refs[ref] if (typeof refVal == "string") { if (this._refs[refVal]) refVal = this._refs[refVal] - else return resolve.call(this, compile, root, refVal) + else return resolve.call(this, localCompile, root, refVal) } refVal = refVal || this._schemas[ref] @@ -45,23 +46,22 @@ function resolve( } if (schema instanceof StoredSchema) { - v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId) + v = schema.validate || localCompile.call(this, schema.schema, root, undefined, baseId) } else if (schema !== undefined) { v = inlineRef(schema, this._opts.inlineRefs) ? schema - : compile.call(this, schema, root, undefined, baseId) + : localCompile.call(this, schema, root, undefined, baseId) } return v } // Resolve schema, its root and baseId -// TODO returns object with properties schema, root, baseId -function resolveSchema( +export function resolveSchema( this: Ajv, root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it ref: string // reference to resolve -) { +): SchemaEnv | undefined { const p = URI.parse(ref) const refPath = _getFullPath(p) let baseId = getFullPath(root.schema.$id) @@ -70,7 +70,8 @@ function resolveSchema( var refVal = this._refs[id] if (typeof refVal == "string") { return resolveRecursive.call(this, root, refVal, p) - } else if (refVal instanceof StoredSchema) { + } + if (refVal instanceof StoredSchema) { if (!refVal.validate) this._compile(refVal) root = refVal } else { @@ -88,19 +89,22 @@ function resolveSchema( if (!root.schema) return baseId = getFullPath(root.schema.$id) } - return getJsonPointer.call(this, p, baseId, root.schema, root) + return getJsonPointer.call(this, p, {schema: root.schema, root, baseId}) } -function resolveRecursive(this: Ajv, root, ref, parsedRef) { - var res = resolveSchema.call(this, root, ref) - if (res) { - var schema = res.schema - var baseId = res.baseId - root = res.root - var id = schema.$id - if (id) baseId = resolveUrl(baseId, id) - return getJsonPointer.call(this, parsedRef, baseId, schema, root) +function resolveRecursive( + this: Ajv, + root: SchemaRoot, + ref: string, + parsedRef: URI.URIComponents +): SchemaEnv | undefined { + const env = resolveSchema.call(this, root, ref) + if (!env) return + const {schema, baseId} = env + if (typeof schema == "object" && schema.$id) { + env.baseId = resolveUrl(baseId, schema.$id) } + return getJsonPointer.call(this, parsedRef, env) } var PREVENT_SCOPE_CHANGE = toHash([ @@ -111,39 +115,45 @@ var PREVENT_SCOPE_CHANGE = toHash([ "definitions", ]) -function getJsonPointer(this: Ajv, parsedRef, baseId, schema, root) { - /* jshint validthis: true */ +function getJsonPointer( + this: Ajv, + parsedRef: URI.URIComponents, + {baseId, schema, root}: SchemaEnv +): SchemaEnv | undefined { + if (typeof schema == "boolean") return parsedRef.fragment = parsedRef.fragment || "" if (parsedRef.fragment.slice(0, 1) !== "/") return var parts = parsedRef.fragment.split("/") - for (var i = 1; i < parts.length; i++) { - var part = parts[i] - if (part) { - part = unescapeFragment(part) - schema = schema[part] - if (schema === undefined) break - var id - if (!PREVENT_SCOPE_CHANGE[part]) { - id = schema.$id - if (id) baseId = resolveUrl(baseId, id) - if (schema.$ref) { - var $ref = resolveUrl(baseId, schema.$ref) - var res = resolveSchema.call(this, root, $ref) - if (res) { - schema = res.schema - root = res.root - baseId = res.baseId - } - } - } + for (let part of parts) { + if (!part) continue + part = unescapeFragment(part) + schema = schema[part] + if (schema === undefined) return + if (PREVENT_SCOPE_CHANGE[part]) continue + if (typeof schema == "object" && schema.$id) { + baseId = resolveUrl(baseId, schema.$id) } } - if (schema !== undefined && schema !== root.schema) { - return {schema, root, baseId} + if (schema === undefined) return + if ( + typeof schema != "boolean" && + schema.$ref && + !schemaHasRulesExcept(schema, this.RULES.all, "$ref") + ) { + const $ref = resolveUrl(baseId, schema.$ref) + const _env = resolveSchema.call(this, root, $ref) + if (_env && !isRootEnv(_env)) return _env } + const env = {schema, root, baseId} + if (!isRootEnv(env)) return env +} + +function isRootEnv({schema, root}: SchemaEnv): boolean { + return schema === root.schema } +// TODO refactor to use keyword definitions var SIMPLE_INLINED = toHash([ "type", "format", @@ -160,61 +170,46 @@ var SIMPLE_INLINED = toHash([ "multipleOf", "required", "enum", + "const", ]) -function inlineRef(schema, limit) { - if (limit === false) return false - if (limit === undefined || limit === true) return checkNoRef(schema) - else if (limit) return countKeys(schema) <= limit +export function inlineRef(schema: Schema, limit: boolean | number = true): boolean { + if (typeof schema == "boolean") return true + if (limit === true) return !hasRef(schema) + if (!limit) return false + return countKeys(schema) <= limit } -function checkNoRef(schema) { - var item - if (Array.isArray(schema)) { - for (var i = 0; i < schema.length; i++) { - item = schema[i] - if (typeof item == "object" && !checkNoRef(item)) return false - } - } else { - for (var key in schema) { - if (key === "$ref") return false - item = schema[key] - if (typeof item == "object" && !checkNoRef(item)) return false - } +function hasRef(schema: SchemaObject): boolean { + for (const key in schema) { + if (key === "$ref") return true + const sch = schema[key] + if (Array.isArray(sch) && sch.some(hasRef)) return true + if (typeof sch == "object" && hasRef(sch)) return true } - return true + return false } -function countKeys(schema) { - var count = 0, - item - if (Array.isArray(schema)) { - for (var i = 0; i < schema.length; i++) { - item = schema[i] - if (typeof item == "object") count += countKeys(item) - if (count === Infinity) return Infinity - } - } else { - for (var key in schema) { - if (key === "$ref") return Infinity - if (SIMPLE_INLINED[key]) { - count++ - } else { - item = schema[key] - if (typeof item == "object") count += countKeys(item) + 1 - if (count === Infinity) return Infinity - } +function countKeys(schema: SchemaObject): number { + let count = 0 + for (const key in schema) { + if (key === "$ref") return Infinity + count++ + if (SIMPLE_INLINED[key]) continue + if (typeof schema[key] == "object") { + eachItem(schema[key], (sch) => (count += countKeys(sch))) } + if (count === Infinity) return Infinity } return count } -export function getFullPath(id: string | undefined, normalize?: boolean): string { +export function getFullPath(id = "", normalize?: boolean): string { if (normalize !== false) id = normalizeId(id) var p = URI.parse(id) return _getFullPath(p) } -function _getFullPath(p) { +function _getFullPath(p: URI.URIComponents): string { return URI.serialize(p).split("#")[0] + "#" } @@ -223,17 +218,17 @@ export function normalizeId(id: string | undefined): string { return id ? id.replace(TRAILING_SLASH_HASH, "") : "" } -export function resolveUrl(baseId: string, id: string): string { +export function resolveUrl(baseId = "", id: string): string { id = normalizeId(id) return URI.resolve(baseId, id) } -/* @this Ajv */ -function resolveIds(this: Ajv, schema) { +export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { + if (typeof schema == "boolean") return {} var schemaId = normalizeId(schema.$id) var baseIds = {"": schemaId} var fullPaths = {"": getFullPath(schemaId, false)} - var localRefs = {} + const localRefs: LocalRefs = {} var self = this traverse( @@ -275,5 +270,3 @@ function resolveIds(this: Ajv, schema) { return localRefs } - -module.exports = resolve diff --git a/lib/compile/stored_schema.ts b/lib/compile/stored_schema.ts index 98f9d77827..10b4f438a7 100644 --- a/lib/compile/stored_schema.ts +++ b/lib/compile/stored_schema.ts @@ -1,38 +1,40 @@ import {Schema, ValidateFunction} from "../types" import {SchemaRoot} from "./index" +import {LocalRefs} from "./resolve" export interface _StoredSchema { + schema: Schema id?: string ref?: string cacheKey?: unknown - schema?: Schema fragment?: true meta?: boolean - root?: SchemaRoot + root?: ValidateFunction | SchemaRoot refs?: {[ref: string]: number} - refVal?: unknown[] - localRefs?: unknown + refVal?: (string | undefined)[] + localRefs?: LocalRefs baseId?: string validate?: ValidateFunction compiling?: boolean } export default class StoredSchema implements _StoredSchema { + schema: Schema id?: string ref?: string cacheKey?: unknown - schema?: Schema fragment?: true meta?: boolean - root?: SchemaRoot + root?: ValidateFunction | SchemaRoot refs?: {[ref: string]: number} - refVal?: unknown[] - localRefs?: unknown + refVal?: (string | undefined)[] + localRefs?: LocalRefs baseId?: string validate?: ValidateFunction compiling?: boolean constructor(obj: _StoredSchema) { + this.schema = obj.schema Object.assign(this, obj) } } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 4feb90a6d9..6bf4410ff9 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,5 +1,5 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" -import {CompilationContext} from "../types" +import {CompilationContext, Schema} from "../types" import N from "./names" export enum DataType { @@ -79,7 +79,7 @@ export function schemaHasRules(schema: object | boolean, rules: object): boolean // TODO rules, schema? export function schemaHasRulesExcept( - schema: object, + schema: Schema, rules: object, exceptKeyword: string ): boolean | undefined { @@ -150,3 +150,11 @@ export function escapeJsonPointer(str: string): string { function unescapeJsonPointer(str: string): string { return str.replace(/~1/g, "/").replace(/~0/g, "~") } + +export function eachItem(xs: T | T[], f: (x: T) => void): void { + if (Array.isArray(xs)) { + for (const x of xs) f(x) + } else { + f(xs) + } +} diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index b1bbc3068a..63de27ed1a 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,13 +1,12 @@ import {CompilationContext, Options} from "../../types" -import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util" -import {checkStrictMode} from "../../vocabularies/util" -import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema" -import {getSchemaTypes, coerceAndCheckDataType} from "./dataType" +import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" +import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" -import CodeGen, {_, str, nil, Block, Code, Name} from "../codegen" +import CodeGen, {_, nil, str, Block, Code, Name} from "../codegen" import N from "../names" - -const resolve = require("../resolve") +import {resolveUrl} from "../resolve" +import {schemaHasRules, schemaHasRulesExcept, schemaUnknownRules} from "../util" +import {checkStrictMode} from "../../vocabularies/util" // schema compilation - generates validation function, subschemaCode (below) is used for subschemas export function validateFunctionCode(it: CompilationContext): void { @@ -113,7 +112,7 @@ function initializeTop(gen: CodeGen): void { } function updateContext(it: CompilationContext): void { - if (it.schema.$id) it.baseId = resolve.url(it.baseId, it.schema.$id) + if (it.schema.$id) it.baseId = resolveUrl(it.baseId, it.schema.$id) } function checkAsync(it: CompilationContext): void { diff --git a/lib/types.ts b/lib/types.ts index 945bd800c0..fdd13b2344 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -46,7 +46,7 @@ export interface CurrentOptions { multipleOfPrecision?: boolean | number messages?: boolean sourceCode?: boolean - processCode?: (code: string, schema: object) => string + processCode?: (code: string, schema: Schema) => string codegen?: CodeGenOptions cache?: CacheInterface logger?: Logger | false @@ -88,11 +88,11 @@ export interface ValidateFunction { parentDataProperty?: string | number, rootData?: object | any[] ): boolean | Promise - schema?: object | boolean + schema?: Schema errors?: null | ErrorObject[] - refs?: object + refs?: {[ref: string]: number} refVal?: any[] - root?: ValidateFunction | object + root?: ValidateFunction | SchemaRoot $async?: true source?: object } diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index e2bdeda10e..7034780fd0 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -225,7 +225,7 @@ describe("resolve", () => { }) it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { - var ajv = new Ajv({schemas: schemas, inlineRefs: 3}) + var ajv = new Ajv({schemas: schemas, inlineRefs: 4}) testSchemas(ajv, true) }) @@ -299,13 +299,13 @@ describe("resolve", () => { function testSchemas(ajv, expectedInlined) { var v1 = ajv.getSchema("http://e.com/obj.json"), v2 = ajv.getSchema("http://e.com/obj1.json"), - vl = ajv.getSchema("http://e.com/list.json") + v3 = ajv.getSchema("http://e.com/list.json") testObjSchema(v1) testObjSchema(v2) - testListSchema(vl) + testListSchema(v3) testInlined(v1, expectedInlined) testInlined(v2, expectedInlined) - testInlined(vl, false) + testInlined(v3, false) } function testObjSchema(validate) { From d324d65b96432139bbd5876e42fe2a0b77fa4dfb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 5 Sep 2020 20:45:18 +0100 Subject: [PATCH 160/322] refactor: resolve getSchemaRefs --- lib/compile/resolve.ts | 110 +++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 59 deletions(-) diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 5bed82cf5e..5a2b534a56 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,7 +1,7 @@ import {Schema, ValidateFunction, SchemaObject} from "../types" import {SchemaEnv, SchemaRoot} from "./index" import StoredSchema from "./stored_schema" -import {eachItem, toHash, schemaHasRulesExcept, escapeFragment, unescapeFragment} from "./util" +import {eachItem, toHash, schemaHasRulesExcept, unescapeFragment} from "./util" import Ajv from "../ajv" import equal from "fast-deep-equal" import traverse = require("json-schema-traverse") @@ -24,36 +24,38 @@ export function resolve( root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve ): Schema | ValidateFunction | undefined { - var refVal = this._refs[ref] - if (typeof refVal == "string") { - if (this._refs[refVal]) refVal = this._refs[refVal] - else return resolve.call(this, localCompile, root, refVal) + let schOrRef = this._refs[ref] + if (typeof schOrRef == "string") { + if (this._refs[schOrRef]) schOrRef = this._refs[schOrRef] + else return resolve.call(this, localCompile, root, schOrRef) } - refVal = refVal || this._schemas[ref] - if (refVal instanceof StoredSchema) { - return inlineRef(refVal.schema, this._opts.inlineRefs) - ? refVal.schema - : refVal.validate || this._compile(refVal) + schOrRef = schOrRef || this._schemas[ref] + if (schOrRef instanceof StoredSchema) { + return inlineRef(schOrRef.schema, this._opts.inlineRefs) + ? schOrRef.schema + : schOrRef.validate || this._compile(schOrRef) } - var res = resolveSchema.call(this, root, ref) - var schema, v, baseId - if (res) { - schema = res.schema - root = res.root - baseId = res.baseId + var env = resolveSchema.call(this, root, ref) + var schema, baseId + if (env) { + schema = env.schema + root = env.root + baseId = env.baseId } if (schema instanceof StoredSchema) { - v = schema.validate || localCompile.call(this, schema.schema, root, undefined, baseId) - } else if (schema !== undefined) { - v = inlineRef(schema, this._opts.inlineRefs) + if (!schema.validate) { + schema.validate = localCompile.call(this, schema.schema, root, undefined, baseId) + } + return schema.validate + } + if (schema !== undefined) { + return inlineRef(schema, this._opts.inlineRefs) ? schema : localCompile.call(this, schema, root, undefined, baseId) } - - return v } // Resolve schema, its root and baseId @@ -123,7 +125,7 @@ function getJsonPointer( if (typeof schema == "boolean") return parsedRef.fragment = parsedRef.fragment || "" if (parsedRef.fragment.slice(0, 1) !== "/") return - var parts = parsedRef.fragment.split("/") + const parts = parsedRef.fragment.split("/") for (let part of parts) { if (!part) continue @@ -225,48 +227,38 @@ export function resolveUrl(baseId = "", id: string): string { export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { if (typeof schema == "boolean") return {} - var schemaId = normalizeId(schema.$id) - var baseIds = {"": schemaId} - var fullPaths = {"": getFullPath(schemaId, false)} + const schemaId = normalizeId(schema.$id) + const baseIds: {[jsonPtr: string]: string} = {"": schemaId} + const pathPrefix = getFullPath(schemaId, false) const localRefs: LocalRefs = {} - var self = this - - traverse( - schema, - {allKeys: true}, - (sch, jsonPtr, _1, parentJsonPtr, parentKeyword, _2, keyIndex) => { - if (jsonPtr === "") return - var id = sch.$id - var baseId = baseIds[parentJsonPtr] - var fullPath = fullPaths[parentJsonPtr] + "/" + parentKeyword - if (keyIndex !== undefined) { - fullPath += "/" + (typeof keyIndex == "number" ? keyIndex : escapeFragment(keyIndex)) - } - - if (typeof id == "string") { - id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id) + const self = this - var refVal = self._refs[id] - if (typeof refVal == "string") refVal = self._refs[refVal] - if (typeof refVal == "object" && refVal.schema) { - if (!equal(sch, refVal.schema)) { - throw new Error('id "' + id + '" resolves to more than one schema') - } - } else if (id !== normalizeId(fullPath)) { - if (id[0] === "#") { - if (localRefs[id] && !equal(sch, localRefs[id])) { - throw new Error('id "' + id + '" resolves to more than one schema') - } - localRefs[id] = sch - } else { - self._refs[id] = fullPath - } + traverse(schema, {allKeys: true}, (sch, jsonPtr, _, parentJsonPtr) => { + if (parentJsonPtr === undefined) return + const fullPath = pathPrefix + jsonPtr + let id = sch.$id + let baseId = baseIds[parentJsonPtr] + if (typeof id == "string") { + id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id) + let schOrRef = self._refs[id] + if (typeof schOrRef == "string") schOrRef = self._refs[schOrRef] + if (typeof schOrRef == "object" && schOrRef.schema) { + checkAmbiguosId(sch, schOrRef.schema, id) + } else if (id !== normalizeId(fullPath)) { + if (id[0] === "#") { + if (localRefs[id]) checkAmbiguosId(sch, localRefs[id], id) + localRefs[id] = sch + } else { + self._refs[id] = fullPath } } - baseIds[jsonPtr] = baseId - fullPaths[jsonPtr] = fullPath } - ) + baseIds[jsonPtr] = baseId + }) return localRefs + + function checkAmbiguosId(sch1: Schema, sch2: Schema, id: string) { + if (!equal(sch1, sch2)) throw new Error(`id "${id}" resolves to more than one schema`) + } } From c93f7a08480a5fcc92e754b4fc3bc200d64322c5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 5 Sep 2020 20:57:06 +0100 Subject: [PATCH 161/322] chore: update json-schema-traverse to include types --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4a9795698..9025004c55 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^0.5.0", "uri-js": "^4.2.2" }, "devDependencies": { From d61a1a507edaf7efbefd611e344fc0c173170730 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 11:33:36 +0100 Subject: [PATCH 162/322] refactor: validate function wrapper --- lib/ajv.ts | 77 +------- lib/cache.ts | 2 +- lib/compile/error_classes.ts | 1 - lib/compile/index.ts | 351 ++++++++++++++++++++++++++++------- lib/compile/resolve.ts | 153 +-------------- lib/compile/stored_schema.ts | 40 ---- lib/types.ts | 9 +- 7 files changed, 306 insertions(+), 327 deletions(-) delete mode 100644 lib/compile/stored_schema.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 46596d6f76..f324d59ac8 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -12,13 +12,12 @@ import { AddedFormat, LoadSchemaFunction, } from "./types" -import StoredSchema from "./compile/stored_schema" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" -import {compileSchema, SchemaRoot, Compilation} from "./compile" -import {resolveSchema, normalizeId, getSchemaRefs} from "./compile/resolve" +import {StoredSchema, compileSchemaFragment, compileStoredSchema, Compilation} from "./compile" +import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" import applicatorVocabulary from "./vocabularies/applicator" @@ -91,7 +90,7 @@ export default class Ajv { if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"') } else { const schemaObj = _addSchema.call(this, schemaKeyRef) - v = schemaObj.validate || this._compile(schemaObj) + v = schemaObj.validate || compileStoredSchema.call(this, schemaObj) } const valid = v(data) @@ -105,18 +104,7 @@ export default class Ajv { _meta?: boolean // true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. ): ValidateFunction { const schemaObj = _addSchema.call(this, schema, undefined, _meta) - return schemaObj.validate || this._compile(schemaObj) - } - - _compile(this: Ajv, schemaObj: StoredSchema, root?: SchemaRoot): ValidateFunction { - if (schemaObj.compiling) return _makeValidate(schemaObj, root) - schemaObj.compiling = true - const v = _tryCompile.call(this, schemaObj, root) - schemaObj.validate = v - schemaObj.refs = v.refs - schemaObj.refVal = v.refVal - schemaObj.root = v.root - return v + return schemaObj.validate || compileStoredSchema.call(this, schemaObj) } // Creates validating function for passed schema with asynchronous loading of missing schemas. @@ -156,7 +144,7 @@ export default class Ajv { function _compileAsync(schemaObj: StoredSchema): ValidateFunction | Promise { try { - return self._compile(schemaObj) + return compileStoredSchema.call(self, schemaObj) } catch (e) { if (e instanceof MissingRefError) return loadMissingSchema(schemaObj, e) throw e @@ -256,11 +244,11 @@ export default class Ajv { const schemaObj = _getSchemaObj.call(this, keyRef) switch (typeof schemaObj) { case "object": - return schemaObj.validate || this._compile(schemaObj) + return schemaObj.validate || compileStoredSchema.call(this, schemaObj) case "string": return this.getSchema(schemaObj) case "undefined": - return _getSchemaFragment.call(this, keyRef) + return compileSchemaFragment.call(this, keyRef) } } @@ -415,25 +403,6 @@ function defaultMeta(this: Ajv): string | SchemaObject | undefined { return this._opts.defaultMeta } -function _getSchemaFragment(this: Ajv, ref: string): ValidateFunction | undefined { - const _root: SchemaRoot = {schema: {}, refVal: [undefined], refs: {}} - const res = resolveSchema.call(this, _root, ref) - if (!res) return - const schema = res.schema - const root = res.root - const baseId = res.baseId - const validate = compileSchema.call(this, schema, root, undefined, baseId) - this._fragments[ref] = new StoredSchema({ - ref, - fragment: true, - schema, - root, - baseId, - validate, - }) - return validate -} - function _getSchemaObj(this: Ajv, keyRef: string): StoredSchema | undefined { keyRef = normalizeId(keyRef) return this._schemas[keyRef] || this._refs[keyRef] || this._fragments[keyRef] @@ -500,38 +469,6 @@ function _addSchema( return schemaObj } -function _tryCompile(this: Ajv, schemaObj: StoredSchema, root?: SchemaRoot) { - const currentOpts = this._opts - if (schemaObj.meta) this._opts = this._metaOpts - - try { - return compileSchema.call(this, schemaObj.schema, root, schemaObj.localRefs) - } catch (e) { - delete schemaObj.validate - throw e - } finally { - schemaObj.compiling = false - if (schemaObj.meta) this._opts = currentOpts - } -} - -function _makeValidate(schemaObj: StoredSchema, root?: SchemaRoot): ValidateFunction { - const callValidate: ValidateFunction = function (this: Ajv | any, ...args) { - if (schemaObj.validate === undefined) throw new Error("ajv implementation error") - const _validate = schemaObj.validate - const result = _validate.apply(this, args) - callValidate.errors = _validate.errors - return result - } - const sch = schemaObj.schema - schemaObj.validate = callValidate - callValidate.schema = sch - callValidate.errors = null - callValidate.root = root ? root : callValidate - if (typeof sch == "object" && sch.$async === true) callValidate.$async = true - return callValidate -} - function addDefaultMetaSchema(this: Ajv): void { let $dataSchema if (this._opts.$data) { diff --git a/lib/cache.ts b/lib/cache.ts index c165fcc2b4..ea9c6b29e2 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,4 +1,4 @@ -import StoredSchema from "./compile/stored_schema" +import {StoredSchema} from "./compile" import {CacheInterface} from "./types" export default class Cache implements CacheInterface { diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts index 6c0e752ab6..844feaf8c1 100644 --- a/lib/compile/error_classes.ts +++ b/lib/compile/error_classes.ts @@ -1,6 +1,5 @@ import {ErrorObject} from "../types" import {resolveUrl, normalizeId, getFullPath} from "./resolve" -// const resolve = require("./resolve") export class ValidationError extends Error { errors: ErrorObject[] diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 260c920963..8093918570 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,9 +1,11 @@ -import {Schema, SchemaObject, ValidateFunction} from "../types" +import {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" import CodeGen, {_, nil, str, Code, Scope} from "./codegen" import N from "./names" -import {LocalRefs, getFullPath, inlineRef, resolve, resolveUrl} from "./resolve" +import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" +import {toHash, schemaHasRulesExcept, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" import Ajv from "../ajv" +import URI = require("uri-js") const equal = require("fast-deep-equal") const ucs2length = require("./ucs2length") @@ -15,6 +17,45 @@ const ucs2length = require("./ucs2length") // this error is thrown by async schemas to return validation errors via exception const ValidationError = require("./error_classes").ValidationError +interface _StoredSchema extends Compilation { + schema: Schema + id?: string + ref?: string + cacheKey?: unknown + fragment?: true + meta?: boolean + root?: SchemaRoot + refs?: {[ref: string]: number} + refVal?: (string | undefined)[] + localRefs?: LocalRefs + baseId?: string + validate?: ValidateFunction | ValidateWrapper + callValidate?: ValidateWrapper + compiling?: boolean +} + +export class StoredSchema implements _StoredSchema { + schema: Schema + id?: string + ref?: string + cacheKey?: unknown + fragment?: true + meta?: boolean + root?: SchemaRoot + refs?: {[ref: string]: number} + refVal?: (string | undefined)[] + localRefs?: LocalRefs + baseId?: string + validate?: ValidateFunction | ValidateWrapper + callValidate?: ValidateWrapper + compiling?: boolean + + constructor(obj: _StoredSchema) { + this.schema = obj.schema + Object.assign(this, obj) + } +} + export type ResolvedRef = InlineResolvedRef | FuncResolvedRef export interface InlineResolvedRef { @@ -35,88 +76,133 @@ export interface SchemaRoot { refs: {[ref: string]: number} } -export interface SchemaEnv { +export interface CompileEnv { schema: Schema - root: SchemaRoot + root?: SchemaRoot + localRefs?: LocalRefs baseId?: string } -export interface Compilation extends SchemaEnv { +export interface SchemaEnv extends CompileEnv { + root: SchemaRoot +} + +export interface Compilation extends CompileEnv { validate?: ValidateFunction - callValidate?: ValidateFunction + callValidate?: ValidateWrapper } -// Compiles schema to validation function -export function compileSchema( +export function compileStoredSchema( this: Ajv, - schema: Schema, - passedRoot?: SchemaRoot, // object with information about the root schema for this schema - localRefs?: LocalRefs, // the hash of local references inside the schema (created by resolve.id), used for inline resolution - baseId?: string // base ID for IDs in the schema -): ValidateFunction { - var self = this, - opts = this._opts, - refVal = [undefined], - refs: {[ref: string]: number} = {} + schemaObj: StoredSchema, + root?: SchemaRoot +): ValidateFunction | ValidateWrapper { + if (schemaObj.compiling) return validateWrapper(schemaObj) + const v = _tryCompile.call(this, schemaObj, root) + schemaObj.validate = v + schemaObj.refs = v.refs + schemaObj.refVal = v.refVal + schemaObj.root = v.root + return v +} - const scope: Scope = {} +export function compileSchemaFragment(this: Ajv, ref: string): ValidateFunction | undefined { + const root: SchemaRoot = {schema: {}, refVal: [undefined], refs: {}} + const env = resolveSchema.call(this, root, ref) + if (!env) return + const validate = compileSchema.call(this, env) + this._fragments[ref] = new StoredSchema({...env, ref, fragment: true, validate}) + return validate +} + +function _tryCompile( + this: Ajv, + schemaObj: StoredSchema, + root?: SchemaRoot +): ValidateFunction | ValidateWrapper { + const currentOpts = this._opts + const {meta, schema, localRefs} = schemaObj + if (meta) this._opts = this._metaOpts + + try { + schemaObj.compiling = true + const v = compileSchema.call(this, {schema, root, localRefs}) + extendValidateWrapper(v, schemaObj, this._opts.sourceCode) + return v + } catch (e) { + delete schemaObj.validate + delete schemaObj.callValidate + throw e + } finally { + schemaObj.compiling = false + if (meta) this._opts = currentOpts + } +} +function validateWrapper(c: Compilation): ValidateWrapper { + if (!c.callValidate) { + const wrapper: ValidateWrapper = function (this: Ajv | any, ...args) { + if (wrapper.validate === undefined) throw new Error("ajv implementation error") + const v = wrapper.validate + const valid = v.apply(this, args) + wrapper.errors = v.errors + return valid + } + c.callValidate = wrapper + } + c.validate = c.callValidate + return c.callValidate +} + +function extendValidateWrapper(v: ValidateFunction, c: Compilation, sourceCode?: boolean): void { + const cv = c.callValidate + if (cv) { + cv.validate = v + cv.schema = v.schema + cv.errors = null + cv.refs = v.refs + cv.refVal = v.refVal + cv.root = v.root + cv.$async = v.$async + if (sourceCode) cv.source = v.source + } +} + +// Compiles schema to validation function +export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateWrapper { + const {schema, root: passedRoot, localRefs} = env + const self = this + const opts = this._opts + const refVal = [undefined] + const refs: {[ref: string]: number} = {} const root: SchemaRoot = passedRoot || { schema: typeof schema == "boolean" ? {} : schema, refVal, refs, } - const env = {schema, root, baseId} let compilation = getCompilation.call(this, env) + if (compilation) return validateWrapper(compilation) - if (compilation) { - const c: Compilation = compilation - if (!c.callValidate) { - const validate: ValidateFunction = function (this: Ajv | any, ...args) { - const v = c.validate - const valid = v.apply(this, args) - validate.errors = v.errors - return valid - } - c.callValidate = validate - } - return c.callValidate - } - + const scope: Scope = {} compilation = env - this._compilations.add(compilation) - var formats = this._formats - var RULES = this.RULES + const formats = this._formats try { - var v = localCompile(schema, root, localRefs, baseId) - compilation.validate = v - var cv = compilation.callValidate - if (cv) { - cv.schema = v.schema - cv.errors = null - cv.refs = v.refs - cv.refVal = v.refVal - cv.root = v.root - cv.$async = v.$async - if (opts.sourceCode) cv.source = v.source - } + this._compilations.add(compilation) + const v = localCompile({...env, root}) + extendValidateWrapper(v, compilation) return v } finally { this._compilations.delete(compilation) } - function localCompile( - _schema: Schema, - _root: SchemaRoot, - _localRefs?: LocalRefs, - _baseId?: string - ): ValidateFunction { - var isRoot = !_root || (_root && _root.schema === _schema) - if (_root.schema !== root.schema) { - return compileSchema.call(self, _schema, _root, _localRefs, _baseId) + function localCompile(_env: SchemaEnv): ValidateFunction { + const {schema: _schema, root: _root, baseId} = _env + var isRoot = isRootEnv(_env) + if (_root !== root) { + return compileSchema.call(self, _env) } var $async = typeof _schema == "object" && _schema.$async === true @@ -139,11 +225,11 @@ export function compileSchema( isRoot, root: _root, rootId, - baseId: _baseId || rootId, + baseId: baseId || rootId, schemaPath: nil, errSchemaPath: "#", errorPath: str``, - RULES, // TODO refactor - it is available on the instance + RULES: self.RULES, // TODO refactor - it is available on the instance formats, opts, resolveRef, // TODO move to gen.globals @@ -175,7 +261,7 @@ export function compileSchema( validate = makeValidate( self, - RULES, + self.RULES, formats, root, refVal, @@ -232,7 +318,7 @@ export function compileSchema( if (localSchema) { _v = inlineRef(localSchema, opts.inlineRefs) ? localSchema - : compileSchema.call(self, localSchema, root, localRefs, _baseId) + : compileSchema.call(self, {schema: localSchema, root, localRefs, baseId: _baseId}) } } @@ -271,13 +357,13 @@ export function compileSchema( } // Index of schema compilation in the currently compiled list -function getCompilation(this: Ajv, env: SchemaEnv): Compilation | void { +function getCompilation(this: Ajv, env: CompileEnv): Compilation | void { for (const c of this._compilations) { if (equalEnv(c, env)) return c } } -function equalEnv(e1: SchemaEnv, e2: SchemaEnv): boolean { +function equalEnv(e1: CompileEnv, e2: CompileEnv): boolean { return e1.schema === e2.schema && e1.root === e2.root && e1.baseId === e2.baseId } @@ -288,3 +374,142 @@ function refValCode(i: number, refVal): Code { function vars(arr: unknown[], statement: (i: number, arr?: unknown[]) => Code): Code { return arr.map((_el, i) => statement(i, arr)).reduce((res: Code, c: Code) => _`${res}${c}`, nil) } + +// resolve and compile the references ($ref) +// TODO returns SchemaObject (if the schema can be inlined) or validation function +export function resolve( + this: Ajv, + localCompile: (env: SchemaEnv) => ValidateFunction, // reference to schema compilation function (localCompile) + root: SchemaRoot, // information about the root schema for the current schema + ref: string // reference to resolve +): Schema | ValidateFunction | undefined { + let schOrRef = this._refs[ref] + if (typeof schOrRef == "string") { + if (this._refs[schOrRef]) schOrRef = this._refs[schOrRef] + else return resolve.call(this, localCompile, root, schOrRef) + } + + schOrRef = schOrRef || this._schemas[ref] + if (schOrRef instanceof StoredSchema) { + return inlineRef(schOrRef.schema, this._opts.inlineRefs) + ? schOrRef.schema + : schOrRef.validate || compileStoredSchema.call(this, schOrRef) + } + + var env = resolveSchema.call(this, root, ref) + var schema, baseId + if (env) { + schema = env.schema + root = env.root + baseId = env.baseId + } + + if (schema instanceof StoredSchema) { + if (!schema.validate) { + schema.validate = localCompile.call(this, {schema: schema.schema, root, baseId}) + } + return schema.validate + } + if (schema !== undefined) { + return inlineRef(schema, this._opts.inlineRefs) + ? schema + : localCompile.call(this, {schema, root, baseId}) + } +} + +// Resolve schema, its root and baseId +export function resolveSchema( + this: Ajv, + root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it + ref: string // reference to resolve +): SchemaEnv | undefined { + const p = URI.parse(ref) + const refPath = _getFullPath(p) + let baseId = getFullPath(root.schema.$id) + if (Object.keys(root.schema).length === 0 || refPath !== baseId) { + var id = normalizeId(refPath) + var refVal = this._refs[id] + if (typeof refVal == "string") { + return resolveRecursive.call(this, root, refVal, p) + } + if (refVal instanceof StoredSchema) { + if (!refVal.validate) compileStoredSchema.call(this, refVal) + root = refVal + } else { + refVal = this._schemas[id] + if (refVal instanceof StoredSchema) { + if (!refVal.validate) compileStoredSchema.call(this, refVal) + if (id === normalizeId(ref)) { + return {schema: refVal, root, baseId} + } + root = refVal + } else { + return + } + } + if (!root.schema) return + baseId = getFullPath(root.schema.$id) + } + return getJsonPointer.call(this, p, {schema: root.schema, root, baseId}) +} + +function resolveRecursive( + this: Ajv, + root: SchemaRoot, + ref: string, + parsedRef: URI.URIComponents +): SchemaEnv | undefined { + const env = resolveSchema.call(this, root, ref) + if (!env) return + const {schema, baseId} = env + if (typeof schema == "object" && schema.$id) { + env.baseId = resolveUrl(baseId, schema.$id) + } + return getJsonPointer.call(this, parsedRef, env) +} + +var PREVENT_SCOPE_CHANGE = toHash([ + "properties", + "patternProperties", + "enum", + "dependencies", + "definitions", +]) + +function getJsonPointer( + this: Ajv, + parsedRef: URI.URIComponents, + {baseId, schema, root}: SchemaEnv +): SchemaEnv | undefined { + if (typeof schema == "boolean") return + parsedRef.fragment = parsedRef.fragment || "" + if (parsedRef.fragment.slice(0, 1) !== "/") return + const parts = parsedRef.fragment.split("/") + + for (let part of parts) { + if (!part) continue + part = unescapeFragment(part) + schema = schema[part] + if (schema === undefined) return + if (PREVENT_SCOPE_CHANGE[part]) continue + if (typeof schema == "object" && schema.$id) { + baseId = resolveUrl(baseId, schema.$id) + } + } + if (schema === undefined) return + if ( + typeof schema != "boolean" && + schema.$ref && + !schemaHasRulesExcept(schema, this.RULES.all, "$ref") + ) { + const $ref = resolveUrl(baseId, schema.$ref) + const _env = resolveSchema.call(this, root, $ref) + if (_env && !isRootEnv(_env)) return _env + } + const env = {schema, root, baseId} + if (!isRootEnv(env)) return env +} + +function isRootEnv({schema, root}: SchemaEnv): boolean { + return schema === root.schema +} diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 5a2b534a56..f2e76c021e 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,160 +1,15 @@ -import {Schema, ValidateFunction, SchemaObject} from "../types" -import {SchemaEnv, SchemaRoot} from "./index" -import StoredSchema from "./stored_schema" -import {eachItem, toHash, schemaHasRulesExcept, unescapeFragment} from "./util" +import {Schema, SchemaObject} from "../types" +import {eachItem, toHash} from "./util" import Ajv from "../ajv" import equal from "fast-deep-equal" import traverse = require("json-schema-traverse") import URI = require("uri-js") +// the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution export interface LocalRefs { [ref: string]: SchemaObject } -// resolve and compile the references ($ref) -// TODO returns SchemaObject (if the schema can be inlined) or validation function -export function resolve( - this: Ajv, - localCompile: ( - _schema: Schema, - _root: SchemaRoot, - localRefs?: LocalRefs, - baseId?: string - ) => ValidateFunction, // reference to schema compilation function (localCompile) - root: SchemaRoot, // information about the root schema for the current schema - ref: string // reference to resolve -): Schema | ValidateFunction | undefined { - let schOrRef = this._refs[ref] - if (typeof schOrRef == "string") { - if (this._refs[schOrRef]) schOrRef = this._refs[schOrRef] - else return resolve.call(this, localCompile, root, schOrRef) - } - - schOrRef = schOrRef || this._schemas[ref] - if (schOrRef instanceof StoredSchema) { - return inlineRef(schOrRef.schema, this._opts.inlineRefs) - ? schOrRef.schema - : schOrRef.validate || this._compile(schOrRef) - } - - var env = resolveSchema.call(this, root, ref) - var schema, baseId - if (env) { - schema = env.schema - root = env.root - baseId = env.baseId - } - - if (schema instanceof StoredSchema) { - if (!schema.validate) { - schema.validate = localCompile.call(this, schema.schema, root, undefined, baseId) - } - return schema.validate - } - if (schema !== undefined) { - return inlineRef(schema, this._opts.inlineRefs) - ? schema - : localCompile.call(this, schema, root, undefined, baseId) - } -} - -// Resolve schema, its root and baseId -export function resolveSchema( - this: Ajv, - root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it - ref: string // reference to resolve -): SchemaEnv | undefined { - const p = URI.parse(ref) - const refPath = _getFullPath(p) - let baseId = getFullPath(root.schema.$id) - if (Object.keys(root.schema).length === 0 || refPath !== baseId) { - var id = normalizeId(refPath) - var refVal = this._refs[id] - if (typeof refVal == "string") { - return resolveRecursive.call(this, root, refVal, p) - } - if (refVal instanceof StoredSchema) { - if (!refVal.validate) this._compile(refVal) - root = refVal - } else { - refVal = this._schemas[id] - if (refVal instanceof StoredSchema) { - if (!refVal.validate) this._compile(refVal) - if (id === normalizeId(ref)) { - return {schema: refVal, root, baseId} - } - root = refVal - } else { - return - } - } - if (!root.schema) return - baseId = getFullPath(root.schema.$id) - } - return getJsonPointer.call(this, p, {schema: root.schema, root, baseId}) -} - -function resolveRecursive( - this: Ajv, - root: SchemaRoot, - ref: string, - parsedRef: URI.URIComponents -): SchemaEnv | undefined { - const env = resolveSchema.call(this, root, ref) - if (!env) return - const {schema, baseId} = env - if (typeof schema == "object" && schema.$id) { - env.baseId = resolveUrl(baseId, schema.$id) - } - return getJsonPointer.call(this, parsedRef, env) -} - -var PREVENT_SCOPE_CHANGE = toHash([ - "properties", - "patternProperties", - "enum", - "dependencies", - "definitions", -]) - -function getJsonPointer( - this: Ajv, - parsedRef: URI.URIComponents, - {baseId, schema, root}: SchemaEnv -): SchemaEnv | undefined { - if (typeof schema == "boolean") return - parsedRef.fragment = parsedRef.fragment || "" - if (parsedRef.fragment.slice(0, 1) !== "/") return - const parts = parsedRef.fragment.split("/") - - for (let part of parts) { - if (!part) continue - part = unescapeFragment(part) - schema = schema[part] - if (schema === undefined) return - if (PREVENT_SCOPE_CHANGE[part]) continue - if (typeof schema == "object" && schema.$id) { - baseId = resolveUrl(baseId, schema.$id) - } - } - if (schema === undefined) return - if ( - typeof schema != "boolean" && - schema.$ref && - !schemaHasRulesExcept(schema, this.RULES.all, "$ref") - ) { - const $ref = resolveUrl(baseId, schema.$ref) - const _env = resolveSchema.call(this, root, $ref) - if (_env && !isRootEnv(_env)) return _env - } - const env = {schema, root, baseId} - if (!isRootEnv(env)) return env -} - -function isRootEnv({schema, root}: SchemaEnv): boolean { - return schema === root.schema -} - // TODO refactor to use keyword definitions var SIMPLE_INLINED = toHash([ "type", @@ -211,7 +66,7 @@ export function getFullPath(id = "", normalize?: boolean): string { return _getFullPath(p) } -function _getFullPath(p: URI.URIComponents): string { +export function _getFullPath(p: URI.URIComponents): string { return URI.serialize(p).split("#")[0] + "#" } diff --git a/lib/compile/stored_schema.ts b/lib/compile/stored_schema.ts deleted file mode 100644 index 10b4f438a7..0000000000 --- a/lib/compile/stored_schema.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {Schema, ValidateFunction} from "../types" -import {SchemaRoot} from "./index" -import {LocalRefs} from "./resolve" - -export interface _StoredSchema { - schema: Schema - id?: string - ref?: string - cacheKey?: unknown - fragment?: true - meta?: boolean - root?: ValidateFunction | SchemaRoot - refs?: {[ref: string]: number} - refVal?: (string | undefined)[] - localRefs?: LocalRefs - baseId?: string - validate?: ValidateFunction - compiling?: boolean -} - -export default class StoredSchema implements _StoredSchema { - schema: Schema - id?: string - ref?: string - cacheKey?: unknown - fragment?: true - meta?: boolean - root?: ValidateFunction | SchemaRoot - refs?: {[ref: string]: number} - refVal?: (string | undefined)[] - localRefs?: LocalRefs - baseId?: string - validate?: ValidateFunction - compiling?: boolean - - constructor(obj: _StoredSchema) { - this.schema = obj.schema - Object.assign(this, obj) - } -} diff --git a/lib/types.ts b/lib/types.ts index fdd13b2344..8ce95f4016 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,8 +1,7 @@ import CodeGen, {Code, Name, CodeGenOptions} from "./compile/codegen" import {ValidationRules} from "./compile/rules" -import {ResolvedRef, SchemaRoot} from "./compile" +import {ResolvedRef, SchemaRoot, StoredSchema} from "./compile" import KeywordContext from "./compile/context" -import StoredSchema from "./compile/stored_schema" import Ajv from "./ajv" export interface SchemaObject { @@ -92,11 +91,15 @@ export interface ValidateFunction { errors?: null | ErrorObject[] refs?: {[ref: string]: number} refVal?: any[] - root?: ValidateFunction | SchemaRoot + root?: SchemaRoot $async?: true source?: object } +export interface ValidateWrapper extends ValidateFunction { + validate?: ValidateFunction +} + export interface SchemaValidateFunction { ( schema: any, From ee218155f5941612090b7f886a5c3ace36ba8ae7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 11:43:47 +0100 Subject: [PATCH 163/322] enable noImplicitReturns --- lib/compile/index.ts | 2 ++ lib/compile/util.ts | 7 +++++-- tsconfig.json | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 8093918570..7313c78ae3 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -415,6 +415,7 @@ export function resolve( ? schema : localCompile.call(this, {schema, root, baseId}) } + return undefined } // Resolve schema, its root and baseId @@ -508,6 +509,7 @@ function getJsonPointer( } const env = {schema, root, baseId} if (!isRootEnv(env)) return env + return undefined } function isRootEnv({schema, root}: SchemaEnv): boolean { diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 6bf4410ff9..5d5e1ae506 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -72,9 +72,10 @@ export function toHash(arr: string[]): {[key: string]: true} { } // TODO rules, schema? -export function schemaHasRules(schema: object | boolean, rules: object): boolean | undefined { +export function schemaHasRules(schema: object | boolean, rules: object): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (rules[key]) return true + return false } // TODO rules, schema? @@ -82,15 +83,17 @@ export function schemaHasRulesExcept( schema: Schema, rules: object, exceptKeyword: string -): boolean | undefined { +): boolean { if (typeof schema == "boolean") return !schema && exceptKeyword !== "not" for (const key in schema) if (key !== exceptKeyword && rules[key]) return true + return false } // TODO rules, schema? export function schemaUnknownRules(schema: object, rules: object): string | undefined { if (typeof schema === "boolean") return for (const key in schema) if (!rules[key]) return key + return } const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ diff --git a/tsconfig.json b/tsconfig.json index d9d2f99ce8..0fd078484c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "lib": ["ES2018", "DOM"], "types": ["node"], "noImplicitAny": false, - "noImplicitReturns": false, "allowJs": true, "target": "ES2018" } From 7ca77d7a12cea482fad96a4e347001ab5b1e6c5c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 13:41:06 +0100 Subject: [PATCH 164/322] refactor: resolveRef; add missing types --- lib/ajv.ts | 8 +- lib/compile/index.ts | 115 ++++++++++++++------------ lib/compile/util.ts | 13 ++- lib/compile/validate/applicability.ts | 4 +- lib/compile/validate/dataType.ts | 2 +- lib/compile/validate/index.ts | 5 +- lib/types.ts | 15 ++-- lib/vocabularies/applicator/anyOf.ts | 4 +- lib/vocabularies/applicator/oneOf.ts | 4 +- lib/vocabularies/util.ts | 3 +- lib/vocabularies/validation/enum.ts | 2 +- 11 files changed, 100 insertions(+), 75 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index f324d59ac8..b0d7316cea 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -33,8 +33,12 @@ const META_SUPPORT_DATA = ["/properties"] type CompileAsyncCallback = (err: Error | null, validate?: ValidateFunction) => void +interface IndexableOptions extends Options { + [opt: string]: unknown +} + export default class Ajv { - _opts: Options + _opts: IndexableOptions _cache: CacheInterface _schemas: {[key: string]: StoredSchema} = {} _refs: {[ref: string]: StoredSchema | string} = {} @@ -42,7 +46,7 @@ export default class Ajv { _formats: {[name: string]: AddedFormat} = {} _compilations: Set = new Set() _loadingSchemas: {[ref: string]: Promise} = {} - _metaOpts: Options + _metaOpts: IndexableOptions RULES: ValidationRules logger: Logger errors?: ErrorObject[] | null // errors from the last validation diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 7313c78ae3..8588647c87 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -25,8 +25,8 @@ interface _StoredSchema extends Compilation { fragment?: true meta?: boolean root?: SchemaRoot - refs?: {[ref: string]: number} - refVal?: (string | undefined)[] + refs?: {[ref: string]: number | undefined} + refVal?: (RefVal | undefined)[] localRefs?: LocalRefs baseId?: string validate?: ValidateFunction | ValidateWrapper @@ -42,8 +42,8 @@ export class StoredSchema implements _StoredSchema { fragment?: true meta?: boolean root?: SchemaRoot - refs?: {[ref: string]: number} - refVal?: (string | undefined)[] + refs?: {[ref: string]: number | undefined} + refVal?: (RefVal | undefined)[] localRefs?: LocalRefs baseId?: string validate?: ValidateFunction | ValidateWrapper @@ -70,10 +70,13 @@ export interface FuncResolvedRef { inline?: false } +// reference to compiled schema or schema to be inlined +export type RefVal = Schema | ValidateFunction + export interface SchemaRoot { schema: SchemaObject - refVal: (string | undefined)[] - refs: {[ref: string]: number} + refVal: (RefVal | undefined)[] + refs: {[ref: string]: number | undefined} } export interface CompileEnv { @@ -173,8 +176,8 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va const {schema, root: passedRoot, localRefs} = env const self = this const opts = this._opts - const refVal = [undefined] - const refs: {[ref: string]: number} = {} + const refVal: (RefVal | undefined)[] = [undefined] + const refs: {[ref: string]: number | undefined} = {} const root: SchemaRoot = passedRoot || { schema: typeof schema == "boolean" ? {} : schema, refVal, @@ -295,45 +298,48 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va function resolveRef(_baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { ref = resolveUrl(_baseId, ref) - var refIndex = refs[ref] - var _refVal, refCode - if (refIndex !== undefined) { - _refVal = refVal[refIndex] - refCode = _`refVal[${refIndex}]` - return resolvedRef(_refVal, refCode) - } - if (!isRoot && root.refs) { - var rootRefId = root.refs[ref] - if (rootRefId !== undefined) { - _refVal = root.refVal[rootRefId] - refCode = addLocalRef(ref, _refVal) - return resolvedRef(_refVal, refCode) - } - } + const res = getExistingRef(ref, isRoot) + if (res) return res - refCode = addLocalRef(ref) - let _v = resolve.call(self, localCompile, root, ref) - if (_v === undefined) { - var localSchema = localRefs && localRefs[ref] + const refCode = localRefCode(ref) + let schOrFunc = resolve.call(self, localCompile, root, ref) + if (schOrFunc === undefined) { + const localSchema = localRefs && localRefs[ref] if (localSchema) { - _v = inlineRef(localSchema, opts.inlineRefs) + schOrFunc = inlineRef(localSchema, opts.inlineRefs) ? localSchema : compileSchema.call(self, {schema: localSchema, root, localRefs, baseId: _baseId}) } } - if (_v === undefined) { + if (schOrFunc === undefined) { removeLocalRef(ref) } else { - replaceLocalRef(ref, _v) - return resolvedRef(_v, refCode) + replaceLocalRef(ref, schOrFunc) + return resolvedRef(schOrFunc, refCode) + } + } + + function getExistingRef(ref: string, isRoot: boolean): ResolvedRef | void { + const idx = refs[ref] + if (idx !== undefined) { + const schOrFunc = refVal[idx] + return resolvedRef(schOrFunc, _`refVal[${idx}]`) + } + if (!isRoot && root.refs) { + // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaRoot) + const rootIdx = root.refs[ref] + if (rootIdx !== undefined) { + const schOrFunc = root.refVal[rootIdx] + return resolvedRef(schOrFunc, localRefCode(ref, schOrFunc)) + } } } // TODO gen.globals - function addLocalRef(ref: string, _v?): Code { + function localRefCode(ref: string, schOrFunc?: RefVal): Code { var refId = refVal.length - refVal[refId] = _v + refVal[refId] = schOrFunc refs[ref] = refId return _`refVal${refId}` } @@ -344,15 +350,15 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va } // TODO gen.globals remove? - function replaceLocalRef(ref: string, _v) { + function replaceLocalRef(ref: string, schOrFunc: RefVal) { var refId = refs[ref] - refVal[refId] = _v + if (refId !== undefined) refVal[refId] = schOrFunc } - function resolvedRef(_refVal, code: Code): ResolvedRef { - return typeof _refVal == "object" || typeof _refVal == "boolean" - ? {code: code, schema: _refVal, inline: true} - : {code: code, $async: _refVal && !!_refVal.$async} + function resolvedRef(schOrFunc: RefVal | undefined, code: Code): ResolvedRef { + return typeof schOrFunc == "object" || typeof schOrFunc == "boolean" + ? {code: code, schema: schOrFunc, inline: true} + : {code: code, $async: schOrFunc && !!schOrFunc.$async} } } @@ -367,11 +373,14 @@ function equalEnv(e1: CompileEnv, e2: CompileEnv): boolean { return e1.schema === e2.schema && e1.root === e2.root && e1.baseId === e2.baseId } -function refValCode(i: number, refVal): Code { +function refValCode(i: number, refVal: (RefVal | undefined)[]): Code { return refVal[i] === undefined ? nil : _`const refVal${i} = refVal[${i}];` } -function vars(arr: unknown[], statement: (i: number, arr?: unknown[]) => Code): Code { +function vars( + arr: (RefVal | undefined)[], + statement: (i: number, arr: (RefVal | undefined)[]) => Code +): Code { return arr.map((_el, i) => statement(i, arr)).reduce((res: Code, c: Code) => _`${res}${c}`, nil) } @@ -382,7 +391,7 @@ export function resolve( localCompile: (env: SchemaEnv) => ValidateFunction, // reference to schema compilation function (localCompile) root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve -): Schema | ValidateFunction | undefined { +): RefVal | undefined { let schOrRef = this._refs[ref] if (typeof schOrRef == "string") { if (this._refs[schOrRef]) schOrRef = this._refs[schOrRef] @@ -429,21 +438,21 @@ export function resolveSchema( let baseId = getFullPath(root.schema.$id) if (Object.keys(root.schema).length === 0 || refPath !== baseId) { var id = normalizeId(refPath) - var refVal = this._refs[id] - if (typeof refVal == "string") { - return resolveRecursive.call(this, root, refVal, p) + var schOrRef = this._refs[id] + if (typeof schOrRef == "string") { + return resolveRecursive.call(this, root, schOrRef, p) } - if (refVal instanceof StoredSchema) { - if (!refVal.validate) compileStoredSchema.call(this, refVal) - root = refVal + if (schOrRef instanceof StoredSchema) { + if (!schOrRef.validate) compileStoredSchema.call(this, schOrRef) + root = schOrRef } else { - refVal = this._schemas[id] - if (refVal instanceof StoredSchema) { - if (!refVal.validate) compileStoredSchema.call(this, refVal) + schOrRef = this._schemas[id] + if (schOrRef instanceof StoredSchema) { + if (!schOrRef.validate) compileStoredSchema.call(this, schOrRef) if (id === normalizeId(ref)) { - return {schema: refVal, root, baseId} + return {schema: schOrRef, root, baseId} } - root = refVal + root = schOrRef } else { return } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 5d5e1ae506..c4220c8386 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,6 +1,7 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" import {CompilationContext, Schema} from "../types" import N from "./names" +import {Rule} from "./rules" export enum DataType { Correct, @@ -65,14 +66,15 @@ export function checkDataTypes( return cond } +// TODO refactor to use Set export function toHash(arr: string[]): {[key: string]: true} { - const hash = {} + const hash: {[key: string]: true} = {} for (const item of arr) hash[item] = true return hash } // TODO rules, schema? -export function schemaHasRules(schema: object | boolean, rules: object): boolean { +export function schemaHasRules(schema: Schema, rules: {[key: string]: boolean | Rule}): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (rules[key]) return true return false @@ -81,7 +83,7 @@ export function schemaHasRules(schema: object | boolean, rules: object): boolean // TODO rules, schema? export function schemaHasRulesExcept( schema: Schema, - rules: object, + rules: {[key: string]: boolean | Rule}, exceptKeyword: string ): boolean { if (typeof schema == "boolean") return !schema && exceptKeyword !== "not" @@ -90,7 +92,10 @@ export function schemaHasRulesExcept( } // TODO rules, schema? -export function schemaUnknownRules(schema: object, rules: object): string | undefined { +export function schemaUnknownRules( + schema: object, + rules: {[key: string]: boolean | Rule} +): string | undefined { if (typeof schema === "boolean") return for (const key in schema) if (!rules[key]) return key return diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 5e28602e01..93afbfa850 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,4 +1,4 @@ -import {CompilationContext} from "../../types" +import {CompilationContext, SchemaObject} from "../../types" import {RuleGroup, Rule} from "../rules" export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: string): boolean { @@ -10,7 +10,7 @@ export function shouldUseGroup(schema: object, group: RuleGroup): boolean { return group.rules.some((rule) => shouldUseRule(schema, rule)) } -export function shouldUseRule(schema: object, rule: Rule): boolean | undefined { +export function shouldUseRule(schema: SchemaObject, rule: Rule): boolean | undefined { return ( schema[rule.keyword] !== undefined || rule.definition.implements?.some((kwd) => schema[kwd] !== undefined) diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index f854488904..36cac65b91 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -79,7 +79,7 @@ function coerceData(it: CompilationContext, types: string[], coerceTo: string[]) assignParentData(it, coerced) }) - function coerceSpecificType(t) { + function coerceSpecificType(t: string): void { switch (t) { case "string": gen diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 63de27ed1a..765b89680c 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -7,6 +7,7 @@ import N from "../names" import {resolveUrl} from "../resolve" import {schemaHasRules, schemaHasRulesExcept, schemaUnknownRules} from "../util" import {checkStrictMode} from "../../vocabularies/util" +import {SchemaObject} from "json-schema-traverse" // schema compilation - generates validation function, subschemaCode (below) is used for subschemas export function validateFunctionCode(it: CompilationContext): void { @@ -36,9 +37,9 @@ function validateFunction({gen, schema, async, opts}: CompilationContext, body: ) } -function funcSourceUrl(schema, opts: Options): Code { +function funcSourceUrl(schema: SchemaObject, opts: Options): Code { return schema.$id && (opts.sourceCode || opts.processCode) - ? _`/*# sourceURL=${schema.$id as string} */` + ? _`/*# sourceURL=${schema.$id} */` : nil } diff --git a/lib/types.ts b/lib/types.ts index 8ce95f4016..29bc8657e0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,6 @@ -import CodeGen, {Code, Name, CodeGenOptions} from "./compile/codegen" +import CodeGen, {Code, Name, CodeGenOptions, Scope} from "./compile/codegen" import {ValidationRules} from "./compile/rules" -import {ResolvedRef, SchemaRoot, StoredSchema} from "./compile" +import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" import KeywordContext from "./compile/context" import Ajv from "./ajv" @@ -78,6 +78,11 @@ export interface CacheInterface { clear(): void } +interface SourceCode { + code: string + scope: Scope +} + export interface ValidateFunction { ( this: Ajv | any, @@ -89,11 +94,11 @@ export interface ValidateFunction { ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] - refs?: {[ref: string]: number} - refVal?: any[] + refs?: {[ref: string]: number | undefined} + refVal?: (RefVal | undefined)[] root?: SchemaRoot $async?: true - source?: object + source?: SourceCode } export interface ValidateWrapper extends ValidateFunction { diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index fd9931a515..7f3ee1584c 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, Schema} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -17,7 +17,7 @@ const def: CodeKeywordDefinition = { const schValid = gen.name("_valid") gen.block(() => { - schema.forEach((_sch, i: number) => { + schema.forEach((_sch: Schema, i: number) => { applySubschema( it, { diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index fbcfdf1a53..6e3caa476b 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, Schema} from "../../types" import KeywordContext from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -25,7 +25,7 @@ const def: CodeKeywordDefinition = { ) function validateOneOf() { - schema.forEach((sch, i: number) => { + schema.forEach((sch: Schema, i: number) => { if (alwaysValidSchema(it, sch)) { gen.var(schValid, true) } else { diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index da004fdc3f..2b13aae8b2 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -3,6 +3,7 @@ import {CompilationContext} from "../types" import KeywordContext from "../compile/context" import CodeGen, {_, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" +import {SchemaObject} from "json-schema-traverse" export function schemaRefOrVal( {topSchemaRef, schemaPath}: CompilationContext, @@ -33,7 +34,7 @@ export function allSchemaProperties(schema?: object): string[] { return schema ? Object.keys(schema).filter((p) => p !== "__proto__") : [] } -export function schemaProperties(it: CompilationContext, schema: object): string[] { +export function schemaProperties(it: CompilationContext, schema: SchemaObject): string[] { return allSchemaProperties(schema).filter((p) => !alwaysValidSchema(it, schema[p])) } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 280e663da4..cd2dc363ee 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -16,7 +16,7 @@ const def: CodeKeywordDefinition = { cxt.block$data(valid, loopEnum) } else { const vSchema = gen.const("schema", schemaCode) - valid = or(...schema.map((_x, i) => equalCode(vSchema, i))) + valid = or(...schema.map((_x: any, i: number) => equalCode(vSchema, i))) } cxt.pass(valid) From b67b15effd1766a9d15fd4bb0ea2686409eaf1d7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 19:08:42 +0100 Subject: [PATCH 165/322] enable noImplicitAny, rename interfaces, refactor --- .eslintrc.yml | 2 +- lib/compile/context.ts | 25 ++--- lib/compile/errors.ts | 10 +- lib/compile/index.ts | 9 +- lib/compile/subschema.ts | 16 +-- lib/compile/util.ts | 35 +++--- lib/compile/validate/applicability.ts | 6 +- lib/compile/validate/boolSchema.ts | 13 +-- lib/compile/validate/dataType.ts | 22 ++-- lib/compile/validate/defaults.ts | 6 +- lib/compile/validate/index.ts | 105 +++++++++--------- lib/compile/validate/iterate.ts | 13 +-- lib/compile/validate/keyword.ts | 18 +-- lib/types.ts | 50 +++++---- .../applicator/additionalItems.ts | 4 +- .../applicator/additionalProperties.ts | 4 +- lib/vocabularies/applicator/allOf.ts | 4 +- lib/vocabularies/applicator/anyOf.ts | 4 +- lib/vocabularies/applicator/contains.ts | 4 +- lib/vocabularies/applicator/dependencies.ts | 4 +- lib/vocabularies/applicator/if.ts | 8 +- lib/vocabularies/applicator/items.ts | 4 +- lib/vocabularies/applicator/not.ts | 4 +- lib/vocabularies/applicator/oneOf.ts | 4 +- .../applicator/patternProperties.ts | 4 +- lib/vocabularies/applicator/properties.ts | 6 +- lib/vocabularies/applicator/propertyNames.ts | 4 +- lib/vocabularies/core/ref.ts | 4 +- lib/vocabularies/format/format.ts | 4 +- lib/vocabularies/missing.ts | 8 +- lib/vocabularies/util.ts | 43 +++---- lib/vocabularies/validation/const.ts | 4 +- lib/vocabularies/validation/enum.ts | 4 +- lib/vocabularies/validation/limit.ts | 4 +- lib/vocabularies/validation/limitItems.ts | 4 +- lib/vocabularies/validation/limitLength.ts | 4 +- .../validation/limitProperties.ts | 4 +- lib/vocabularies/validation/multipleOf.ts | 4 +- lib/vocabularies/validation/pattern.ts | 4 +- lib/vocabularies/validation/required.ts | 4 +- lib/vocabularies/validation/uniqueItems.ts | 4 +- tsconfig.json | 1 - 42 files changed, 238 insertions(+), 250 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index dacbb32c16..f793a40556 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -39,7 +39,7 @@ overrides: rules: block-scoped-var: error callback-return: error - complexity: [error, 17] + complexity: [error, 18] curly: [error, multi-line, consistent] dot-location: [error, property] dot-notation: error diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 998e039f84..8b75940967 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -1,8 +1,9 @@ import { KeywordDefinition, - KeywordErrorContext, - KeywordContextParams, - CompilationContext, + KeywordErrorCtx, + KeywordCtxParams, + SchemaObjCtx, + SchemaObject, } from "../types" import {schemaRefOrVal} from "../vocabularies/util" import {getData, checkDataTypes, DataType} from "./util" @@ -16,7 +17,7 @@ import { import CodeGen, {_, nil, or, Code, Name} from "./codegen" import N from "./names" -export default class KeywordContext implements KeywordErrorContext { +export default class KeywordCtx implements KeywordErrorCtx { gen: CodeGen allErrors: boolean keyword: string @@ -26,13 +27,13 @@ export default class KeywordContext implements KeywordErrorContext { schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) schemaType?: string | string[] - parentSchema: any + parentSchema: SchemaObject errsCount?: Name - params: KeywordContextParams - it: CompilationContext + params: KeywordCtxParams + it: SchemaObjCtx def: KeywordDefinition - constructor(it: CompilationContext, def: KeywordDefinition, keyword: string) { + constructor(it: SchemaObjCtx, def: KeywordDefinition, keyword: string) { validateKeywordUsage(it, def, keyword) this.gen = it.gen this.allErrors = it.allErrors @@ -113,7 +114,7 @@ export default class KeywordContext implements KeywordErrorContext { if (!this.allErrors) this.gen.if(cond) } - setParams(obj: KeywordContextParams, assign?: true): void { + setParams(obj: KeywordCtxParams, assign?: true): void { if (assign) Object.assign(this.params, obj) else this.params = obj } @@ -173,11 +174,7 @@ function validSchemaType(schema: any, schemaType: string | string[]): boolean { : typeof schema == schemaType } -function validateKeywordUsage( - it: CompilationContext, - def: KeywordDefinition, - keyword: string -): void { +function validateKeywordUsage(it: SchemaObjCtx, def: KeywordDefinition, keyword: string): void { if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { throw new Error("ajv implementation error") } diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 06ae9acd9e..258abe8cc5 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,4 +1,4 @@ -import {KeywordErrorContext, KeywordErrorDefinition} from "../types" +import {KeywordErrorCtx, KeywordErrorDefinition} from "../types" import CodeGen, {_, str, Code, Name} from "./codegen" import N from "./names" @@ -14,7 +14,7 @@ export const keyword$DataError: KeywordErrorDefinition = { } export function reportError( - cxt: KeywordErrorContext, + cxt: KeywordErrorCtx, error: KeywordErrorDefinition, overrideAllErrors?: boolean ): void { @@ -27,7 +27,7 @@ export function reportError( } } -export function reportExtraError(cxt: KeywordErrorContext, error: KeywordErrorDefinition): void { +export function reportExtraError(cxt: KeywordErrorCtx, error: KeywordErrorDefinition): void { const {gen, compositeRule, allErrors, async} = cxt.it const errObj = errorObjectCode(cxt, error) addError(gen, errObj) @@ -50,7 +50,7 @@ export function extendErrors({ data, errsCount, it, -}: KeywordErrorContext): void { +}: KeywordErrorCtx): void { if (errsCount === undefined) throw new Error("ajv implementation error") const err = gen.name("err") gen.for(_`let i=${errsCount}; i<${N.errors}; i++`, () => { @@ -81,7 +81,7 @@ function returnErrors(gen: CodeGen, async: boolean, errs: Code): void { } } -function errorObjectCode(cxt: KeywordErrorContext, error: KeywordErrorDefinition): Code { +function errorObjectCode(cxt: KeywordErrorCtx, error: KeywordErrorDefinition): Code { const { keyword, data, diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 8588647c87..042a5edcad 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -2,7 +2,7 @@ import {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" import CodeGen, {_, nil, str, Code, Scope} from "./codegen" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" -import {toHash, schemaHasRulesExcept, unescapeFragment} from "./util" +import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" import Ajv from "../ajv" import URI = require("uri-js") @@ -29,7 +29,7 @@ interface _StoredSchema extends Compilation { refVal?: (RefVal | undefined)[] localRefs?: LocalRefs baseId?: string - validate?: ValidateFunction | ValidateWrapper + validate?: ValidateFunction callValidate?: ValidateWrapper compiling?: boolean } @@ -46,7 +46,7 @@ export class StoredSchema implements _StoredSchema { refVal?: (RefVal | undefined)[] localRefs?: LocalRefs baseId?: string - validate?: ValidateFunction | ValidateWrapper + validate?: ValidateFunction callValidate?: ValidateWrapper compiling?: boolean @@ -498,6 +498,7 @@ function getJsonPointer( for (let part of parts) { if (!part) continue + if (typeof schema == "boolean") return part = unescapeFragment(part) schema = schema[part] if (schema === undefined) return @@ -510,7 +511,7 @@ function getJsonPointer( if ( typeof schema != "boolean" && schema.$ref && - !schemaHasRulesExcept(schema, this.RULES.all, "$ref") + !schemaHasRulesButRef({schema, RULES: this.RULES}) ) { const $ref = resolveUrl(baseId, schema.$ref) const _env = resolveSchema.call(this, root, $ref) diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 6a3dee455d..231f334388 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,11 +1,11 @@ -import {CompilationContext} from "../types" +import {Schema, SchemaObjCtx} from "../types" import {subschemaCode} from "./validate" import {escapeFragment, escapeJsonPointer} from "./util" import {_, str, Code, Name, getProperty} from "./codegen" export interface SubschemaContext { // TODO use Optional? - schema: object | boolean + schema: Schema schemaPath: Code errSchemaPath: string topSchemaRef?: Code @@ -32,7 +32,7 @@ export type SubschemaApplication = Partial interface SubschemaApplicationParams { keyword: string schemaProp: string | number - schema: object | boolean + schema: Schema schemaPath: Code errSchemaPath: string topSchemaRef: Code @@ -45,11 +45,7 @@ interface SubschemaApplicationParams { allErrors: boolean } -export function applySubschema( - it: CompilationContext, - appl: SubschemaApplication, - valid: Name -): void { +export function applySubschema(it: SchemaObjCtx, appl: SubschemaApplication, valid: Name): void { const subschema = getSubschema(it, appl) extendSubschemaData(subschema, it, appl) extendSubschemaMode(subschema, appl) @@ -58,7 +54,7 @@ export function applySubschema( } function getSubschema( - it: CompilationContext, + it: SchemaObjCtx, {keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaApplication ): SubschemaContext { if (keyword !== undefined && schema !== undefined) { @@ -97,7 +93,7 @@ function getSubschema( function extendSubschemaData( subschema: SubschemaContext, - it: CompilationContext, + it: SchemaObjCtx, {dataProp, dataPropType: dpType, data, propertyName}: SubschemaApplication ) { if (data !== undefined && dataProp !== undefined) { diff --git a/lib/compile/util.ts b/lib/compile/util.ts index c4220c8386..4f4325e23a 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,7 +1,7 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" -import {CompilationContext, Schema} from "../types" +import {SchemaCtx, Schema} from "../types" import N from "./names" -import {Rule} from "./rules" +import {Rule, ValidationRules} from "./rules" export enum DataType { Correct, @@ -73,39 +73,34 @@ export function toHash(arr: string[]): {[key: string]: true} { return hash } -// TODO rules, schema? export function schemaHasRules(schema: Schema, rules: {[key: string]: boolean | Rule}): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (rules[key]) return true return false } -// TODO rules, schema? -export function schemaHasRulesExcept( - schema: Schema, - rules: {[key: string]: boolean | Rule}, - exceptKeyword: string -): boolean { - if (typeof schema == "boolean") return !schema && exceptKeyword !== "not" - for (const key in schema) if (key !== exceptKeyword && rules[key]) return true +export function schemaCtxHasRules({schema, RULES}: SchemaCtx): boolean { + if (typeof schema == "boolean") return !schema + for (const key in schema) if (RULES.all[key]) return true return false } -// TODO rules, schema? -export function schemaUnknownRules( - schema: object, - rules: {[key: string]: boolean | Rule} -): string | undefined { - if (typeof schema === "boolean") return - for (const key in schema) if (!rules[key]) return key - return +interface SchemaAndRules { + schema: Schema + RULES: ValidationRules +} + +export function schemaHasRulesButRef({schema, RULES}: SchemaAndRules): boolean { + if (typeof schema == "boolean") return !schema + for (const key in schema) if (key !== "$ref" && RULES.all[key]) return true + return false } const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ export function getData( $data: string, - {dataLevel, dataNames, dataPathArr}: CompilationContext + {dataLevel, dataNames, dataPathArr}: SchemaCtx ): Code | number { let jsonPointer let data: Code diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 93afbfa850..f69eecde44 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,12 +1,12 @@ -import {CompilationContext, SchemaObject} from "../../types" +import {SchemaObjCtx, SchemaObject} from "../../types" import {RuleGroup, Rule} from "../rules" -export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: string): boolean { +export function schemaHasRulesForType({RULES, schema}: SchemaObjCtx, ty: string): boolean { const group = RULES.types[ty] return group && group !== true && shouldUseGroup(schema, group) } -export function shouldUseGroup(schema: object, group: RuleGroup): boolean { +export function shouldUseGroup(schema: SchemaObject, group: RuleGroup): boolean { return group.rules.some((rule) => shouldUseRule(schema, rule)) } diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index d4b5701ef1..2f3df0e742 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,4 +1,4 @@ -import {KeywordErrorDefinition, CompilationContext, KeywordErrorContext} from "../../types" +import {KeywordErrorDefinition, SchemaCtx, KeywordErrorCtx} from "../../types" import {reportError} from "../errors" import {_, Name} from "../codegen" import N from "../names" @@ -7,11 +7,11 @@ const boolError: KeywordErrorDefinition = { message: "boolean schema is false", } -export function topBoolOrEmptySchema(it: CompilationContext): void { +export function topBoolOrEmptySchema(it: SchemaCtx): void { const {gen, schema} = it if (schema === false) { falseSchemaError(it, false) - } else if (schema.$async === true) { + } else if (typeof schema == "object" && schema.$async === true) { gen.return(N.data) } else { gen.assign(_`${N.validate}.errors`, null) @@ -19,7 +19,7 @@ export function topBoolOrEmptySchema(it: CompilationContext): void { } } -export function boolOrEmptySchema(it: CompilationContext, valid: Name): void { +export function boolOrEmptySchema(it: SchemaCtx, valid: Name): void { const {gen, schema} = it if (schema === false) { gen.var(valid, false) // TODO var @@ -29,17 +29,16 @@ export function boolOrEmptySchema(it: CompilationContext, valid: Name): void { } } -function falseSchemaError(it: CompilationContext, overrideAllErrors?: boolean) { +function falseSchemaError(it: SchemaCtx, overrideAllErrors?: boolean) { const {gen, data} = it // TODO maybe some other interface should be used for non-keyword validation errors... - const cxt: KeywordErrorContext = { + const cxt: KeywordErrorCtx = { gen, keyword: "false schema", data, schema: false, schemaCode: false, schemaValue: false, - parentSchema: false, params: {}, it, } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 36cac65b91..7e26abbd2d 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,9 +1,4 @@ -import { - CompilationContext, - KeywordErrorDefinition, - KeywordErrorContext, - SchemaObject, -} from "../../types" +import {SchemaObjCtx, KeywordErrorDefinition, KeywordErrorCtx, SchemaObject} from "../../types" import {toHash, checkDataTypes, DataType} from "../util" import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" @@ -11,7 +6,7 @@ import {reportError} from "../errors" import {_, str, Name} from "../codegen" import {ValidationRules} from "../rules" -export function getSchemaTypes({RULES}: CompilationContext, schema: SchemaObject): string[] { +export function getSchemaTypes({RULES}: SchemaObjCtx, schema: SchemaObject): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach((t) => checkType(t, RULES)) @@ -29,7 +24,7 @@ export function checkType(t: string, RULES: ValidationRules): void { throw new Error('"type" keyword must be allowed string or string[]: ' + t) } -export function coerceAndCheckDataType(it: CompilationContext, types: string[]): boolean { +export function coerceAndCheckDataType(it: SchemaObjCtx, types: string[]): boolean { const {gen, data, opts} = it const coerceTo = coerceToTypes(types, opts.coerceTypes) const checkTypes = @@ -52,7 +47,7 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string : [] } -function coerceData(it: CompilationContext, types: string[], coerceTo: string[]): void { +function coerceData(it: SchemaObjCtx, types: string[], coerceTo: string[]): void { const {gen, data, opts} = it const dataType = gen.let("dataType", _`typeof ${data}`) const coerced = gen.let("coerced", _`undefined`) @@ -127,10 +122,7 @@ function coerceData(it: CompilationContext, types: string[], coerceTo: string[]) } } -function assignParentData( - {gen, parentData, parentDataProperty}: CompilationContext, - expr: Name -): void { +function assignParentData({gen, parentData, parentDataProperty}: SchemaObjCtx, expr: Name): void { // TODO use gen.property gen.if(_`${parentData} !== undefined`, () => gen.assign(_`${parentData}[${parentDataProperty}]`, expr) @@ -143,12 +135,12 @@ const typeError: KeywordErrorDefinition = { typeof schema == "string" ? _`{type: ${schema}}` : _`{type: ${schemaValue}}`, } -export function reportTypeError(it: CompilationContext): void { +export function reportTypeError(it: SchemaObjCtx): void { const cxt = getTypeErrorContext(it) reportError(cxt, typeError) } -function getTypeErrorContext(it: CompilationContext): KeywordErrorContext { +function getTypeErrorContext(it: SchemaObjCtx): KeywordErrorCtx { const {gen, data, schema} = it const schemaCode = schemaRefOrVal(it, schema, "type") return { diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 2ded322dd4..3993c41481 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,8 +1,8 @@ -import {CompilationContext} from "../../types" +import {SchemaObjCtx} from "../../types" import {_, getProperty, stringify} from "../codegen" import {checkStrictMode} from "../../vocabularies/util" -export function assignDefaults(it: CompilationContext, ty?: string): void { +export function assignDefaults(it: SchemaObjCtx, ty?: string): void { const {properties, items} = it.schema if (ty === "object" && properties) { for (const key in properties) { @@ -13,7 +13,7 @@ export function assignDefaults(it: CompilationContext, ty?: string): void { } } -function assignDefault(it: CompilationContext, prop: string | number, defaultValue: any): void { +function assignDefault(it: SchemaObjCtx, prop: string | number, defaultValue: any): void { const {gen, compositeRule, data, opts} = it if (defaultValue === undefined) return const childData = _`${data}${getProperty(prop)}` diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 765b89680c..fa03d59654 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,32 +1,26 @@ -import {CompilationContext, Options} from "../../types" +import {Schema, SchemaCtx, SchemaObjCtx, Options} from "../../types" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" import CodeGen, {_, nil, str, Block, Code, Name} from "../codegen" import N from "../names" import {resolveUrl} from "../resolve" -import {schemaHasRules, schemaHasRulesExcept, schemaUnknownRules} from "../util" -import {checkStrictMode} from "../../vocabularies/util" -import {SchemaObject} from "json-schema-traverse" +import {schemaCtxHasRules, schemaHasRulesButRef} from "../util" +import {checkStrictMode, checkUnknownRules} from "../../vocabularies/util" // schema compilation - generates validation function, subschemaCode (below) is used for subschemas -export function validateFunctionCode(it: CompilationContext): void { - const {schema, opts} = it - checkKeywords(it) - if (isBoolOrEmpty(it)) { - validateFunction(it, () => topBoolOrEmptySchema(it)) - return +export function validateFunctionCode(it: SchemaCtx): void { + if (isSchemaObj(it)) { + checkKeywords(it) + if (schemaCtxHasRules(it)) { + topSchemaObjCode(it) + return + } } - validateFunction(it, () => { - if (opts.$comment && schema.$comment) commentKeyword(it) - checkNoDefault(it) - initializeTop(it.gen) - typeAndKeywords(it) - returnResults(it) - }) + validateFunction(it, () => topBoolOrEmptySchema(it)) } -function validateFunction({gen, schema, async, opts}: CompilationContext, body: Block) { +function validateFunction({gen, schema, async, opts}: SchemaCtx, body: Block) { gen.return(() => gen.func( N.validate, @@ -37,20 +31,42 @@ function validateFunction({gen, schema, async, opts}: CompilationContext, body: ) } -function funcSourceUrl(schema: SchemaObject, opts: Options): Code { - return schema.$id && (opts.sourceCode || opts.processCode) +function topSchemaObjCode(it: SchemaObjCtx): void { + const {schema, opts} = it + validateFunction(it, () => { + if (opts.$comment && schema.$comment) commentKeyword(it) + checkNoDefault(it) + initializeTop(it.gen) + typeAndKeywords(it) + returnResults(it) + }) + return +} + +function funcSourceUrl(schema: Schema, opts: Options): Code { + return typeof schema == "object" && schema.$id && (opts.sourceCode || opts.processCode) ? _`/*# sourceURL=${schema.$id} */` : nil } // schema compilation - this function is used recursively to generate code for sub-schemas -export function subschemaCode(it: CompilationContext, valid: Name): void { - const {schema, gen, opts} = it - checkKeywords(it) - if (isBoolOrEmpty(it)) { - boolOrEmptySchema(it, valid) - return +export function subschemaCode(it: SchemaCtx, valid: Name): void { + if (isSchemaObj(it)) { + checkKeywords(it) + if (schemaCtxHasRules(it)) { + subSchemaObjCode(it, valid) + return + } } + boolOrEmptySchema(it, valid) +} + +function isSchemaObj(it: SchemaCtx): it is SchemaObjCtx { + return typeof it.schema != "boolean" +} + +function subSchemaObjCode(it: SchemaObjCtx, valid: Name): void { + const {schema, gen, opts} = it if (opts.$comment && schema.$comment) commentKeyword(it) updateContext(it) checkAsync(it) @@ -61,35 +77,20 @@ export function subschemaCode(it: CompilationContext, valid: Name): void { gen.var(valid, _`${errsCount} === ${N.errors}`) } -function checkKeywords(it: CompilationContext) { - checkUnknownKeywords(it) +function checkKeywords(it: SchemaObjCtx) { + checkUnknownRules(it) checkRefsAndKeywords(it) } -function isBoolOrEmpty({schema, RULES}: CompilationContext): boolean { - return typeof schema == "boolean" || !schemaHasRules(schema, RULES.all) -} - -function typeAndKeywords(it: CompilationContext, errsCount?: Name): void { +function typeAndKeywords(it: SchemaObjCtx, errsCount?: Name): void { const types = getSchemaTypes(it, it.schema) const checkedTypes = coerceAndCheckDataType(it, types) schemaKeywords(it, types, !checkedTypes, errsCount) } -function checkUnknownKeywords(it: CompilationContext): void { - if (!it.opts.strict) return - const unknownKeyword = schemaUnknownRules(it.schema, it.RULES.keywords) - if (unknownKeyword) checkStrictMode(it, `unknown keyword: "${unknownKeyword}"`) -} - -function checkRefsAndKeywords({ - schema, - errSchemaPath, - RULES, - opts, - logger, -}: CompilationContext): void { - if (schema.$ref && schemaHasRulesExcept(schema, RULES.all, "$ref")) { +function checkRefsAndKeywords(it: SchemaObjCtx): void { + const {schema, errSchemaPath, opts, logger} = it + if (schema.$ref && schemaHasRulesButRef(it)) { if (opts.extendRefs === "fail") { throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`) } else if (opts.extendRefs !== true) { @@ -98,7 +99,7 @@ function checkRefsAndKeywords({ } } -function checkNoDefault(it: CompilationContext): void { +function checkNoDefault(it: SchemaObjCtx): void { const {schema, opts} = it if (schema.default !== undefined && opts.useDefaults && opts.strict) { checkStrictMode(it, "default is ignored in the schema root") @@ -112,15 +113,15 @@ function initializeTop(gen: CodeGen): void { // gen.if(_`${N.dataPath} === undefined`, () => gen.assign(N.dataPath, _`""`)) // TODO maybe add it } -function updateContext(it: CompilationContext): void { +function updateContext(it: SchemaObjCtx): void { if (it.schema.$id) it.baseId = resolveUrl(it.baseId, it.schema.$id) } -function checkAsync(it: CompilationContext): void { +function checkAsync(it: SchemaObjCtx): void { if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") } -function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: CompilationContext): void { +function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: SchemaObjCtx): void { const msg = schema.$comment if ($comment === true) { gen.code(_`console.log(${msg})`) // should it use logger? @@ -130,7 +131,7 @@ function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: Compilat } } -function returnResults({gen, async}: CompilationContext) { +function returnResults({gen, async}: SchemaCtx) { if (async) { gen.if( _`${N.errors} === 0`, diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 470b61853e..90aa155a17 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -1,6 +1,6 @@ -import {CompilationContext} from "../../types" +import {SchemaObjCtx} from "../../types" import {shouldUseGroup, shouldUseRule} from "./applicability" -import {checkDataType, schemaHasRulesExcept} from "../util" +import {checkDataType, schemaHasRulesButRef} from "../util" import {keywordCode} from "./keyword" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" @@ -9,16 +9,13 @@ import {_, Name} from "../codegen" import N from "../names" export function schemaKeywords( - it: CompilationContext, + it: SchemaObjCtx, types: string[], typeErrors: boolean, errsCount?: Name ): void { const {gen, schema, data, RULES, allErrors, opts} = it - if ( - schema.$ref && - !(opts.extendRefs === true && schemaHasRulesExcept(schema, RULES.all, "$ref")) - ) { + if (schema.$ref && !(opts.extendRefs === true && schemaHasRulesButRef(it))) { gen.block(() => keywordCode(it, "$ref", (RULES.all.$ref).definition)) // TODO typecast return } @@ -47,7 +44,7 @@ export function schemaKeywords( } } -function iterateKeywords(it: CompilationContext, group: RuleGroup) { +function iterateKeywords(it: SchemaObjCtx, group: RuleGroup) { const { gen, schema, diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 47839403c6..8af1d7f6d2 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -2,10 +2,10 @@ import { KeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, - CompilationContext, + SchemaObjCtx, KeywordCompilationResult, } from "../../types" -import KeywordContext from "../context" +import KeywordCtx from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" import {callValidateCode} from "../../vocabularies/util" @@ -13,12 +13,12 @@ import CodeGen, {_, nil, Code, Name} from "../codegen" import N from "../names" export function keywordCode( - it: CompilationContext, + it: SchemaObjCtx, keyword: string, def: KeywordDefinition, ruleType?: string ): void { - const cxt = new KeywordContext(it, def, keyword) + const cxt = new KeywordCtx(it, def, keyword) if ("code" in def) { def.code(cxt, ruleType) } else if (cxt.$data && def.validate) { @@ -30,7 +30,7 @@ export function keywordCode( } } -function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition): void { +function macroKeywordCode(cxt: KeywordCtx, def: MacroKeywordDefinition): void { const {gen, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) const schemaRef = useKeyword(gen, keyword, macroSchema) @@ -51,7 +51,7 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition): voi cxt.pass(valid, () => cxt.error(true)) } -function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition): void { +function funcKeywordCode(cxt: KeywordCtx, def: FuncKeywordDefinition): void { const {gen, keyword, schema, parentSchema, $data, it} = cxt checkAsync(it, def) const validate = @@ -107,12 +107,12 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition): void } } -function modifyData(cxt: KeywordContext) { +function modifyData(cxt: KeywordCtx) { const {gen, data, it} = cxt gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`)) } -function addErrs(cxt: KeywordContext, errs: Code): void { +function addErrs(cxt: KeywordCtx, errs: Code): void { const {gen} = cxt gen.if( _`Array.isArray(${errs})`, @@ -126,7 +126,7 @@ function addErrs(cxt: KeywordContext, errs: Code): void { ) } -function checkAsync(it: CompilationContext, def: FuncKeywordDefinition) { +function checkAsync(it: SchemaObjCtx, def: FuncKeywordDefinition) { if (def.async && !it.async) throw new Error("async keyword in sync schema") } diff --git a/lib/types.ts b/lib/types.ts index 29bc8657e0..870e8ca9cf 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,7 @@ import CodeGen, {Code, Name, CodeGenOptions, Scope} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" -import KeywordContext from "./compile/context" +import KeywordCtx from "./compile/context" import Ajv from "./ajv" export interface SchemaObject { @@ -12,6 +12,10 @@ export interface SchemaObject { export type Schema = SchemaObject | boolean +export interface SchemaMap { + [key: string]: Schema +} + export type LoadSchemaFunction = ( uri: string, cb?: (err: Error | null, schema?: SchemaObject) => void @@ -49,8 +53,8 @@ export interface CurrentOptions { codegen?: CodeGenOptions cache?: CacheInterface logger?: Logger | false - serialize?: false | ((schema: object | boolean) => any) - $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: any) => any) + serialize?: false | ((schema: Schema) => any) + $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: SchemaObject) => any) allowMatchingProperties?: boolean // disables a strict mode restriction } @@ -109,7 +113,7 @@ export interface SchemaValidateFunction { ( schema: any, data: any, - parentSchema?: object, + parentSchema?: SchemaObject, dataPath?: string, parentData?: object | any[], parentDataProperty?: string | number, @@ -129,13 +133,13 @@ export interface ErrorObject { message?: string // These are added with the `verbose` option. schema?: any - parentSchema?: object + parentSchema?: SchemaObject data?: any } export type KeywordCompilationResult = object | boolean | SchemaValidateFunction | ValidateFunction -export interface CompilationContext { +export interface SchemaCtx { gen: CodeGen allErrors: boolean data: Name @@ -146,9 +150,9 @@ export interface CompilationContext { dataLevel: number topSchemaRef: Code async: boolean - schema: any + schema: Schema isRoot: boolean - root: SchemaRoot // TODO ? + root: SchemaRoot rootId: string // TODO ? baseId: string schemaPath: Code @@ -160,9 +164,13 @@ export interface CompilationContext { RULES: ValidationRules formats: {[index: string]: AddedFormat} opts: Options - resolveRef: (...args: any[]) => ResolvedRef | void - logger: Logger // TODO ? - self: any // TODO + resolveRef: (baseId: string, ref: string, isRoot: boolean) => ResolvedRef | void + logger: Logger + self: Ajv +} + +export interface SchemaObjCtx extends SchemaCtx { + schema: SchemaObject } interface _KeywordDef { @@ -180,20 +188,20 @@ interface _KeywordDef { } export interface CodeKeywordDefinition extends _KeywordDef { - code: (cxt: KeywordContext, ruleType?: string) => void + code: (cxt: KeywordCtx, ruleType?: string) => void trackErrors?: boolean } export type MacroKeywordFunc = ( schema: any, parentSchema: object, - it: CompilationContext + it: SchemaCtx ) => object | boolean export type CompileKeywordFunc = ( schema: any, parentSchema: object, - it: CompilationContext + it: SchemaCtx ) => ValidateFunction export interface FuncKeywordDefinition extends _KeywordDef { @@ -217,28 +225,28 @@ export type KeywordDefinition = | MacroKeywordDefinition export interface KeywordErrorDefinition { - message: string | ((cxt: KeywordErrorContext) => Code) - params?: (cxt: KeywordErrorContext) => Code + message: string | ((cxt: KeywordErrorCtx) => Code) + params?: (cxt: KeywordErrorCtx) => Code } export type Vocabulary = (KeywordDefinition | string)[] -export interface KeywordErrorContext { +export interface KeywordErrorCtx { gen: CodeGen keyword: string data: Name $data?: string | false schema: any - parentSchema: any + parentSchema?: SchemaObject schemaCode: Code | number | boolean schemaValue: Code | number | boolean schemaType?: string | string[] errsCount?: Name - params: KeywordContextParams - it: CompilationContext + params: KeywordCtxParams + it: SchemaCtx } -export type KeywordContextParams = {[x: string]: Code | string | number} +export type KeywordCtxParams = {[x: string]: Code | string | number} export type FormatMode = "fast" | "full" diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 5b9cfdddc6..2c9bc8cd0b 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: ["boolean", "object"], before: "uniqueItems", - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, parentSchema, data, it} = cxt const len = gen.const("len", _`${data}.length`) const items = parentSchema.items diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 10f1a85bb3..fd8294a4bd 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: ["boolean", "object", "undefined"], // "undefined" is needed to support option removeAdditional: "all" trackErrors: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, parentSchema, data, errsCount, it} = cxt if (!errsCount) throw new Error("ajv implementation error") const {allErrors, opts} = it diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 0529055781..5c38fa138a 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,12 +1,12 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" const def: CodeKeywordDefinition = { keyword: "allOf", schemaType: "array", - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, it} = cxt const valid = gen.name("valid") schema.forEach((sch: object | boolean, i: number) => { diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 7f3ee1584c..78d71768cf 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", trackErrors: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, it} = cxt const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) if (alwaysValid) return diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 8e79e4246f..1fa65219b4 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "boolean"], before: "uniqueItems", trackErrors: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, data, it} = cxt if (alwaysValidSchema(it, schema)) { diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 6c2759d53a..57a60c6907 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" @@ -16,7 +16,7 @@ const def: CodeKeywordDefinition = { keyword: "dependencies", type: "object", schemaType: "object", - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, data, it} = cxt const [propDeps, schDeps] = splitDependencies() const valid = gen.name("valid") diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index d58d583ec3..9aa79fc257 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, CompilationContext} from "../../types" -import KeywordContext from "../../compile/context" +import {CodeKeywordDefinition, SchemaObjCtx} from "../../types" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], trackErrors: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, parentSchema, it} = cxt if (parentSchema.then === undefined && parentSchema.else === undefined) { checkStrictMode(it, '"if" without "then" and "else" is ignored') @@ -64,7 +64,7 @@ const def: CodeKeywordDefinition = { module.exports = def -function hasSchema(it: CompilationContext, keyword: string): boolean { +function hasSchema(it: SchemaObjCtx, keyword: string): boolean { const schema = it.schema[keyword] return schema !== undefined && !alwaysValidSchema(it, schema) } diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 45527668a0..58580d5e7f 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: ["object", "array", "boolean"], before: "uniqueItems", - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, data, it} = cxt const len = gen.const("len", _`${data}.length`) if (Array.isArray(schema)) { diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index 184312fe45..bf86a42afc 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { keyword: "not", schemaType: ["object", "boolean"], trackErrors: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, it} = cxt if (alwaysValidSchema(it, schema)) { cxt.fail() diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 6e3caa476b..395acf2358 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", trackErrors: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, it} = cxt const valid = gen.let("valid", false) const passing = gen.let("passing", null) diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index dabbdd2117..679077c680 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {schemaProperties, usePattern, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "patternProperties", type: "object", schemaType: "object", - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, data, parentSchema, it} = cxt const patterns = schemaProperties(it, schema) if (patterns.length === 0) return diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 4f8ab9f281..e665240927 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {schemaProperties, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" import apDef from "./additionalProperties" @@ -8,10 +8,10 @@ const def: CodeKeywordDefinition = { keyword: "properties", type: "object", schemaType: "object", - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, parentSchema, data, it} = cxt if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { - apDef.code(new KeywordContext(it, apDef, "additionalProperties")) + apDef.code(new KeywordCtx(it, apDef, "additionalProperties")) } const properties = schemaProperties(it, schema) if (properties.length === 0) return diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 5ed5776e86..138ad2b225 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "propertyNames", type: "object", schemaType: ["object", "boolean"], - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, data, it} = cxt if (alwaysValidSchema(it, schema)) return const valid = gen.name("valid") diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 120fbf9ede..db3fae4d29 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" @@ -10,7 +10,7 @@ import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, it} = cxt const {resolveRef, allErrors, baseId, isRoot, root, opts, logger} = it const ref = getRef() diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 464ba4f6e2..99cdadffe6 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { type: ["number", "string"], schemaType: "string", $data: true, - code(cxt: KeywordContext, ruleType?: string) { + code(cxt: KeywordCtx, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt const {formats, opts, logger, errSchemaPath} = it if (opts.format === false) return diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 18544c506c..38304b32d7 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,8 +1,8 @@ -import KeywordContext from "../compile/context" +import KeywordCtx from "../compile/context" import {noPropertyInData} from "./util" import {_, or, Code, Name} from "../compile/codegen" -export function checkReportMissingProp(cxt: KeywordContext, prop: string): void { +export function checkReportMissingProp(cxt: KeywordCtx, prop: string): void { const {gen, data, it} = cxt gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => { cxt.setParams({missingProperty: _`${prop}`}, true) @@ -11,7 +11,7 @@ export function checkReportMissingProp(cxt: KeywordContext, prop: string): void } export function checkMissingProp( - {data, it: {opts}}: KeywordContext, + {data, it: {opts}}: KeywordCtx, properties: string[], missing: Name ): Code { @@ -22,7 +22,7 @@ export function checkMissingProp( ) } -export function reportMissingProp(cxt: KeywordContext, missing: Name): void { +export function reportMissingProp(cxt: KeywordCtx, missing: Name): void { cxt.setParams({missingProperty: missing}, true) cxt.error() } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 2b13aae8b2..315176de7c 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,19 +1,15 @@ import {schemaHasRules} from "../compile/util" -import {CompilationContext} from "../types" -import KeywordContext from "../compile/context" +import {Schema, SchemaMap, SchemaCtx, SchemaObjCtx} from "../types" +import KeywordCtx from "../compile/context" import CodeGen, {_, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" -import {SchemaObject} from "json-schema-traverse" export function schemaRefOrVal( - {topSchemaRef, schemaPath}: CompilationContext, + {topSchemaRef, schemaPath}: SchemaObjCtx, schema: unknown, keyword: string, $data?: string | false ): Code | number | boolean { - // return $data || typeof schema === "object" - // ? `${topSchemaRef}${schemaPath + getProperty(keyword)}` - // : _`${schema}` if (!$data) { if (typeof schema == "number" || typeof schema == "boolean") return schema if (typeof schema == "string") return _`${schema}` @@ -21,21 +17,28 @@ export function schemaRefOrVal( return _`${topSchemaRef}${schemaPath}${getProperty(keyword)}` } -export function alwaysValidSchema( - {RULES}: CompilationContext, - schema: boolean | object -): boolean | void { - return typeof schema == "boolean" - ? schema === true - : Object.keys(schema).length === 0 && !schemaHasRules(schema, RULES.all) +export function alwaysValidSchema(it: SchemaCtx, schema: Schema): boolean | void { + if (typeof schema == "boolean") return schema + if (Object.keys(schema).length === 0) return true + checkUnknownRules(it, schema) + return !schemaHasRules(schema, it.RULES.all) } -export function allSchemaProperties(schema?: object): string[] { - return schema ? Object.keys(schema).filter((p) => p !== "__proto__") : [] +export function checkUnknownRules(it: SchemaCtx, schema: Schema = it.schema): void { + if (!it.opts.strict) return + if (typeof schema === "boolean") return + const rules = it.RULES.keywords + for (const key in schema) { + if (!rules[key]) checkStrictMode(it, `unknown keyword: "${key}"`) + } +} + +export function allSchemaProperties(schemaMap?: SchemaMap): string[] { + return schemaMap ? Object.keys(schemaMap).filter((p) => p !== "__proto__") : [] } -export function schemaProperties(it: CompilationContext, schema: SchemaObject): string[] { - return allSchemaProperties(schema).filter((p) => !alwaysValidSchema(it, schema[p])) +export function schemaProperties(it: SchemaCtx, schemaMap: SchemaMap): string[] { + return allSchemaProperties(schemaMap).filter((p) => !alwaysValidSchema(it, schemaMap[p])) } function isOwnProperty(data: Name, property: Name | string): Code { @@ -57,7 +60,7 @@ export function noPropertyInData( } export function callValidateCode( - {schemaCode, data, it}: KeywordContext, + {schemaCode, data, it}: KeywordCtx, func: Code, context: Code, passSchema?: boolean @@ -78,7 +81,7 @@ export function usePattern(gen: CodeGen, pattern: string): Name { }) } -export function checkStrictMode(it: CompilationContext, msg: string): void { +export function checkStrictMode(it: SchemaCtx, msg: string): void { const {opts, logger} = it if (opts.strict) { if (opts.strict === "log") logger.warn(msg) diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 4ea9c1d563..5653bc0faf 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,11 +1,11 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {_} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "const", $data: true, - code: (cxt: KeywordContext) => cxt.fail$data(_`!equal(${cxt.data}, ${cxt.schemaCode})`), + code: (cxt: KeywordCtx) => cxt.fail$data(_`!equal(${cxt.data}, ${cxt.schemaCode})`), error: { message: "should be equal to constant", params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index cd2dc363ee..ca7039a477 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,12 +1,12 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "enum", schemaType: "array", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, data, $data, schema, schemaCode, it} = cxt if (!$data && schema.length === 0) throw new Error("enum must have non-empty array") const useLoop = typeof it.opts.loopEnum == "number" && schema.length >= it.opts.loopEnum diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 6bbbfc7d82..12325c1eb3 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" // import {bad$DataType} from "../util" import {_, str, operators, Code} from "../../compile/codegen" @@ -17,7 +17,7 @@ const def: CodeKeywordDefinition = { type: "number", schemaType: "number", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {keyword, data, schemaCode} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) cxt.fail$data(_`(${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data}))`) diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 4b89f6daf0..987cd7f659 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: "number", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {keyword, data, schemaCode} = cxt const op = keyword === "maxItems" ? operators.GT : operators.LT cxt.fail$data(_`${data}.length ${op} ${schemaCode}`) diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 1d210e9b21..ae794f600b 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { type: "string", schemaType: "number", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {keyword, data, schemaCode, it} = cxt const op = keyword === "maxLength" ? operators.GT : operators.LT const len = it.opts.unicode === false ? _`${data}.length` : _`ucs2length(${data})` diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index 613bb3484d..7f7580ef81 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "number", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {keyword, data, schemaCode} = cxt const op = keyword === "maxProperties" ? operators.GT : operators.LT cxt.fail$data(_`Object.keys(${data}).length ${op} ${schemaCode}`) diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 4b21e88411..782e68dbb8 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" // import {bad$DataType} from "../util" import {_, str} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { type: "number", schemaType: "number", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, data, schemaCode, it} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) const prec = it.opts.multipleOfPrecision diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index fa4da72df4..bcfcf19b09 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {usePattern} from "../util" import {_, str} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { type: "string", schemaType: "string", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, data, $data, schema, schemaCode} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 10311bb066..9da74972d0 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {_, str, nil, Name} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "array", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return const useLoop = typeof it.opts.loopRequired == "number" && schema.length >= it.opts.loopRequired diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 1d9903f329..0a90abe680 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordContext from "../../compile/context" +import KeywordCtx from "../../compile/context" import {getSchemaTypes} from "../../compile/validate/dataType" import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: "boolean", $data: true, - code(cxt: KeywordContext) { + code(cxt: KeywordCtx) { const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt if (!$data && !schema) return const valid = gen.let("valid") diff --git a/tsconfig.json b/tsconfig.json index 0fd078484c..868a90fef5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "outDir": "dist", "lib": ["ES2018", "DOM"], "types": ["node"], - "noImplicitAny": false, "allowJs": true, "target": "ES2018" } From 323f8f5d4a1c7174d7cb546ddb01a41690c905de Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 19:19:37 +0100 Subject: [PATCH 166/322] eslint: enable @typescript-eslint/ban-types --- .eslintrc.yml | 1 - lib/ajv.ts | 2 +- lib/compile/index.ts | 2 +- lib/types.ts | 24 +++++++++------------ lib/vocabularies/applicator/allOf.ts | 4 ++-- lib/vocabularies/applicator/anyOf.ts | 2 +- lib/vocabularies/applicator/dependencies.ts | 9 ++++---- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index f793a40556..4603f767ca 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -22,7 +22,6 @@ overrides: rules: no-var: 0 "@typescript-eslint/restrict-template-expressions": off - "@typescript-eslint/ban-types": off "@typescript-eslint/no-empty-interface": off "@typescript-eslint/no-explicit-any": off "@typescript-eslint/no-unsafe-call": off diff --git a/lib/ajv.ts b/lib/ajv.ts index b0d7316cea..c95a2f953e 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -598,7 +598,7 @@ const $dataRef = { $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", } -function schemaOrData(schema: object | boolean): object { +function schemaOrData(schema: Schema): SchemaObject { return {anyOf: [schema, $dataRef]} } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 042a5edcad..3e54d6131f 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -60,7 +60,7 @@ export type ResolvedRef = InlineResolvedRef | FuncResolvedRef export interface InlineResolvedRef { code: Code - schema: object | boolean + schema: Schema inline: true } diff --git a/lib/types.ts b/lib/types.ts index 870e8ca9cf..ccce0a5249 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -92,9 +92,9 @@ export interface ValidateFunction { this: Ajv | any, data: any, dataPath?: string, - parentData?: object | any[], + parentData?: Record | unknown[], parentDataProperty?: string | number, - rootData?: object | any[] + rootData?: Record | unknown[] ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] @@ -115,9 +115,9 @@ export interface SchemaValidateFunction { data: any, parentSchema?: SchemaObject, dataPath?: string, - parentData?: object | any[], + parentData?: Record | unknown[], parentDataProperty?: string | number, - rootData?: object | any[] + rootData?: Record | unknown[] ): boolean | Promise errors?: ErrorObject[] } @@ -126,7 +126,7 @@ export interface ErrorObject { keyword: string dataPath: string schemaPath: string - params: object // TODO add interface + params: Record // TODO add interface // Added to validation errors of propertyNames keyword schema propertyName?: string // Excluded if messages set to false. @@ -137,7 +137,7 @@ export interface ErrorObject { data?: any } -export type KeywordCompilationResult = object | boolean | SchemaValidateFunction | ValidateFunction +export type KeywordCompilationResult = Schema | SchemaValidateFunction | ValidateFunction export interface SchemaCtx { gen: CodeGen @@ -180,7 +180,7 @@ interface _KeywordDef { $data?: boolean implements?: string[] before?: string - metaSchema?: object + metaSchema?: SchemaObject validateSchema?: ValidateFunction // compiled keyword metaSchema - should not be passed dependencies?: string[] // keywords that must be present in the same schema error?: KeywordErrorDefinition @@ -192,16 +192,12 @@ export interface CodeKeywordDefinition extends _KeywordDef { trackErrors?: boolean } -export type MacroKeywordFunc = ( - schema: any, - parentSchema: object, - it: SchemaCtx -) => object | boolean +export type MacroKeywordFunc = (schema: any, parentSchema: SchemaObject, it: SchemaCtx) => Schema export type CompileKeywordFunc = ( schema: any, - parentSchema: object, - it: SchemaCtx + parentSchema: SchemaObject, + it: SchemaObjCtx ) => ValidateFunction export interface FuncKeywordDefinition extends _KeywordDef { diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 5c38fa138a..e1df5256f0 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, Schema} from "../../types" import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCtx) { const {gen, schema, it} = cxt const valid = gen.name("valid") - schema.forEach((sch: object | boolean, i: number) => { + schema.forEach((sch: Schema, i: number) => { if (alwaysValidSchema(it, sch)) return applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) cxt.ok(valid) diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 78d71768cf..b34a231a48 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { trackErrors: true, code(cxt: KeywordCtx) { const {gen, schema, it} = cxt - const alwaysValid = schema.some((sch: object | boolean) => alwaysValidSchema(it, sch)) + const alwaysValid = schema.some((sch: Schema) => alwaysValidSchema(it, sch)) if (alwaysValid) return const valid = gen.let("valid", false) diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 57a60c6907..6b8a1a7733 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, SchemaMap} from "../../types" import KeywordCtx from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" @@ -8,9 +8,8 @@ import {_, str} from "../../compile/codegen" interface PropertyDependencies { [x: string]: string[] } -interface SchemaDependencies { - [x: string]: object | boolean -} + +type SchemaDependencies = SchemaMap const def: CodeKeywordDefinition = { keyword: "dependencies", @@ -60,7 +59,7 @@ const def: CodeKeywordDefinition = { } } - function validateSchemaDeps(schemaDeps: {[x: string]: object | boolean}): void { + function validateSchemaDeps(schemaDeps: SchemaMap): void { for (const prop in schemaDeps) { if (alwaysValidSchema(it, schemaDeps[prop])) continue gen.if( From 4f47291db26664e0609abbaf1ffb9321e3216caa Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 20:26:01 +0100 Subject: [PATCH 167/322] @typescript-eslint: enable no-empty-interface, no-explicit-any, no-unsafe-call, no-unsafe-return --- .eslintrc.yml | 4 -- lib/ajv.ts | 5 +- lib/compile/codegen.ts | 2 +- lib/compile/context.ts | 2 +- lib/compile/index.ts | 8 +-- lib/compile/validate/defaults.ts | 2 +- lib/types.ts | 52 ++++++++++--------- .../applicator/additionalProperties.ts | 8 +-- lib/vocabularies/applicator/allOf.ts | 1 + lib/vocabularies/applicator/anyOf.ts | 1 + lib/vocabularies/applicator/items.ts | 8 +-- lib/vocabularies/applicator/oneOf.ts | 4 +- lib/vocabularies/format/format.ts | 5 +- lib/vocabularies/validation/enum.ts | 3 +- 14 files changed, 56 insertions(+), 49 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 4603f767ca..59959a2541 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -22,13 +22,9 @@ overrides: rules: no-var: 0 "@typescript-eslint/restrict-template-expressions": off - "@typescript-eslint/no-empty-interface": off - "@typescript-eslint/no-explicit-any": off - "@typescript-eslint/no-unsafe-call": off "@typescript-eslint/no-unsafe-member-access": off "@typescript-eslint/no-unsafe-assignment": off "@typescript-eslint/restrict-plus-operands": off - "@typescript-eslint/no-unsafe-return": off "@typescript-eslint/no-var-requires": off "@typescript-eslint/no-empty-function": off "@typescript-eslint/no-this-alias": off diff --git a/lib/ajv.ts b/lib/ajv.ts index c95a2f953e..6e6334bfd3 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -358,7 +358,7 @@ export default class Ajv { ): string { if (!errors || errors.length === 0) return "No errors" return errors - .map((e) => dataVar + e.dataPath + " " + e.message) + .map((e) => `${dataVar}${e.dataPath} ${e.message}`) .reduce((text, msg) => text + msg + separator) } @@ -586,8 +586,7 @@ function _addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: str } } -function keywordMetaschema(this: any, def: KeywordDefinition): void { - // TODO this Ajv +function keywordMetaschema(this: Ajv, def: KeywordDefinition): void { let metaSchema = def.metaSchema if (metaSchema === undefined) return if (def.$data && this._opts.$data) metaSchema = schemaOrData(metaSchema) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 321ead5e69..b04903a19e 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -80,7 +80,7 @@ interface NameRec { value: NameValue } -type ValueReference = any // possibly make CodeGen parameterized type on this type +type ValueReference = unknown // possibly make CodeGen parameterized type on this type export interface NameValue { ref?: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 8b75940967..089e181533 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -162,7 +162,7 @@ export default class KeywordCtx implements KeywordErrorCtx { } } -function validSchemaType(schema: any, schemaType: string | string[]): boolean { +function validSchemaType(schema: unknown, schemaType: string | string[]): boolean { // TODO add tests if (Array.isArray(schemaType)) { return schemaType.some((st) => validSchemaType(schema, st)) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 3e54d6131f..908802e5d3 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -144,7 +144,7 @@ function _tryCompile( function validateWrapper(c: Compilation): ValidateWrapper { if (!c.callValidate) { - const wrapper: ValidateWrapper = function (this: Ajv | any, ...args) { + const wrapper: ValidateWrapper = function (this: Ajv | unknown, ...args) { if (wrapper.validate === undefined) throw new Error("ajv implementation error") const v = wrapper.validate const valid = v.apply(this, args) @@ -246,10 +246,10 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) // console.log("\n\n\n *** \n", sourceCode) - var validate + let validate: ValidateFunction try { // TODO refactor to fewer variables - maybe only self and scope - var makeValidate = new Function( + const makeValidate = new Function( "self", "RULES", "formats", @@ -284,7 +284,7 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va validate.errors = null validate.refs = refs validate.refVal = refVal - validate.root = isRoot ? validate : _root + validate.root = isRoot ? root : _root if ($async) validate.$async = true if (opts.sourceCode === true) { validate.source = { diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 3993c41481..d8428c3384 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -13,7 +13,7 @@ export function assignDefaults(it: SchemaObjCtx, ty?: string): void { } } -function assignDefault(it: SchemaObjCtx, prop: string | number, defaultValue: any): void { +function assignDefault(it: SchemaObjCtx, prop: string | number, defaultValue: unknown): void { const {gen, compositeRule, data, opts} = it if (defaultValue === undefined) return const childData = _`${data}${getProperty(prop)}` diff --git a/lib/types.ts b/lib/types.ts index ccce0a5249..25fa71c6eb 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -7,7 +7,7 @@ import Ajv from "./ajv" export interface SchemaObject { $id?: string $schema?: string - [x: string]: any + [x: string]: any // TODO } export type Schema = SchemaObject | boolean @@ -53,8 +53,8 @@ export interface CurrentOptions { codegen?: CodeGenOptions cache?: CacheInterface logger?: Logger | false - serialize?: false | ((schema: Schema) => any) - $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: SchemaObject) => any) + serialize?: false | ((schema: Schema) => unknown) + $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: SchemaObject) => unknown) allowMatchingProperties?: boolean // disables a strict mode restriction } @@ -70,15 +70,15 @@ export interface Options extends CurrentOptions { } export interface Logger { - log(...args: any[]): any - warn(...args: any[]): any - error(...args: any[]): any + log(...args: unknown[]): unknown + warn(...args: unknown[]): unknown + error(...args: unknown[]): unknown } export interface CacheInterface { - put: (key: any, value: StoredSchema) => void - get: (key: any) => StoredSchema - del(key: any): void + put(key: unknown, value: StoredSchema): void + get(key: unknown): StoredSchema + del(key: unknown): void clear(): void } @@ -89,13 +89,13 @@ interface SourceCode { export interface ValidateFunction { ( - this: Ajv | any, - data: any, + this: Ajv | unknown, + data: unknown, dataPath?: string, parentData?: Record | unknown[], parentDataProperty?: string | number, rootData?: Record | unknown[] - ): boolean | Promise + ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] refs?: {[ref: string]: number | undefined} @@ -111,14 +111,14 @@ export interface ValidateWrapper extends ValidateFunction { export interface SchemaValidateFunction { ( - schema: any, - data: any, + schema: unknown, + data: unknown, parentSchema?: SchemaObject, dataPath?: string, parentData?: Record | unknown[], parentDataProperty?: string | number, rootData?: Record | unknown[] - ): boolean | Promise + ): boolean | Promise errors?: ErrorObject[] } @@ -132,9 +132,9 @@ export interface ErrorObject { // Excluded if messages set to false. message?: string // These are added with the `verbose` option. - schema?: any + schema?: unknown parentSchema?: SchemaObject - data?: any + data?: unknown } export type KeywordCompilationResult = Schema | SchemaValidateFunction | ValidateFunction @@ -192,10 +192,14 @@ export interface CodeKeywordDefinition extends _KeywordDef { trackErrors?: boolean } -export type MacroKeywordFunc = (schema: any, parentSchema: SchemaObject, it: SchemaCtx) => Schema +export type MacroKeywordFunc = ( + schema: unknown, + parentSchema: SchemaObject, + it: SchemaCtx +) => Schema export type CompileKeywordFunc = ( - schema: any, + schema: unknown, parentSchema: SchemaObject, it: SchemaObjCtx ) => ValidateFunction @@ -232,7 +236,7 @@ export interface KeywordErrorCtx { keyword: string data: Name $data?: string | false - schema: any + schema: any // TODO parentSchema?: SchemaObject schemaCode: Code | number | boolean schemaValue: Code | number | boolean @@ -268,12 +272,12 @@ export interface AsyncFormatDefinition { compare?: FormatCompare } -export type FormatValidate = FormatValidator | AsyncFormatValidator | RegExp +export type FormatValidate = FormatValidator | AsyncFormatValidator | RegExp export type AddedFormat = | RegExp - | FormatValidator - | FormatDefinition - | AsyncFormatDefinition + | FormatValidator // TODO should be string, not SN + | FormatDefinition + | AsyncFormatDefinition export type Format = AddedFormat | string diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index fd8294a4bd..8c46b81ab4 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,5 +1,4 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import {CodeKeywordDefinition, KeywordErrorCtx} from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" @@ -10,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: ["boolean", "object", "undefined"], // "undefined" is needed to support option removeAdditional: "all" trackErrors: true, - code(cxt: KeywordCtx) { + code(cxt) { const {gen, schema, parentSchema, data, errsCount, it} = cxt if (!errsCount) throw new Error("ajv implementation error") const {allErrors, opts} = it @@ -94,7 +93,8 @@ const def: CodeKeywordDefinition = { }, error: { message: "should NOT have additional properties", - params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`, + params: ({params}: KeywordErrorCtx): Code => + _`{additionalProperty: ${params.additionalProperty}}`, }, } diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index e1df5256f0..30702fcb3c 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -8,6 +8,7 @@ const def: CodeKeywordDefinition = { schemaType: "array", code(cxt: KeywordCtx) { const {gen, schema, it} = cxt + if (!Array.isArray(schema)) throw new Error("ajv implementation error") const valid = gen.name("valid") schema.forEach((sch: Schema, i: number) => { if (alwaysValidSchema(it, sch)) return diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index b34a231a48..7b94946fdb 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -10,6 +10,7 @@ const def: CodeKeywordDefinition = { trackErrors: true, code(cxt: KeywordCtx) { const {gen, schema, it} = cxt + if (!Array.isArray(schema)) throw new Error("ajv implementation error") const alwaysValid = schema.some((sch: Schema) => alwaysValidSchema(it, sch)) if (alwaysValid) return diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 58580d5e7f..2bcbe461ac 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, Schema} from "../../types" import KeywordCtx from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" @@ -13,14 +13,14 @@ const def: CodeKeywordDefinition = { const {gen, schema, data, it} = cxt const len = gen.const("len", _`${data}.length`) if (Array.isArray(schema)) { - validateDefinedItems() + validateDefinedItems(schema) } else if (!alwaysValidSchema(it, schema)) { validateItems() } - function validateDefinedItems(): void { + function validateDefinedItems(schArr: Schema[]): void { const valid = gen.name("valid") - schema.forEach((sch: any, i: number) => { + schArr.forEach((sch: Schema, i: number) => { if (alwaysValidSchema(it, sch)) return gen.if(_`${len} > ${i}`, () => applySubschema( diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 395acf2358..8f47aba68e 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -10,6 +10,8 @@ const def: CodeKeywordDefinition = { trackErrors: true, code(cxt: KeywordCtx) { const {gen, schema, it} = cxt + if (!Array.isArray(schema)) throw new Error("ajv implementation error") + const schArr: Schema[] = schema const valid = gen.let("valid", false) const passing = gen.let("passing", null) const schValid = gen.name("_valid") @@ -25,7 +27,7 @@ const def: CodeKeywordDefinition = { ) function validateOneOf() { - schema.forEach((sch: Schema, i: number) => { + schArr.forEach((sch: Schema, i: number) => { if (alwaysValidSchema(it, sch)) { gen.var(schValid, true) } else { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 99cdadffe6..e3875b961d 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -54,7 +54,10 @@ const def: CodeKeywordDefinition = { if (fmtType === ruleType) cxt.pass(validCondition()) function unknownFormat() { - if (opts.unknownFormats === "ignore") return logger.warn(unknownMsg()) + if (opts.unknownFormats === "ignore") { + logger.warn(unknownMsg()) + return + } if (Array.isArray(opts.unknownFormats) && opts.unknownFormats.includes(schema)) return throw new Error(unknownMsg()) diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index ca7039a477..3390fe6485 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -15,8 +15,9 @@ const def: CodeKeywordDefinition = { valid = gen.let("valid") cxt.block$data(valid, loopEnum) } else { + if (!Array.isArray(schema)) throw new Error("ajv implementation error") const vSchema = gen.const("schema", schemaCode) - valid = or(...schema.map((_x: any, i: number) => equalCode(vSchema, i))) + valid = or(...schema.map((_x: unknown, i: number) => equalCode(vSchema, i))) } cxt.pass(valid) From 3e1694fc568c2256ebbe874aa142a68f54cbb401 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 20:33:57 +0100 Subject: [PATCH 168/322] eslint: enable no-var (excluding tests) --- .eslintrc.yml | 1 - lib/compile/index.ts | 18 +++++++++--------- lib/compile/resolve.ts | 6 +++--- spec/.eslintrc.yml | 1 + 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 59959a2541..8d6d37f7d8 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -20,7 +20,6 @@ overrides: project: ["./tsconfig.json"] plugins: ["@typescript-eslint"] rules: - no-var: 0 "@typescript-eslint/restrict-template-expressions": off "@typescript-eslint/no-unsafe-member-access": off "@typescript-eslint/no-unsafe-assignment": off diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 908802e5d3..0004517b0e 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -203,12 +203,12 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va function localCompile(_env: SchemaEnv): ValidateFunction { const {schema: _schema, root: _root, baseId} = _env - var isRoot = isRootEnv(_env) + const isRoot = isRootEnv(_env) if (_root !== root) { return compileSchema.call(self, _env) } - var $async = typeof _schema == "object" && _schema.$async === true + const $async = typeof _schema == "object" && _schema.$async === true const rootId = getFullPath(_root.schema.$id) const gen = new CodeGen({...opts.codegen, forInOwn: opts.ownProperties}) @@ -338,7 +338,7 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va // TODO gen.globals function localRefCode(ref: string, schOrFunc?: RefVal): Code { - var refId = refVal.length + const refId = refVal.length refVal[refId] = schOrFunc refs[ref] = refId return _`refVal${refId}` @@ -351,7 +351,7 @@ export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | Va // TODO gen.globals remove? function replaceLocalRef(ref: string, schOrFunc: RefVal) { - var refId = refs[ref] + const refId = refs[ref] if (refId !== undefined) refVal[refId] = schOrFunc } @@ -405,8 +405,8 @@ export function resolve( : schOrRef.validate || compileStoredSchema.call(this, schOrRef) } - var env = resolveSchema.call(this, root, ref) - var schema, baseId + const env = resolveSchema.call(this, root, ref) + let schema, baseId if (env) { schema = env.schema root = env.root @@ -437,8 +437,8 @@ export function resolveSchema( const refPath = _getFullPath(p) let baseId = getFullPath(root.schema.$id) if (Object.keys(root.schema).length === 0 || refPath !== baseId) { - var id = normalizeId(refPath) - var schOrRef = this._refs[id] + const id = normalizeId(refPath) + let schOrRef = this._refs[id] if (typeof schOrRef == "string") { return resolveRecursive.call(this, root, schOrRef, p) } @@ -478,7 +478,7 @@ function resolveRecursive( return getJsonPointer.call(this, parsedRef, env) } -var PREVENT_SCOPE_CHANGE = toHash([ +const PREVENT_SCOPE_CHANGE = toHash([ "properties", "patternProperties", "enum", diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index f2e76c021e..141935bf3b 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -11,7 +11,7 @@ export interface LocalRefs { } // TODO refactor to use keyword definitions -var SIMPLE_INLINED = toHash([ +const SIMPLE_INLINED = toHash([ "type", "format", "pattern", @@ -62,7 +62,7 @@ function countKeys(schema: SchemaObject): number { export function getFullPath(id = "", normalize?: boolean): string { if (normalize !== false) id = normalizeId(id) - var p = URI.parse(id) + const p = URI.parse(id) return _getFullPath(p) } @@ -70,7 +70,7 @@ export function _getFullPath(p: URI.URIComponents): string { return URI.serialize(p).split("#")[0] + "#" } -var TRAILING_SLASH_HASH = /#\/?$/ +const TRAILING_SLASH_HASH = /#\/?$/ export function normalizeId(id: string | undefined): string { return id ? id.replace(TRAILING_SLASH_HASH, "") : "" } diff --git a/spec/.eslintrc.yml b/spec/.eslintrc.yml index 6ba17543ac..ebb177cedd 100644 --- a/spec/.eslintrc.yml +++ b/spec/.eslintrc.yml @@ -1,6 +1,7 @@ parserOptions: sourceType: script rules: + no-var: 0 no-console: 0 no-empty: [2, allowEmptyCatch: true] quotes: 0 From a0d93890b553fdd0541ed7b2d46bda037ada877c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 6 Sep 2020 20:38:22 +0100 Subject: [PATCH 169/322] eslint: enable no-control-regex --- .eslintrc.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 8d6d37f7d8..86043a7769 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -64,7 +64,6 @@ rules: radix: error semi: 0 valid-jsdoc: [error, requireReturn: false] - no-control-regex: 0 no-useless-escape: error no-void: error - # no-var: error + no-var: error From b156e060287b09e1ec1012509114e76703999085 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 7 Sep 2020 12:20:11 +0100 Subject: [PATCH 170/322] tests: ts tests, replace require-globify and brfs with jsontests script, remove vars from tests --- .gitignore | 2 + karma.conf.js | 8 +- lib/types.ts | 2 +- package.json | 19 +- scripts/jsontests.js | 26 +++ scripts/prepare-tests | 4 +- spec/.eslintrc.yml | 22 ++- spec/_json/README.md | 3 + spec/after_test.js | 4 +- spec/ajv.spec.js | 72 ++++---- spec/ajv_instances.js | 12 +- spec/ajv_options.js | 6 +- spec/async.spec.js | 66 +++---- spec/async_schemas.spec.js | 18 +- spec/async_validate.spec.js | 53 +++--- spec/{boolean.spec.js => boolean.spec.ts} | 50 ++--- spec/browser_test_suite.js | 11 -- spec/coercion.spec.js | 68 +++---- spec/errors.spec.js | 172 +++++++++--------- spec/extras.spec.js | 16 +- ...1_addKeyword_and_schema_without_id.spec.js | 6 +- ...1_allErrors_custom_keyword_skipped.spec.js | 8 +- spec/issues/182_nan_validation.spec.js | 2 +- .../204_options_schemas_data_together.spec.js | 8 +- spec/issues/210_mutual_recur_frags.spec.js | 10 +- .../240_mutual_recur_frags_common_ref.spec.js | 24 +-- .../259_validate_meta_against_itself.spec.js | 6 +- .../273_error_schemaPath_refd_schema.spec.js | 8 +- .../342_uniqueItems_non-json_objects.spec.js | 14 +- .../485_type_validation_priority.spec.js | 10 +- spec/issues/50_refs_with_definitions.spec.js | 10 +- .../521_wrong_warning_id_property.spec.js | 6 +- .../533_missing_ref_error_when_ignore.spec.js | 12 +- ...3_removeAdditional_to_remove_proto.spec.js | 10 +- .../768_passContext_recursive_ref.spec.js | 18 +- spec/issues/8_shared_refs.spec.js | 10 +- ...5_removeAdditional_custom_keywords.spec.js | 10 +- spec/json-schema.spec.js | 71 +++----- spec/{keyword.spec.js => keyword.spec.ts} | 168 +++++++++-------- spec/options/comment.spec.js | 22 +-- spec/options/meta_validateSchema.spec.js | 14 +- spec/options/options_add_schemas.spec.js | 32 ++-- spec/options/options_code.spec.js | 28 +-- spec/options/options_refs.spec.js | 24 +-- spec/options/options_reporting.spec.js | 32 ++-- spec/options/options_validation.spec.js | 28 +-- spec/options/ownProperties.spec.js | 74 ++++---- spec/options/removeAdditional.spec.js | 20 +- spec/options/schemaId.spec.js | 8 +- spec/options/strictDefaults.spec.js | 32 ++-- spec/options/strictKeywords.spec.js | 16 +- spec/options/strictNumbers.spec.js | 8 +- spec/options/unknownFormats.spec.js | 16 +- spec/options/useDefaults.spec.js | 40 ++-- spec/promise.js | 11 -- spec/resolve.spec.js | 46 ++--- spec/schema-tests.spec.js | 18 +- spec/security.spec.js | 12 +- spec/tsconfig.json | 9 + spec/typescript/index.ts | 60 ------ 60 files changed, 754 insertions(+), 841 deletions(-) create mode 100644 scripts/jsontests.js create mode 100644 spec/_json/README.md rename spec/{boolean.spec.js => boolean.spec.ts} (92%) delete mode 100644 spec/browser_test_suite.js rename spec/{keyword.spec.js => keyword.spec.ts} (90%) delete mode 100644 spec/promise.js create mode 100644 spec/tsconfig.json delete mode 100644 spec/typescript/index.ts diff --git a/.gitignore b/.gitignore index 944880f903..8fc84d941f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ dist/ bundle/ package-lock.json + +spec/_json/*.js \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index 4d811aa69c..b69332b25d 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -11,13 +11,7 @@ module.exports = function (config) { frameworks: ["mocha"], // list of files / patterns to load in the browser - files: [ - "bundle/ajv.min.js", - "node_modules/chai/chai.js", - "node_modules/ajv-async/dist/ajv-async.min.js", - "node_modules/bluebird/js/browser/bluebird.core.min.js", - ".browser/*.spec.js", - ], + files: ["bundle/ajv.min.js", "node_modules/chai/chai.js", ".browser/*.spec.js"], // test results reporter to use // possible values: 'dots', 'progress' diff --git a/lib/types.ts b/lib/types.ts index 25fa71c6eb..7524dd08ff 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -119,7 +119,7 @@ export interface SchemaValidateFunction { parentDataProperty?: string | number, rootData?: Record | unknown[] ): boolean | Promise - errors?: ErrorObject[] + errors?: Partial[] } export interface ErrorObject { diff --git a/package.json b/package.json index 9025004c55..3ce620fa9f 100644 --- a/package.json +++ b/package.json @@ -12,19 +12,20 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint lib/{**/,}*.ts spec/{**/,}*.js scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint lib/{**/,}*.{ts,js} spec/{**/,}*.{ts,js} scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", - "test-spec": "mocha spec/{**/,}*.spec.js -R dot", - "test-fast": "AJV_FAST_TEST=true npm run test-spec", + "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register spec/{**/,}*.spec.{js,ts} -R dot", + "test-fast": "cross-env AJV_FAST_TEST=true npm run test-spec", "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", "bundle": "rm -rf bundle && node ./scripts/bundle.js", "build": "rm -rf dist && tsc && cp -r lib/refs dist/refs", + "json-tests": "node scripts/jsontests", "test-karma": "karma start", - "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && npm run test-karma", + "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && karma start", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", - "test": "npm run eslint && npm run build && npm run test-cov", + "test": "npm run eslint && npm run build && npm run json-tests && npm run test-cov", "prepublish": "npm run build", "watch": "watch \"npm run build\" ./lib" }, @@ -67,15 +68,16 @@ }, "devDependencies": { "@ajv-validator/config": "^0.1.1", + "@types/chai": "^4.2.12", + "@types/mocha": "^8.0.3", "@types/node": "^14.0.27", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", "ajv-formats": "^0.1.0", - "bluebird": "^3.5.3", - "brfs": "^2.0.0", "browserify": "^16.2.0", "chai": "^4.0.1", "coveralls": "^3.0.1", + "cross-env": "^7.0.2", "eslint": "^7.3.1", "eslint-config-prettier": "^6.11.0", "gh-pages-generator": "^0.2.3", @@ -91,8 +93,9 @@ "mocha": "^8.0.1", "nyc": "^15.0.0", "prettier": "^2.0.5", - "require-globify": "^1.3.0", "terser": "^5.2.1", + "ts-node": "^9.0.0", + "tsify": "^5.0.2", "typescript": "^4.0.0", "watch": "^1.0.0" }, diff --git a/scripts/jsontests.js b/scripts/jsontests.js new file mode 100644 index 0000000000..cb7e605d8a --- /dev/null +++ b/scripts/jsontests.js @@ -0,0 +1,26 @@ +"use strict" + +const testSuitePaths = { + draft6: "spec/JSON-Schema-Test-Suite/tests/draft6/", + draft7: "spec/JSON-Schema-Test-Suite/tests/draft7/", + tests: "spec/tests/", + security: "spec/security/", + extras: "spec/extras/", + async: "spec/async/", +} + +const glob = require("glob") +const fs = require("fs") + +for (const suite in testSuitePaths) { + const p = testSuitePaths[suite] + const files = glob + .sync(`${p}{**/,}*.json`) + .map((f) => { + const name = f.replace(p, "").replace(/\.json$/, "") + const testPath = f.replace(/^spec/, "..") + return `\n {name: "${name}", test: require("${testPath}")},` + }) + .reduce((list, f) => list + f) + fs.writeFileSync(`./spec/_json/${suite}.js`, `module.exports = [${files}\n]\n`) +} diff --git a/scripts/prepare-tests b/scripts/prepare-tests index 684703318b..9b6915724d 100755 --- a/scripts/prepare-tests +++ b/scripts/prepare-tests @@ -7,6 +7,6 @@ mkdir -p .browser echo echo Preparing browser tests: -find spec -type f -name '*.spec.js' | \ +find spec -type f -name '*.spec.*s' | \ xargs -I {} sh -c \ -'export f="{}"; echo $f; browserify $f -t require-globify -t brfs -x ajv -u buffer -o $(echo $f | sed -e "s/spec/.browser/");' +'export f="{}"; echo $f; browserify $f -p [ tsify -p ./spec/tsconfig.json ] -x ajv -u buffer -o $(echo $f | sed -e "s/spec/.browser/");' diff --git a/spec/.eslintrc.yml b/spec/.eslintrc.yml index ebb177cedd..c479fb62d6 100644 --- a/spec/.eslintrc.yml +++ b/spec/.eslintrc.yml @@ -1,7 +1,6 @@ parserOptions: sourceType: script rules: - no-var: 0 no-console: 0 no-empty: [2, allowEmptyCatch: true] quotes: 0 @@ -12,3 +11,24 @@ globals: before: false beforeEach: false afterEach: false +overrides: + - files: ["*.ts"] + extends: + - "eslint:recommended" + - "plugin:@typescript-eslint/recommended" + - "plugin:@typescript-eslint/recommended-requiring-type-checking" + - "prettier/@typescript-eslint" + parser: "@typescript-eslint/parser" + parserOptions: + project: ["./spec/tsconfig.json"] + plugins: ["@typescript-eslint"] + rules: + "@typescript-eslint/no-unsafe-member-access": off + "@typescript-eslint/no-unsafe-assignment": off + "@typescript-eslint/no-unsafe-call": off + "@typescript-eslint/no-unsafe-return": off + "@typescript-eslint/restrict-plus-operands": off + "@typescript-eslint/no-var-requires": off + "@typescript-eslint/no-empty-function": off + "@typescript-eslint/no-explicit-any": off + no-invalid-this: off diff --git a/spec/_json/README.md b/spec/_json/README.md new file mode 100644 index 0000000000..8993f75ab5 --- /dev/null +++ b/spec/_json/README.md @@ -0,0 +1,3 @@ +# Test suites from JSON tests + +These files are generated automatically during the test. diff --git a/spec/after_test.js b/spec/after_test.js index 7a12f0e99b..4d566430d6 100644 --- a/spec/after_test.js +++ b/spec/after_test.js @@ -1,6 +1,6 @@ "use strict" -var should = require("./chai").should() +const should = require("./chai").should() exports.error = function (res) { console.log("ajv options:", res.validator._opts) @@ -13,7 +13,7 @@ exports.each = function (res) { should.equal(res.errors, null) } else { res.errors.should.be.an("array") - for (var i = 0; i < res.errors.length; i++) { + for (let i = 0; i < res.errors.length; i++) { res.errors[i].should.be.an("object") } } diff --git a/spec/ajv.spec.js b/spec/ajv.spec.js index 4cc63a89e7..4eec472d7f 100644 --- a/spec/ajv.spec.js +++ b/spec/ajv.spec.js @@ -1,6 +1,6 @@ "use strict" -var Ajv = require("./ajv"), +const Ajv = require("./ajv"), should = require("./chai").should(), stableStringify = require("fast-json-stable-stringify") @@ -8,7 +8,7 @@ const codegen = require("../dist/compile/codegen") const {_} = codegen describe("Ajv", () => { - var ajv + let ajv beforeEach(() => { ajv = new Ajv() @@ -20,7 +20,7 @@ describe("Ajv", () => { describe("compile method", () => { it("should compile schema and return validating function", () => { - var validate = ajv.compile({type: "integer"}) + const validate = ajv.compile({type: "integer"}) validate.should.be.a("function") validate(1).should.equal(true) validate(1.1).should.equal(false) @@ -28,12 +28,12 @@ describe("Ajv", () => { }) it("should cache compiled functions for the same schema", () => { - var v1 = ajv.compile({ + const v1 = ajv.compile({ $id: "//e.com/int.json", type: "integer", minimum: 1, }) - var v2 = ajv.compile({ + const v2 = ajv.compile({ $id: "//e.com/int.json", minimum: 1, type: "integer", @@ -57,8 +57,8 @@ describe("Ajv", () => { it("should throw if compiled schema has an invalid JavaScript code", () => { const _ajv = new Ajv({logger: false}) _ajv.addKeyword({keyword: "even", code: badEvenCode}) - var schema = {even: true} - var validate = _ajv.compile(schema) + let schema = {even: true} + const validate = _ajv.compile(schema) validate(2).should.equal(true) validate(3).should.equal(false) @@ -68,7 +68,7 @@ describe("Ajv", () => { }) function badEvenCode(cxt) { - var op = cxt.schema ? _`===` : _`!===` // invalid on purpose + const op = cxt.schema ? _`===` : _`!===` // invalid on purpose cxt.pass(_`${cxt.data} % 2 ${op} 0`) } }) @@ -130,7 +130,7 @@ describe("Ajv", () => { describe("addSchema method", () => { it("should add and compile schema with key", () => { ajv.addSchema({type: "integer"}, "int") - var validate = ajv.getSchema("int") + const validate = ajv.getSchema("int") validate.should.be.a("function") validate(1).should.equal(true) @@ -170,8 +170,8 @@ describe("Ajv", () => { {$id: "//e.com/str.json", type: "string"}, ]) - var validate0 = ajv.getSchema("//e.com/int.json") - var validate1 = ajv.getSchema("//e.com/str.json") + const validate0 = ajv.getSchema("//e.com/int.json") + const validate1 = ajv.getSchema("//e.com/str.json") validate0(1).should.equal(true) validate0("1").should.equal(false) @@ -230,7 +230,7 @@ describe("Ajv", () => { }) it("should return instance of itself", () => { - var res = ajv.addSchema({type: "integer"}, "int") + const res = ajv.addSchema({type: "integer"}, "int") res.should.equal(ajv) }) }) @@ -238,25 +238,25 @@ describe("Ajv", () => { describe("getSchema method", () => { it("should return compiled schema by key", () => { ajv.addSchema({type: "integer"}, "int") - var validate = ajv.getSchema("int") + const validate = ajv.getSchema("int") validate(1).should.equal(true) validate("1").should.equal(false) }) it("should return compiled schema by id or ref", () => { ajv.addSchema({$id: "//e.com/int.json", type: "integer"}) - var validate = ajv.getSchema("//e.com/int.json") + const validate = ajv.getSchema("//e.com/int.json") validate(1).should.equal(true) validate("1").should.equal(false) }) it("should return compiled schema without key or with empty key", () => { ajv.addSchema({type: "integer"}) - var validate = ajv.getSchema("") + const validate = ajv.getSchema("") validate(1).should.equal(true) validate("1").should.equal(false) - var v = ajv.getSchema() + const v = ajv.getSchema() v(1).should.equal(true) v("1").should.equal(false) }) @@ -270,7 +270,7 @@ describe("Ajv", () => { }, }) - var vInt = ajv.getSchema("http://e.com/types.json#/definitions/int") + const vInt = ajv.getSchema("http://e.com/types.json#/definitions/int") vInt(1).should.equal(true) vInt("1").should.equal(false) }) @@ -284,7 +284,7 @@ describe("Ajv", () => { }, }) - var vInt = ajv.getSchema("//e.com/types.json#/definitions/int") + const vInt = ajv.getSchema("//e.com/types.json#/definitions/int") vInt(1).should.equal(true) vInt("1").should.equal(false) }) @@ -298,7 +298,7 @@ describe("Ajv", () => { }, }) - var vInt = ajv.getSchema("http://e.com/types.json#int") + const vInt = ajv.getSchema("http://e.com/types.json#int") vInt(1).should.equal(true) vInt("1").should.equal(false) }) @@ -306,10 +306,10 @@ describe("Ajv", () => { describe("removeSchema method", () => { it("should remove schema by key", () => { - var schema = {type: "integer"}, + const schema = {type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema, "int") - var v = ajv.getSchema("int") + const v = ajv.getSchema("int") v.should.be.a("function") ajv._cache.get(str).validate.should.equal(v) @@ -320,11 +320,11 @@ describe("Ajv", () => { }) it("should remove schema by id", () => { - var schema = {$id: "//e.com/int.json", type: "integer"}, + const schema = {$id: "//e.com/int.json", type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema) - var v = ajv.getSchema("//e.com/int.json") + const v = ajv.getSchema("//e.com/int.json") v.should.be.a("function") ajv._cache.get(str).validate.should.equal(v) @@ -334,7 +334,7 @@ describe("Ajv", () => { }) it("should remove schema by schema object", () => { - var schema = {type: "integer"}, + const schema = {type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema) ajv._cache.get(str).should.be.an("object") @@ -343,7 +343,7 @@ describe("Ajv", () => { }) it("should remove schema with id by schema object", () => { - var schema = {$id: "//e.com/int.json", type: "integer"}, + const schema = {$id: "//e.com/int.json", type: "integer"}, str = stableStringify(schema) ajv.addSchema(schema) ajv._cache.get(str).should.be.an("object") @@ -360,12 +360,12 @@ describe("Ajv", () => { }) it("should remove all schemas but meta-schemas if called without an arguments", () => { - var schema1 = {$id: "//e.com/int.json", type: "integer"}, + const schema1 = {$id: "//e.com/int.json", type: "integer"}, str1 = stableStringify(schema1) ajv.addSchema(schema1) ajv._cache.get(str1).should.be.an("object") - var schema2 = {type: "integer"}, + const schema2 = {type: "integer"}, str2 = stableStringify(schema2) ajv.addSchema(schema2) ajv._cache.get(str2).should.be.an("object") @@ -376,17 +376,17 @@ describe("Ajv", () => { }) it("should remove all schemas but meta-schemas with key/id matching pattern", () => { - var schema1 = {$id: "//e.com/int.json", type: "integer"}, + const schema1 = {$id: "//e.com/int.json", type: "integer"}, str1 = stableStringify(schema1) ajv.addSchema(schema1) ajv._cache.get(str1).should.be.an("object") - var schema2 = {$id: "str.json", type: "string"}, + const schema2 = {$id: "str.json", type: "string"}, str2 = stableStringify(schema2) ajv.addSchema(schema2, "//e.com/str.json") ajv._cache.get(str2).should.be.an("object") - var schema3 = {type: "integer"}, + const schema3 = {type: "integer"}, str3 = stableStringify(schema3) ajv.addSchema(schema3) ajv._cache.get(str3).should.be.an("object") @@ -398,7 +398,7 @@ describe("Ajv", () => { }) it("should return instance of itself", () => { - var res = ajv.addSchema({type: "integer"}, "int").removeSchema("int") + const res = ajv.addSchema({type: "integer"}, "int").removeSchema("int") res.should.equal(ajv) }) }) @@ -427,12 +427,12 @@ describe("Ajv", () => { }) it("should return instance of itself", () => { - var res = ajv.addFormat("identifier", /^[a-z_$][a-z0-9_$]*$/i) + const res = ajv.addFormat("identifier", /^[a-z_$][a-z0-9_$]*$/i) res.should.equal(ajv) }) function testFormat() { - var validate = ajv.compile({format: "identifier"}) + const validate = ajv.compile({format: "identifier"}) validate("Abc1").should.equal(true) validate("123").should.equal(false) validate(123).should.equal(true) @@ -447,7 +447,7 @@ describe("Ajv", () => { }, }) - var validate = ajv.compile({ + const validate = ajv.compile({ format: "positive", }) validate(-2).should.equal(false) @@ -465,7 +465,7 @@ describe("Ajv", () => { }, }) - var validate = ajv.compile({ + const validate = ajv.compile({ properties: { data: {format: {$data: "1/frmt"}}, frmt: {type: "string"}, @@ -481,7 +481,7 @@ describe("Ajv", () => { describe("validateSchema method", () => { it("should validate schema against meta-schema", () => { - var valid = ajv.validateSchema({ + let valid = ajv.validateSchema({ $schema: "http://json-schema.org/draft-07/schema#", type: "number", }) diff --git a/spec/ajv_instances.js b/spec/ajv_instances.js index 68aedfcfcd..f6dcfb6725 100644 --- a/spec/ajv_instances.js +++ b/spec/ajv_instances.js @@ -1,22 +1,22 @@ "use strict" -var Ajv = require("./ajv") +const Ajv = require("./ajv") module.exports = getAjvInstances function getAjvInstances(options, extraOpts = {}) { - return _getAjvInstances(options, {...extraOpts, logger: false}) + return _getAjvInstances(options, {...extraOpts, logger: false, codegen: {lines: true}}) } function _getAjvInstances(opts, useOpts) { - var optNames = Object.keys(opts) + const optNames = Object.keys(opts) if (optNames.length) { opts = Object.assign({}, opts) - var useOpts1 = Object.assign({}, useOpts) - var optName = optNames[0] + const useOpts1 = Object.assign({}, useOpts) + const optName = optNames[0] useOpts1[optName] = opts[optName] delete opts[optName] - var instances = _getAjvInstances(opts, useOpts), + const instances = _getAjvInstances(opts, useOpts), instances1 = _getAjvInstances(opts, useOpts1) return instances.concat(instances1) } diff --git a/spec/ajv_options.js b/spec/ajv_options.js index 21cbf63e35..0b0f08db7d 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -1,9 +1,9 @@ "use strict" -var isBrowser = typeof window == "object" -var fullTest = isBrowser || !process.env.AJV_FAST_TEST +const isBrowser = typeof window == "object" +const fullTest = isBrowser || !process.env.AJV_FAST_TEST -var options = fullTest +const options = fullTest ? { allErrors: true, verbose: true, diff --git a/spec/async.spec.js b/spec/async.spec.js index 913f4c92bd..b41b490374 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -1,12 +1,12 @@ "use strict" -var Ajv = require("./ajv"), +const Ajv = require("./ajv"), should = require("./chai").should() describe("compileAsync method", () => { - var ajv, loadCallCount + let ajv, loadCallCount - var SCHEMAS = { + const SCHEMAS = { "http://example.com/object.json": { $id: "http://example.com/object.json", properties: { @@ -85,7 +85,7 @@ describe("compileAsync method", () => { }) it("should compile schemas loading missing schemas with options.loadSchema function", () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, @@ -100,7 +100,7 @@ describe("compileAsync method", () => { }) it("should compile schemas loading missing schemas and return function via callback", (done) => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, @@ -117,7 +117,7 @@ describe("compileAsync method", () => { }) it("should correctly load schemas when missing reference has JSON path", () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json#/properties/b"}, @@ -132,7 +132,7 @@ describe("compileAsync method", () => { }) it("should correctly compile with remote schemas that have mutual references", () => { - var schema = { + const schema = { $id: "http://example.com/root.json", properties: { tree: {$ref: "tree.json"}, @@ -140,17 +140,17 @@ describe("compileAsync method", () => { } return ajv.compileAsync(schema).then((validate) => { validate.should.be.a("function") - var validData = { + const validData = { tree: [{name: "a", subtree: [{name: "a.a"}]}, {name: "b"}], } - var invalidData = {tree: [{name: "a", subtree: [{name: 1}]}]} + const invalidData = {tree: [{name: "a", subtree: [{name: 1}]}]} validate(validData).should.equal(true) validate(invalidData).should.equal(false) }) }) it("should correctly compile with remote schemas that reference the compiled schema", () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "recursive.json"}, @@ -159,15 +159,15 @@ describe("compileAsync method", () => { return ajv.compileAsync(schema).then((validate) => { should.equal(loadCallCount, 1) validate.should.be.a("function") - var validData = {a: {b: {a: {b: {}}}}} - var invalidData = {a: {b: {a: {}}}} + const validData = {a: {b: {a: {b: {}}}}} + const invalidData = {a: {b: {a: {}}}} validate(validData).should.equal(true) validate(invalidData).should.equal(false) }) }) it('should resolve reference containing "properties" segment with the same property (issue #220)', () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: { @@ -211,17 +211,17 @@ describe("compileAsync method", () => { }) it("should return compiled schema on the next tick if there are no references (#51)", () => { - var schema = { + const schema = { $id: "http://example.com/int2plus.json", type: "integer", minimum: 2, } - var beforeCallback1 - var p1 = ajv.compileAsync(schema).then((validate) => { + let beforeCallback1 = false + const p1 = ajv.compileAsync(schema).then((validate) => { beforeCallback1.should.equal(true) spec(validate) - var beforeCallback2 - var p2 = ajv.compileAsync(schema).then((_validate) => { + let beforeCallback2 = false + const p2 = ajv.compileAsync(schema).then((_validate) => { beforeCallback2.should.equal(true) spec(_validate) }) @@ -234,15 +234,15 @@ describe("compileAsync method", () => { function spec(validate) { should.equal(loadCallCount, 0) validate.should.be.a("function") - var validData = 2 - var invalidData = 1 + const validData = 2 + const invalidData = 1 validate(validData).should.equal(true) validate(invalidData).should.equal(false) } }) it("should queue calls so only one compileAsync executes at a time (#52)", () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, @@ -264,7 +264,7 @@ describe("compileAsync method", () => { }) it("should throw exception if loadSchema is not passed", (done) => { - var schema = { + const schema = { $id: "http://example.com/int2plus.json", type: "integer", minimum: 2, @@ -283,7 +283,7 @@ describe("compileAsync method", () => { describe("should return error via callback", () => { it("if passed schema is invalid", (done) => { - var invalidSchema = { + const invalidSchema = { $id: "http://example.com/int2plus.json", type: "integer", minimum: "invalid", @@ -292,7 +292,7 @@ describe("compileAsync method", () => { }) it("if loaded schema is invalid", (done) => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "invalid.json"}, @@ -302,7 +302,7 @@ describe("compileAsync method", () => { }) it("if required schema is loaded but the reference cannot be resolved", (done) => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json#/definitions/not_found"}, @@ -312,7 +312,7 @@ describe("compileAsync method", () => { }) it("if loadSchema returned error", (done) => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, @@ -328,7 +328,7 @@ describe("compileAsync method", () => { it("if schema compilation throws some other exception", (done) => { ajv.addKeyword({keyword: "badkeyword", compile: badCompile}) - var schema = {badkeyword: true} + const schema = {badkeyword: true} ajv.compileAsync(schema, shouldFail(done)) function badCompile(/* schema */) { @@ -347,7 +347,7 @@ describe("compileAsync method", () => { describe("should return error via promise", () => { it("if passed schema is invalid", () => { - var invalidSchema = { + const invalidSchema = { $id: "http://example.com/int2plus.json", type: "integer", minimum: "invalid", @@ -356,7 +356,7 @@ describe("compileAsync method", () => { }) it("if loaded schema is invalid", () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "invalid.json"}, @@ -366,7 +366,7 @@ describe("compileAsync method", () => { }) it("if required schema is loaded but the reference cannot be resolved", () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json#/definitions/not_found"}, @@ -376,7 +376,7 @@ describe("compileAsync method", () => { }) it("if loadSchema returned error", () => { - var schema = { + const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, @@ -392,7 +392,7 @@ describe("compileAsync method", () => { it("if schema compilation throws some other exception", () => { ajv.addKeyword({keyword: "badkeyword", compile: badCompile}) - var schema = {badkeyword: true} + const schema = {badkeyword: true} return shouldReject(ajv.compileAsync(schema)) function badCompile(/* schema */) { @@ -413,7 +413,7 @@ describe("compileAsync method", () => { describe("schema with multiple remote properties, the first is recursive schema (#801)", () => { it("should validate data", () => { - var schema = { + const schema = { $id: "http://example.com/list.json", type: "object", properties: { diff --git a/spec/async_schemas.spec.js b/spec/async_schemas.spec.js index ea60f064be..94a16ff7b0 100644 --- a/spec/async_schemas.spec.js +++ b/spec/async_schemas.spec.js @@ -1,29 +1,21 @@ "use strict" -var jsonSchemaTest = require("json-schema-test"), - Promise = require("./promise"), +const jsonSchemaTest = require("json-schema-test"), getAjvInstances = require("./ajv_async_instances"), Ajv = require("./ajv"), - suite = require("./browser_test_suite"), after = require("./after_test") -var instances = getAjvInstances({$data: true}) +const instances = getAjvInstances({$data: true}) instances.forEach(addAsyncFormatsAndKeywords) jsonSchemaTest(instances, { description: "asynchronous schemas tests of " + instances.length + " ajv instances with different options", - suites: { - "async schemas": - typeof window == "object" - ? suite(require("./async/{**/,}*.json", {mode: "list"})) - : "./async/{**/,}*.json", - }, + suites: {"async schemas": require("./_json/async")}, async: true, asyncValid: "data", assert: require("./chai").assert, - Promise: Promise, afterError: after.error, // afterEach: after.each, cwd: __dirname, @@ -87,7 +79,7 @@ function checkIdExists(schema, data) { } function checkIdExistsWithError(schema, data) { - var table = schema.table + const table = schema.table switch (table) { case "users": return check(table, [1, 5, 8]) @@ -100,7 +92,7 @@ function checkIdExistsWithError(schema, data) { function check(_table, IDs) { if (IDs.indexOf(data) >= 0) return Promise.resolve(true) - var error = { + const error = { keyword: "idExistsWithError", message: "id not found in table " + _table, } diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.js index 813d8b530e..8e904b900b 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.js @@ -1,13 +1,12 @@ "use strict" -var Ajv = require("./ajv"), - Promise = require("./promise"), +const Ajv = require("./ajv"), getAjvInstances = require("./ajv_async_instances"), should = require("./chai").should() describe("async schemas, formats and keywords", function () { this.timeout(30000) - var ajv, instances + let ajv, instances beforeEach(() => { instances = getAjvInstances() @@ -16,7 +15,7 @@ describe("async schemas, formats and keywords", function () { describe("async schemas without async elements", () => { it("should return result as promise", () => { - var schema = { + const schema = { $async: true, type: "string", maxLength: 3, @@ -27,7 +26,7 @@ describe("async schemas, formats and keywords", function () { }) function test(_ajv) { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) return Promise.all([ shouldBeValid(validate("abc"), "abc"), @@ -38,7 +37,7 @@ describe("async schemas, formats and keywords", function () { }) it("should fail compilation if async schema is inside sync schema", () => { - var schema = { + const schema = { properties: { foo: { $async: true, @@ -63,7 +62,7 @@ describe("async schemas, formats and keywords", function () { it("should fail compilation if async format is inside sync schema", () => { instances.forEach((_ajv) => { - var schema = { + const schema = { type: "string", format: "english_word", } @@ -100,7 +99,7 @@ describe("async schemas, formats and keywords", function () { it("should fail compilation if async keyword is inside sync schema", () => { instances.forEach((_ajv) => { - var schema = { + const schema = { type: "object", properties: { userId: { @@ -122,7 +121,7 @@ describe("async schemas, formats and keywords", function () { it("should return user-defined error", () => { return Promise.all( instances.map((_ajv) => { - var schema = { + const schema = { $async: true, type: "object", properties: { @@ -137,7 +136,7 @@ describe("async schemas, formats and keywords", function () { }, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) return Promise.all([ shouldBeInvalid(validate({userId: 5, postId: 10}), ["id not found in table posts"]), @@ -163,7 +162,7 @@ describe("async schemas, formats and keywords", function () { } function checkIdExistsWithError(schema, data) { - var table = schema.table + const table = schema.table switch (table) { case "users": return check(table, [1, 5, 8]) @@ -175,7 +174,7 @@ describe("async schemas, formats and keywords", function () { function check(_table, IDs) { if (IDs.indexOf(data) >= 0) return Promise.resolve(true) - var error = { + const error = { keyword: "idExistsWithError", message: "id not found in table " + _table, } @@ -191,7 +190,7 @@ describe("async schemas, formats and keywords", function () { }) it("should validate referenced async schema", () => { - var schema = { + const schema = { $async: true, definitions: { english_word: { @@ -208,8 +207,8 @@ describe("async schemas, formats and keywords", function () { return repeat(() => { return Promise.all( instances.map((_ajv) => { - var validate = _ajv.compile(schema) - var validData = {word: "tomorrow"} + const validate = _ajv.compile(schema) + const validData = {word: "tomorrow"} return Promise.all([ shouldBeValid(validate(validData), validData), @@ -223,7 +222,7 @@ describe("async schemas, formats and keywords", function () { }) it("should validate recursive async schema", () => { - var schema = { + const schema = { $async: true, definitions: { english_word: { @@ -244,7 +243,7 @@ describe("async schemas, formats and keywords", function () { }) it("should validate recursive ref to async sub-schema, issue #612", () => { - var schema = { + const schema = { $async: true, type: "object", properties: { @@ -270,7 +269,7 @@ describe("async schemas, formats and keywords", function () { }) it("should validate ref from referenced async schema to root schema", () => { - var schema = { + const schema = { $async: true, definitions: { wordOrRoot: { @@ -294,7 +293,7 @@ describe("async schemas, formats and keywords", function () { }) it("should validate refs between two async schemas", () => { - var schemaObj = { + const schemaObj = { $id: "http://e.com/obj.json#", $async: true, type: "object", @@ -303,7 +302,7 @@ describe("async schemas, formats and keywords", function () { }, } - var schemaWord = { + const schemaWord = { $id: "http://e.com/word.json#", $async: true, anyOf: [ @@ -319,7 +318,7 @@ describe("async schemas, formats and keywords", function () { }) it("should fail compilation if sync schema references async schema", () => { - var schema = { + const schema = { $id: "http://e.com/obj.json#", type: "object", properties: { @@ -327,7 +326,7 @@ describe("async schemas, formats and keywords", function () { }, } - var schemaWord = { + const schemaWord = { $id: "http://e.com/word.json#", $async: true, anyOf: [ @@ -364,8 +363,8 @@ describe("async schemas, formats and keywords", function () { _ajv.addSchema(refSchema) } catch (e) {} } - var validate = _ajv.compile(schema) - var data + const validate = _ajv.compile(schema) + let data return Promise.all([ shouldBeValid(validate((data = {foo: "tomorrow"})), data), @@ -406,7 +405,7 @@ function checkWordOnServer(str) { } function shouldThrowFunc(message, func) { - var err + let err should.throw(() => { try { func() @@ -423,14 +422,14 @@ function shouldBeValid(p, data) { return p.then((valid) => valid.should.equal(data)) } -var SHOULD_BE_INVALID = "test: should be invalid" +const SHOULD_BE_INVALID = "test: should be invalid" function shouldBeInvalid(p, expectedMessages) { return checkNotValid(p).then((err) => { err.should.be.instanceof(Ajv.ValidationError) err.errors.should.be.an("array") err.validation.should.equal(true) if (expectedMessages) { - var messages = err.errors.map((e) => e.message) + const messages = err.errors.map((e) => e.message) messages.should.eql(expectedMessages) } }) diff --git a/spec/boolean.spec.js b/spec/boolean.spec.ts similarity index 92% rename from spec/boolean.spec.js rename to spec/boolean.spec.ts index d940f38216..f45d5e89f0 100644 --- a/spec/boolean.spec.js +++ b/spec/boolean.spec.ts @@ -1,10 +1,10 @@ "use strict" -var Ajv = require("./ajv") +const Ajv = require("./ajv") require("./chai").should() describe("boolean schemas", () => { - var ajvs + let ajvs before(() => { ajvs = [ @@ -30,7 +30,7 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var validate = ajv.compile(boolSchema) + const validate = ajv.compile(boolSchema) testSchema(validate, valid) } } @@ -51,7 +51,7 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + const schema = { type: "object", properties: { foo: boolSchema, @@ -64,7 +64,7 @@ describe("boolean schemas", () => { }, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({foo: 1, bar: {baz: 1}}).should.equal(valid) validate({foo: "1", bar: {baz: "1"}}).should.equal(valid) validate({foo: {}, bar: {baz: {}}}).should.equal(valid) @@ -93,12 +93,12 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + let schema = { type: "array", items: boolSchema, } - var validate = ajv.compile(schema) + let validate = ajv.compile(schema) validate([1]).should.equal(valid) validate(["1"]).should.equal(valid) validate([{}]).should.equal(valid) @@ -150,7 +150,7 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + const schema = { type: "object", dependencies: { foo: boolSchema, @@ -163,7 +163,7 @@ describe("boolean schemas", () => { }, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({foo: 1, bar: 1, baz: 1}).should.equal(valid) validate({foo: "1", bar: "1", baz: "1"}).should.equal(valid) validate({foo: {}, bar: {}, baz: {}}).should.equal(valid) @@ -192,7 +192,7 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + const schema = { type: "object", patternProperties: { "^f": boolSchema, @@ -205,7 +205,7 @@ describe("boolean schemas", () => { }, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({foo: 1, bar: {baz: 1}}).should.equal(valid) validate({foo: "1", bar: {baz: "1"}}).should.equal(valid) validate({foo: {}, bar: {baz: {}}}).should.equal(valid) @@ -234,12 +234,12 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + const schema = { type: "object", propertyNames: boolSchema, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({foo: 1}).should.equal(valid) validate({bar: 1}).should.equal(valid) @@ -263,12 +263,12 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + const schema = { type: "array", contains: boolSchema, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate([1]).should.equal(valid) validate(["foo"]).should.equal(valid) validate([{}]).should.equal(valid) @@ -297,11 +297,11 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + const schema = { not: boolSchema, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) testSchema(validate, valid) } } @@ -322,11 +322,11 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + let schema = { allOf: [false, boolSchema], } - var validate = ajv.compile(schema) + let validate = ajv.compile(schema) testSchema(validate, false) schema = { @@ -354,11 +354,11 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + let schema = { anyOf: [false, boolSchema], } - var validate = ajv.compile(schema) + let validate = ajv.compile(schema) testSchema(validate, valid) schema = { @@ -386,11 +386,11 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + let schema = { oneOf: [false, boolSchema], } - var validate = ajv.compile(schema) + let validate = ajv.compile(schema) testSchema(validate, valid) schema = { @@ -418,14 +418,14 @@ describe("boolean schemas", () => { function test(boolSchema, valid) { return function (ajv) { - var schema = { + const schema = { $ref: "#/definitions/bool", definitions: { bool: boolSchema, }, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) testSchema(validate, valid) } } diff --git a/spec/browser_test_suite.js b/spec/browser_test_suite.js deleted file mode 100644 index 464cb95948..0000000000 --- a/spec/browser_test_suite.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict" - -module.exports = function (suite) { - suite.forEach((file) => { - if (file.name.indexOf("optional/format") === 0) { - file.name = file.name.replace("optional/", "") - } - file.test = file.module - }) - return suite -} diff --git a/spec/coercion.spec.js b/spec/coercion.spec.js index 8df4dbb98e..c2fb3b04cf 100644 --- a/spec/coercion.spec.js +++ b/spec/coercion.spec.js @@ -1,9 +1,9 @@ "use strict" -var Ajv = require("./ajv") +const Ajv = require("./ajv") require("./chai").should() -var coercionRules = { +const coercionRules = { string: { number: [ {from: 1, to: "1"}, @@ -121,7 +121,7 @@ var coercionRules = { }, } -var coercionArrayRules = JSON.parse(JSON.stringify(coercionRules)) +const coercionArrayRules = JSON.parse(JSON.stringify(coercionRules)) coercionArrayRules.string.array = [ {from: ["abc"], to: "abc"}, {from: [123], to: "123"}, @@ -178,7 +178,7 @@ coercionArrayRules.array = { } describe("Type coercion", () => { - var ajv, fullAjv, instances + let ajv, fullAjv, instances beforeEach(() => { ajv = new Ajv({coerceTypes: true, verbose: true}) @@ -189,7 +189,7 @@ describe("Type coercion", () => { it("should coerce scalar values", () => { testRules(coercionRules, (test, schema, canCoerce /*, toType, fromType*/) => { instances.forEach((_ajv) => { - var valid = _ajv.validate(schema, test.from) + const valid = _ajv.validate(schema, test.from) //if (valid !== canCoerce) console.log('true', toType, fromType, test, ajv.errors); valid.should.equal(canCoerce) }) @@ -203,7 +203,7 @@ describe("Type coercion", () => { testRules(coercionArrayRules, (test, schema, canCoerce, toType, fromType) => { instances.forEach((_ajv) => { - var valid = _ajv.validate(schema, test.from) + const valid = _ajv.validate(schema, test.from) if (valid !== canCoerce) console.log(toType, ".", fromType, test, schema, ajv.errors) valid.should.equal(canCoerce) }) @@ -212,19 +212,19 @@ describe("Type coercion", () => { it("should coerce values in objects/arrays and update properties/items", () => { testRules(coercionRules, (test, schema, canCoerce /*, toType, fromType*/) => { - var schemaObject = { + const schemaObject = { type: "object", properties: { foo: schema, }, } - var schemaArray = { + const schemaArray = { type: "array", items: schema, } - var schemaArrObj = { + const schemaArrObj = { type: "array", items: schemaObject, } @@ -236,7 +236,7 @@ describe("Type coercion", () => { }) function testCoercion(_ajv, _schema, fromData, toData) { - var valid = _ajv.validate(_schema, fromData) + const valid = _ajv.validate(_schema, fromData) //if (valid !== canCoerce) console.log(schema, fromData, toData); valid.should.equal(canCoerce) if (valid) fromData.should.eql(toData) @@ -245,7 +245,7 @@ describe("Type coercion", () => { }) it("should coerce to multiple types in order with number type", () => { - var schema = { + const schema = { type: "object", properties: { foo: { @@ -255,7 +255,7 @@ describe("Type coercion", () => { } instances.forEach((_ajv) => { - var data + let data _ajv.validate(schema, (data = {foo: "1"})).should.equal(true) data.should.eql({foo: 1}) @@ -287,7 +287,7 @@ describe("Type coercion", () => { }) it("should coerce to multiple types in order with integer type", () => { - var schema = { + const schema = { type: "object", properties: { foo: { @@ -297,7 +297,7 @@ describe("Type coercion", () => { } instances.forEach((_ajv) => { - var data + let data _ajv.validate(schema, (data = {foo: "1"})).should.equal(true) data.should.eql({foo: 1}) @@ -326,7 +326,7 @@ describe("Type coercion", () => { }) it("should fail to coerce non-number if multiple properties/items are coerced (issue #152)", () => { - var schema = { + const schema = { type: "object", properties: { foo: {type: "number"}, @@ -334,17 +334,17 @@ describe("Type coercion", () => { }, } - var schema2 = { + const schema2 = { type: "array", items: {type: "number"}, } instances.forEach((_ajv) => { - var data = {foo: "123", bar: "bar"} + const data = {foo: "123", bar: "bar"} _ajv.validate(schema, data).should.equal(false) data.should.eql({foo: 123, bar: "bar"}) - var data2 = ["123", "bar"] + const data2 = ["123", "bar"] _ajv.validate(schema2, data2).should.equal(false) data2.should.eql([123, "bar"]) }) @@ -353,7 +353,7 @@ describe("Type coercion", () => { it("should update data if the schema is in ref that is not inlined", () => { instances.push(new Ajv({coerceTypes: true, inlineRefs: false})) - var schema = { + const schema = { type: "object", definitions: { foo: {type: "number"}, @@ -363,7 +363,7 @@ describe("Type coercion", () => { }, } - var schema2 = { + const schema2 = { type: "object", definitions: { foo: { @@ -378,14 +378,14 @@ describe("Type coercion", () => { }, } - var schemaRecursive = { + const schemaRecursive = { type: ["object", "number"], properties: { foo: {$ref: "#"}, }, } - var schemaRecursive2 = { + const schemaRecursive2 = { $id: "http://e.com/schema.json#", definitions: { foo: { @@ -408,7 +408,7 @@ describe("Type coercion", () => { testCoercion(schemaRecursive2, {foo: {foo: {foo: "1"}}}, {foo: {foo: {foo: 1}}}) function testCoercion(_schema, fromData, toData) { - var valid = _ajv.validate(_schema, fromData) + const valid = _ajv.validate(_schema, fromData) // if (!valid) console.log(schema, fromData, toData); valid.should.equal(true) fromData.should.eql(toData) @@ -417,13 +417,13 @@ describe("Type coercion", () => { }) it("should generate one error for type with coerceTypes option (issue #469)", () => { - var schema = { + const schema = { type: "number", minimum: 10, } instances.forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) validate(9).should.equal(false) validate.errors.length.should.equal(1) @@ -435,13 +435,13 @@ describe("Type coercion", () => { }) it('should check "uniqueItems" after coercion', () => { - var schema = { + const schema = { items: {type: "number"}, uniqueItems: true, } instances.forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) validate([1, "2", 3]).should.equal(true) validate([1, "2", 2]).should.equal(false) @@ -451,13 +451,13 @@ describe("Type coercion", () => { }) it('should check "contains" after coercion', () => { - var schema = { + const schema = { items: {type: "number"}, contains: {const: 2}, } instances.forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) validate([1, "2", 3]).should.equal(true) validate([1, "3", 4]).should.equal(false) @@ -466,12 +466,12 @@ describe("Type coercion", () => { }) function testRules(rules, cb) { - for (var toType in rules) { - for (var fromType in rules[toType]) { - var tests = rules[toType][fromType] + for (const toType in rules) { + for (const fromType in rules[toType]) { + const tests = rules[toType][fromType] tests.forEach((test) => { - var canCoerce = test.to !== undefined - var schema = canCoerce + const canCoerce = test.to !== undefined + const schema = canCoerce ? Array.isArray(test.to) ? {type: toType, items: {type: fromType, enum: [test.to[0]]}} : {type: toType, enum: [test.to]} diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 049e6acae4..3c1a8d52b5 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -1,10 +1,10 @@ "use strict" -var Ajv = require("./ajv"), +const Ajv = require("./ajv"), should = require("./chai").should() describe("Validation errors", () => { - var ajv, ajvJP, fullAjv + let ajv, ajvJP, fullAjv beforeEach(() => { createInstances() @@ -25,7 +25,7 @@ describe("Validation errors", () => { } it("error should include dataPath", () => { - var schema = { + const schema = { properties: { foo: {type: "number"}, }, @@ -35,7 +35,7 @@ describe("Validation errors", () => { }) it('"refs" error should include dataPath', () => { - var schema = { + const schema = { definitions: { num: {type: "number"}, }, @@ -53,7 +53,7 @@ describe("Validation errors", () => { }) function testAdditional() { - var schema = { + const schema = { properties: { foo: {}, bar: {}, @@ -61,19 +61,19 @@ describe("Validation errors", () => { additionalProperties: false, } - var data = {foo: 1, bar: 2}, + const data = {foo: 1, bar: 2}, invalidData = {foo: 1, bar: 2, baz: 3, quux: 4} - var msg = "should NOT have additional properties" + const msg = "should NOT have additional properties" - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) shouldBeError(validate.errors[0], "additionalProperties", "#/additionalProperties", "", msg, { additionalProperty: "baz", }) - var validateJP = ajvJP.compile(schema) + const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData) shouldBeError( @@ -85,7 +85,7 @@ describe("Validation errors", () => { {additionalProperty: "baz"} ) - var fullValidate = fullAjv.compile(schema) + const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData, 2) shouldBeError( @@ -113,7 +113,7 @@ describe("Validation errors", () => { }) function testAdditionalIsSchema() { - var schema = { + const schema = { properties: { foo: {type: "integer"}, bar: {type: "integer"}, @@ -126,26 +126,26 @@ describe("Validation errors", () => { }, } - var data = {foo: 1, bar: 2, baz: {quux: "abc"}}, + const data = {foo: 1, bar: 2, baz: {quux: "abc"}}, invalidData = {foo: 1, bar: 2, baz: {quux: 3}, boo: {quux: 4}} - var schPath = "#/additionalProperties/properties/quux/type" + const schPath = "#/additionalProperties/properties/quux/type" - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) shouldBeError(validate.errors[0], "type", schPath, "/baz/quux", "should be string", { type: "string", }) - var validateJP = ajvJP.compile(schema) + const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData) shouldBeError(validateJP.errors[0], "type", schPath, "/baz/quux", "should be string", { type: "string", }) - var fullValidate = fullAjv.compile(schema) + const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData, 2) shouldBeError(fullValidate.errors[0], "type", schPath, "['baz'].quux", "should be string", { @@ -163,7 +163,7 @@ describe("Validation errors", () => { }) function testRequired() { - var schema = { + const schema = { required: ["foo", "bar", "baz"], } @@ -175,11 +175,11 @@ describe("Validation errors", () => { }) function testRequiredLargeSchema() { - var schema = {required: []}, - data = {}, + let schema = {required: []} + const data = {}, invalidData1 = {}, invalidData2 = {} - for (var i = 0; i < 100; i++) { + for (let i = 0; i < 100; i++) { schema.required.push("" + i) // properties from '0' to '99' are required data[i] = invalidData1[i] = invalidData2[i] = i } @@ -195,8 +195,8 @@ describe("Validation errors", () => { function test(extraErrors, schemaPathPrefix) { extraErrors = extraErrors || 0 - var schPath = (schemaPathPrefix || "#") + "/required" - var validate = ajv.compile(schema) + const schPath = (schemaPathPrefix || "#") + "/required" + const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("1"), { @@ -207,7 +207,7 @@ describe("Validation errors", () => { missingProperty: "2", }) - var validateJP = ajvJP.compile(schema) + const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("1"), { @@ -218,7 +218,7 @@ describe("Validation errors", () => { missingProperty: "2", }) - var fullValidate = fullAjv.compile(schema) + const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("1"), { @@ -239,7 +239,7 @@ describe("Validation errors", () => { }) function testRequiredAndProperties() { - var schema = { + const schema = { properties: { foo: {type: "number"}, bar: {type: "number"}, @@ -256,7 +256,7 @@ describe("Validation errors", () => { }) function testRequiredInAnyOf() { - var schema = { + const schema = { anyOf: [{required: ["foo", "bar", "baz"]}], } @@ -266,7 +266,7 @@ describe("Validation errors", () => { it("should not validate required twice in large schemas with loopRequired option", () => { ajv = new Ajv({loopRequired: 1, allErrors: true}) - var schema = { + const schema = { properties: { foo: {type: "integer"}, bar: {type: "integer"}, @@ -274,7 +274,7 @@ describe("Validation errors", () => { required: ["foo", "bar"], } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({}).should.equal(false) validate.errors.should.have.length(2) @@ -283,7 +283,7 @@ describe("Validation errors", () => { it("should not validate required twice with $data ref", () => { ajv = new Ajv({$data: true, allErrors: true}) - var schema = { + const schema = { properties: { foo: {type: "integer"}, bar: {type: "integer"}, @@ -291,7 +291,7 @@ describe("Validation errors", () => { required: {$data: "0/requiredProperties"}, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({requiredProperties: ["foo", "bar"]}).should.equal(false) validate.errors.should.have.length(2) @@ -343,33 +343,33 @@ describe("Validation errors", () => { }) function testDependencies() { - var schema = { + const schema = { dependencies: { a: ["foo", "bar", "baz"], }, } - var data = {a: 0, foo: 1, bar: 2, baz: 3}, + const data = {a: 0, foo: 1, bar: 2, baz: 3}, invalidData1 = {a: 0, foo: 1, baz: 3}, invalidData2 = {a: 0, bar: 2} - var msg = "should have properties foo, bar, baz when property a is present" + const msg = "should have properties foo, bar, baz when property a is present" - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params("bar")) shouldBeInvalid(validate, invalidData2) shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params("foo")) - var validateJP = ajvJP.compile(schema) + const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1) shouldBeError(validateJP.errors[0], "dependencies", "#/dependencies", "", msg, params("bar")) shouldBeInvalid(validateJP, invalidData2) shouldBeError(validateJP.errors[0], "dependencies", "#/dependencies", "", msg, params("foo")) - var fullValidate = fullAjv.compile(schema) + const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1) shouldBeError( @@ -399,7 +399,7 @@ describe("Validation errors", () => { ) function params(missing) { - var p = { + const p = { property: "a", deps: "foo, bar, baz", depsCount: 3, @@ -411,14 +411,14 @@ describe("Validation errors", () => { }) function _testRequired(schema, schemaPathPrefix, extraErrors) { - var schPath = (schemaPathPrefix || "#") + "/required" + const schPath = (schemaPathPrefix || "#") + "/required" extraErrors = extraErrors || 0 - var data = {foo: 1, bar: 2, baz: 3}, + const data = {foo: 1, bar: 2, baz: 3}, invalidData1 = {foo: 1, baz: 3}, invalidData2 = {bar: 2} - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("bar"), { @@ -429,7 +429,7 @@ describe("Validation errors", () => { missingProperty: "foo", }) - var validateJP = ajvJP.compile(schema) + const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("bar"), { @@ -440,7 +440,7 @@ describe("Validation errors", () => { missingProperty: "foo", }) - var fullValidate = fullAjv.compile(schema) + const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("bar"), { @@ -460,7 +460,7 @@ describe("Validation errors", () => { } it('"items" errors should include item index without quotes in dataPath (#48)', () => { - var schema1 = { + const schema1 = { $id: "schema1", type: "array", items: { @@ -469,25 +469,25 @@ describe("Validation errors", () => { }, } - var data = [10, 11, 12], + const data = [10, 11, 12], invalidData1 = [1, 10], invalidData2 = [10, 9, 11, 8, 12] - var validate = ajv.compile(schema1) + let validate = ajv.compile(schema1) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") shouldBeInvalid(validate, invalidData2) shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") - var validateJP = ajvJP.compile(schema1) + const validateJP = ajvJP.compile(schema1) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1) shouldBeError(validateJP.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") shouldBeInvalid(validateJP, invalidData2) shouldBeError(validateJP.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") - var fullValidate = fullAjv.compile(schema1) + const fullValidate = fullAjv.compile(schema1) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1) shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "[0]", "should be >= 10") @@ -495,7 +495,7 @@ describe("Validation errors", () => { shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "[1]", "should be >= 10") shouldBeError(fullValidate.errors[1], "minimum", "#/items/minimum", "[3]", "should be >= 10") - var schema2 = { + const schema2 = { $id: "schema2", type: "array", items: [{minimum: 10}, {minimum: 9}, {minimum: 12}], @@ -510,13 +510,13 @@ describe("Validation errors", () => { }) it("should have correct schema path for additionalItems", () => { - var schema = { + const schema = { type: "array", items: [{type: "integer"}, {type: "integer"}], additionalItems: false, } - var data = [1, 2], + const data = [1, 2], invalidData = [1, 2, 3] test(ajv) @@ -524,7 +524,7 @@ describe("Validation errors", () => { test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) shouldBeError( @@ -539,17 +539,17 @@ describe("Validation errors", () => { describe('"propertyNames" errors', () => { it("should add propertyName to errors", () => { - var schema = { + const schema = { type: "object", propertyNames: {pattern: "bar"}, } - var data = { + const data = { bar: {}, "bar.baz@email.example.com": {}, } - var invalidData = { + const invalidData = { bar: {}, "bar.baz@email.example.com": {}, foo: {}, @@ -561,7 +561,7 @@ describe("Validation errors", () => { test(fullAjv, 4) function test(_ajv, numErrors) { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData, numErrors) shouldBeError( @@ -600,7 +600,7 @@ describe("Validation errors", () => { describe("oneOf errors", () => { it("should have errors from inner schemas", () => { - var schema = { + const schema = { oneOf: [{type: "number"}, {type: "integer"}], } @@ -608,7 +608,7 @@ describe("Validation errors", () => { test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) validate("foo").should.equal(false) validate.errors.length.should.equal(3) validate(1).should.equal(false) @@ -618,7 +618,7 @@ describe("Validation errors", () => { }) it("should return passing schemas in error params", () => { - var schema = { + const schema = { oneOf: [{type: "number"}, {type: "integer"}, {const: 1.5}], } @@ -626,9 +626,9 @@ describe("Validation errors", () => { test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) validate(1).should.equal(false) - var err = validate.errors.pop() + let err = validate.errors.pop() err.keyword.should.equal("oneOf") err.params.should.eql({passingSchemas: [0, 1]}) @@ -649,7 +649,7 @@ describe("Validation errors", () => { describe("anyOf errors", () => { it("should have errors from inner schemas", () => { - var schema = { + const schema = { anyOf: [{type: "number"}, {type: "integer"}], } @@ -657,7 +657,7 @@ describe("Validation errors", () => { test(fullAjv) function test(_ajv) { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) validate("foo").should.equal(false) validate.errors.length.should.equal(3) validate(1).should.equal(true) @@ -677,12 +677,12 @@ describe("Validation errors", () => { }) function test(_ajv, numErrors) { - var schema = { + const schema = { type: "integer", minimum: 5, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, 5) shouldBeInvalid(validate, 5.5) shouldBeInvalid(validate, 4) @@ -701,13 +701,13 @@ describe("Validation errors", () => { }) function test(_ajv, numErrors) { - var schema = { + const schema = { type: "array", minItems: 2, minimum: 5, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, [1, 2]) shouldBeInvalid(validate, [1]) shouldBeInvalid(validate, 5) @@ -725,14 +725,14 @@ describe("Validation errors", () => { }) function test(_ajv, numErrors) { - var schema = { + const schema = { type: ["array", "object"], minItems: 2, minProperties: 2, minimum: 5, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, [1, 2]) shouldBeValid(validate, {foo: 1, bar: 2}) shouldBeInvalid(validate, [1]) @@ -745,14 +745,14 @@ describe("Validation errors", () => { describe("exclusiveMaximum/Minimum errors", () => { it("should include limits in error message", () => { - var schema = { + const schema = { type: "integer", exclusiveMinimum: 2, exclusiveMaximum: 5, } ;[ajv, fullAjv].forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, 3) shouldBeValid(validate, 4) @@ -769,14 +769,14 @@ describe("Validation errors", () => { }) function testError(keyword, message, params) { - var err = validate.errors[0] + const err = validate.errors[0] shouldBeError(err, keyword, "#/" + keyword, "", message, params) } }) }) it("should include limits in error message with $data", () => { - var schema = { + const schema = { properties: { smaller: { type: "number", @@ -795,7 +795,7 @@ describe("Validation errors", () => { logger: false, }) ;[ajv, fullAjv].forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, {smaller: 2, larger: 4}) shouldBeValid(validate, {smaller: 3, larger: 4}) @@ -806,7 +806,7 @@ describe("Validation errors", () => { testError() function testError() { - var err = validate.errors[0] + const err = validate.errors[0] shouldBeError( err, "exclusiveMaximum", @@ -821,10 +821,10 @@ describe("Validation errors", () => { }) describe("if/then/else errors", () => { - var validate, numErrors + let validate, numErrors it("if/then/else should include failing keyword in message and params", () => { - var schema = { + const schema = { if: {maximum: 10}, then: {multipleOf: 2}, else: {multipleOf: 5}, @@ -844,7 +844,7 @@ describe("Validation errors", () => { }) it("if/then should include failing keyword in message and params", () => { - var schema = { + const schema = { if: {maximum: 10}, then: {multipleOf: 2}, } @@ -861,7 +861,7 @@ describe("Validation errors", () => { }) it("if/else should include failing keyword in message and params", () => { - var schema = { + const schema = { if: {maximum: 10}, else: {multipleOf: 5}, } @@ -883,7 +883,7 @@ describe("Validation errors", () => { } function testIfError(ifClause, multipleOf) { - var err = validate.errors[0] + let err = validate.errors[0] shouldBeError( err, "multipleOf", @@ -904,13 +904,13 @@ describe("Validation errors", () => { describe("uniqueItems errors", () => { it("should not return uniqueItems error when non-unique items are of a different type than required", () => { - var schema = { + const schema = { items: {type: "number"}, uniqueItems: true, } ;[ajvJP, fullAjv].forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, [1, 2, 3]) shouldBeInvalid(validate, [1, 2, 2]) @@ -923,13 +923,13 @@ describe("Validation errors", () => { {i: 1, j: 2} ) - var expectedErrors = _ajv._opts.allErrors ? 2 : 1 + const expectedErrors = _ajv._opts.allErrors ? 2 : 1 shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors) testTypeError(0, _ajv._opts.jsPropertySyntax ? "[1]" : "/1") if (expectedErrors === 2) testTypeError(1, _ajv._opts.jsPropertySyntax ? "[2]" : "/2") function testTypeError(i, dataPath) { - var err = validate.errors[i] + const err = validate.errors[i] shouldBeError(err, "type", "#/items/type", dataPath, "should be number") } }) @@ -955,12 +955,12 @@ describe("Validation errors", () => { } function _testSchema1(_ajv, schema, schemaPathPrefix) { - var schPath = (schemaPathPrefix || "#/properties/foo") + "/type" + const schPath = (schemaPathPrefix || "#/properties/foo") + "/type" - var data = {foo: 1}, + const data = {foo: 1}, invalidData = {foo: "bar"} - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) shouldBeError( diff --git a/spec/extras.spec.js b/spec/extras.spec.js index 1b5c338d52..895a80c7b4 100644 --- a/spec/extras.spec.js +++ b/spec/extras.spec.js @@ -1,27 +1,19 @@ "use strict" -var jsonSchemaTest = require("json-schema-test"), +const jsonSchemaTest = require("json-schema-test"), getAjvInstances = require("./ajv_instances"), options = require("./ajv_options"), - suite = require("./browser_test_suite"), after = require("./after_test") -var instances = getAjvInstances(options, { +const instances = getAjvInstances(options, { $data: true, unknownFormats: ["allowedUnknown"], }) jsonSchemaTest(instances, { description: - "Extra keywords schemas tests of " + - instances.length + - " ajv instances with different options", - suites: { - extras: - typeof window == "object" - ? suite(require("./extras/{**/,}*.json", {mode: "list"})) - : "./extras/{**/,}*.json", - }, + "Extra keywords schemas tests of " + instances.length + " ajv instances with different options", + suites: {extras: require("./_json/extras")}, assert: require("./chai").assert, afterError: after.error, afterEach: after.each, diff --git a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js index f5a2d67495..12f76907b5 100644 --- a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js +++ b/spec/issues/1001_addKeyword_and_schema_without_id.spec.js @@ -1,17 +1,17 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #1001: addKeyword breaks schema without ID", () => { it("should allow using schemas without ID with addKeyword", () => { - var schema = { + const schema = { definitions: { foo: {}, }, } - var ajv = new Ajv() + const ajv = new Ajv() ajv.addSchema(schema) ajv.addKeyword("myKeyword") ajv.getSchema("#/definitions/foo").should.be.a("function") diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js index 438e8111c5..3553eac4ed 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.js @@ -1,6 +1,6 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #181, user-defined keyword is not validated in allErrors mode if there were previous error", () => { @@ -36,16 +36,16 @@ describe("issue #181, user-defined keyword is not validated in allErrors mode if }) function testKeywordErrors(def) { - var ajv = new Ajv({allErrors: true}) + const ajv = new Ajv({allErrors: true}) ajv.addKeyword(def) - var schema = { + const schema = { required: ["foo"], alwaysFails: true, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({foo: 1}).should.equal(false) validate.errors.should.have.length(1) diff --git a/spec/issues/182_nan_validation.spec.js b/spec/issues/182_nan_validation.spec.js index 0b68438094..cd95f7a299 100644 --- a/spec/issues/182_nan_validation.spec.js +++ b/spec/issues/182_nan_validation.spec.js @@ -1,6 +1,6 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #182, NaN validation", () => { diff --git a/spec/issues/204_options_schemas_data_together.spec.js b/spec/issues/204_options_schemas_data_together.spec.js index 54eea554bc..15ab14a34b 100644 --- a/spec/issues/204_options_schemas_data_together.spec.js +++ b/spec/issues/204_options_schemas_data_together.spec.js @@ -1,17 +1,17 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #204, options schemas and $data used together", () => { it("should use v5 metaschemas by default", () => { - var ajv = new Ajv({ + const ajv = new Ajv({ schemas: [{$id: "str", type: "string"}], $data: true, }) - var schema = {const: 42} - var validate = ajv.compile(schema) + const schema = {const: 42} + const validate = ajv.compile(schema) validate(42).should.equal(true) validate(43).should.equal(false) diff --git a/spec/issues/210_mutual_recur_frags.spec.js b/spec/issues/210_mutual_recur_frags.spec.js index a10ac80c8f..b7377e8fa6 100644 --- a/spec/issues/210_mutual_recur_frags.spec.js +++ b/spec/issues/210_mutual_recur_frags.spec.js @@ -1,11 +1,11 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #210, mutual recursive $refs that are schema fragments", () => { it("should compile and validate schema when one ref is fragment", () => { - var ajv = new Ajv() + const ajv = new Ajv() ajv.addSchema({ $id: "foo", @@ -29,14 +29,14 @@ describe("issue #210, mutual recursive $refs that are schema fragments", () => { }, }) - var validate = ajv.compile({$ref: "foo#/definitions/bar"}) + const validate = ajv.compile({$ref: "foo#/definitions/bar"}) validate({baz: {quux: {baz: 42}}}).should.equal(true) validate({baz: {quux: {baz: "foo"}}}).should.equal(false) }) it("should compile and validate schema when both refs are fragments", () => { - var ajv = new Ajv() + const ajv = new Ajv() ajv.addSchema({ $id: "foo", @@ -64,7 +64,7 @@ describe("issue #210, mutual recursive $refs that are schema fragments", () => { }, }) - var validate = ajv.compile({$ref: "foo#/definitions/bar"}) + const validate = ajv.compile({$ref: "foo#/definitions/bar"}) validate({baz: {quux: {baz: 42}}}).should.equal(true) validate({baz: {quux: {baz: "foo"}}}).should.equal(false) diff --git a/spec/issues/240_mutual_recur_frags_common_ref.spec.js b/spec/issues/240_mutual_recur_frags_common_ref.spec.js index 69cf8f755c..372b0c33f2 100644 --- a/spec/issues/240_mutual_recur_frags_common_ref.spec.js +++ b/spec/issues/240_mutual_recur_frags_common_ref.spec.js @@ -1,10 +1,10 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #240, mutually recursive fragment refs reference a common schema", () => { - var apiSchema = { + const apiSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://api.schema#", $defs: { @@ -24,7 +24,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema }, } - var domainSchema = { + const domainSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://domain.schema#", properties: { @@ -38,9 +38,9 @@ describe("issue #240, mutually recursive fragment refs reference a common schema } it("should compile and validate schema when one ref is fragment", () => { - var ajv = new Ajv() + const ajv = new Ajv() - var librarySchema = { + const librarySchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://library.schema#", properties: { @@ -74,7 +74,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema }, } - var catalogItemSchema = { + const catalogItemSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://catalog_item.schema#", properties: { @@ -103,7 +103,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema }, } - var catalogItemResourceIdentifierSchema = { + const catalogItemResourceIdentifierSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://catalog_item_resource_identifier.schema#", allOf: [ @@ -126,14 +126,14 @@ describe("issue #240, mutually recursive fragment refs reference a common schema ajv.addSchema(catalogItemResourceIdentifierSchema) ajv.addSchema(apiSchema) - var validate = ajv.compile(domainSchema) + const validate = ajv.compile(domainSchema) testSchema(validate) }) it("should compile and validate schema when both refs are fragments", () => { - var ajv = new Ajv() + const ajv = new Ajv() - var librarySchema = { + const librarySchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://library.schema#", properties: { @@ -165,7 +165,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema }, } - var catalogItemSchema = { + const catalogItemSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://catalog_item.schema#", properties: { @@ -198,7 +198,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema ajv.addSchema(catalogItemSchema) ajv.addSchema(apiSchema) - var validate = ajv.compile(domainSchema) + const validate = ajv.compile(domainSchema) testSchema(validate) }) diff --git a/spec/issues/259_validate_meta_against_itself.spec.js b/spec/issues/259_validate_meta_against_itself.spec.js index 0ecdd6a082..cd891340c7 100644 --- a/spec/issues/259_validate_meta_against_itself.spec.js +++ b/spec/issues/259_validate_meta_against_itself.spec.js @@ -1,12 +1,12 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #259, support validating [meta-]schemas against themselves", () => { it('should add schema before validation if "id" is the same as "$schema"', () => { - var ajv = new Ajv({strict: false}) - var hyperSchema = require("../remotes/hyper-schema.json") + const ajv = new Ajv({strict: false}) + const hyperSchema = require("../remotes/hyper-schema.json") ajv.addMetaSchema(hyperSchema) }) }) diff --git a/spec/issues/273_error_schemaPath_refd_schema.spec.js b/spec/issues/273_error_schemaPath_refd_schema.spec.js index d8f4825dc1..78260b8dcc 100644 --- a/spec/issues/273_error_schemaPath_refd_schema.spec.js +++ b/spec/issues/273_error_schemaPath_refd_schema.spec.js @@ -1,6 +1,6 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe.skip("issue #273, schemaPath in error in referenced schema", () => { @@ -9,19 +9,19 @@ describe.skip("issue #273, schemaPath in error in referenced schema", () => { test(new Ajv({inlineRefs: false})) function test(ajv) { - var schema = { + const schema = { properties: { a: {$ref: "int"}, }, } - var referencedSchema = { + const referencedSchema = { id: "int", type: "integer", } ajv.addSchema(referencedSchema) - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate({a: "foo"}).should.equal(false) validate.errors[0].schemaPath.should.equal("int#/type") diff --git a/spec/issues/342_uniqueItems_non-json_objects.spec.js b/spec/issues/342_uniqueItems_non-json_objects.spec.js index 005e43de8f..e1d8f7a218 100644 --- a/spec/issues/342_uniqueItems_non-json_objects.spec.js +++ b/spec/issues/342_uniqueItems_non-json_objects.spec.js @@ -1,13 +1,13 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #342, support uniqueItems with some non-JSON objects", () => { - var validate + let validate before(() => { - var ajv = new Ajv() + const ajv = new Ajv() validate = ajv.compile({uniqueItems: true}) }) @@ -18,12 +18,8 @@ describe("issue #342, support uniqueItems with some non-JSON objects", () => { }) it("should allow different Dates", () => { - validate([new Date("2016-11-11"), new Date("2016-11-12")]).should.equal( - true - ) - validate([new Date("2016-11-11"), new Date("2016-11-11")]).should.equal( - false - ) + validate([new Date("2016-11-11"), new Date("2016-11-12")]).should.equal(true) + validate([new Date("2016-11-11"), new Date("2016-11-11")]).should.equal(false) validate([new Date("2016-11-11"), {}]).should.equal(true) }) diff --git a/spec/issues/485_type_validation_priority.spec.js b/spec/issues/485_type_validation_priority.spec.js index 331dc140c7..f664598bd4 100644 --- a/spec/issues/485_type_validation_priority.spec.js +++ b/spec/issues/485_type_validation_priority.spec.js @@ -1,12 +1,12 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #485, order of type validation", () => { it("should validate types before keywords", () => { - var ajv = new Ajv({allErrors: true}) - var validate = ajv.compile({ + const ajv = new Ajv({allErrors: true}) + const validate = ajv.compile({ type: ["integer", "string"], required: ["foo"], minimum: 2, @@ -23,9 +23,7 @@ describe("issue #485, order of type validation", () => { function checkErrors(expectedErrs) { validate.errors.should.have.length(expectedErrs.length) - expectedErrs.forEach((keyword, i) => - validate.errors[i].keyword.should.equal(keyword) - ) + expectedErrs.forEach((keyword, i) => validate.errors[i].keyword.should.equal(keyword)) } }) }) diff --git a/spec/issues/50_refs_with_definitions.spec.js b/spec/issues/50_refs_with_definitions.spec.js index 323af8f396..de3ecc1c67 100644 --- a/spec/issues/50_refs_with_definitions.spec.js +++ b/spec/issues/50_refs_with_definitions.spec.js @@ -1,7 +1,7 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe('issue #50: references with "definitions"', () => { it("should be supported by addSchema", spec("addSchema")) @@ -10,9 +10,7 @@ describe('issue #50: references with "definitions"', () => { function spec(method) { return () => { - var result - - var ajv = new Ajv() + const ajv = new Ajv() ajv[method]({ $id: "http://example.com/test/person.json#", @@ -34,7 +32,7 @@ describe('issue #50: references with "definitions"', () => { }, }) - result = ajv.validate("http://example.com/test/employee.json#", { + const result = ajv.validate("http://example.com/test/employee.json#", { person: { name: "Alice", }, diff --git a/spec/issues/521_wrong_warning_id_property.spec.js b/spec/issues/521_wrong_warning_id_property.spec.js index 3ebde21835..c52dfc7901 100644 --- a/spec/issues/521_wrong_warning_id_property.spec.js +++ b/spec/issues/521_wrong_warning_id_property.spec.js @@ -1,12 +1,12 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe('issue #521, incorrect warning with "id" property', () => { it("should not log warning", () => { - var ajv = new Ajv() - var consoleWarn = console.warn + const ajv = new Ajv() + const consoleWarn = console.warn console.warn = () => { throw new Error("should not log warning") } diff --git a/spec/issues/533_missing_ref_error_when_ignore.spec.js b/spec/issues/533_missing_ref_error_when_ignore.spec.js index 6d2ecd1754..b319705d85 100644 --- a/spec/issues/533_missing_ref_error_when_ignore.spec.js +++ b/spec/issues/533_missing_ref_error_when_ignore.spec.js @@ -1,10 +1,10 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', () => { - var schema = { + const schema = { type: "object", properties: { foo: {$ref: "#/definitions/missing"}, @@ -13,14 +13,14 @@ describe('issue #533, throwing missing ref exception with option missingRefs: "i } it("should pass validation without throwing exception", () => { - var ajv = new Ajv({missingRefs: "ignore", logger: false}) - var validate = ajv.compile(schema) + const ajv = new Ajv({missingRefs: "ignore", logger: false}) + const validate = ajv.compile(schema) validate({foo: "anything"}).should.equal(true) validate({foo: "anything", bar: "whatever"}).should.equal(true) }) it("should throw exception during schema compilation with option missingRefs: true", () => { - var ajv = new Ajv() + const ajv = new Ajv() should.throw(() => { ajv.compile(schema) }) diff --git a/spec/issues/743_removeAdditional_to_remove_proto.spec.js b/spec/issues/743_removeAdditional_to_remove_proto.spec.js index 9de9372303..10421292e9 100644 --- a/spec/issues/743_removeAdditional_to_remove_proto.spec.js +++ b/spec/issues/743_removeAdditional_to_remove_proto.spec.js @@ -1,13 +1,13 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #743, property __proto__ should be removed with removeAdditional option", () => { it("should remove additional properties", () => { - var ajv = new Ajv({removeAdditional: true}) + const ajv = new Ajv({removeAdditional: true}) - var schema = { + const schema = { properties: { obj: { additionalProperties: false, @@ -26,13 +26,13 @@ describe("issue #743, property __proto__ should be removed with removeAdditional }, } - var obj = Object.create(null) + const obj = Object.create(null) obj.__proto__ = null // should be removed obj.additional = "will be removed" obj.a = "valid" obj.b = "valid" - var data = {obj: obj} + const data = {obj: obj} ajv.validate(schema, data).should.equal(true) Object.keys(data.obj).should.eql(["a", "b"]) diff --git a/spec/issues/768_passContext_recursive_ref.spec.js b/spec/issues/768_passContext_recursive_ref.spec.js index 06907219e8..8dac93ff13 100644 --- a/spec/issues/768_passContext_recursive_ref.spec.js +++ b/spec/issues/768_passContext_recursive_ref.spec.js @@ -1,10 +1,10 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #768, fix passContext in recursive $ref", () => { - var ajv, contexts + let ajv, contexts beforeEach(() => { contexts = [] @@ -12,8 +12,8 @@ describe("issue #768, fix passContext in recursive $ref", () => { describe("passContext = true", () => { it("should pass this value as context to user-defined keyword validation function", () => { - var validate = getValidate(true) - var self = {} + const validate = getValidate(true) + const self = {} validate.call(self, {bar: "a", baz: {bar: "b"}}) contexts.should.have.length(2) contexts.forEach((ctx) => ctx.should.equal(self)) @@ -22,7 +22,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { describe("passContext = false", () => { it("should pass ajv instance as context to user-defined keyword validation function", () => { - var validate = getValidate(false) + const validate = getValidate(false) validate({bar: "a", baz: {bar: "b"}}) contexts.should.have.length(2) contexts.forEach((ctx) => ctx.should.equal(ajv)) @@ -31,8 +31,8 @@ describe("issue #768, fix passContext in recursive $ref", () => { describe("ref is fragment and passContext = true", () => { it("should pass this value as context to user-defined keyword validation function", () => { - var validate = getValidateFragments(true) - var self = {} + const validate = getValidateFragments(true) + const self = {} validate.call(self, {baz: {corge: "a", quux: {baz: {corge: "b"}}}}) contexts.should.have.length(2) contexts.forEach((ctx) => ctx.should.equal(self)) @@ -41,7 +41,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { describe("ref is fragment and passContext = false", () => { it("should pass ajv instance as context to user-defined keyword validation function", () => { - var validate = getValidateFragments(false) + const validate = getValidateFragments(false) validate({baz: {corge: "a", quux: {baz: {corge: "b"}}}}) contexts.should.have.length(2) contexts.forEach((ctx) => ctx.should.equal(ajv)) @@ -52,7 +52,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { ajv = new Ajv({passContext: passContext}) ajv.addKeyword({keyword: "testValidate", validate: storeContext}) - var schema = { + const schema = { $id: "foo", type: "object", required: ["bar"], diff --git a/spec/issues/8_shared_refs.spec.js b/spec/issues/8_shared_refs.spec.js index fe24eaa97a..79af369f84 100644 --- a/spec/issues/8_shared_refs.spec.js +++ b/spec/issues/8_shared_refs.spec.js @@ -1,6 +1,6 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #8: schema with shared references", () => { @@ -10,14 +10,14 @@ describe("issue #8: schema with shared references", () => { function spec(method) { return () => { - var ajv = new Ajv() + const ajv = new Ajv() - var propertySchema = { + const propertySchema = { type: "string", maxLength: 4, } - var schema = { + const schema = { $id: "obj.json#", type: "object", properties: { @@ -28,7 +28,7 @@ describe("issue #8: schema with shared references", () => { ajv[method](schema) - var result = ajv.validate("obj.json#", {foo: "abc", bar: "def"}) + let result = ajv.validate("obj.json#", {foo: "abc", bar: "def"}) result.should.equal(true) result = ajv.validate("obj.json#", {foo: "abcde", bar: "fghg"}) diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.js index 669eb9712a..dda3450ed9 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.js @@ -1,11 +1,11 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("issue #955: option removeAdditional breaks user-defined keywords", () => { it("should support user-defined keywords with option removeAdditional", () => { - var ajv = new Ajv({removeAdditional: "all"}) + const ajv = new Ajv({removeAdditional: "all"}) ajv.addKeyword({ keyword: "minTrimmedLength", @@ -18,7 +18,7 @@ describe("issue #955: option removeAdditional breaks user-defined keywords", () metaSchema: {type: "integer"}, }) - var schema = { + const schema = { type: "object", properties: { foo: { @@ -29,9 +29,9 @@ describe("issue #955: option removeAdditional breaks user-defined keywords", () required: ["foo"], } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) - var data = { + let data = { foo: " bar ", baz: "", } diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 2724869bb6..854f64ba00 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -1,70 +1,47 @@ "use strict" -var jsonSchemaTest = require("json-schema-test"), +const jsonSchemaTest = require("json-schema-test"), getAjvInstances = require("./ajv_instances"), options = require("./ajv_options"), - suite = require("./browser_test_suite"), after = require("./after_test") -var remoteRefs = { +const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), "http://localhost:1234/subSchemas.json": require("./JSON-Schema-Test-Suite/remotes/subSchemas.json"), "http://localhost:1234/folder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json"), "http://localhost:1234/name.json": require("./JSON-Schema-Test-Suite/remotes/name.json"), } -var SKIP = { +const SKIP = { 6: [ "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here "optional/format", - "format", ], 7: [ "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here "optional/content", - "format/date", - "format/date-time", - "format/email", - "format/hostname", - "format/idn-email", - "format/idn-hostname", - "format/ipv4", - "format/ipv6", - "format/iri", - "format/iri-reference", - "format/json-pointer", - "format/regex", - "format/relative-json-pointer", - "format/time", - "format/uri", - "format/uri-reference", - "format/uri-template", + "optional/format/date", + "optional/format/date-time", + "optional/format/email", + "optional/format/hostname", + "optional/format/idn-email", + "optional/format/idn-hostname", + "optional/format/ipv4", + "optional/format/ipv6", + "optional/format/iri", + "optional/format/iri-reference", + "optional/format/json-pointer", + "optional/format/regex", + "optional/format/relative-json-pointer", + "optional/format/time", + "optional/format/uri", + "optional/format/uri-reference", + "optional/format/uri-template", ], } -runTest( - getAjvInstances(options, {meta: false, strict: false}), - 6, - typeof window == "object" - ? suite( - require("./JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json", { - mode: "list", - }) - ) - : "./JSON-Schema-Test-Suite/tests/draft6/{**/,}*.json" -) - -runTest( - getAjvInstances(options, {strict: false}), - 7, - typeof window == "object" - ? suite( - require("./JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json", { - mode: "list", - }) - ) - : "./JSON-Schema-Test-Suite/tests/draft7/{**/,}*.json" -) +runTest(getAjvInstances(options, {meta: false, strict: false}), 6, require("./_json/draft6")) +runTest(getAjvInstances(options, {strict: false}), 7, require("./_json/draft7")) function runTest(instances, draft, tests) { instances.forEach((ajv) => { @@ -74,7 +51,7 @@ function runTest(instances, draft, tests) { ajv._opts.defaultMeta = "http://json-schema.org/draft-06/schema#" break } - for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id) + for (const id in remoteRefs) ajv.addSchema(remoteRefs[id], id) }) jsonSchemaTest(instances, { @@ -84,7 +61,7 @@ function runTest(instances, draft, tests) { ": " + instances.length + " ajv instances with different options", - suites: {tests: tests}, + suites: {tests}, only: [], skip: SKIP[draft], assert: require("./chai").assert, diff --git a/spec/keyword.spec.js b/spec/keyword.spec.ts similarity index 90% rename from spec/keyword.spec.js rename to spec/keyword.spec.ts index 62477aa191..6c0386d7c4 100644 --- a/spec/keyword.spec.js +++ b/spec/keyword.spec.ts @@ -1,15 +1,15 @@ -"use strict" +import {ErrorObject, SchemaObject, SchemaValidateFunction} from "../lib/types" +// currently most tests include compiled code, if any code re-compiled locally, instanceof would fail +import {_, nil} from "../dist/compile/codegen" -var getAjvInstances = require("./ajv_instances"), +const getAjvInstances = require("./ajv_instances"), should = require("./chai").should(), equal = require("../dist/compile/equal") -const Ajv = require("..") -const codegen = require("../dist/compile/codegen") -const {_, nil} = codegen +const Ajv = require("./ajv") describe("User-defined keywords", () => { - var ajv, instances + let ajv, instances beforeEach(() => { instances = getAjvInstances({ @@ -87,22 +87,23 @@ describe("User-defined keywords", () => { }) it('should allow defining errors for "validate" keyword', () => { + const validateRange: SchemaValidateFunction = _validateRange testRangeKeyword({keyword: "x-range", type: "number", validate: validateRange}, true) - function validateRange(schema, data, parentSchema) { + function _validateRange(schema, data, parentSchema) { validateRangeSchema(schema, parentSchema) - var min = schema[0], + const min = schema[0], max = schema[1], exclusive = parentSchema.exclusiveRange === true - var minOk = exclusive ? data > min : data >= min - var maxOk = exclusive ? data < max : data <= max - var valid = minOk && maxOk + const minOk = exclusive ? data > min : data >= min + const maxOk = exclusive ? data < max : data <= max + const valid = minOk && maxOk if (!valid) { - var err = {keyword: "x-range"} + const err: Partial = {keyword: "x-range"} validateRange.errors = [err] - var comparison, limit + let comparison, limit if (minOk) { comparison = exclusive ? "<" : "<=" limit = max @@ -223,8 +224,8 @@ describe("User-defined keywords", () => { function compileRange(schema, parentSchema) { validateRangeSchema(schema, parentSchema) - var min = schema[0] - var max = schema[1] + const min = schema[0] + const max = schema[1] return parentSchema.exclusiveRange === true ? (data) => data > min && data < max @@ -257,10 +258,10 @@ describe("User-defined keywords", () => { instances.forEach((_ajv) => { _ajv.addKeyword({ keyword: "macroRef", - macro: function (schema, parentSchema, it) { + macro: function (schema, _parentSchema, it) { it.baseId.should.equal("#") - var ref = schema.$ref - var validate = _ajv.getSchema(ref) + const ref = schema.$ref + const validate = _ajv.getSchema(ref) if (validate) return validate.schema throw new ajv.constructor.MissingRefError(it.baseId, ref) }, @@ -275,7 +276,7 @@ describe("User-defined keywords", () => { }, }, }) - var schema = { + const schema = { macroRef: { $ref: "#/definitions/schema", }, @@ -285,10 +286,7 @@ describe("User-defined keywords", () => { }, }, } - var validate - ;(function compileMacroRef() { - validate = _ajv.compile(schema) - }.should.not.throw()) + const validate = _ajv.compile(schema) shouldBeValid(validate, "foo") shouldBeInvalid(validate, 1, 2) }) @@ -303,7 +301,7 @@ describe("User-defined keywords", () => { }) _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) - var schema = { + const schema = { deepProperties: { "a.b.c": {type: "number", range: [2, 4]}, "d.e.f.g": {type: "string"}, @@ -355,7 +353,7 @@ describe("User-defined keywords", () => { } */ - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, { a: {b: {c: 3}}, @@ -394,15 +392,15 @@ describe("User-defined keywords", () => { throw new Error("schema of deepProperty should be an object") } - var expanded = [] + const expanded: any[] = [] - for (var prop in _schema) { - var path = prop.split(".") - var properties = {} + for (const prop in _schema) { + const path = prop.split(".") + const properties = {} if (path.length === 1) { properties[prop] = _schema[prop] } else { - var deepProperties = {} + const deepProperties = {} deepProperties[path.slice(1).join(".")] = _schema[prop] properties[path[0]] = {deepProperties: deepProperties} } @@ -419,13 +417,13 @@ describe("User-defined keywords", () => { _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) _ajv.addKeyword({keyword: "even", type: "number", macro: macroEven}) - var schema = { + const schema = { range: [4, 6], even: true, } - var validate = _ajv.compile(schema) - var numErrors = _ajv._opts.allErrors ? 4 : 2 + const validate = _ajv.compile(schema) + const numErrors = _ajv._opts.allErrors ? 4 : 2 shouldBeInvalid(validate, 2, 2) shouldBeInvalid(validate, 3, numErrors) @@ -441,12 +439,12 @@ describe("User-defined keywords", () => { instances.forEach((_ajv) => { _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) - var schema = { + const schema = { range: [1, 4], minimum: 2.5, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, 3) shouldBeInvalid(validate, 2) @@ -457,11 +455,11 @@ describe("User-defined keywords", () => { instances.forEach((_ajv) => { _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) - var schema = { + const schema = { allOf: [{range: [4, 8]}, {range: [2, 6]}], } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeInvalid(validate, 2, 2) shouldBeInvalid(validate, 3, 2) @@ -479,7 +477,7 @@ describe("User-defined keywords", () => { _ajv.addKeyword({keyword: "exclusiveRange", metaSchema: {type: "boolean"}}) _ajv.addKeyword({keyword: "myContains", type: "array", macro: macroContains}) - var schema = { + const schema = { myContains: { type: "number", range: [4, 7], @@ -487,7 +485,7 @@ describe("User-defined keywords", () => { }, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeInvalid(validate, [1, 2, 3], 2) shouldBeInvalid(validate, [2, 3, 4], 2) @@ -504,7 +502,7 @@ describe("User-defined keywords", () => { it("should throw exception if macro expansion is an invalid schema", () => { ajv.addKeyword({keyword: "invalid", macro: macroInvalid}) - var schema = {invalid: true} + const schema = {invalid: true} should.throw(() => { ajv.compile(schema) @@ -527,7 +525,7 @@ describe("User-defined keywords", () => { function macroRange(schema, parentSchema) { validateRangeSchema(schema, parentSchema) - var exclusive = !!parentSchema.exclusiveRange + const exclusive = !!parentSchema.exclusiveRange return exclusive ? {exclusiveMinimum: schema[0], exclusiveMaximum: schema[1]} @@ -622,7 +620,7 @@ describe("User-defined keywords", () => { }) it('should validate rule with "compile" and "validate" funcs', () => { - var compileCalled + let compileCalled testEvenKeyword$data({ keyword: "x-even-$data", type: "number", @@ -654,7 +652,7 @@ describe("User-defined keywords", () => { }) it('should validate with "compile" and "validate" funcs with meta-schema', () => { - var compileCalled + let compileCalled testEvenKeyword$data({ keyword: "x-even-$data", type: "number", @@ -684,7 +682,7 @@ describe("User-defined keywords", () => { }) it('should validate rule with "macro" and "validate" funcs', () => { - var macroCalled + let macroCalled testEvenKeyword$data( { keyword: "x-even-$data", @@ -711,7 +709,7 @@ describe("User-defined keywords", () => { }) it('should validate with "macro" and "validate" funcs with meta-schema', () => { - var macroCalled + let macroCalled testEvenKeyword$data( { keyword: "x-even-$data", @@ -730,7 +728,7 @@ describe("User-defined keywords", () => { return data % 2 ? !schema : schema } - function macroEven(schema) { + function macroEven(schema): SchemaObject | void { macroCalled = true if (schema === true) return {multipleOf: 2} if (schema === false) return {not: {multipleOf: 2}} @@ -793,11 +791,11 @@ describe("User-defined keywords", () => { }) }) - function testEvenKeyword(evenDefinition, numErrors) { + function testEvenKeyword(evenDefinition, numErrors = 1) { instances.forEach((_ajv) => { _ajv.addKeyword(evenDefinition) - var schema = {"x-even": true} - var validate = _ajv.compile(schema) + const schema = {"x-even": true} + const validate = _ajv.compile(schema) shouldBeValid(validate, 2) shouldBeValid(validate, "abc") @@ -806,12 +804,12 @@ describe("User-defined keywords", () => { }) } - function testEvenKeyword$data(definition, numErrors) { + function testEvenKeyword$data(definition, numErrors = 1) { instances.forEach((_ajv) => { _ajv.addKeyword(definition) - var schema = {"x-even-$data": true} - var validate = _ajv.compile(schema) + let schema: any = {"x-even-$data": true} + let validate = _ajv.compile(schema) shouldBeValid(validate, 2) shouldBeValid(validate, "abc") @@ -843,12 +841,12 @@ describe("User-defined keywords", () => { }) } - function testConstantKeyword(definition, numErrors) { + function testConstantKeyword(definition, numErrors?: number) { instances.forEach((_ajv) => { _ajv.addKeyword(definition) - var schema = {myConstant: "abc"} - var validate = _ajv.compile(schema) + const schema = {myConstant: "abc"} + const validate = _ajv.compile(schema) shouldBeValid(validate, "abc") shouldBeInvalid(validate, 2, numErrors) @@ -856,11 +854,11 @@ describe("User-defined keywords", () => { }) } - function testMultipleConstantKeyword(definition, numErrors) { + function testMultipleConstantKeyword(definition, numErrors?: number) { instances.forEach((_ajv) => { _ajv.addKeyword(definition) - var schema = { + const schema = { properties: { a: {"x-constant": 1}, b: {"x-constant": 1}, @@ -868,7 +866,7 @@ describe("User-defined keywords", () => { additionalProperties: {"x-constant": {foo: "bar"}}, items: {"x-constant": {foo: "bar"}}, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, {a: 1, b: 1}) shouldBeInvalid(validate, {a: 2, b: 1}, numErrors) @@ -883,13 +881,13 @@ describe("User-defined keywords", () => { }) } - function testRangeKeyword(definition, createsErrors, numErrors) { + function testRangeKeyword(definition, createsErrors?: boolean, numErrors?: number) { instances.forEach((_ajv) => { _ajv.addKeyword(definition) _ajv.addKeyword({keyword: "exclusiveRange", schemaType: "boolean"}) - var schema = {"x-range": [2, 4]} - var validate = _ajv.compile(schema) + let schema: SchemaObject = {"x-range": [2, 4]} + let validate = _ajv.compile(schema) shouldBeValid(validate, 2) shouldBeValid(validate, 3) @@ -930,12 +928,12 @@ describe("User-defined keywords", () => { }) } - function testMultipleRangeKeyword(definition, numErrors) { + function testMultipleRangeKeyword(definition, numErrors?: number) { instances.forEach((_ajv) => { _ajv.addKeyword(definition) _ajv.addKeyword({keyword: "exclusiveRange", schemaType: "boolean"}) - var schema = { + const schema = { properties: { a: {"x-range": [2, 4], exclusiveRange: true}, b: {"x-range": [2, 4], exclusiveRange: false}, @@ -943,7 +941,7 @@ describe("User-defined keywords", () => { additionalProperties: {"x-range": [5, 7]}, items: {"x-range": [5, 7]}, } - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) shouldBeValid(validate, {a: 3.99, b: 4}) shouldBeInvalid(validate, {a: 4, b: 4}, numErrors) @@ -956,7 +954,7 @@ describe("User-defined keywords", () => { }) } - function shouldBeRangeError(error, dataPath, schemaPath, comparison, limit, exclusive) { + function shouldBeRangeError(error, dataPath, schemaPath, comparison, limit, exclusive?: boolean) { delete error.schema delete error.data error.should.eql({ @@ -973,7 +971,7 @@ describe("User-defined keywords", () => { } function validateRangeSchema(schema, parentSchema) { - var schemaValid = + const schemaValid = Array.isArray(schema) && schema.length === 2 && typeof schema[0] == "number" && @@ -982,7 +980,7 @@ describe("User-defined keywords", () => { throw new Error("Invalid schema for range keyword, should be array of 2 numbers") } - var exclusiveRangeSchemaValid = + const exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined || typeof parentSchema.exclusiveRange == "boolean" if (!exclusiveRangeSchemaValid) { throw new Error("Invalid schema for exclusiveRange keyword, should be boolean") @@ -994,9 +992,9 @@ describe("User-defined keywords", () => { should.not.exist(validate.errors) } - function shouldBeInvalid(validate, data, numErrors) { + function shouldBeInvalid(validate, data, numErrors = 1) { validate(data).should.equal(false) - validate.errors.should.have.length(numErrors || 1) + validate.errors.should.have.length(numErrors) } function shouldBeInvalidSchema(schema) { @@ -1008,7 +1006,7 @@ describe("User-defined keywords", () => { } describe("addKeyword method", () => { - var TEST_TYPES = [undefined, "number", "string", "boolean", ["number", "string"]] + const TEST_TYPES = [undefined, "number", "string", "boolean", ["number", "string"]] it("should throw if defined keyword is passed", () => { testThrow(["minimum", "maximum", "multipleOf", "minLength", "maxLength"]) @@ -1059,7 +1057,7 @@ describe("User-defined keywords", () => { }) it("should return instance of itself", () => { - var res = ajv.addKeyword("any") + const res = ajv.addKeyword("any") res.should.equal(ajv) }) @@ -1097,7 +1095,7 @@ describe("User-defined keywords", () => { // TODO change to account for pre-defined keywords with definitions it("should return keyword definition", () => { - var definition = { + const definition = { keyword: "mykeyword", validate: () => true, } @@ -1117,9 +1115,9 @@ describe("User-defined keywords", () => { validate: (_schema, data) => data > 0, }) - var schema = {positive: true} + const schema = {positive: true} - var validate = ajv.compile(schema) + let validate = ajv.compile(schema) validate(0).should.equal(false) validate(1).should.equal(true) @@ -1127,7 +1125,7 @@ describe("User-defined keywords", () => { ajv.addKeyword({ keyword: "positive", type: "number", - validate: function (sch, data) { + validate: function (_sch, data) { return data >= 0 }, }) @@ -1142,7 +1140,7 @@ describe("User-defined keywords", () => { ajv.addKeyword({ keyword: "positive", type: "number", - validate: function (sch, data) { + validate: function (_sch, data) { return data >= 0 }, }) @@ -1156,8 +1154,8 @@ describe("User-defined keywords", () => { it("should remove and allow redefining standard keyword", () => { ajv = new Ajv({strict: false}) - var schema = {minimum: 1} - var validate = ajv.compile(schema) + const schema = {minimum: 1} + let validate = ajv.compile(schema) validate(0).should.equal(false) validate(1).should.equal(true) validate(2).should.equal(true) @@ -1185,7 +1183,7 @@ describe("User-defined keywords", () => { }) it("should return instance of itself", () => { - var res = ajv.addKeyword("any").removeKeyword("any") + const res = ajv.addKeyword("any").removeKeyword("any") res.should.equal(ajv) }) }) @@ -1202,8 +1200,8 @@ describe("User-defined keywords", () => { }) function testModifying(withOption) { - var collectionFormat = { - csv: function (data, dataPath, parentData, parentDataProperty) { + const collectionFormat = { + csv: function (data, _dataPath, parentData, parentDataProperty) { parentData[parentDataProperty] = data.split(",") return true }, @@ -1221,7 +1219,7 @@ describe("User-defined keywords", () => { }, }) - var validate = ajv.compile({ + const validate = ajv.compile({ type: "object", properties: { foo: { @@ -1237,7 +1235,7 @@ describe("User-defined keywords", () => { additionalProperties: false, }) - var obj = {foo: "bar,baz,quux"} + const obj: any = {foo: "bar,baz,quux"} validate(obj).should.equal(true) obj.should.eql({foo: ["bar", "baz", "quux"]}) @@ -1281,14 +1279,14 @@ describe("User-defined keywords", () => { ajv.compile(invalidSchema) }) - var schema = { + const schema = { properties: { foo: true, }, allRequired: true, } - var v = ajv.compile(schema) + const v = ajv.compile(schema) v({foo: 1}).should.equal(true) v({}).should.equal(false) }) diff --git a/spec/options/comment.spec.js b/spec/options/comment.spec.js index b1987bc699..a6e6e11583 100644 --- a/spec/options/comment.spec.js +++ b/spec/options/comment.spec.js @@ -1,11 +1,11 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("$comment option", () => { describe("= true", () => { - var logCalls, consoleLog + let logCalls, consoleLog beforeEach(() => { consoleLog = console.log @@ -21,7 +21,7 @@ describe("$comment option", () => { } it("should log the text from $comment keyword", () => { - var schema = { + const schema = { $comment: "object root", properties: { foo: {$comment: "property foo"}, @@ -29,11 +29,11 @@ describe("$comment option", () => { }, } - var ajv = new Ajv({$comment: true}) - var fullAjv = new Ajv({allErrors: true, $comment: true}) + const ajv = new Ajv({$comment: true}) + const fullAjv = new Ajv({allErrors: true, $comment: true}) ;[ajv, fullAjv].forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) test({}, true, [["object root"]]) test({foo: 1}, true, [["object root"], ["property foo"]]) @@ -52,14 +52,14 @@ describe("$comment option", () => { }) describe("function hook", () => { - var hookCalls + let hookCalls function hook() { hookCalls.push(Array.prototype.slice.call(arguments)) } it("should pass the text from $comment keyword to the hook", () => { - var schema = { + const schema = { $comment: "object root", properties: { foo: {$comment: "property foo"}, @@ -67,11 +67,11 @@ describe("$comment option", () => { }, } - var ajv = new Ajv({$comment: hook}) - var fullAjv = new Ajv({allErrors: true, $comment: hook}) + const ajv = new Ajv({$comment: hook}) + const fullAjv = new Ajv({allErrors: true, $comment: hook}) ;[ajv, fullAjv].forEach((_ajv) => { - var validate = _ajv.compile(schema) + const validate = _ajv.compile(schema) test({}, true, [["object root", "#/$comment", schema]]) test({foo: 1}, true, [ diff --git a/spec/options/meta_validateSchema.spec.js b/spec/options/meta_validateSchema.spec.js index c1c6ec070c..7f076d069c 100644 --- a/spec/options/meta_validateSchema.spec.js +++ b/spec/options/meta_validateSchema.spec.js @@ -1,7 +1,7 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("meta and validateSchema options", () => { it("should add draft-7 meta schema by default", () => { @@ -22,7 +22,7 @@ describe("meta and validateSchema options", () => { }) it("should throw if meta: false and validateSchema: true", () => { - var ajv = new Ajv({meta: false, logger: false}) + const ajv = new Ajv({meta: false, logger: false}) should.not.exist(ajv.getSchema("http://json-schema.org/draft-07/schema")) should.not.throw(() => { ajv.addSchema({type: "wrong_type"}, "integer") @@ -30,7 +30,7 @@ describe("meta and validateSchema options", () => { }) it("should skip schema validation with validateSchema: false", () => { - var ajv = new Ajv() + let ajv = new Ajv() should.throw(() => { ajv.addSchema({type: 123}, "integer") }) @@ -79,19 +79,19 @@ describe("meta and validateSchema options", () => { }) it("should validate v6 schema", () => { - var ajv = new Ajv() + const ajv = new Ajv() ajv.validateSchema({contains: {minimum: 2}}).should.equal(true) ajv.validateSchema({contains: 2}).should.equal(false) }) it("should use option meta as default meta schema", () => { - var meta = { + const meta = { $schema: "http://json-schema.org/draft-07/schema", properties: { myKeyword: {type: "boolean"}, }, } - var ajv = new Ajv({meta: meta}) + let ajv = new Ajv({meta: meta}) ajv.validateSchema({myKeyword: true}).should.equal(true) ajv.validateSchema({myKeyword: 2}).should.equal(false) ajv diff --git a/spec/options/options_add_schemas.spec.js b/spec/options/options_add_schemas.spec.js index cf3b7d13e7..bbb69263f8 100644 --- a/spec/options/options_add_schemas.spec.js +++ b/spec/options/options_add_schemas.spec.js @@ -1,12 +1,12 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("options to add schemas", () => { describe("schemas", () => { it("should add schemas from object", () => { - var ajv = new Ajv({ + const ajv = new Ajv({ schemas: { int: {type: "integer"}, str: {type: "string"}, @@ -20,7 +20,7 @@ describe("options to add schemas", () => { }) it("should add schemas from array", () => { - var ajv = new Ajv({ + const ajv = new Ajv({ schemas: [ {$id: "int", type: "integer"}, {$id: "str", type: "string"}, @@ -37,7 +37,7 @@ describe("options to add schemas", () => { describe("addUsedSchema", () => { ;[true, undefined].forEach((optionValue) => { describe("= " + optionValue, () => { - var ajv + let ajv beforeEach(() => { ajv = new Ajv({addUsedSchema: optionValue}) @@ -45,8 +45,8 @@ describe("options to add schemas", () => { describe("compile and validate", () => { it("should add schema", () => { - var schema = {$id: "str", type: "string"} - var validate = ajv.compile(schema) + let schema = {$id: "str", type: "string"} + const validate = ajv.compile(schema) validate("abc").should.equal(true) validate(1).should.equal(false) ajv.getSchema("str").should.equal(validate) @@ -63,8 +63,8 @@ describe("options to add schemas", () => { ajv.compile({$id: "str", minLength: 2}) }) - var schema = {$id: "int", type: "integer"} - var schema2 = {$id: "int", minimum: 0} + const schema = {$id: "int", type: "integer"} + const schema2 = {$id: "int", minimum: 0} ajv.validate(schema, 1).should.equal(true) should.throw(() => { ajv.validate(schema2, 1) @@ -75,7 +75,7 @@ describe("options to add schemas", () => { }) describe("= false", () => { - var ajv + let ajv beforeEach(() => { ajv = new Ajv({addUsedSchema: false}) @@ -83,8 +83,8 @@ describe("options to add schemas", () => { describe("compile and validate", () => { it("should NOT add schema", () => { - var schema = {$id: "str", type: "string"} - var validate = ajv.compile(schema) + let schema = {$id: "str", type: "string"} + const validate = ajv.compile(schema) validate("abc").should.equal(true) validate(1).should.equal(false) should.equal(ajv.getSchema("str"), undefined) @@ -101,8 +101,8 @@ describe("options to add schemas", () => { ajv.compile({$id: "str", minLength: 2}) }) - var schema = {$id: "int", type: "integer"} - var schema2 = {$id: "int", minimum: 0} + const schema = {$id: "int", type: "integer"} + const schema2 = {$id: "int", minimum: 0} ajv.validate(schema, 1).should.equal(true) should.not.throw(() => { ajv.validate(schema2, 1).should.equal(true) @@ -113,11 +113,11 @@ describe("options to add schemas", () => { }) describe("serialize", () => { - var serializeCalled + let serializeCalled it("should use user-defined function to serialize schema to string", () => { serializeCalled = undefined - var ajv = new Ajv({serialize: serialize}) + const ajv = new Ajv({serialize: serialize}) ajv.addSchema({type: "string"}) should.equal(serializeCalled, true) }) diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.js index 838f49e738..410d1ed463 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.js @@ -1,7 +1,7 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("code generation options", () => { describe("sourceCode", () => { @@ -10,7 +10,7 @@ describe("code generation options", () => { test(new Ajv({sourceCode: true})) function test(ajv) { - var validate = ajv.compile({type: "number"}) + const validate = ajv.compile({type: "number"}) validate.source.code.should.be.a("string") } }) @@ -22,7 +22,7 @@ describe("code generation options", () => { test(new Ajv({sourceCode: false})) function test(ajv) { - var validate = ajv.compile({type: "number"}) + const validate = ajv.compile({type: "number"}) should.not.exist(validate.source) should.not.exist(validate.sourceCode) } @@ -32,14 +32,14 @@ describe("code generation options", () => { describe("processCode", () => { it("should process generated code", () => { - var ajv = new Ajv() - var validate = ajv.compile({type: "string"}) + const ajv = new Ajv() + let validate = ajv.compile({type: "string"}) // TODO re-enable this test when option to strip whitespace is added // validate.toString().split("\n").length.should.equal(1) const unprocessedLines = validate.toString().split("\n").length - var beautify = require("js-beautify").js_beautify - var ajvPC = new Ajv({processCode: beautify}) + const beautify = require("js-beautify").js_beautify + const ajvPC = new Ajv({processCode: beautify}) validate = ajvPC.compile({type: "string"}) validate.toString().split("\n").length.should.be.above(unprocessedLines) validate("foo").should.equal(true) @@ -48,7 +48,7 @@ describe("code generation options", () => { }) describe("passContext option", () => { - var ajv, contexts + let ajv, contexts beforeEach(() => { contexts = [] @@ -56,8 +56,8 @@ describe("code generation options", () => { describe("= true", () => { it("should pass this value as context to user-defined keyword validation function", () => { - var validate = getValidate(true) - var self = {} + const validate = getValidate(true) + const self = {} validate.call(self, {}) contexts.should.have.length(4) contexts.forEach((ctx) => ctx.should.equal(self)) @@ -66,8 +66,8 @@ describe("code generation options", () => { describe("= false", () => { it("should pass ajv instance as context to user-defined keyword validation function", () => { - var validate = getValidate(false) - var self = {} + const validate = getValidate(false) + const self = {} validate.call(self, {}) contexts.should.have.length(4) contexts.forEach((ctx) => ctx.should.equal(ajv)) @@ -79,7 +79,7 @@ describe("code generation options", () => { ajv.addKeyword({keyword: "testValidate", validate: storeContext}) ajv.addKeyword({keyword: "testCompile", compile: compileTestValidate}) - var schema = { + const schema = { definitions: { test1: { testValidate: true, diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.js index 4a68739106..3f400fc8fc 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.js @@ -1,7 +1,7 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("referenced schema options", () => { describe("extendRefs", () => { @@ -33,7 +33,7 @@ describe("referenced schema options", () => { function testFail(ajv) { should.throw(() => { - var schema = { + const schema = { definitions: { int: {type: "integer"}, }, @@ -44,7 +44,7 @@ describe("referenced schema options", () => { }) should.not.throw(() => { - var schema = { + const schema = { definitions: { int: {type: "integer"}, }, @@ -57,7 +57,7 @@ describe("referenced schema options", () => { }) function test(ajv, shouldExtendRef) { - var schema = { + let schema = { definitions: { int: {type: "integer"}, }, @@ -65,7 +65,7 @@ describe("referenced schema options", () => { minimum: 10, } - var validate = ajv.compile(schema) + let validate = ajv.compile(schema) validate(10).should.equal(true) validate(1).should.equal(!shouldExtendRef) @@ -92,15 +92,15 @@ describe("referenced schema options", () => { } function testWarning(ajv, msgPattern) { - var oldConsole + let oldConsole try { oldConsole = console.warn - var consoleMsg + let consoleMsg console.warn = function () { consoleMsg = Array.prototype.join.call(arguments, " ") } - var schema = { + const schema = { definitions: { int: {type: "integer"}, }, @@ -119,7 +119,7 @@ describe("referenced schema options", () => { describe("missingRefs", () => { it("should throw if ref is missing without this option", () => { - var ajv = new Ajv() + const ajv = new Ajv() should.throw(() => { ajv.compile({$ref: "missing_reference"}) }) @@ -130,7 +130,7 @@ describe("referenced schema options", () => { testMissingRefsIgnore(new Ajv({missingRefs: "ignore", allErrors: true, logger: false})) function testMissingRefsIgnore(ajv) { - var validate = ajv.compile({$ref: "missing_reference"}) + const validate = ajv.compile({$ref: "missing_reference"}) validate({}).should.equal(true) } }) @@ -144,7 +144,7 @@ describe("referenced schema options", () => { ) function testMissingRefsFail(ajv) { - var validate = ajv.compile({ + let validate = ajv.compile({ anyOf: [{type: "number"}, {$ref: "missing_reference"}], }) validate(123).should.equal(true) diff --git a/spec/options/options_reporting.spec.js b/spec/options/options_reporting.spec.js index 7968715e51..58de18531c 100644 --- a/spec/options/options_reporting.spec.js +++ b/spec/options/options_reporting.spec.js @@ -1,7 +1,7 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("reporting options", () => { describe("verbose", () => { @@ -10,13 +10,13 @@ describe("reporting options", () => { testVerbose(new Ajv({verbose: true, allErrors: true})) function testVerbose(ajv) { - var schema = {properties: {foo: {minimum: 5}}} - var validate = ajv.compile(schema) + const schema = {properties: {foo: {minimum: 5}}} + const validate = ajv.compile(schema) - var data = {foo: 3} + const data = {foo: 3} validate(data).should.equal(false) validate.errors.should.have.length(1) - var err = validate.errors[0] + const err = validate.errors[0] should.equal(err.schema, 5) err.parentSchema.should.eql({minimum: 5}) @@ -32,7 +32,7 @@ describe("reporting options", () => { test(new Ajv({allErrors: true}), true) function test(ajv, allErrors) { - var format1called = false, + let format1called = false, format2called = false ajv.addFormat("format1", () => { @@ -45,7 +45,7 @@ describe("reporting options", () => { return false }) - var schema1 = { + const schema1 = { allOf: [{format: "format1"}, {format: "format2"}], } @@ -54,7 +54,7 @@ describe("reporting options", () => { format1called.should.equal(true) format2called.should.equal(allErrors) - var schema2 = { + const schema2 = { not: schema1, } @@ -72,8 +72,8 @@ describe("reporting options", () => { * The logger option tests are based on the meta scenario which writes into the logger.warn */ - var origConsoleWarn = console.warn - var consoleCalled + const origConsoleWarn = console.warn + let consoleCalled beforeEach(() => { consoleCalled = false @@ -85,7 +85,7 @@ describe("reporting options", () => { }) it("no user-defined logger is given - global console should be used", () => { - var ajv = new Ajv({meta: false}) + const ajv = new Ajv({meta: false}) ajv.compile({ type: "number", @@ -96,15 +96,15 @@ describe("reporting options", () => { }) it("user-defined logger is an object - logs should only report to it", () => { - var loggerCalled = false + let loggerCalled = false - var logger = { + const logger = { warn: log, log: log, error: log, } - var ajv = new Ajv({ + const ajv = new Ajv({ meta: false, logger: logger, }) @@ -123,7 +123,7 @@ describe("reporting options", () => { }) it("logger option is false - no logs should be reported", () => { - var ajv = new Ajv({ + const ajv = new Ajv({ meta: false, logger: false, }) diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.js index 9ec583ef43..f001aea48a 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.js @@ -1,17 +1,17 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() -var DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ +const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("validation options", () => { describe("format", () => { it("should not validate formats if option format == false", () => { - var ajv = new Ajv({formats: {date: DATE_FORMAT}}), + const ajv = new Ajv({formats: {date: DATE_FORMAT}}), ajvFF = new Ajv({formats: {date: DATE_FORMAT}, format: false}) - var schema = {format: "date"} - var invalideDateTime = "06/19/1963" // expects hyphens + const schema = {format: "date"} + const invalideDateTime = "06/19/1963" // expects hyphens ajv.validate(schema, invalideDateTime).should.equal(false) ajvFF.validate(schema, invalideDateTime).should.equal(true) @@ -20,13 +20,13 @@ describe("validation options", () => { describe("formats", () => { it("should add formats from options", () => { - var ajv = new Ajv({ + const ajv = new Ajv({ formats: { identifier: /^[a-z_$][a-z0-9_$]*$/i, }, }) - var validate = ajv.compile({format: "identifier"}) + const validate = ajv.compile({format: "identifier"}) validate("Abc1").should.equal(true) validate("foo bar").should.equal(false) @@ -37,7 +37,7 @@ describe("validation options", () => { describe("keywords", () => { it("should add keywords from options", () => { - var ajv = new Ajv({ + const ajv = new Ajv({ keywords: [ { keyword: "identifier", @@ -49,7 +49,7 @@ describe("validation options", () => { ], }) - var validate = ajv.compile({identifier: true}) + const validate = ajv.compile({identifier: true}) validate("Abc1").should.equal(true) validate("foo bar").should.equal(false) @@ -60,13 +60,13 @@ describe("validation options", () => { describe("unicode", () => { it("should use String.prototype.length with deprecated unicode option == false", () => { - var ajvUnicode = new Ajv() + const ajvUnicode = new Ajv() testUnicode(new Ajv({unicode: false, logger: false})) testUnicode(new Ajv({unicode: false, allErrors: true, logger: false})) function testUnicode(ajv) { - var validateWithUnicode = ajvUnicode.compile({minLength: 2}) - var validate = ajv.compile({minLength: 2}) + let validateWithUnicode = ajvUnicode.compile({minLength: 2}) + let validate = ajv.compile({minLength: 2}) validateWithUnicode("😀").should.equal(false) validate("😀").should.equal(true) @@ -86,8 +86,8 @@ describe("validation options", () => { test(new Ajv({multipleOfPrecision: 7, allErrors: true})) function test(ajv) { - var schema = {multipleOf: 0.01} - var validate = ajv.compile(schema) + let schema = {multipleOf: 0.01} + let validate = ajv.compile(schema) validate(4.18).should.equal(true) validate(4.181).should.equal(false) diff --git a/spec/options/ownProperties.spec.js b/spec/options/ownProperties.spec.js index a1a84dc562..7e0a26d9a9 100644 --- a/spec/options/ownProperties.spec.js +++ b/spec/options/ownProperties.spec.js @@ -1,10 +1,10 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("ownProperties option", () => { - var ajv, ajvOP, ajvOP1 + let ajv, ajvOP, ajvOP1 beforeEach(() => { ajv = new Ajv({allErrors: true}) @@ -13,36 +13,36 @@ describe("ownProperties option", () => { }) it("should only validate own properties with additionalProperties", () => { - var schema = { + const schema = { properties: {a: {type: "number"}}, additionalProperties: false, } - var obj = {a: 1} - var proto = {b: 2} + const obj = {a: 1} + const proto = {b: 2} test(schema, obj, proto) }) it("should only validate own properties with properties keyword", () => { - var schema = { + const schema = { properties: { a: {type: "number"}, b: {type: "number"}, }, } - var obj = {a: 1} - var proto = {b: "not a number"} + const obj = {a: 1} + const proto = {b: "not a number"} test(schema, obj, proto) }) it("should only validate own properties with required keyword", () => { - var schema = { + const schema = { required: ["a", "b"], } - var obj = {a: 1} - var proto = {b: 2} + const obj = {a: 1} + const proto = {b: 2} test(schema, obj, proto, 1, true) }) @@ -51,12 +51,12 @@ describe("ownProperties option", () => { ajvOP = new Ajv({ownProperties: true, allErrors: true, loopRequired: 1}) ajvOP1 = new Ajv({ownProperties: true, loopRequired: 1}) - var schema = { + const schema = { required: ["a", "b", "c", "d"], } - var obj = {a: 1, b: 2} - var proto = {c: 3, d: 4} + const obj = {a: 1, b: 2} + const proto = {c: 3, d: 4} test(schema, obj, proto, 2, true) }) @@ -65,7 +65,7 @@ describe("ownProperties option", () => { ajvOP = new Ajv({ownProperties: true, allErrors: true, $data: true}) ajvOP1 = new Ajv({ownProperties: true, $data: true}) - var schema = { + const schema = { required: {$data: "0/req"}, properties: { req: { @@ -75,16 +75,16 @@ describe("ownProperties option", () => { }, } - var obj = { + const obj = { req: ["a", "b"], a: 1, } - var proto = {b: 2} + const proto = {b: 2} test(schema, obj, proto, 1, true) }) it("should only validate own properties with properties and required keyword", () => { - var schema = { + const schema = { properties: { a: {type: "number"}, b: {type: "number"}, @@ -92,21 +92,21 @@ describe("ownProperties option", () => { required: ["a", "b"], } - var obj = {a: 1} - var proto = {b: 2} + const obj = {a: 1} + const proto = {b: 2} test(schema, obj, proto, 1, true) }) it("should only validate own properties with dependencies keyword", () => { - var schema = { + const schema = { dependencies: { a: ["c"], b: ["d"], }, } - var obj = {a: 1, c: 3} - var proto = {b: 2} + let obj = {a: 1, c: 3} + let proto = {b: 2} test(schema, obj, proto) obj = {a: 1, b: 2, c: 3} @@ -115,15 +115,15 @@ describe("ownProperties option", () => { }) it("should only validate own properties with schema dependencies", () => { - var schema = { + const schema = { dependencies: { a: {not: {required: ["c"]}}, b: {not: {required: ["d"]}}, }, } - var obj = {a: 1, d: 3} - var proto = {b: 2} + let obj = {a: 1, d: 3} + let proto = {b: 2} test(schema, obj, proto) obj = {a: 1, b: 2} @@ -132,34 +132,34 @@ describe("ownProperties option", () => { }) it("should only validate own properties with patternProperties", () => { - var schema = { + const schema = { patternProperties: {"f.*o": {type: "integer"}}, } - var obj = {fooo: 1} - var proto = {foo: "not a number"} + const obj = {fooo: 1} + const proto = {foo: "not a number"} test(schema, obj, proto) }) it("should only validate own properties with propertyNames", () => { - var schema = { + const schema = { propertyNames: { pattern: "foo", }, } - var obj = {foo: 2} - var proto = {bar: 1} + const obj = {foo: 2} + const proto = {bar: 1} test(schema, obj, proto, 2) }) function test(schema, obj, proto, errors, reverse) { errors = errors || 1 - var validate = ajv.compile(schema) - var validateOP = ajvOP.compile(schema) - var validateOP1 = ajvOP1.compile(schema) - var data = Object.create(proto) - for (var key in obj) data[key] = obj[key] + const validate = ajv.compile(schema) + const validateOP = ajvOP.compile(schema) + const validateOP1 = ajvOP1.compile(schema) + const data = Object.create(proto) + for (const key in obj) data[key] = obj[key] if (reverse) { validate(data).should.equal(true) diff --git a/spec/options/removeAdditional.spec.js b/spec/options/removeAdditional.spec.js index 6e488b578b..180226232d 100644 --- a/spec/options/removeAdditional.spec.js +++ b/spec/options/removeAdditional.spec.js @@ -1,18 +1,18 @@ "use strict" -var Ajv = require("../ajv") +const Ajv = require("../ajv") require("../chai").should() describe("removeAdditional option", () => { it("should remove all additional properties", () => { - var ajv = new Ajv({removeAdditional: "all"}) + const ajv = new Ajv({removeAdditional: "all"}) ajv.addSchema({ $id: "//test/fooBar", properties: {foo: {type: "string"}, bar: {type: "string"}}, }) - var object = { + const object = { foo: "foo", bar: "bar", baz: "baz-to-be-removed", @@ -25,7 +25,7 @@ describe("removeAdditional option", () => { }) it("should remove properties that would error when `additionalProperties = false`", () => { - var ajv = new Ajv({removeAdditional: true}) + const ajv = new Ajv({removeAdditional: true}) ajv.addSchema({ $id: "//test/fooBar", @@ -33,7 +33,7 @@ describe("removeAdditional option", () => { additionalProperties: false, }) - var object = { + const object = { foo: "foo", bar: "bar", baz: "baz-to-be-removed", @@ -46,9 +46,9 @@ describe("removeAdditional option", () => { }) it("should remove properties that would error when `additionalProperties = false` (many properties, boolean schema)", () => { - var ajv = new Ajv({removeAdditional: true}) + const ajv = new Ajv({removeAdditional: true}) - var schema = { + const schema = { properties: { obj: { additionalProperties: false, @@ -67,7 +67,7 @@ describe("removeAdditional option", () => { }, } - var data = { + const data = { obj: { a: "valid", b: "should not be removed", @@ -85,7 +85,7 @@ describe("removeAdditional option", () => { }) it("should remove properties that would error when `additionalProperties` is a schema", () => { - var ajv = new Ajv({removeAdditional: "failing"}) + const ajv = new Ajv({removeAdditional: "failing"}) ajv.addSchema({ $id: "//test/fooBar", @@ -93,7 +93,7 @@ describe("removeAdditional option", () => { additionalProperties: {type: "string"}, }) - var object = { + let object = { foo: "foo", bar: "bar", baz: "baz-to-be-kept", diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.js index b4c275a66c..feaab2e4ac 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.js @@ -1,7 +1,7 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("removed schemaId option", () => { it("should use $id and throw exception when id is used", () => { @@ -10,7 +10,7 @@ describe("removed schemaId option", () => { function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) - var validate = ajv.getSchema("mySchema1") + const validate = ajv.getSchema("mySchema1") validate("foo").should.equal(true) validate(1).should.equal(false) @@ -24,7 +24,7 @@ describe("removed schemaId option", () => { function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) - var validate = ajv.getSchema("mySchema1") + let validate = ajv.getSchema("mySchema1") validate("foo").should.equal(true) validate(1).should.equal(false) diff --git a/spec/options/strictDefaults.spec.js b/spec/options/strictDefaults.spec.js index be7d62d391..a1f41b5660 100644 --- a/spec/options/strictDefaults.spec.js +++ b/spec/options/strictDefaults.spec.js @@ -1,19 +1,19 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("strict option with defaults (replaced strictDefaults)", () => { describe("useDefaults = true", () => { describe("strict = false", () => { it("should NOT throw an error or log a warning given an ignored default", () => { - var output = {} - var ajv = new Ajv({ + const output = {} + const ajv = new Ajv({ useDefaults: true, strict: false, logger: getLogger(output), }) - var schema = { + const schema = { default: 5, properties: {}, } @@ -23,13 +23,13 @@ describe("strict option with defaults (replaced strictDefaults)", () => { }) it("should NOT throw an error or log a warning given an ignored default #2", () => { - var output = {} - var ajv = new Ajv({ + const output = {} + const ajv = new Ajv({ useDefaults: true, strict: false, logger: getLogger(output), }) - var schema = { + const schema = { oneOf: [ {enum: ["foo", "bar"]}, { @@ -68,7 +68,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { test(new Ajv({useDefaults: true, strict: true})) function test(ajv) { - var schema = { + const schema = { oneOf: [ {enum: ["foo", "bar"]}, { @@ -89,13 +89,13 @@ describe("strict option with defaults (replaced strictDefaults)", () => { describe('strict = "log"', () => { it('should log a warning given an ignored default in the schema root when strict is "log"', () => { - var output = {} - var ajv = new Ajv({ + const output = {} + const ajv = new Ajv({ useDefaults: true, strict: "log", logger: getLogger(output), }) - var schema = { + const schema = { default: 5, properties: {}, } @@ -104,13 +104,13 @@ describe("strict option with defaults (replaced strictDefaults)", () => { }) it('should log a warning given an ignored default in oneOf when strict is "log"', () => { - var output = {} - var ajv = new Ajv({ + const output = {} + const ajv = new Ajv({ useDefaults: true, strict: "log", logger: getLogger(output), }) - var schema = { + const schema = { oneOf: [ {enum: ["foo", "bar"]}, { @@ -153,7 +153,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { test(new Ajv({strict: true})) function test(ajv) { - var schema = { + const schema = { oneOf: [ {enum: ["foo", "bar"]}, { diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.js index 51c8f2d1a2..9680576e11 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.js @@ -1,17 +1,17 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() +const Ajv = require("../ajv") +const should = require("../chai").should() describe("strict option with keywords (replaced strictKeywords)", () => { describe("strict = false", () => { it("should NOT throw an error or log a warning given an unknown keyword", () => { - var output = {} - var ajv = new Ajv({ + const output = {} + const ajv = new Ajv({ strict: false, logger: getLogger(output), }) - var schema = { + const schema = { properties: {}, unknownKeyword: 1, } @@ -40,12 +40,12 @@ describe("strict option with keywords (replaced strictKeywords)", () => { describe('strict = "log"', () => { it("should log an error given an unknown keyword in the schema root", () => { - var output = {} - var ajv = new Ajv({ + const output = {} + const ajv = new Ajv({ strict: "log", logger: getLogger(output), }) - var schema = { + const schema = { properties: {}, unknownKeyword: 1, } diff --git a/spec/options/strictNumbers.spec.js b/spec/options/strictNumbers.spec.js index f7d587b1b5..e8a908409d 100644 --- a/spec/options/strictNumbers.spec.js +++ b/spec/options/strictNumbers.spec.js @@ -12,7 +12,7 @@ describe("strict option with keywords (replaced structNumbers)", () => { function testStrict(ajv) { return () => { it("should fail validation for NaN/Infinity as type number", () => { - var validate = ajv.compile({type: "number"}) + const validate = ajv.compile({type: "number"}) validate("1.1").should.equal(false) validate(1.1).should.equal(true) validate(1).should.equal(true) @@ -21,7 +21,7 @@ function testStrict(ajv) { }) it("should fail validation for NaN as type integer", () => { - var validate = ajv.compile({type: "integer"}) + const validate = ajv.compile({type: "integer"}) validate("1.1").should.equal(false) validate(1.1).should.equal(false) validate(1).should.equal(true) @@ -34,7 +34,7 @@ function testStrict(ajv) { function testNotStrict(_ajv) { return () => { it("should NOT fail validation for NaN/Infinity as type number", () => { - var validate = _ajv.compile({type: "number"}) + const validate = _ajv.compile({type: "number"}) validate("1.1").should.equal(false) validate(1.1).should.equal(true) validate(1).should.equal(true) @@ -43,7 +43,7 @@ function testNotStrict(_ajv) { }) it("should NOT fail validation for NaN/Infinity as type integer", () => { - var validate = _ajv.compile({type: "integer"}) + const validate = _ajv.compile({type: "integer"}) validate("1.1").should.equal(false) validate(1.1).should.equal(false) validate(1).should.equal(true) diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.js index c6c5ba1374..c85a776755 100644 --- a/spec/options/unknownFormats.spec.js +++ b/spec/options/unknownFormats.spec.js @@ -1,8 +1,8 @@ "use strict" -var Ajv = require("../ajv") -var should = require("../chai").should() -var DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ +const Ajv = require("../ajv") +const should = require("../chai").should() +const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("unknownFormats option", () => { describe("= true (default)", () => { @@ -23,7 +23,7 @@ describe("unknownFormats option", () => { function test(ajv) { ajv.addFormat("date", DATE_FORMAT) - var validate = ajv.compile({ + const validate = ajv.compile({ properties: { foo: {format: {$data: "1/bar"}}, bar: {type: "string"}, @@ -45,7 +45,7 @@ describe("unknownFormats option", () => { test(new Ajv({unknownFormats: "ignore", logger: false})) function test(ajv) { - var validate = ajv.compile({format: "unknown"}) + const validate = ajv.compile({format: "unknown"}) validate("anything").should.equal(true) } }) @@ -55,7 +55,7 @@ describe("unknownFormats option", () => { function test(ajv) { ajv.addFormat("date", DATE_FORMAT) - var validate = ajv.compile({ + const validate = ajv.compile({ properties: { foo: {format: {$data: "1/bar"}}, bar: {type: "string"}, @@ -76,7 +76,7 @@ describe("unknownFormats option", () => { test(new Ajv({unknownFormats: ["allowed"]})) function test(ajv) { - var validate = ajv.compile({format: "allowed"}) + const validate = ajv.compile({format: "allowed"}) validate("anything").should.equal(true) should.throw(() => { @@ -90,7 +90,7 @@ describe("unknownFormats option", () => { function test(ajv) { ajv.addFormat("date", DATE_FORMAT) - var validate = ajv.compile({ + const validate = ajv.compile({ properties: { foo: {format: {$data: "1/bar"}}, bar: {type: "string"}, diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.js index 53d70bd0cc..38baf11840 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.js @@ -1,12 +1,12 @@ "use strict" -var Ajv = require("../ajv") -var getAjvInstances = require("../ajv_instances") +const Ajv = require("../ajv") +const getAjvInstances = require("../ajv_instances") require("../chai").should() describe("useDefaults option", () => { it("should replace undefined property with default value", () => { - var instances = getAjvInstances( + const instances = getAjvInstances( { allErrors: true, loopRequired: 3, @@ -17,7 +17,7 @@ describe("useDefaults option", () => { instances.forEach(test) function test(ajv) { - var schema = { + const schema = { properties: { foo: {type: "string", default: "abc"}, bar: {type: "number", default: 1}, @@ -30,9 +30,9 @@ describe("useDefaults option", () => { minProperties: 6, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) - var data = {} + let data = {} validate(data).should.equal(true) data.should.eql({ foo: "abc", @@ -61,7 +61,7 @@ describe("useDefaults option", () => { test(new Ajv({useDefaults: true, allErrors: true})) function test(ajv) { - var schema = { + const schema = { items: [ {type: "string", default: "abc"}, {type: "number", default: 1}, @@ -70,9 +70,9 @@ describe("useDefaults option", () => { minItems: 3, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) - var data = [] + let data = [] validate(data).should.equal(true) data.should.eql(["abc", 1, false]) @@ -92,7 +92,7 @@ describe("useDefaults option", () => { test(new Ajv({useDefaults: true, allErrors: true})) function test(ajv) { - var schema = { + const schema = { if: {required: ["foo"]}, then: { properties: { @@ -106,9 +106,9 @@ describe("useDefaults option", () => { }, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) - var data = {} + let data = {} validate(data).should.equal(true) data.should.eql({foo: 1}) @@ -127,7 +127,7 @@ describe("useDefaults option", () => { }) function test(ajv) { - var schema = { + const schema = { properties: { items: { type: "array", @@ -136,16 +136,16 @@ describe("useDefaults option", () => { }, } - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) - var data = {} + const data = {} validate(data).should.equal(true) data.items.should.eql(["a-default"]) data.items.push("another-value") data.items.should.eql(["a-default", "another-value"]) - var data2 = {} + const data2 = {} validate(data2).should.equal(true) data2.items.should.eql(["a-default"]) @@ -153,7 +153,7 @@ describe("useDefaults option", () => { }) describe('defaults with "empty" values', () => { - var schema, data + let schema, data beforeEach(() => { schema = { @@ -187,7 +187,7 @@ describe("useDefaults option", () => { test(new Ajv({useDefaults: "shared"})) function test(ajv) { - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate(data).should.equal(true) data.should.eql({ obj: { @@ -202,8 +202,8 @@ describe("useDefaults option", () => { }) it('should assign defaults when useDefaults = "empty"', () => { - var ajv = new Ajv({useDefaults: "empty"}) - var validate = ajv.compile(schema) + const ajv = new Ajv({useDefaults: "empty"}) + const validate = ajv.compile(schema) validate(data).should.equal(true) data.should.eql({ obj: { diff --git a/spec/promise.js b/spec/promise.js deleted file mode 100644 index 98caf040ee..0000000000 --- a/spec/promise.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict" - -var g = - typeof global == "object" ? global : typeof window == "object" ? window : this - -if (!g.Promise) { - g.Promise = require("" + "bluebird") - g.Promise.config({warnings: false}) -} - -module.exports = g.Promise diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index 7034780fd0..6afaf5b55e 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -1,11 +1,11 @@ "use strict" -var Ajv = require("./ajv"), +const Ajv = require("./ajv"), should = require("./chai").should(), getAjvInstances = require("./ajv_instances") describe("resolve", () => { - var instances + let instances beforeEach(() => { instances = getAjvInstances({ @@ -18,7 +18,7 @@ describe("resolve", () => { describe("resolve.ids method", () => { it("should resolve ids in schema", () => { // Example from http://json-schema.org/latest/json-schema-core.html#anchor29 - var schema = { + const schema = { $id: "http://x.y.z/rootschema.json#", $defs: { schema1: { @@ -58,8 +58,8 @@ describe("resolve", () => { } instances.forEach((ajv) => { - var validate = ajv.compile(schema) - var data = {foo: 1, bar: "abc", baz: true, bax: null} + const validate = ajv.compile(schema) + const data = {foo: 1, bar: "abc", baz: true, bax: null} validate(data).should.equal(true) }) }) @@ -95,7 +95,7 @@ describe("resolve", () => { }) it("should resolve ids defined as urn's (issue #423)", () => { - var schema = { + const schema = { type: "object", properties: { ip1: { @@ -110,12 +110,12 @@ describe("resolve", () => { required: ["ip1", "ip2"], } - var data = { + const data = { ip1: "0.0.0.0", ip2: "0.0.0.0", } instances.forEach((ajv) => { - var validate = ajv.compile(schema) + const validate = ajv.compile(schema) validate(data).should.equal(true) }) }) @@ -124,7 +124,7 @@ describe("resolve", () => { describe("protocol-relative URIs", () => { it("should resolve fragment", () => { instances.forEach((ajv) => { - var schema = { + const schema = { $id: "//e.com/types", definitions: { int: {type: "integer"}, @@ -132,7 +132,7 @@ describe("resolve", () => { } ajv.addSchema(schema) - var validate = ajv.compile({$ref: "//e.com/types#/definitions/int"}) + const validate = ajv.compile({$ref: "//e.com/types#/definitions/int"}) validate(1).should.equal(true) validate("foo").should.equal(false) }) @@ -203,7 +203,7 @@ describe("resolve", () => { }) describe("inline referenced schemas without refs in them", () => { - var schemas = [ + const schemas = [ {$id: "http://e.com/obj.json#", properties: {a: {$ref: "int.json#"}}}, {$id: "http://e.com/int.json#", type: "integer", minimum: 2, maximum: 4}, { @@ -215,29 +215,29 @@ describe("resolve", () => { ] it("by default should inline schema if it doesn't contain refs", () => { - var ajv = new Ajv({schemas: schemas}) + const ajv = new Ajv({schemas: schemas}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs == false", () => { - var ajv = new Ajv({schemas: schemas, inlineRefs: false}) + const ajv = new Ajv({schemas: schemas, inlineRefs: false}) testSchemas(ajv, false) }) it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { - var ajv = new Ajv({schemas: schemas, inlineRefs: 4}) + const ajv = new Ajv({schemas: schemas, inlineRefs: 4}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { - var ajv = new Ajv({schemas: schemas, inlineRefs: 2}) + const ajv = new Ajv({schemas: schemas, inlineRefs: 2}) testSchemas(ajv, false) }) it("should avoid schema substitution when refs are inlined (issue #77)", () => { - var ajv = new Ajv({verbose: true}) + const ajv = new Ajv({verbose: true}) - var schemaMessage = { + const schemaMessage = { $schema: "http://json-schema.org/draft-07/schema#", $id: "http://e.com/message.json#", type: "object", @@ -250,7 +250,7 @@ describe("resolve", () => { } // header schema - var schemaHeader = { + const schemaHeader = { $schema: "http://json-schema.org/draft-07/schema#", $id: "http://e.com/header.json#", type: "object", @@ -265,7 +265,7 @@ describe("resolve", () => { } // a good message - var validMessage = { + const validMessage = { header: { version: 4, msgType: 0, @@ -273,7 +273,7 @@ describe("resolve", () => { } // a bad message - var invalidMessage = { + const invalidMessage = { header: { version: 6, msgType: 0, @@ -283,7 +283,7 @@ describe("resolve", () => { // add schemas and get validator function ajv.addSchema(schemaHeader) ajv.addSchema(schemaMessage) - var v = ajv.getSchema("http://e.com/message.json#") + const v = ajv.getSchema("http://e.com/message.json#") v(validMessage).should.equal(true) v.schema.$id.should.equal("http://e.com/message.json#") @@ -297,7 +297,7 @@ describe("resolve", () => { }) function testSchemas(ajv, expectedInlined) { - var v1 = ajv.getSchema("http://e.com/obj.json"), + const v1 = ajv.getSchema("http://e.com/obj.json"), v2 = ajv.getSchema("http://e.com/obj1.json"), v3 = ajv.getSchema("http://e.com/list.json") testObjSchema(v1) @@ -321,7 +321,7 @@ describe("resolve", () => { } function testInlined(validate, expectedInlined) { - var inlined = !/refVal/.test(validate.toString()) + const inlined = !/refVal/.test(validate.toString()) inlined.should.equal(expectedInlined) } }) diff --git a/spec/schema-tests.spec.js b/spec/schema-tests.spec.js index da6cda0b7b..831de62713 100644 --- a/spec/schema-tests.spec.js +++ b/spec/schema-tests.spec.js @@ -1,21 +1,20 @@ "use strict" -var jsonSchemaTest = require("json-schema-test"), +const jsonSchemaTest = require("json-schema-test"), addFormats = require("ajv-formats"), getAjvInstances = require("./ajv_instances"), options = require("./ajv_options"), - suite = require("./browser_test_suite"), after = require("./after_test") -var instances = getAjvInstances(options, {strict: false, unknownFormats: ["allowedUnknown"]}) +const instances = getAjvInstances(options, {strict: false, unknownFormats: ["allowedUnknown"]}) -var remoteRefs = { +const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), "http://localhost:1234/folder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json"), "http://localhost:1234/name.json": require("./remotes/name.json"), } -var remoteRefsWithIds = [ +const remoteRefsWithIds = [ require("./remotes/bar.json"), require("./remotes/foo.json"), require("./remotes/buu.json"), @@ -30,12 +29,7 @@ instances.forEach(addRemoteRefsAndFormats) jsonSchemaTest(instances, { description: "Schema tests of " + instances.length + " ajv instances with different options", - suites: { - "Advanced schema tests": - typeof window == "object" - ? suite(require("./tests/{**/,}*.json", {mode: "list"})) - : "./tests/{**/,}*.json", - }, + suites: {"Schema tests": require("./_json/tests")}, only: [], assert: require("./chai").assert, afterError: after.error, @@ -45,7 +39,7 @@ jsonSchemaTest(instances, { }) function addRemoteRefsAndFormats(ajv) { - for (var id in remoteRefs) ajv.addSchema(remoteRefs[id], id) + for (const id in remoteRefs) ajv.addSchema(remoteRefs[id], id) ajv.addSchema(remoteRefsWithIds) addFormats(ajv) } diff --git a/spec/security.spec.js b/spec/security.spec.js index febb6dfc81..0e628d6d9f 100644 --- a/spec/security.spec.js +++ b/spec/security.spec.js @@ -1,24 +1,18 @@ "use strict" -var jsonSchemaTest = require("json-schema-test"), +const jsonSchemaTest = require("json-schema-test"), getAjvInstances = require("./ajv_instances"), options = require("./ajv_options"), - suite = require("./browser_test_suite"), after = require("./after_test") -var instances = getAjvInstances(options, { +const instances = getAjvInstances(options, { schemas: [require("../dist/refs/json-schema-secure.json")], }) jsonSchemaTest(instances, { description: "Secure schemas tests of " + instances.length + " ajv instances with different options", - suites: { - security: - typeof window == "object" - ? suite(require("./security/{**/,}*.json", {mode: "list"})) - : "./security/{**/,}*.json", - }, + suites: {security: require("./_json/security")}, assert: require("./chai").assert, afterError: after.error, afterEach: after.each, diff --git a/spec/tsconfig.json b/spec/tsconfig.json new file mode 100644 index 0000000000..20d1d4e305 --- /dev/null +++ b/spec/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "..", + "include": ["."], + "compilerOptions": { + "outDir": ".distspec", + "types": ["node", "mocha"], + "noImplicitAny": false + } +} diff --git a/spec/typescript/index.ts b/spec/typescript/index.ts deleted file mode 100644 index 9fac0b1f49..0000000000 --- a/spec/typescript/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import ajv = require("../..") - -// #region new() -const options: ajv.Options = { - verbose: true, -} - -let instance: ajv.Ajv - -instance = ajv() -instance = ajv(options) - -instance = new ajv() -instance = new ajv(options) -// #endregion new() - -// #region validate() -let data = { - foo: 42, -} - -let result = instance.validate("", data) - -if (typeof result === "boolean") { - // sync - console.log(result) -} else { - // async - result.then((value) => { - data = value - }) -} -// #endregion validate() - -// #region compile() -const validator = instance.compile({}) -result = validator(data) - -if (typeof result === "boolean") { - // sync - console.log(result) -} else { - // async - result.then((value) => { - data = value - }) -} -// #endregion compile() - -// #region errors -const validationError: ajv.ValidationError = new ajv.ValidationError([]) -validationError instanceof ajv.ValidationError -validationError.ajv === true -validationError.validation === true - -ajv.MissingRefError.message("", "") -const missingRefError: ajv.MissingRefError = new ajv.MissingRefError("", "", "") -missingRefError instanceof ajv.MissingRefError -missingRefError.missingRef -// #endregion From 773a0fafd7c2dfe7515f4f08cda08fde0328bec8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 7 Sep 2020 13:50:15 +0100 Subject: [PATCH 171/322] refactor: make CodeGen normal export --- lib/ajv.ts | 2 +- lib/compile/codegen.ts | 2 +- lib/compile/context.ts | 2 +- lib/compile/errors.ts | 2 +- lib/compile/index.ts | 14 +++++++------- lib/compile/validate/index.ts | 2 +- lib/compile/validate/keyword.ts | 2 +- lib/types.ts | 2 +- lib/vocabularies/util.ts | 2 +- package.json | 4 ++-- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 6e6334bfd3..0cfe3e3853 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -509,7 +509,7 @@ function addInitialKeywords(this: Ajv, defs: Vocabulary | {[x: string]: KeywordD } this.logger.warn("keywords option as map is deprecated, pass array") for (const keyword in defs) { - const def = defs[name] + const def = defs[keyword] if (!def.keyword) def.keyword = keyword this.addKeyword(def) } diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index b04903a19e..30386e7efa 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -141,7 +141,7 @@ export interface CodeGenOptions { forInOwn?: boolean } -export default class CodeGen { +export class CodeGen { #names: {[prefix: string]: NameGroup} = {} #valuePrefixes: {[prefix: string]: Name} = {} #out = "" diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 089e181533..a43fa6d0b9 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -14,7 +14,7 @@ import { keywordError, keyword$DataError, } from "./errors" -import CodeGen, {_, nil, or, Code, Name} from "./codegen" +import {CodeGen, _, nil, or, Code, Name} from "./codegen" import N from "./names" export default class KeywordCtx implements KeywordErrorCtx { diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 258abe8cc5..ae6e1e8f2f 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,5 +1,5 @@ import {KeywordErrorCtx, KeywordErrorDefinition} from "../types" -import CodeGen, {_, str, Code, Name} from "./codegen" +import {CodeGen, _, str, Code, Name} from "./codegen" import N from "./names" export const keywordError: KeywordErrorDefinition = { diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 0004517b0e..653089562b 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,10 +1,10 @@ -import {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" -import CodeGen, {_, nil, str, Code, Scope} from "./codegen" +import type {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" +import type Ajv from "../ajv" +import {CodeGen, _, nil, str, Code, Scope} from "./codegen" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" -import Ajv from "../ajv" import URI = require("uri-js") const equal = require("fast-deep-equal") @@ -64,7 +64,7 @@ export interface InlineResolvedRef { inline: true } -export interface FuncResolvedRef { +interface FuncResolvedRef { code: Code $async?: boolean inline?: false @@ -172,7 +172,7 @@ function extendValidateWrapper(v: ValidateFunction, c: Compilation, sourceCode?: } // Compiles schema to validation function -export function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateWrapper { +function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateWrapper { const {schema, root: passedRoot, localRefs} = env const self = this const opts = this._opts @@ -386,7 +386,7 @@ function vars( // resolve and compile the references ($ref) // TODO returns SchemaObject (if the schema can be inlined) or validation function -export function resolve( +function resolve( this: Ajv, localCompile: (env: SchemaEnv) => ValidateFunction, // reference to schema compilation function (localCompile) root: SchemaRoot, // information about the root schema for the current schema @@ -428,7 +428,7 @@ export function resolve( } // Resolve schema, its root and baseId -export function resolveSchema( +function resolveSchema( this: Ajv, root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it ref: string // reference to resolve diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index fa03d59654..1e3178be0f 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -2,7 +2,7 @@ import {Schema, SchemaCtx, SchemaObjCtx, Options} from "../../types" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" -import CodeGen, {_, nil, str, Block, Code, Name} from "../codegen" +import {CodeGen, _, nil, str, Block, Code, Name} from "../codegen" import N from "../names" import {resolveUrl} from "../resolve" import {schemaCtxHasRules, schemaHasRulesButRef} from "../util" diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 8af1d7f6d2..9581dd492c 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -9,7 +9,7 @@ import KeywordCtx from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" import {callValidateCode} from "../../vocabularies/util" -import CodeGen, {_, nil, Code, Name} from "../codegen" +import {CodeGen, _, nil, Code, Name} from "../codegen" import N from "../names" export function keywordCode( diff --git a/lib/types.ts b/lib/types.ts index 7524dd08ff..2573a45b8c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,4 @@ -import CodeGen, {Code, Name, CodeGenOptions, Scope} from "./compile/codegen" +import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" import KeywordCtx from "./compile/context" diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 315176de7c..6d0b78f8b6 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,7 +1,7 @@ import {schemaHasRules} from "../compile/util" import {Schema, SchemaMap, SchemaCtx, SchemaObjCtx} from "../types" import KeywordCtx from "../compile/context" -import CodeGen, {_, nil, Code, Name, getProperty} from "../compile/codegen" +import {CodeGen, _, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" export function schemaRefOrVal( diff --git a/package.json b/package.json index 3ce620fa9f..e59ca514f3 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "test-cov": "nyc npm run test-spec", "bundle": "rm -rf bundle && node ./scripts/bundle.js", "build": "rm -rf dist && tsc && cp -r lib/refs dist/refs", - "json-tests": "node scripts/jsontests", + "json-tests": "rm -rf spec/_json/*.js && node scripts/jsontests", "test-karma": "karma start", "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && karma start", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", - "test": "npm run eslint && npm run build && npm run json-tests && npm run test-cov", + "test": "npm run build && npm run json-tests && npm run eslint && npm run test-cov", "prepublish": "npm run build", "watch": "watch \"npm run build\" ./lib" }, From 3d8d46f0a2c4288f98d5e872461f79ed34f7a647 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 7 Sep 2020 16:10:03 +0100 Subject: [PATCH 172/322] test: run full test only on travis --- .travis.yml | 8 +++++--- package.json | 2 +- spec/ajv_options.js | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 80bb5bf497..bff4323756 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: node_js -before_script: - - git submodule update --init - - npm install -g codeclimate-test-reporter node_js: - 10 - 12 - 14 +before_script: + - git submodule update --init + - npm install -g codeclimate-test-reporter +script: + - npm run test-ci after_script: - codeclimate-test-reporter < coverage/lcov.info - coveralls < coverage/lcov.info diff --git a/package.json b/package.json index e59ca514f3..7fcd0c9feb 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register spec/{**/,}*.spec.{js,ts} -R dot", - "test-fast": "cross-env AJV_FAST_TEST=true npm run test-spec", "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", "bundle": "rm -rf bundle && node ./scripts/bundle.js", @@ -26,6 +25,7 @@ "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && karma start", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", "test": "npm run build && npm run json-tests && npm run eslint && npm run test-cov", + "test-ci": "AJV_FULL_TEST=true npm test", "prepublish": "npm run build", "watch": "watch \"npm run build\" ./lib" }, diff --git a/spec/ajv_options.js b/spec/ajv_options.js index 0b0f08db7d..8480806f64 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.js @@ -1,7 +1,7 @@ "use strict" const isBrowser = typeof window == "object" -const fullTest = isBrowser || !process.env.AJV_FAST_TEST +const fullTest = !isBrowser && process.env.AJV_FULL_TEST const options = fullTest ? { From 5f73aae5849bba8cf286f64f8afc5affceb12c58 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 7 Sep 2020 17:38:16 +0100 Subject: [PATCH 173/322] hint to install submodule when test is run --- package.json | 2 +- scripts/jsontests.js | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7fcd0c9feb..74d765146c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint lib/{**/,}*.{ts,js} spec/{**/,}*.{ts,js} scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint lib/{**/,}*.ts spec/{**/,}*.{ts,js} scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register spec/{**/,}*.spec.{js,ts} -R dot", diff --git a/scripts/jsontests.js b/scripts/jsontests.js index cb7e605d8a..4e01f4978e 100644 --- a/scripts/jsontests.js +++ b/scripts/jsontests.js @@ -14,13 +14,17 @@ const fs = require("fs") for (const suite in testSuitePaths) { const p = testSuitePaths[suite] - const files = glob - .sync(`${p}{**/,}*.json`) + const files = glob.sync(`${p}{**/,}*.json`) + if (files.length === 0) { + console.error(`Missing folder ${p}\nTry: git submodule update --init\n`) + process.exit(1) + } + const code = files .map((f) => { const name = f.replace(p, "").replace(/\.json$/, "") const testPath = f.replace(/^spec/, "..") return `\n {name: "${name}", test: require("${testPath}")},` }) .reduce((list, f) => list + f) - fs.writeFileSync(`./spec/_json/${suite}.js`, `module.exports = [${files}\n]\n`) + fs.writeFileSync(`./spec/_json/${suite}.js`, `module.exports = [${code}\n]\n`) } From 01b93fec1dcb4b39e3337c0379206c7926eaabf2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 7 Sep 2020 17:42:55 +0100 Subject: [PATCH 174/322] refactor: remove private fields --- lib/compile/codegen.ts | 130 ++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/lib/compile/codegen.ts b/lib/compile/codegen.ts index 30386e7efa..872148da4d 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen.ts @@ -12,23 +12,23 @@ export type Block = Code | (() => void) export type Code = _Code | Name class _Code { - #str: string + _str: string constructor(s: string) { - this.#str = s + this._str = s } toString(): string { - return this.#str + return this._str } isQuoted(): boolean { - const len = this.#str.length - return len >= 2 && this.#str[0] === '"' && this.#str[len - 1] === '"' + const len = this._str.length + return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"' } add(c: _Code): void { - this.#str += c.#str + this._str += c._str } } @@ -85,7 +85,7 @@ type ValueReference = unknown // possibly make CodeGen parameterized type on thi export interface NameValue { ref?: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used - code?: Code // this is the code creating the value needed for standalone code without closure - can be a primitive value, function or import (`require`) + code?: Code // this is the code creating the value needed for standalone code wit_out closure - can be a primitive value, function or import (`require`) } export interface Scope { @@ -142,26 +142,26 @@ export interface CodeGenOptions { } export class CodeGen { - #names: {[prefix: string]: NameGroup} = {} - #valuePrefixes: {[prefix: string]: Name} = {} - #out = "" - #blocks: BlockKind[] = [] - #blockStarts: number[] = [] - #n = "" + _names: {[prefix: string]: NameGroup} = {} + _valuePrefixes: {[prefix: string]: Name} = {} + _out = "" + _blocks: BlockKind[] = [] + _blockStarts: number[] = [] + _n = "" opts: CodeGenOptions constructor(opts: CodeGenOptions = {}) { this.opts = opts - if (opts.lines) this.#n = "\n" + if (opts.lines) this._n = "\n" } toString(): string { - return this.#out + return this._out } _nameGroup(prefix: string): NameGroup { - let ng = this.#names[prefix] - if (!ng) ng = this.#names[prefix] = {prefix, index: 0} + let ng = this._names[prefix] + if (!ng) ng = this._names[prefix] = {prefix, index: 0} return ng } @@ -179,7 +179,7 @@ export class CodeGen { const valueKey = key ?? ref ?? code if (!valueKey) throw new Error("CodeGen: ref or code must be passed in value") const ng = this._nameGroup(prefix) - this.#valuePrefixes[prefix] = new Name(prefix) + this._valuePrefixes[prefix] = new Name(prefix) if (!ng.values) { ng.values = new Map() } else { @@ -197,7 +197,7 @@ export class CodeGen { if (v.ref) { if (!scope[prefix]) scope[prefix] = [] scope[prefix][i] = v.ref - const prefName = this.#valuePrefixes[prefix] + const prefName = this._valuePrefixes[prefix] return _`${scopeName}.${prefName}[${i}]` } if (v.code) return v.code @@ -215,9 +215,9 @@ export class CodeGen { _reduceValues(valueCode: (n: NameRec, pref: string, index: number) => Code): Code { let code: Code = nil - for (const prefix in this.#valuePrefixes) { + for (const prefix in this._valuePrefixes) { let i = 0 - const values = this.#names[prefix].values + const values = this._names[prefix].values if (!values) throw new Error("ajv implementation error") values.forEach((rec: NameRec) => { code = _`${code}const ${rec.name} = ${valueCode(rec, prefix, i++)};` @@ -233,8 +233,8 @@ export class CodeGen { _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { const name = this._toName(nameOrPrefix) if (this.opts.es5) varKind = varKinds.var - if (rhs === undefined) this.#out += `${varKind} ${name};` + this.#n - else this.#out += `${varKind} ${name} = ${rhs};` + this.#n + if (rhs === undefined) this._out += `${varKind} ${name};` + this._n + else this._out += `${varKind} ${name} = ${rhs};` + this._n return name } @@ -251,26 +251,26 @@ export class CodeGen { } assign(name: Code, rhs: SafeExpr): CodeGen { - this.#out += `${name} = ${rhs};` + this.#n + this._out += `${name} = ${rhs};` + this._n return this } code(c: Block | SafeExpr): CodeGen { if (typeof c == "function") c() - else this.#out += c + ";" + this.#n + else this._out += c + ";" + this._n return this } if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen { - this.#blocks.push(BlockKind.If) - this.#out += `if(${condition}){` + this.#n + this._blocks.push(BlockKind.If) + this._out += `if(${condition}){` + this._n if (thenBody && elseBody) { this.code(thenBody).else().code(elseBody).endIf() } else if (thenBody) { this.code(thenBody).endIf() } else if (elseBody) { - throw new Error('CodeGen: "else" body without "then" body') + throw new Error('CodeGen: "else" body wit_out "then" body') } return this } @@ -281,30 +281,30 @@ export class CodeGen { } elseIf(condition: Code): CodeGen { - if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"') - this.#out += `}else if(${condition}){` + this.#n + if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" wit_out "if"') + this._out += `}else if(${condition}){` + this._n return this } else(): CodeGen { - if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" without "if"') + if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" wit_out "if"') this._lastBlock = BlockKind.Else - this.#out += "}else{" + this.#n + this._out += "}else{" + this._n return this } endIf(): CodeGen { // TODO possibly remove empty branches here const b = this._lastBlock - if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" without "if"') - this.#blocks.pop() - this.#out += "}" + this.#n + if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" wit_out "if"') + this._blocks.pop() + this._out += "}" + this._n return this } for(iteration: Code, forBody?: Block): CodeGen { - this.#blocks.push(BlockKind.For) - this.#out += `for(${iteration}){` + this.#n + this._blocks.push(BlockKind.For) + this._out += `for(${iteration}){` + this._n if (forBody) this.code(forBody).endFor() return this } @@ -346,8 +346,8 @@ export class CodeGen { } _loop(forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { - this.#blocks.push(BlockKind.For) - this.#out += forCode + this.#n + this._blocks.push(BlockKind.For) + this._out += forCode + this._n forBody(name) this.endFor() return this @@ -355,95 +355,95 @@ export class CodeGen { endFor(): CodeGen { const b = this._lastBlock - if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"') - this.#blocks.pop() - this.#out += "}" + this.#n + if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" wit_out "for"') + this._blocks.pop() + this._out += "}" + this._n return this } label(label?: Code): CodeGen { - this.#out += label + ":" + this.#n + this._out += label + ":" + this._n return this } break(label?: Code): CodeGen { - this.#out += (label ? `break ${label};` : "break;") + this.#n + this._out += (label ? `break ${label};` : "break;") + this._n return this } return(value: Block | SafeExpr): CodeGen { - this.#out += "return " + this._out += "return " this.code(value) - this.#out += ";" + this.#n + this._out += ";" + this._n return this } try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen { - if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') - this.#out += "try{" + this.#n + if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" wit_out "catch" and "finally"') + this._out += "try{" + this._n this.code(tryBody) if (catchCode) { const err = this.name("e") - this.#out += `}catch(${err}){` + this.#n + this._out += `}catch(${err}){` + this._n catchCode(err) } if (finallyCode) { - this.#out += "}finally{" + this.#n + this._out += "}finally{" + this._n this.code(finallyCode) } - this.#out += "}" + this.#n + this._out += "}" + this._n return this } throw(err: Code): CodeGen { - this.#out += `throw ${err};` + this.#n + this._out += `throw ${err};` + this._n return this } block(body?: Block, expectedToClose?: number): CodeGen { - this.#blockStarts.push(this.#blocks.length) + this._blockStarts.push(this._blocks.length) if (body) this.code(body).endBlock(expectedToClose) return this } endBlock(expectedToClose?: number): CodeGen { // TODO maybe close blocks one by one, eliminating empty branches - const len = this.#blockStarts.pop() + const len = this._blockStarts.pop() if (len === undefined) throw new Error("CodeGen: not in block sequence") - const toClose = this.#blocks.length - len + const toClose = this._blocks.length - len if (toClose < 0 || (expectedToClose !== undefined && toClose !== expectedToClose)) { throw new Error("CodeGen: block sequence already ended or incorrect number of blocks") } - this.#blocks.length = len - if (toClose > 0) this.#out += "}".repeat(toClose) + this.#n + this._blocks.length = len + if (toClose > 0) this._out += "}".repeat(toClose) + this._n return this } func(name: Name, args: Code = nil, async?: boolean, funcBody?: Block): CodeGen { - this.#blocks.push(BlockKind.Func) - this.#out += `${async ? "async " : ""}function ${name}(${args}){` + this.#n + this._blocks.push(BlockKind.Func) + this._out += `${async ? "async " : ""}function ${name}(${args}){` + this._n if (funcBody) this.code(funcBody).endFunc() return this } endFunc(): CodeGen { const b = this._lastBlock - if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" without "func"') - this.#blocks.pop() - this.#out += "}" + this.#n + if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" wit_out "func"') + this._blocks.pop() + this._out += "}" + this._n return this } get _lastBlock(): BlockKind { - return this.#blocks[this._last()] + return this._blocks[this._last()] } set _lastBlock(b: BlockKind) { - this.#blocks[this._last()] = b + this._blocks[this._last()] = b } _last(): number { - const len = this.#blocks.length + const len = this._blocks.length if (len === 0) throw new Error("CodeGen: not in block") return len - 1 } From ddc035bc823beef6fe1fb7d207c8f87caa9573e0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 7 Sep 2020 19:52:36 +0100 Subject: [PATCH 175/322] refactor: tests to typescript --- .eslintrc.yml | 1 + lib/ajv.ts | 2 +- lib/compile/error_classes.ts | 4 +- lib/types.ts | 30 ++++++------ package.json | 4 +- scripts/prepare-tests | 2 +- spec/{after_test.js => after_test.ts} | 11 +++-- spec/ajv-async.js | 4 -- spec/ajv.js | 4 -- spec/{ajv.spec.js => ajv.spec.ts} | 22 ++++----- spec/ajv.ts | 8 ++++ spec/ajv_async_instances.js | 16 ------- spec/ajv_async_instances.ts | 16 +++++++ spec/{ajv_instances.js => ajv_instances.ts} | 16 +++---- spec/{ajv_options.js => ajv_options.ts} | 6 ++- spec/{async.spec.js => async.spec.ts} | 20 ++++---- ..._schemas.spec.js => async_schemas.spec.ts} | 16 +++---- ...alidate.spec.js => async_validate.spec.ts} | 27 ++++++----- spec/boolean.spec.ts | 14 +++--- spec/chai.js | 3 -- spec/chai.ts | 8 ++++ spec/{coercion.spec.js => coercion.spec.ts} | 18 ++++---- spec/{errors.spec.js => errors.spec.ts} | 46 ++++++++++--------- spec/{extras.spec.js => extras.spec.ts} | 14 +++--- ..._addKeyword_and_schema_without_id.spec.ts} | 6 +-- ..._allErrors_custom_keyword_skipped.spec.ts} | 36 +++++++-------- ...ion.spec.js => 182_nan_validation.spec.ts} | 10 ++-- ...204_options_schemas_data_together.spec.ts} | 6 +-- ...spec.js => 210_mutual_recur_frags.spec.ts} | 8 ++-- ...240_mutual_recur_frags_common_ref.spec.ts} | 8 ++-- ... 259_validate_meta_against_itself.spec.ts} | 6 +-- ... 273_error_schemaPath_refd_schema.spec.ts} | 8 ++-- ... 342_uniqueItems_non-json_objects.spec.ts} | 6 +-- ...s => 485_type_validation_priority.spec.ts} | 8 ++-- ...ec.js => 50_refs_with_definitions.spec.ts} | 6 +-- ... => 521_wrong_warning_id_property.spec.ts} | 6 +-- ...533_missing_ref_error_when_ignore.spec.ts} | 8 ++-- ..._removeAdditional_to_remove_proto.spec.ts} | 6 +-- ... => 768_passContext_recursive_ref.spec.ts} | 12 ++--- ...red_refs.spec.js => 8_shared_refs.spec.ts} | 6 +-- ..._removeAdditional_custom_keywords.spec.ts} | 10 ++-- ...son-schema.spec.js => json-schema.spec.ts} | 30 +++++------- spec/keyword.spec.ts | 9 ++-- .../{comment.spec.js => comment.spec.ts} | 20 ++++---- ...ma.spec.js => meta_validateSchema.spec.ts} | 26 +++++------ .../{nullable.spec.js => nullable.spec.ts} | 6 +-- ...as.spec.js => options_add_schemas.spec.ts} | 14 +++--- ...ions_code.spec.js => options_code.spec.ts} | 24 +++++----- ...ions_refs.spec.js => options_refs.spec.ts} | 46 +++++++++---------- ...ting.spec.js => options_reporting.spec.ts} | 32 ++++++------- ...ion.spec.js => options_validation.spec.ts} | 24 +++++----- ...operties.spec.js => ownProperties.spec.ts} | 45 +++++++++--------- ...ional.spec.js => removeAdditional.spec.ts} | 26 +++++------ .../{schemaId.spec.js => schemaId.spec.ts} | 12 ++--- .../{strict.spec.js => strict.spec.ts} | 20 ++++---- ...efaults.spec.js => strictDefaults.spec.ts} | 44 +++++++++--------- ...eywords.spec.js => strictKeywords.spec.ts} | 20 ++++---- ...tNumbers.spec.js => strictNumbers.spec.ts} | 12 ++--- ...Formats.spec.js => unknownFormats.spec.ts} | 20 ++++---- ...seDefaults.spec.js => useDefaults.spec.ts} | 29 +++++------- spec/{resolve.spec.js => resolve.spec.ts} | 11 ++--- ...ema-tests.spec.js => schema-tests.spec.ts} | 17 ++++--- spec/{security.spec.js => security.spec.ts} | 15 +++--- spec/tsconfig.json | 2 +- 64 files changed, 446 insertions(+), 526 deletions(-) rename spec/{after_test.js => after_test.ts} (73%) delete mode 100644 spec/ajv-async.js delete mode 100644 spec/ajv.js rename spec/{ajv.spec.js => ajv.spec.ts} (97%) create mode 100644 spec/ajv.ts delete mode 100644 spec/ajv_async_instances.js create mode 100644 spec/ajv_async_instances.ts rename spec/{ajv_instances.js => ajv_instances.ts} (64%) rename spec/{ajv_options.js => ajv_options.ts} (76%) rename spec/{async.spec.js => async.spec.ts} (96%) rename spec/{async_schemas.spec.js => async_schemas.spec.ts} (89%) rename spec/{async_validate.spec.js => async_validate.spec.ts} (94%) delete mode 100644 spec/chai.js create mode 100644 spec/chai.ts rename spec/{coercion.spec.js => coercion.spec.ts} (96%) rename spec/{errors.spec.js => errors.spec.ts} (96%) rename spec/{extras.spec.js => extras.spec.ts} (62%) rename spec/issues/{1001_addKeyword_and_schema_without_id.spec.js => 1001_addKeyword_and_schema_without_id.spec.ts} (84%) rename spec/issues/{181_allErrors_custom_keyword_skipped.spec.js => 181_allErrors_custom_keyword_skipped.spec.ts} (66%) rename spec/issues/{182_nan_validation.spec.js => 182_nan_validation.spec.ts} (87%) rename spec/issues/{204_options_schemas_data_together.spec.js => 204_options_schemas_data_together.spec.ts} (87%) rename spec/issues/{210_mutual_recur_frags.spec.js => 210_mutual_recur_frags.spec.ts} (94%) rename spec/issues/{240_mutual_recur_frags_common_ref.spec.js => 240_mutual_recur_frags_common_ref.spec.ts} (98%) rename spec/issues/{259_validate_meta_against_itself.spec.js => 259_validate_meta_against_itself.spec.ts} (78%) rename spec/issues/{273_error_schemaPath_refd_schema.spec.js => 273_error_schemaPath_refd_schema.spec.ts} (85%) rename spec/issues/{342_uniqueItems_non-json_objects.spec.js => 342_uniqueItems_non-json_objects.spec.ts} (93%) rename spec/issues/{485_type_validation_priority.spec.js => 485_type_validation_priority.spec.ts} (84%) rename spec/issues/{50_refs_with_definitions.spec.js => 50_refs_with_definitions.spec.ts} (93%) rename spec/issues/{521_wrong_warning_id_property.spec.js => 521_wrong_warning_id_property.spec.ts} (88%) rename spec/issues/{533_missing_ref_error_when_ignore.spec.js => 533_missing_ref_error_when_ignore.spec.ts} (83%) rename spec/issues/{743_removeAdditional_to_remove_proto.spec.js => 743_removeAdditional_to_remove_proto.spec.ts} (90%) rename spec/issues/{768_passContext_recursive_ref.spec.js => 768_passContext_recursive_ref.spec.ts} (93%) rename spec/issues/{8_shared_refs.spec.js => 8_shared_refs.spec.ts} (91%) rename spec/issues/{955_removeAdditional_custom_keywords.spec.js => 955_removeAdditional_custom_keywords.spec.ts} (84%) rename spec/{json-schema.spec.js => json-schema.spec.ts} (77%) rename spec/options/{comment.spec.js => comment.spec.ts} (85%) rename spec/options/{meta_validateSchema.spec.js => meta_validateSchema.spec.ts} (84%) rename spec/options/{nullable.spec.js => nullable.spec.ts} (95%) rename spec/options/{options_add_schemas.spec.js => options_add_schemas.spec.ts} (93%) rename spec/options/{options_code.spec.js => options_code.spec.ts} (88%) rename spec/options/{options_refs.spec.js => options_refs.spec.ts} (73%) rename spec/options/{options_reporting.spec.js => options_reporting.spec.ts} (86%) rename spec/options/{options_validation.spec.js => options_validation.spec.ts} (82%) rename spec/options/{ownProperties.spec.js => ownProperties.spec.ts} (79%) rename spec/options/{removeAdditional.spec.js => removeAdditional.spec.ts} (84%) rename spec/options/{schemaId.spec.js => schemaId.spec.ts} (77%) rename spec/options/{strict.spec.js => strict.spec.ts} (88%) rename spec/options/{strictDefaults.spec.js => strictDefaults.spec.ts} (83%) rename spec/options/{strictKeywords.spec.js => strictKeywords.spec.ts} (87%) rename spec/options/{strictNumbers.spec.js => strictNumbers.spec.ts} (83%) rename spec/options/{unknownFormats.spec.js => unknownFormats.spec.ts} (87%) rename spec/options/{useDefaults.spec.js => useDefaults.spec.ts} (87%) rename spec/{resolve.spec.js => resolve.spec.ts} (97%) rename spec/{schema-tests.spec.js => schema-tests.spec.ts} (79%) rename spec/{security.spec.js => security.spec.ts} (59%) diff --git a/.eslintrc.yml b/.eslintrc.yml index 86043a7769..d7e6521c56 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -29,6 +29,7 @@ overrides: "@typescript-eslint/no-this-alias": off "@typescript-eslint/no-implied-eval": off "@typescript-eslint/no-floating-promises": off + "@typescript-eslint/no-explicit-any": off no-invalid-this: off rules: block-scoped-var: error diff --git a/lib/ajv.ts b/lib/ajv.ts index 0cfe3e3853..81418a932a 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -117,7 +117,7 @@ export default class Ajv { schema: SchemaObject, metaOrCallback?: boolean | CompileAsyncCallback, // optional true to compile meta-schema; this parameter can be skipped callback?: CompileAsyncCallback - ): Promise { + ): Promise { /* eslint no-shadow: 0 */ const self = this if (typeof this._opts.loadSchema != "function") { diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts index 844feaf8c1..e1da3682ed 100644 --- a/lib/compile/error_classes.ts +++ b/lib/compile/error_classes.ts @@ -2,11 +2,11 @@ import {ErrorObject} from "../types" import {resolveUrl, normalizeId, getFullPath} from "./resolve" export class ValidationError extends Error { - errors: ErrorObject[] + errors: Partial[] ajv: true validation: true - constructor(errors: ErrorObject[]) { + constructor(errors: Partial[]) { super("validation failed") this.errors = errors this.ajv = this.validation = true diff --git a/lib/types.ts b/lib/types.ts index 2573a45b8c..58cce992bb 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -37,7 +37,7 @@ export interface CurrentOptions { removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" - meta?: SchemaObject | false + meta?: SchemaObject | boolean defaultMeta?: string | SchemaObject validateSchema?: boolean | "log" addUsedSchema?: boolean @@ -89,13 +89,13 @@ interface SourceCode { export interface ValidateFunction { ( - this: Ajv | unknown, - data: unknown, + this: Ajv | any, + data: any, dataPath?: string, - parentData?: Record | unknown[], + parentData?: Record | any[], parentDataProperty?: string | number, - rootData?: Record | unknown[] - ): boolean | Promise + rootData?: Record | any[] + ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] refs?: {[ref: string]: number | undefined} @@ -111,14 +111,14 @@ export interface ValidateWrapper extends ValidateFunction { export interface SchemaValidateFunction { ( - schema: unknown, - data: unknown, + schema: any, + data: any, parentSchema?: SchemaObject, dataPath?: string, - parentData?: Record | unknown[], + parentData?: Record | any[], parentDataProperty?: string | number, - rootData?: Record | unknown[] - ): boolean | Promise + rootData?: Record | any[] + ): boolean | Promise errors?: Partial[] } @@ -192,14 +192,10 @@ export interface CodeKeywordDefinition extends _KeywordDef { trackErrors?: boolean } -export type MacroKeywordFunc = ( - schema: unknown, - parentSchema: SchemaObject, - it: SchemaCtx -) => Schema +export type MacroKeywordFunc = (schema: any, parentSchema: SchemaObject, it: SchemaCtx) => Schema export type CompileKeywordFunc = ( - schema: unknown, + schema: any, parentSchema: SchemaObject, it: SchemaObjCtx ) => ValidateFunction diff --git a/package.json b/package.json index 74d765146c..8fc4695357 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint lib/{**/,}*.ts spec/{**/,}*.{ts,js} scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint lib/{**/,}*.ts spec/{**/,}*.*s scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", - "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register spec/{**/,}*.spec.{js,ts} -R dot", + "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register spec/{**/,}*.spec.ts -R dot", "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", "bundle": "rm -rf bundle && node ./scripts/bundle.js", diff --git a/scripts/prepare-tests b/scripts/prepare-tests index 9b6915724d..cb5aa03ecb 100755 --- a/scripts/prepare-tests +++ b/scripts/prepare-tests @@ -9,4 +9,4 @@ echo Preparing browser tests: find spec -type f -name '*.spec.*s' | \ xargs -I {} sh -c \ -'export f="{}"; echo $f; browserify $f -p [ tsify -p ./spec/tsconfig.json ] -x ajv -u buffer -o $(echo $f | sed -e "s/spec/.browser/");' +'export f="{}"; echo $f; browserify $f -p [ tsify -p ./spec/tsconfig.json ] -x ajv -u buffer -o $(echo $f | sed -e "s/spec/.browser/" | sed -e "s/.spec.ts/.spec.js/");' diff --git a/spec/after_test.js b/spec/after_test.ts similarity index 73% rename from spec/after_test.js rename to spec/after_test.ts index 4d566430d6..24175e3914 100644 --- a/spec/after_test.js +++ b/spec/after_test.ts @@ -1,12 +1,15 @@ -"use strict" - const should = require("./chai").should() -exports.error = function (res) { +module.exports = { + error: afterError, + each: afterEach, +} + +export function afterError(res): void { console.log("ajv options:", res.validator._opts) } -exports.each = function (res) { +export function afterEach(res): void { // console.log(res.errors); res.valid.should.be.a("boolean") if (res.valid === true) { diff --git a/spec/ajv-async.js b/spec/ajv-async.js deleted file mode 100644 index 9773e5e6a4..0000000000 --- a/spec/ajv-async.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict" - -module.exports = - typeof window == "object" ? window.ajvAsync : require("" + "ajv-async") diff --git a/spec/ajv.js b/spec/ajv.js deleted file mode 100644 index fbd6e578bb..0000000000 --- a/spec/ajv.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict" - -module.exports = - typeof window == "object" ? window.Ajv : require("" + "../dist/ajv") diff --git a/spec/ajv.spec.js b/spec/ajv.spec.ts similarity index 97% rename from spec/ajv.spec.js rename to spec/ajv.spec.ts index 4eec472d7f..29cdfcc4d3 100644 --- a/spec/ajv.spec.js +++ b/spec/ajv.spec.ts @@ -1,21 +1,17 @@ -"use strict" - -const Ajv = require("./ajv"), - should = require("./chai").should(), - stableStringify = require("fast-json-stable-stringify") - -const codegen = require("../dist/compile/codegen") -const {_} = codegen +import _Ajv from "./ajv" +import stableStringify from "fast-json-stable-stringify" +import {_} from "../dist/compile/codegen" +const should = require("./chai").should() describe("Ajv", () => { let ajv beforeEach(() => { - ajv = new Ajv() + ajv = new _Ajv() }) it("should create instance", () => { - ajv.should.be.instanceof(Ajv) + ajv.should.be.instanceof(_Ajv) }) describe("compile method", () => { @@ -55,10 +51,10 @@ describe("Ajv", () => { }) it("should throw if compiled schema has an invalid JavaScript code", () => { - const _ajv = new Ajv({logger: false}) + const _ajv = new _Ajv({logger: false}) _ajv.addKeyword({keyword: "even", code: badEvenCode}) let schema = {even: true} - const validate = _ajv.compile(schema) + const validate: any = _ajv.compile(schema) validate(2).should.equal(true) validate(3).should.equal(false) @@ -457,7 +453,7 @@ describe("Ajv", () => { }) it("should validate numbers with format via $data", () => { - ajv = new Ajv({$data: true}) + ajv = new _Ajv({$data: true}) ajv.addFormat("positive", { type: "number", validate: function (x) { diff --git a/spec/ajv.ts b/spec/ajv.ts new file mode 100644 index 0000000000..33f9a4ffd6 --- /dev/null +++ b/spec/ajv.ts @@ -0,0 +1,8 @@ +import type Ajv from "../dist/ajv" + +const AjvClass: typeof Ajv = + typeof window == "object" ? (window as any).Ajv : require("" + "../dist/ajv") + +export default AjvClass + +module.exports = AjvClass diff --git a/spec/ajv_async_instances.js b/spec/ajv_async_instances.js deleted file mode 100644 index 387eb6c196..0000000000 --- a/spec/ajv_async_instances.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict" - -const getAjvInstances = require("./ajv_instances") - -module.exports = getAjvSyncInstances - -function getAjvSyncInstances(extraOpts) { - return getAjvInstances( - { - strict: false, - allErrors: true, - codegen: {lines: true}, - }, - extraOpts - ) -} diff --git a/spec/ajv_async_instances.ts b/spec/ajv_async_instances.ts new file mode 100644 index 0000000000..974bc9951e --- /dev/null +++ b/spec/ajv_async_instances.ts @@ -0,0 +1,16 @@ +import getAjvInstances from "./ajv_instances" +import type Ajv from "../dist/ajv" +import type {Options} from "../dist/types" + +export default function getAjvSyncInstances(extraOpts?: Options): Ajv[] { + return getAjvInstances( + { + strict: false, + allErrors: true, + codegen: {lines: true}, + }, + extraOpts + ) +} + +module.exports = getAjvSyncInstances diff --git a/spec/ajv_instances.js b/spec/ajv_instances.ts similarity index 64% rename from spec/ajv_instances.js rename to spec/ajv_instances.ts index f6dcfb6725..26e6f2883c 100644 --- a/spec/ajv_instances.js +++ b/spec/ajv_instances.ts @@ -1,14 +1,12 @@ -"use strict" +import _Ajv from "./ajv" +import type Ajv from "../dist/ajv" +import type {Options} from "../dist/types" -const Ajv = require("./ajv") - -module.exports = getAjvInstances - -function getAjvInstances(options, extraOpts = {}) { +export default function getAjvInstances(options: Options, extraOpts: Options = {}): Ajv[] { return _getAjvInstances(options, {...extraOpts, logger: false, codegen: {lines: true}}) } -function _getAjvInstances(opts, useOpts) { +function _getAjvInstances(opts: Options, useOpts: Options): Ajv[] { const optNames = Object.keys(opts) if (optNames.length) { opts = Object.assign({}, opts) @@ -20,5 +18,7 @@ function _getAjvInstances(opts, useOpts) { instances1 = _getAjvInstances(opts, useOpts1) return instances.concat(instances1) } - return [new Ajv(useOpts)] + return [new _Ajv(useOpts)] } + +module.exports = getAjvInstances diff --git a/spec/ajv_options.js b/spec/ajv_options.ts similarity index 76% rename from spec/ajv_options.js rename to spec/ajv_options.ts index 8480806f64..8118488e01 100644 --- a/spec/ajv_options.js +++ b/spec/ajv_options.ts @@ -1,9 +1,9 @@ -"use strict" +import type {Options} from "../dist/types" const isBrowser = typeof window == "object" const fullTest = !isBrowser && process.env.AJV_FULL_TEST -const options = fullTest +const options: Options = fullTest ? { allErrors: true, verbose: true, @@ -13,4 +13,6 @@ const options = fullTest } : {allErrors: true, codegen: {es5: true, lines: true}} +export default options + module.exports = options diff --git a/spec/async.spec.js b/spec/async.spec.ts similarity index 96% rename from spec/async.spec.js rename to spec/async.spec.ts index b41b490374..f6ef037ba6 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.ts @@ -1,7 +1,7 @@ -"use strict" +import _Ajv from "./ajv" +import type {SchemaObject} from "../dist/types" -const Ajv = require("./ajv"), - should = require("./chai").should() +const should = require("./chai").should() describe("compileAsync method", () => { let ajv, loadCallCount @@ -81,7 +81,7 @@ describe("compileAsync method", () => { beforeEach(() => { loadCallCount = 0 - ajv = new Ajv({loadSchema: loadSchema}) + ajv = new _Ajv({loadSchema}) }) it("should compile schemas loading missing schemas with options.loadSchema function", () => { @@ -216,11 +216,11 @@ describe("compileAsync method", () => { type: "integer", minimum: 2, } - let beforeCallback1 = false + let beforeCallback1: any = false const p1 = ajv.compileAsync(schema).then((validate) => { beforeCallback1.should.equal(true) spec(validate) - let beforeCallback2 = false + let beforeCallback2: any = false const p2 = ajv.compileAsync(schema).then((_validate) => { beforeCallback2.should.equal(true) spec(_validate) @@ -269,7 +269,7 @@ describe("compileAsync method", () => { type: "integer", minimum: 2, } - ajv = new Ajv() + ajv = new _Ajv() should.throw(() => { ajv.compileAsync(schema, () => { done(new Error("it should have thrown exception")) @@ -318,7 +318,7 @@ describe("compileAsync method", () => { a: {$ref: "object.json"}, }, } - ajv = new Ajv({loadSchema: badLoadSchema}) + ajv = new _Ajv({loadSchema: badLoadSchema}) ajv.compileAsync(schema, shouldFail(done)) function badLoadSchema() { @@ -382,7 +382,7 @@ describe("compileAsync method", () => { a: {$ref: "object.json"}, }, } - ajv = new Ajv({loadSchema: badLoadSchema}) + ajv = new _Ajv({loadSchema: badLoadSchema}) return shouldReject(ajv.compileAsync(schema)) function badLoadSchema() { @@ -427,7 +427,7 @@ describe("compileAsync method", () => { }) }) - function loadSchema(uri) { + function loadSchema(uri: string): Promise { loadCallCount++ return new Promise((resolve, reject) => { setTimeout(() => { diff --git a/spec/async_schemas.spec.js b/spec/async_schemas.spec.ts similarity index 89% rename from spec/async_schemas.spec.js rename to spec/async_schemas.spec.ts index 94a16ff7b0..fea359999a 100644 --- a/spec/async_schemas.spec.js +++ b/spec/async_schemas.spec.ts @@ -1,11 +1,9 @@ -"use strict" +import getAjvAsyncInstances from "./ajv_async_instances" +import jsonSchemaTest from "json-schema-test" +import {afterError} from "./after_test" +import Ajv from "./ajv" -const jsonSchemaTest = require("json-schema-test"), - getAjvInstances = require("./ajv_async_instances"), - Ajv = require("./ajv"), - after = require("./after_test") - -const instances = getAjvInstances({$data: true}) +const instances = getAjvAsyncInstances({$data: true}) instances.forEach(addAsyncFormatsAndKeywords) @@ -16,11 +14,11 @@ jsonSchemaTest(instances, { async: true, asyncValid: "data", assert: require("./chai").assert, - afterError: after.error, + afterError, // afterEach: after.each, cwd: __dirname, hideFolder: "async/", - timeout: 90000, + timeout: 10000, }) function addAsyncFormatsAndKeywords(ajv) { diff --git a/spec/async_validate.spec.js b/spec/async_validate.spec.ts similarity index 94% rename from spec/async_validate.spec.js rename to spec/async_validate.spec.ts index 8e904b900b..f7c494587b 100644 --- a/spec/async_validate.spec.js +++ b/spec/async_validate.spec.ts @@ -1,15 +1,14 @@ -"use strict" +import getAjvAsyncInstances from "./ajv_async_instances" +import _Ajv from "./ajv" -const Ajv = require("./ajv"), - getAjvInstances = require("./ajv_async_instances"), - should = require("./chai").should() +const should = require("./chai").should() describe("async schemas, formats and keywords", function () { this.timeout(30000) let ajv, instances beforeEach(() => { - instances = getAjvInstances() + instances = getAjvAsyncInstances() ajv = instances[0] }) @@ -37,7 +36,7 @@ describe("async schemas, formats and keywords", function () { }) it("should fail compilation if async schema is inside sync schema", () => { - const schema = { + const schema: any = { properties: { foo: { $async: true, @@ -62,7 +61,7 @@ describe("async schemas, formats and keywords", function () { it("should fail compilation if async format is inside sync schema", () => { instances.forEach((_ajv) => { - const schema = { + const schema: any = { type: "string", format: "english_word", } @@ -99,7 +98,7 @@ describe("async schemas, formats and keywords", function () { it("should fail compilation if async keyword is inside sync schema", () => { instances.forEach((_ajv) => { - const schema = { + const schema: any = { type: "object", properties: { userId: { @@ -178,14 +177,14 @@ describe("async schemas, formats and keywords", function () { keyword: "idExistsWithError", message: "id not found in table " + _table, } - return Promise.reject(new Ajv.ValidationError([error])) + return Promise.reject(new _Ajv.ValidationError([error])) } } }) describe("async referenced schemas", () => { beforeEach(() => { - instances = getAjvInstances({inlineRefs: false, extendRefs: "ignore"}) + instances = getAjvAsyncInstances({inlineRefs: false, extendRefs: "ignore"}) addFormatEnglishWord() }) @@ -318,7 +317,7 @@ describe("async schemas, formats and keywords", function () { }) it("should fail compilation if sync schema references async schema", () => { - const schema = { + const schema: any = { $id: "http://e.com/obj.json#", type: "object", properties: { @@ -354,7 +353,7 @@ describe("async schemas, formats and keywords", function () { ajv.compile(schema) }) - function recursiveTest(schema, refSchema) { + function recursiveTest(schema, refSchema?) { return repeat(() => { return Promise.all( instances.map((_ajv) => { @@ -423,9 +422,9 @@ function shouldBeValid(p, data) { } const SHOULD_BE_INVALID = "test: should be invalid" -function shouldBeInvalid(p, expectedMessages) { +function shouldBeInvalid(p, expectedMessages?: string[]) { return checkNotValid(p).then((err) => { - err.should.be.instanceof(Ajv.ValidationError) + err.should.be.instanceof(_Ajv.ValidationError) err.errors.should.be.an("array") err.validation.should.equal(true) if (expectedMessages) { diff --git a/spec/boolean.spec.ts b/spec/boolean.spec.ts index f45d5e89f0..358e8cd5db 100644 --- a/spec/boolean.spec.ts +++ b/spec/boolean.spec.ts @@ -1,17 +1,17 @@ -"use strict" +import _Ajv from "./ajv" +import type Ajv from "../dist/ajv" -const Ajv = require("./ajv") require("./chai").should() describe("boolean schemas", () => { - let ajvs + let ajvs: Ajv[] before(() => { ajvs = [ - new Ajv(), - new Ajv({allErrors: true}), - new Ajv({inlineRefs: false}), - new Ajv({strict: false}), + new _Ajv(), + new _Ajv({allErrors: true}), + new _Ajv({inlineRefs: false}), + new _Ajv({strict: false}), ] }) diff --git a/spec/chai.js b/spec/chai.js deleted file mode 100644 index 2bb98a87e9..0000000000 --- a/spec/chai.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict" - -module.exports = typeof window == "object" ? window.chai : require("" + "chai") diff --git a/spec/chai.ts b/spec/chai.ts new file mode 100644 index 0000000000..474b9d3937 --- /dev/null +++ b/spec/chai.ts @@ -0,0 +1,8 @@ +import "chai" + +const chai: Chai.ChaiStatic = + typeof window == "object" ? (window as any).chai : require("" + "chai") + +export default chai + +module.exports = chai diff --git a/spec/coercion.spec.js b/spec/coercion.spec.ts similarity index 96% rename from spec/coercion.spec.js rename to spec/coercion.spec.ts index c2fb3b04cf..c48f4b62bb 100644 --- a/spec/coercion.spec.js +++ b/spec/coercion.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("./ajv") +import _Ajv from "./ajv" require("./chai").should() const coercionRules = { @@ -181,8 +179,8 @@ describe("Type coercion", () => { let ajv, fullAjv, instances beforeEach(() => { - ajv = new Ajv({coerceTypes: true, verbose: true}) - fullAjv = new Ajv({coerceTypes: true, verbose: true, allErrors: true}) + ajv = new _Ajv({coerceTypes: true, verbose: true}) + fullAjv = new _Ajv({coerceTypes: true, verbose: true, allErrors: true}) instances = [ajv, fullAjv] }) @@ -197,8 +195,8 @@ describe("Type coercion", () => { }) it("should coerce scalar values (coerceTypes = array)", () => { - ajv = new Ajv({coerceTypes: "array", verbose: true}) - fullAjv = new Ajv({coerceTypes: "array", verbose: true, allErrors: true}) + ajv = new _Ajv({coerceTypes: "array", verbose: true}) + fullAjv = new _Ajv({coerceTypes: "array", verbose: true, allErrors: true}) instances = [ajv, fullAjv] testRules(coercionArrayRules, (test, schema, canCoerce, toType, fromType) => { @@ -340,18 +338,18 @@ describe("Type coercion", () => { } instances.forEach((_ajv) => { - const data = {foo: "123", bar: "bar"} + const data: any = {foo: "123", bar: "bar"} _ajv.validate(schema, data).should.equal(false) data.should.eql({foo: 123, bar: "bar"}) - const data2 = ["123", "bar"] + const data2: any = ["123", "bar"] _ajv.validate(schema2, data2).should.equal(false) data2.should.eql([123, "bar"]) }) }) it("should update data if the schema is in ref that is not inlined", () => { - instances.push(new Ajv({coerceTypes: true, inlineRefs: false})) + instances.push(new _Ajv({coerceTypes: true, inlineRefs: false})) const schema = { type: "object", diff --git a/spec/errors.spec.js b/spec/errors.spec.ts similarity index 96% rename from spec/errors.spec.js rename to spec/errors.spec.ts index 3c1a8d52b5..7b90976304 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.ts @@ -1,7 +1,6 @@ -"use strict" +import Ajv from "./ajv" -const Ajv = require("./ajv"), - should = require("./chai").should() +const should = require("./chai").should() describe("Validation errors", () => { let ajv, ajvJP, fullAjv @@ -175,7 +174,7 @@ describe("Validation errors", () => { }) function testRequiredLargeSchema() { - let schema = {required: []} + let schema: any = {required: []} const data = {}, invalidData1 = {}, invalidData2 = {} @@ -193,9 +192,8 @@ describe("Validation errors", () => { schema = {anyOf: [schema]} test(1, "#/anyOf/0") - function test(extraErrors, schemaPathPrefix) { - extraErrors = extraErrors || 0 - const schPath = (schemaPathPrefix || "#") + "/required" + function test(extraErrors = 0, schemaPathPrefix = "#") { + const schPath = schemaPathPrefix + "/required" const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) @@ -403,16 +401,15 @@ describe("Validation errors", () => { property: "a", deps: "foo, bar, baz", depsCount: 3, + missingProperty: missing, } - p.missingProperty = missing return p } } }) - function _testRequired(schema, schemaPathPrefix, extraErrors) { - const schPath = (schemaPathPrefix || "#") + "/required" - extraErrors = extraErrors || 0 + function _testRequired(schema, schemaPathPrefix = "#", extraErrors = 0) { + const schPath = schemaPathPrefix + "/required" const data = {foo: 1, bar: 2, baz: 3}, invalidData1 = {foo: 1, baz: 3}, @@ -455,7 +452,7 @@ describe("Validation errors", () => { }) } - function requiredMsg(prop) { + function requiredMsg(prop: string) { return `should have required property '${prop}'` } @@ -676,7 +673,7 @@ describe("Validation errors", () => { test(fullAjv, 2) }) - function test(_ajv, numErrors) { + function test(_ajv, numErrors?: number) { const schema = { type: "integer", minimum: 5, @@ -700,7 +697,7 @@ describe("Validation errors", () => { test(fullAjv, 2) }) - function test(_ajv, numErrors) { + function test(_ajv, numErrors?: number) { const schema = { type: "array", minItems: 2, @@ -724,7 +721,7 @@ describe("Validation errors", () => { test(fullAjv, 2) }) - function test(_ajv, numErrors) { + function test(_ajv, numErrors?: number) { const schema = { type: ["array", "object"], minItems: 2, @@ -940,7 +937,7 @@ describe("Validation errors", () => { it("should have correct message and params", () => { const _ajv = new Ajv({missingRefs: "fail", logger: false}) const schema = {$ref: "#/unknown"} - const validate = _ajv.compile(schema) + const validate: any = _ajv.compile(schema) shouldBeInvalid(validate, {}) shouldBeError(validate.errors[0], "$ref", "#/$ref", "", "can't resolve reference #/unknown", { ref: "#/unknown", @@ -948,14 +945,14 @@ describe("Validation errors", () => { }) }) - function testSchema1(schema, schemaPathPrefix) { + function testSchema1(schema, schemaPathPrefix = "#/properties/foo") { _testSchema1(ajv, schema, schemaPathPrefix) _testSchema1(ajvJP, schema, schemaPathPrefix) _testSchema1(fullAjv, schema, schemaPathPrefix) } function _testSchema1(_ajv, schema, schemaPathPrefix) { - const schPath = (schemaPathPrefix || "#/properties/foo") + "/type" + const schPath = schemaPathPrefix + "/type" const data = {foo: 1}, invalidData = {foo: "bar"} @@ -976,12 +973,19 @@ describe("Validation errors", () => { should.equal(validate.errors, null) } - function shouldBeInvalid(validate, data, numErrors) { + function shouldBeInvalid(validate, data, numErrors = 1) { validate(data).should.equal(false) - should.equal(validate.errors.length, numErrors || 1) + should.equal(validate.errors.length, numErrors) } - function shouldBeError(error, keyword, schemaPath, dataPath, message, params) { + function shouldBeError( + error, + keyword, + schemaPath, + dataPath, + message?: string, + params?: Record + ) { error.keyword.should.equal(keyword) error.schemaPath.should.equal(schemaPath) error.dataPath.should.equal(dataPath) diff --git a/spec/extras.spec.js b/spec/extras.spec.ts similarity index 62% rename from spec/extras.spec.js rename to spec/extras.spec.ts index 895a80c7b4..b4aec3f577 100644 --- a/spec/extras.spec.js +++ b/spec/extras.spec.ts @@ -1,9 +1,7 @@ -"use strict" - -const jsonSchemaTest = require("json-schema-test"), - getAjvInstances = require("./ajv_instances"), - options = require("./ajv_options"), - after = require("./after_test") +import getAjvInstances from "./ajv_instances" +import jsonSchemaTest from "json-schema-test" +import options from "./ajv_options" +import {afterError, afterEach} from "./after_test" const instances = getAjvInstances(options, { $data: true, @@ -15,8 +13,8 @@ jsonSchemaTest(instances, { "Extra keywords schemas tests of " + instances.length + " ajv instances with different options", suites: {extras: require("./_json/extras")}, assert: require("./chai").assert, - afterError: after.error, - afterEach: after.each, + afterError, + afterEach, cwd: __dirname, hideFolder: "extras/", timeout: 90000, diff --git a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js b/spec/issues/1001_addKeyword_and_schema_without_id.spec.ts similarity index 84% rename from spec/issues/1001_addKeyword_and_schema_without_id.spec.js rename to spec/issues/1001_addKeyword_and_schema_without_id.spec.ts index 12f76907b5..abc0b6b51c 100644 --- a/spec/issues/1001_addKeyword_and_schema_without_id.spec.js +++ b/spec/issues/1001_addKeyword_and_schema_without_id.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #1001: addKeyword breaks schema without ID", () => { @@ -11,7 +9,7 @@ describe("issue #1001: addKeyword breaks schema without ID", () => { }, } - const ajv = new Ajv() + const ajv: any = new _Ajv() ajv.addSchema(schema) ajv.addKeyword("myKeyword") ajv.getSchema("#/definitions/foo").should.be.a("function") diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js b/spec/issues/181_allErrors_custom_keyword_skipped.spec.ts similarity index 66% rename from spec/issues/181_allErrors_custom_keyword_skipped.spec.js rename to spec/issues/181_allErrors_custom_keyword_skipped.spec.ts index 3553eac4ed..f727ebec30 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.js +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.ts @@ -1,6 +1,5 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" +import {KeywordDefinition, SchemaValidateFunction} from "../../dist/types" require("../chai").should() describe("issue #181, user-defined keyword is not validated in allErrors mode if there were previous error", () => { @@ -16,27 +15,28 @@ describe("issue #181, user-defined keyword is not validated in allErrors mode if }) it("should validate keyword that creates errors", () => { + const validate: SchemaValidateFunction = (/* value */) => { + validate.errors = validate.errors || [] + validate.errors.push({ + keyword: "alwaysFails", + message: "alwaysFails error", + params: { + keyword: "alwaysFails", + }, + }) + return false + } + testKeywordErrors({ keyword: "alwaysFails", type: "object", errors: true, - validate: function v(/* value */) { - v.errors = v.errors || [] - v.errors.push({ - keyword: "alwaysFails", - message: "alwaysFails error", - params: { - keyword: "alwaysFails", - }, - }) - - return false - }, + validate, }) }) - function testKeywordErrors(def) { - const ajv = new Ajv({allErrors: true}) + function testKeywordErrors(def: KeywordDefinition): void { + const ajv = new _Ajv({allErrors: true}) ajv.addKeyword(def) @@ -45,7 +45,7 @@ describe("issue #181, user-defined keyword is not validated in allErrors mode if alwaysFails: true, } - const validate = ajv.compile(schema) + const validate: any = ajv.compile(schema) validate({foo: 1}).should.equal(false) validate.errors.should.have.length(1) diff --git a/spec/issues/182_nan_validation.spec.js b/spec/issues/182_nan_validation.spec.ts similarity index 87% rename from spec/issues/182_nan_validation.spec.js rename to spec/issues/182_nan_validation.spec.ts index cd95f7a299..df5890f96e 100644 --- a/spec/issues/182_nan_validation.spec.js +++ b/spec/issues/182_nan_validation.spec.ts @@ -1,10 +1,8 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #182, NaN validation", () => { - const ajv = new Ajv() + const ajv = new _Ajv() it("should pass minimum/maximum validation without type", () => { testNaN(ajv, {minimum: 1}, true) @@ -12,7 +10,7 @@ describe("issue #182, NaN validation", () => { }) it("should NOT pass minimum/maximum validation without type when strict: false", () => { - const _ajv = new Ajv({strict: false}) + const _ajv = new _Ajv({strict: false}) testNaN(_ajv, {minimum: 1}, false) testNaN(_ajv, {maximum: 1}, false) }) @@ -23,7 +21,7 @@ describe("issue #182, NaN validation", () => { }) it("should pass type: number validation when strict: false", () => { - const _ajv = new Ajv({strict: false}) + const _ajv = new _Ajv({strict: false}) testNaN(_ajv, {type: "number"}, true) }) diff --git a/spec/issues/204_options_schemas_data_together.spec.js b/spec/issues/204_options_schemas_data_together.spec.ts similarity index 87% rename from spec/issues/204_options_schemas_data_together.spec.js rename to spec/issues/204_options_schemas_data_together.spec.ts index 15ab14a34b..7284b676d8 100644 --- a/spec/issues/204_options_schemas_data_together.spec.js +++ b/spec/issues/204_options_schemas_data_together.spec.ts @@ -1,11 +1,9 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #204, options schemas and $data used together", () => { it("should use v5 metaschemas by default", () => { - const ajv = new Ajv({ + const ajv = new _Ajv({ schemas: [{$id: "str", type: "string"}], $data: true, }) diff --git a/spec/issues/210_mutual_recur_frags.spec.js b/spec/issues/210_mutual_recur_frags.spec.ts similarity index 94% rename from spec/issues/210_mutual_recur_frags.spec.js rename to spec/issues/210_mutual_recur_frags.spec.ts index b7377e8fa6..3dbc6fedbe 100644 --- a/spec/issues/210_mutual_recur_frags.spec.js +++ b/spec/issues/210_mutual_recur_frags.spec.ts @@ -1,11 +1,9 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #210, mutual recursive $refs that are schema fragments", () => { it("should compile and validate schema when one ref is fragment", () => { - const ajv = new Ajv() + const ajv = new _Ajv() ajv.addSchema({ $id: "foo", @@ -36,7 +34,7 @@ describe("issue #210, mutual recursive $refs that are schema fragments", () => { }) it("should compile and validate schema when both refs are fragments", () => { - const ajv = new Ajv() + const ajv = new _Ajv() ajv.addSchema({ $id: "foo", diff --git a/spec/issues/240_mutual_recur_frags_common_ref.spec.js b/spec/issues/240_mutual_recur_frags_common_ref.spec.ts similarity index 98% rename from spec/issues/240_mutual_recur_frags_common_ref.spec.js rename to spec/issues/240_mutual_recur_frags_common_ref.spec.ts index 372b0c33f2..8117d68f17 100644 --- a/spec/issues/240_mutual_recur_frags_common_ref.spec.js +++ b/spec/issues/240_mutual_recur_frags_common_ref.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #240, mutually recursive fragment refs reference a common schema", () => { @@ -38,7 +36,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema } it("should compile and validate schema when one ref is fragment", () => { - const ajv = new Ajv() + const ajv = new _Ajv() const librarySchema = { $schema: "http://json-schema.org/draft-07/schema#", @@ -131,7 +129,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema }) it("should compile and validate schema when both refs are fragments", () => { - const ajv = new Ajv() + const ajv = new _Ajv() const librarySchema = { $schema: "http://json-schema.org/draft-07/schema#", diff --git a/spec/issues/259_validate_meta_against_itself.spec.js b/spec/issues/259_validate_meta_against_itself.spec.ts similarity index 78% rename from spec/issues/259_validate_meta_against_itself.spec.js rename to spec/issues/259_validate_meta_against_itself.spec.ts index cd891340c7..7b00600bb0 100644 --- a/spec/issues/259_validate_meta_against_itself.spec.js +++ b/spec/issues/259_validate_meta_against_itself.spec.ts @@ -1,11 +1,9 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #259, support validating [meta-]schemas against themselves", () => { it('should add schema before validation if "id" is the same as "$schema"', () => { - const ajv = new Ajv({strict: false}) + const ajv = new _Ajv({strict: false}) const hyperSchema = require("../remotes/hyper-schema.json") ajv.addMetaSchema(hyperSchema) }) diff --git a/spec/issues/273_error_schemaPath_refd_schema.spec.js b/spec/issues/273_error_schemaPath_refd_schema.spec.ts similarity index 85% rename from spec/issues/273_error_schemaPath_refd_schema.spec.js rename to spec/issues/273_error_schemaPath_refd_schema.spec.ts index 78260b8dcc..26b3f2c0a2 100644 --- a/spec/issues/273_error_schemaPath_refd_schema.spec.js +++ b/spec/issues/273_error_schemaPath_refd_schema.spec.ts @@ -1,12 +1,10 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe.skip("issue #273, schemaPath in error in referenced schema", () => { it("should have canonic reference with hash after file name", () => { - test(new Ajv()) - test(new Ajv({inlineRefs: false})) + test(new _Ajv()) + test(new _Ajv({inlineRefs: false})) function test(ajv) { const schema = { diff --git a/spec/issues/342_uniqueItems_non-json_objects.spec.js b/spec/issues/342_uniqueItems_non-json_objects.spec.ts similarity index 93% rename from spec/issues/342_uniqueItems_non-json_objects.spec.js rename to spec/issues/342_uniqueItems_non-json_objects.spec.ts index e1d8f7a218..8907f287cf 100644 --- a/spec/issues/342_uniqueItems_non-json_objects.spec.js +++ b/spec/issues/342_uniqueItems_non-json_objects.spec.ts @@ -1,13 +1,11 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #342, support uniqueItems with some non-JSON objects", () => { let validate before(() => { - const ajv = new Ajv() + const ajv = new _Ajv() validate = ajv.compile({uniqueItems: true}) }) diff --git a/spec/issues/485_type_validation_priority.spec.js b/spec/issues/485_type_validation_priority.spec.ts similarity index 84% rename from spec/issues/485_type_validation_priority.spec.js rename to spec/issues/485_type_validation_priority.spec.ts index f664598bd4..4fa5d040e2 100644 --- a/spec/issues/485_type_validation_priority.spec.js +++ b/spec/issues/485_type_validation_priority.spec.ts @@ -1,12 +1,10 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #485, order of type validation", () => { it("should validate types before keywords", () => { - const ajv = new Ajv({allErrors: true}) - const validate = ajv.compile({ + const ajv = new _Ajv({allErrors: true}) + const validate: any = ajv.compile({ type: ["integer", "string"], required: ["foo"], minimum: 2, diff --git a/spec/issues/50_refs_with_definitions.spec.js b/spec/issues/50_refs_with_definitions.spec.ts similarity index 93% rename from spec/issues/50_refs_with_definitions.spec.js rename to spec/issues/50_refs_with_definitions.spec.ts index de3ecc1c67..58d8967643 100644 --- a/spec/issues/50_refs_with_definitions.spec.js +++ b/spec/issues/50_refs_with_definitions.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe('issue #50: references with "definitions"', () => { @@ -10,7 +8,7 @@ describe('issue #50: references with "definitions"', () => { function spec(method) { return () => { - const ajv = new Ajv() + const ajv = new _Ajv() ajv[method]({ $id: "http://example.com/test/person.json#", diff --git a/spec/issues/521_wrong_warning_id_property.spec.js b/spec/issues/521_wrong_warning_id_property.spec.ts similarity index 88% rename from spec/issues/521_wrong_warning_id_property.spec.js rename to spec/issues/521_wrong_warning_id_property.spec.ts index c52dfc7901..21ca27ee0f 100644 --- a/spec/issues/521_wrong_warning_id_property.spec.js +++ b/spec/issues/521_wrong_warning_id_property.spec.ts @@ -1,11 +1,9 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe('issue #521, incorrect warning with "id" property', () => { it("should not log warning", () => { - const ajv = new Ajv() + const ajv = new _Ajv() const consoleWarn = console.warn console.warn = () => { throw new Error("should not log warning") diff --git a/spec/issues/533_missing_ref_error_when_ignore.spec.js b/spec/issues/533_missing_ref_error_when_ignore.spec.ts similarity index 83% rename from spec/issues/533_missing_ref_error_when_ignore.spec.js rename to spec/issues/533_missing_ref_error_when_ignore.spec.ts index b319705d85..bc4943c094 100644 --- a/spec/issues/533_missing_ref_error_when_ignore.spec.js +++ b/spec/issues/533_missing_ref_error_when_ignore.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', () => { @@ -13,14 +11,14 @@ describe('issue #533, throwing missing ref exception with option missingRefs: "i } it("should pass validation without throwing exception", () => { - const ajv = new Ajv({missingRefs: "ignore", logger: false}) + const ajv = new _Ajv({missingRefs: "ignore", logger: false}) const validate = ajv.compile(schema) validate({foo: "anything"}).should.equal(true) validate({foo: "anything", bar: "whatever"}).should.equal(true) }) it("should throw exception during schema compilation with option missingRefs: true", () => { - const ajv = new Ajv() + const ajv = new _Ajv() should.throw(() => { ajv.compile(schema) }) diff --git a/spec/issues/743_removeAdditional_to_remove_proto.spec.js b/spec/issues/743_removeAdditional_to_remove_proto.spec.ts similarity index 90% rename from spec/issues/743_removeAdditional_to_remove_proto.spec.js rename to spec/issues/743_removeAdditional_to_remove_proto.spec.ts index 10421292e9..fa62f2c9d3 100644 --- a/spec/issues/743_removeAdditional_to_remove_proto.spec.js +++ b/spec/issues/743_removeAdditional_to_remove_proto.spec.ts @@ -1,11 +1,9 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #743, property __proto__ should be removed with removeAdditional option", () => { it("should remove additional properties", () => { - const ajv = new Ajv({removeAdditional: true}) + const ajv = new _Ajv({removeAdditional: true}) const schema = { properties: { diff --git a/spec/issues/768_passContext_recursive_ref.spec.js b/spec/issues/768_passContext_recursive_ref.spec.ts similarity index 93% rename from spec/issues/768_passContext_recursive_ref.spec.js rename to spec/issues/768_passContext_recursive_ref.spec.ts index 8dac93ff13..4e1db66399 100644 --- a/spec/issues/768_passContext_recursive_ref.spec.js +++ b/spec/issues/768_passContext_recursive_ref.spec.ts @@ -1,10 +1,8 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #768, fix passContext in recursive $ref", () => { - let ajv, contexts + let ajv, contexts: any[] beforeEach(() => { contexts = [] @@ -49,7 +47,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { }) function getValidate(passContext) { - ajv = new Ajv({passContext: passContext}) + ajv = new _Ajv({passContext}) ajv.addKeyword({keyword: "testValidate", validate: storeContext}) const schema = { @@ -68,7 +66,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { } function getValidateFragments(passContext) { - ajv = new Ajv({passContext: passContext}) + ajv = new _Ajv({passContext}) ajv.addKeyword({keyword: "testValidate", validate: storeContext}) ajv.addSchema({ @@ -97,7 +95,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { return ajv.compile({$ref: "foo#/definitions/bar"}) } - function storeContext() { + function storeContext(this: any) { contexts.push(this) return true } diff --git a/spec/issues/8_shared_refs.spec.js b/spec/issues/8_shared_refs.spec.ts similarity index 91% rename from spec/issues/8_shared_refs.spec.js rename to spec/issues/8_shared_refs.spec.ts index 79af369f84..5a4f3a149d 100644 --- a/spec/issues/8_shared_refs.spec.js +++ b/spec/issues/8_shared_refs.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #8: schema with shared references", () => { @@ -10,7 +8,7 @@ describe("issue #8: schema with shared references", () => { function spec(method) { return () => { - const ajv = new Ajv() + const ajv: any = new _Ajv() const propertySchema = { type: "string", diff --git a/spec/issues/955_removeAdditional_custom_keywords.spec.js b/spec/issues/955_removeAdditional_custom_keywords.spec.ts similarity index 84% rename from spec/issues/955_removeAdditional_custom_keywords.spec.js rename to spec/issues/955_removeAdditional_custom_keywords.spec.ts index dda3450ed9..b1ef2d8da0 100644 --- a/spec/issues/955_removeAdditional_custom_keywords.spec.js +++ b/spec/issues/955_removeAdditional_custom_keywords.spec.ts @@ -1,17 +1,15 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("issue #955: option removeAdditional breaks user-defined keywords", () => { it("should support user-defined keywords with option removeAdditional", () => { - const ajv = new Ajv({removeAdditional: "all"}) + const ajv = new _Ajv({removeAdditional: "all"}) ajv.addKeyword({ keyword: "minTrimmedLength", type: "string", - compile: function (schema) { - return function (str) { + compile: function (schema: number) { + return function (str: string): boolean { return str.trim().length >= schema } }, diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.ts similarity index 77% rename from spec/json-schema.spec.js rename to spec/json-schema.spec.ts index 854f64ba00..4abfa9b6ce 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.ts @@ -1,9 +1,8 @@ -"use strict" - -const jsonSchemaTest = require("json-schema-test"), - getAjvInstances = require("./ajv_instances"), - options = require("./ajv_options"), - after = require("./after_test") +import type Ajv from "../dist/ajv" +import getAjvInstances from "./ajv_instances" +import jsonSchemaTest from "json-schema-test" +import options from "./ajv_options" +import {afterError, afterEach} from "./after_test" const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), @@ -43,8 +42,8 @@ const SKIP = { runTest(getAjvInstances(options, {meta: false, strict: false}), 6, require("./_json/draft6")) runTest(getAjvInstances(options, {strict: false}), 7, require("./_json/draft7")) -function runTest(instances, draft, tests) { - instances.forEach((ajv) => { +function runTest(instances: Ajv[], draft: number, tests) { + instances.forEach((ajv: Ajv) => { switch (draft) { case 6: ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) @@ -55,20 +54,15 @@ function runTest(instances, draft, tests) { }) jsonSchemaTest(instances, { - description: - "JSON-Schema Test Suite draft-0" + - draft + - ": " + - instances.length + - " ajv instances with different options", + description: `JSON-Schema Test Suite draft-0${draft}: ${instances.length} ajv instances with different options`, suites: {tests}, only: [], skip: SKIP[draft], assert: require("./chai").assert, - afterError: after.error, - afterEach: after.each, + afterError, + afterEach, cwd: __dirname, - hideFolder: "draft" + draft + "/", - timeout: 120000, + hideFolder: `draft${draft}/`, + timeout: 30000, }) } diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index 6c0386d7c4..13b94d7889 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -1,13 +1,12 @@ -import {ErrorObject, SchemaObject, SchemaValidateFunction} from "../lib/types" +import type {ErrorObject, SchemaObject, SchemaValidateFunction} from "../lib/types" // currently most tests include compiled code, if any code re-compiled locally, instanceof would fail import {_, nil} from "../dist/compile/codegen" +import getAjvInstances from "./ajv_instances" +import Ajv from "./ajv" -const getAjvInstances = require("./ajv_instances"), - should = require("./chai").should(), +const should = require("./chai").should(), equal = require("../dist/compile/equal") -const Ajv = require("./ajv") - describe("User-defined keywords", () => { let ajv, instances diff --git a/spec/options/comment.spec.js b/spec/options/comment.spec.ts similarity index 85% rename from spec/options/comment.spec.js rename to spec/options/comment.spec.ts index a6e6e11583..c8f5eef5c7 100644 --- a/spec/options/comment.spec.js +++ b/spec/options/comment.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("$comment option", () => { @@ -16,8 +14,8 @@ describe("$comment option", () => { console.log = consoleLog }) - function log() { - logCalls.push(Array.prototype.slice.call(arguments)) + function log(...args: any[]) { + logCalls.push(Array.prototype.slice.call(args)) } it("should log the text from $comment keyword", () => { @@ -29,8 +27,8 @@ describe("$comment option", () => { }, } - const ajv = new Ajv({$comment: true}) - const fullAjv = new Ajv({allErrors: true, $comment: true}) + const ajv = new _Ajv({$comment: true}) + const fullAjv = new _Ajv({allErrors: true, $comment: true}) ;[ajv, fullAjv].forEach((_ajv) => { const validate = _ajv.compile(schema) @@ -54,8 +52,8 @@ describe("$comment option", () => { describe("function hook", () => { let hookCalls - function hook() { - hookCalls.push(Array.prototype.slice.call(arguments)) + function hook(...args: any[]) { + hookCalls.push(Array.prototype.slice.call(args)) } it("should pass the text from $comment keyword to the hook", () => { @@ -67,8 +65,8 @@ describe("$comment option", () => { }, } - const ajv = new Ajv({$comment: hook}) - const fullAjv = new Ajv({allErrors: true, $comment: hook}) + const ajv = new _Ajv({$comment: hook}) + const fullAjv = new _Ajv({allErrors: true, $comment: hook}) ;[ajv, fullAjv].forEach((_ajv) => { const validate = _ajv.compile(schema) diff --git a/spec/options/meta_validateSchema.spec.js b/spec/options/meta_validateSchema.spec.ts similarity index 84% rename from spec/options/meta_validateSchema.spec.js rename to spec/options/meta_validateSchema.spec.ts index 7f076d069c..3b9e3c95a1 100644 --- a/spec/options/meta_validateSchema.spec.js +++ b/spec/options/meta_validateSchema.spec.ts @@ -1,12 +1,10 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("meta and validateSchema options", () => { it("should add draft-7 meta schema by default", () => { - testOptionMeta(new Ajv()) - testOptionMeta(new Ajv({meta: true})) + testOptionMeta(new _Ajv()) + testOptionMeta(new _Ajv({meta: true})) function testOptionMeta(ajv) { ajv.getSchema("http://json-schema.org/draft-07/schema").should.be.a("function") @@ -22,7 +20,7 @@ describe("meta and validateSchema options", () => { }) it("should throw if meta: false and validateSchema: true", () => { - const ajv = new Ajv({meta: false, logger: false}) + const ajv = new _Ajv({meta: false, logger: false}) should.not.exist(ajv.getSchema("http://json-schema.org/draft-07/schema")) should.not.throw(() => { ajv.addSchema({type: "wrong_type"}, "integer") @@ -30,17 +28,17 @@ describe("meta and validateSchema options", () => { }) it("should skip schema validation with validateSchema: false", () => { - let ajv = new Ajv() + let ajv = new _Ajv() should.throw(() => { ajv.addSchema({type: 123}, "integer") }) - ajv = new Ajv({validateSchema: false}) + ajv = new _Ajv({validateSchema: false}) should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) - ajv = new Ajv({validateSchema: false, meta: false}) + ajv = new _Ajv({validateSchema: false, meta: false}) should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) @@ -60,7 +58,7 @@ describe("meta and validateSchema options", () => { }) it("should not throw on invalid schema", () => { - const ajv = new Ajv({validateSchema: "log", logger}) + const ajv = new _Ajv({validateSchema: "log", logger}) should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) @@ -69,7 +67,7 @@ describe("meta and validateSchema options", () => { }) it("should not throw on invalid schema with meta: false", () => { - const ajv = new Ajv({validateSchema: "log", meta: false, logger}) + const ajv = new _Ajv({validateSchema: "log", meta: false, logger}) should.not.throw(() => { ajv.addSchema({type: 123}, "integer") }) @@ -79,7 +77,7 @@ describe("meta and validateSchema options", () => { }) it("should validate v6 schema", () => { - const ajv = new Ajv() + const ajv = new _Ajv() ajv.validateSchema({contains: {minimum: 2}}).should.equal(true) ajv.validateSchema({contains: 2}).should.equal(false) }) @@ -91,7 +89,7 @@ describe("meta and validateSchema options", () => { myKeyword: {type: "boolean"}, }, } - let ajv = new Ajv({meta: meta}) + let ajv = new _Ajv({meta: meta}) ajv.validateSchema({myKeyword: true}).should.equal(true) ajv.validateSchema({myKeyword: 2}).should.equal(false) ajv @@ -101,7 +99,7 @@ describe("meta and validateSchema options", () => { }) .should.equal(true) - ajv = new Ajv() + ajv = new _Ajv() ajv.validateSchema({myKeyword: true}).should.equal(true) ajv.validateSchema({myKeyword: 2}).should.equal(true) }) diff --git a/spec/options/nullable.spec.js b/spec/options/nullable.spec.ts similarity index 95% rename from spec/options/nullable.spec.js rename to spec/options/nullable.spec.ts index 823b72815e..708a3c2653 100644 --- a/spec/options/nullable.spec.js +++ b/spec/options/nullable.spec.ts @@ -1,13 +1,11 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("nullable keyword", () => { let ajv beforeEach(() => { - ajv = new Ajv() + ajv = new _Ajv() }) it('should support keyword "nullable"', () => { diff --git a/spec/options/options_add_schemas.spec.js b/spec/options/options_add_schemas.spec.ts similarity index 93% rename from spec/options/options_add_schemas.spec.js rename to spec/options/options_add_schemas.spec.ts index bbb69263f8..b46f76f73e 100644 --- a/spec/options/options_add_schemas.spec.js +++ b/spec/options/options_add_schemas.spec.ts @@ -1,12 +1,10 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("options to add schemas", () => { describe("schemas", () => { it("should add schemas from object", () => { - const ajv = new Ajv({ + const ajv = new _Ajv({ schemas: { int: {type: "integer"}, str: {type: "string"}, @@ -20,7 +18,7 @@ describe("options to add schemas", () => { }) it("should add schemas from array", () => { - const ajv = new Ajv({ + const ajv = new _Ajv({ schemas: [ {$id: "int", type: "integer"}, {$id: "str", type: "string"}, @@ -40,7 +38,7 @@ describe("options to add schemas", () => { let ajv beforeEach(() => { - ajv = new Ajv({addUsedSchema: optionValue}) + ajv = new _Ajv({addUsedSchema: optionValue}) }) describe("compile and validate", () => { @@ -78,7 +76,7 @@ describe("options to add schemas", () => { let ajv beforeEach(() => { - ajv = new Ajv({addUsedSchema: false}) + ajv = new _Ajv({addUsedSchema: false}) }) describe("compile and validate", () => { @@ -117,7 +115,7 @@ describe("options to add schemas", () => { it("should use user-defined function to serialize schema to string", () => { serializeCalled = undefined - const ajv = new Ajv({serialize: serialize}) + const ajv = new _Ajv({serialize: serialize}) ajv.addSchema({type: "string"}) should.equal(serializeCalled, true) }) diff --git a/spec/options/options_code.spec.js b/spec/options/options_code.spec.ts similarity index 88% rename from spec/options/options_code.spec.js rename to spec/options/options_code.spec.ts index 410d1ed463..11562e5708 100644 --- a/spec/options/options_code.spec.js +++ b/spec/options/options_code.spec.ts @@ -1,13 +1,11 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("code generation options", () => { describe("sourceCode", () => { describe("= true", () => { it("should add source.code property", () => { - test(new Ajv({sourceCode: true})) + test(new _Ajv({sourceCode: true})) function test(ajv) { const validate = ajv.compile({type: "number"}) @@ -18,8 +16,8 @@ describe("code generation options", () => { describe("= false and default", () => { it("should not add source and sourceCode properties", () => { - test(new Ajv()) - test(new Ajv({sourceCode: false})) + test(new _Ajv()) + test(new _Ajv({sourceCode: false})) function test(ajv) { const validate = ajv.compile({type: "number"}) @@ -32,14 +30,14 @@ describe("code generation options", () => { describe("processCode", () => { it("should process generated code", () => { - const ajv = new Ajv() + const ajv = new _Ajv() let validate = ajv.compile({type: "string"}) // TODO re-enable this test when option to strip whitespace is added // validate.toString().split("\n").length.should.equal(1) const unprocessedLines = validate.toString().split("\n").length const beautify = require("js-beautify").js_beautify - const ajvPC = new Ajv({processCode: beautify}) + const ajvPC = new _Ajv({processCode: beautify}) validate = ajvPC.compile({type: "string"}) validate.toString().split("\n").length.should.be.above(unprocessedLines) validate("foo").should.equal(true) @@ -48,7 +46,7 @@ describe("code generation options", () => { }) describe("passContext option", () => { - let ajv, contexts + let ajv, contexts: any[] beforeEach(() => { contexts = [] @@ -75,7 +73,7 @@ describe("code generation options", () => { }) function getValidate(passContext) { - ajv = new Ajv({passContext: passContext, inlineRefs: false}) + ajv = new _Ajv({passContext: passContext, inlineRefs: false}) ajv.addKeyword({keyword: "testValidate", validate: storeContext}) ajv.addKeyword({keyword: "testCompile", compile: compileTestValidate}) @@ -95,7 +93,7 @@ describe("code generation options", () => { return ajv.compile(schema) } - function storeContext() { + function storeContext(this: any) { contexts.push(this) return true } @@ -107,8 +105,8 @@ describe("code generation options", () => { describe("loopEnum option", () => { it("should use loop if more values than specified", () => { - const ajv1 = new Ajv() - const ajv2 = new Ajv({loopEnum: 2}) + const ajv1 = new _Ajv() + const ajv2 = new _Ajv({loopEnum: 2}) test(ajv1, {enum: ["foo", "bar"]}) test(ajv2, {enum: ["foo", "bar"]}) test(ajv1, {enum: ["foo", "bar", "baz"]}) diff --git a/spec/options/options_refs.spec.js b/spec/options/options_refs.spec.ts similarity index 73% rename from spec/options/options_refs.spec.js rename to spec/options/options_refs.spec.ts index 3f400fc8fc..fb39026a1f 100644 --- a/spec/options/options_refs.spec.js +++ b/spec/options/options_refs.spec.ts @@ -1,35 +1,33 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("referenced schema options", () => { describe("extendRefs", () => { describe("= true", () => { it("should allow extending $ref with other keywords", () => { - test(new Ajv({extendRefs: true}), true) + test(new _Ajv({extendRefs: true}), true) }) it("should NOT log warning if extendRefs is true", () => { - testWarning(new Ajv({extendRefs: true})) + testWarning(new _Ajv({extendRefs: true})) }) }) describe('= "ignore" and default', () => { it("should ignore other keywords when $ref is used", () => { - test(new Ajv({logger: false})) - test(new Ajv({extendRefs: "ignore", logger: false}), false) + test(new _Ajv({logger: false})) + test(new _Ajv({extendRefs: "ignore", logger: false}), false) }) it("should log warning when other keywords are used with $ref", () => { - testWarning(new Ajv(), /keywords\signored/) - testWarning(new Ajv({extendRefs: "ignore"}), /keywords\signored/) + testWarning(new _Ajv(), /keywords\signored/) + testWarning(new _Ajv({extendRefs: "ignore"}), /keywords\signored/) }) }) describe('= "fail"', () => { it("should fail schema compilation if other keywords are used with $ref", () => { - testFail(new Ajv({extendRefs: "fail"})) + testFail(new _Ajv({extendRefs: "fail"})) function testFail(ajv) { should.throw(() => { @@ -56,8 +54,8 @@ describe("referenced schema options", () => { }) }) - function test(ajv, shouldExtendRef) { - let schema = { + function test(ajv, shouldExtendRef?: boolean) { + const schema = { definitions: { int: {type: "integer"}, }, @@ -69,7 +67,7 @@ describe("referenced schema options", () => { validate(10).should.equal(true) validate(1).should.equal(!shouldExtendRef) - schema = { + const schema1 = { definitions: { int: {type: "integer"}, }, @@ -85,19 +83,19 @@ describe("referenced schema options", () => { }, } - validate = ajv.compile(schema) + validate = ajv.compile(schema1) validate({foo: 10, bar: 10}).should.equal(true) validate({foo: 1, bar: 10}).should.equal(!shouldExtendRef) validate({foo: 10, bar: 1}).should.equal(false) } - function testWarning(ajv, msgPattern) { + function testWarning(ajv, msgPattern?: RegExp) { let oldConsole try { oldConsole = console.warn let consoleMsg - console.warn = function () { - consoleMsg = Array.prototype.join.call(arguments, " ") + console.warn = function (...args: any[]) { + consoleMsg = Array.prototype.join.call(args, " ") } const schema = { @@ -119,15 +117,15 @@ describe("referenced schema options", () => { describe("missingRefs", () => { it("should throw if ref is missing without this option", () => { - const ajv = new Ajv() + const ajv = new _Ajv() should.throw(() => { ajv.compile({$ref: "missing_reference"}) }) }) it('should not throw and pass validation with missingRef == "ignore"', () => { - testMissingRefsIgnore(new Ajv({missingRefs: "ignore", logger: false})) - testMissingRefsIgnore(new Ajv({missingRefs: "ignore", allErrors: true, logger: false})) + testMissingRefsIgnore(new _Ajv({missingRefs: "ignore", logger: false})) + testMissingRefsIgnore(new _Ajv({missingRefs: "ignore", allErrors: true, logger: false})) function testMissingRefsIgnore(ajv) { const validate = ajv.compile({$ref: "missing_reference"}) @@ -136,11 +134,11 @@ describe("referenced schema options", () => { }) it('should not throw and fail validation with missingRef == "fail" if the ref is used', () => { - testMissingRefsFail(new Ajv({missingRefs: "fail", logger: false})) - testMissingRefsFail(new Ajv({missingRefs: "fail", verbose: true, logger: false})) - testMissingRefsFail(new Ajv({missingRefs: "fail", allErrors: true, logger: false})) + testMissingRefsFail(new _Ajv({missingRefs: "fail", logger: false})) + testMissingRefsFail(new _Ajv({missingRefs: "fail", verbose: true, logger: false})) + testMissingRefsFail(new _Ajv({missingRefs: "fail", allErrors: true, logger: false})) testMissingRefsFail( - new Ajv({missingRefs: "fail", allErrors: true, verbose: true, logger: false}) + new _Ajv({missingRefs: "fail", allErrors: true, verbose: true, logger: false}) ) function testMissingRefsFail(ajv) { diff --git a/spec/options/options_reporting.spec.js b/spec/options/options_reporting.spec.ts similarity index 86% rename from spec/options/options_reporting.spec.js rename to spec/options/options_reporting.spec.ts index 58de18531c..2f6427450b 100644 --- a/spec/options/options_reporting.spec.js +++ b/spec/options/options_reporting.spec.ts @@ -1,13 +1,11 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("reporting options", () => { describe("verbose", () => { it("should add schema, parentSchema and data to errors with verbose option == true", () => { - testVerbose(new Ajv({verbose: true})) - testVerbose(new Ajv({verbose: true, allErrors: true})) + testVerbose(new _Ajv({verbose: true})) + testVerbose(new _Ajv({verbose: true, allErrors: true})) function testVerbose(ajv) { const schema = {properties: {foo: {minimum: 5}}} @@ -28,8 +26,8 @@ describe("reporting options", () => { describe("allErrors", () => { it('should be disabled inside "not" keyword', () => { - test(new Ajv(), false) - test(new Ajv({allErrors: true}), true) + test(new _Ajv(), false) + test(new _Ajv({allErrors: true}), true) function test(ajv, allErrors) { let format1called = false, @@ -85,7 +83,7 @@ describe("reporting options", () => { }) it("no user-defined logger is given - global console should be used", () => { - const ajv = new Ajv({meta: false}) + const ajv = new _Ajv({meta: false}) ajv.compile({ type: "number", @@ -104,7 +102,7 @@ describe("reporting options", () => { error: log, } - const ajv = new Ajv({ + const ajv = new _Ajv({ meta: false, logger: logger, }) @@ -123,7 +121,7 @@ describe("reporting options", () => { }) it("logger option is false - no logs should be reported", () => { - const ajv = new Ajv({ + const ajv = new _Ajv({ meta: false, logger: false, }) @@ -137,12 +135,14 @@ describe("reporting options", () => { }) it("logger option is an object without required methods - an error should be thrown", () => { - ;(() => { - new Ajv({ - meta: false, - logger: {}, - }) - }).should.throw(Error, /logger must implement log, warn and error methods/) + const opts: any = { + meta: false, + logger: {}, + } + ;(() => new _Ajv(opts)).should.throw( + Error, + /logger must implement log, warn and error methods/ + ) }) }) }) diff --git a/spec/options/options_validation.spec.js b/spec/options/options_validation.spec.ts similarity index 82% rename from spec/options/options_validation.spec.js rename to spec/options/options_validation.spec.ts index f001aea48a..f8c8aeac01 100644 --- a/spec/options/options_validation.spec.js +++ b/spec/options/options_validation.spec.ts @@ -1,14 +1,12 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("validation options", () => { describe("format", () => { it("should not validate formats if option format == false", () => { - const ajv = new Ajv({formats: {date: DATE_FORMAT}}), - ajvFF = new Ajv({formats: {date: DATE_FORMAT}, format: false}) + const ajv = new _Ajv({formats: {date: DATE_FORMAT}}), + ajvFF = new _Ajv({formats: {date: DATE_FORMAT}, format: false}) const schema = {format: "date"} const invalideDateTime = "06/19/1963" // expects hyphens @@ -20,7 +18,7 @@ describe("validation options", () => { describe("formats", () => { it("should add formats from options", () => { - const ajv = new Ajv({ + const ajv = new _Ajv({ formats: { identifier: /^[a-z_$][a-z0-9_$]*$/i, }, @@ -37,12 +35,12 @@ describe("validation options", () => { describe("keywords", () => { it("should add keywords from options", () => { - const ajv = new Ajv({ + const ajv = new _Ajv({ keywords: [ { keyword: "identifier", type: "string", - validate: function (schema, data) { + validate: function (_schema, data) { return /^[a-z_$][a-z0-9_$]*$/i.test(data) }, }, @@ -60,9 +58,9 @@ describe("validation options", () => { describe("unicode", () => { it("should use String.prototype.length with deprecated unicode option == false", () => { - const ajvUnicode = new Ajv() - testUnicode(new Ajv({unicode: false, logger: false})) - testUnicode(new Ajv({unicode: false, allErrors: true, logger: false})) + const ajvUnicode = new _Ajv() + testUnicode(new _Ajv({unicode: false, logger: false})) + testUnicode(new _Ajv({unicode: false, allErrors: true, logger: false})) function testUnicode(ajv) { let validateWithUnicode = ajvUnicode.compile({minLength: 2}) @@ -82,8 +80,8 @@ describe("validation options", () => { describe("multipleOfPrecision", () => { it("should allow for some deviation from 0 when validating multipleOf with value < 1", () => { - test(new Ajv({multipleOfPrecision: 7})) - test(new Ajv({multipleOfPrecision: 7, allErrors: true})) + test(new _Ajv({multipleOfPrecision: 7})) + test(new _Ajv({multipleOfPrecision: 7, allErrors: true})) function test(ajv) { let schema = {multipleOf: 0.01} diff --git a/spec/options/ownProperties.spec.js b/spec/options/ownProperties.spec.ts similarity index 79% rename from spec/options/ownProperties.spec.js rename to spec/options/ownProperties.spec.ts index 7e0a26d9a9..59f7881384 100644 --- a/spec/options/ownProperties.spec.js +++ b/spec/options/ownProperties.spec.ts @@ -1,15 +1,13 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("ownProperties option", () => { let ajv, ajvOP, ajvOP1 beforeEach(() => { - ajv = new Ajv({allErrors: true}) - ajvOP = new Ajv({ownProperties: true, allErrors: true}) - ajvOP1 = new Ajv({ownProperties: true}) + ajv = new _Ajv({allErrors: true}) + ajvOP = new _Ajv({ownProperties: true, allErrors: true}) + ajvOP1 = new _Ajv({ownProperties: true}) }) it("should only validate own properties with additionalProperties", () => { @@ -47,9 +45,9 @@ describe("ownProperties option", () => { }) it("should only validate own properties with required keyword - many properties", () => { - ajv = new Ajv({allErrors: true, loopRequired: 1}) - ajvOP = new Ajv({ownProperties: true, allErrors: true, loopRequired: 1}) - ajvOP1 = new Ajv({ownProperties: true, loopRequired: 1}) + ajv = new _Ajv({allErrors: true, loopRequired: 1}) + ajvOP = new _Ajv({ownProperties: true, allErrors: true, loopRequired: 1}) + ajvOP1 = new _Ajv({ownProperties: true, loopRequired: 1}) const schema = { required: ["a", "b", "c", "d"], @@ -61,9 +59,9 @@ describe("ownProperties option", () => { }) it("should only validate own properties with required keyword as $data", () => { - ajv = new Ajv({allErrors: true, $data: true}) - ajvOP = new Ajv({ownProperties: true, allErrors: true, $data: true}) - ajvOP1 = new Ajv({ownProperties: true, $data: true}) + ajv = new _Ajv({allErrors: true, $data: true}) + ajvOP = new _Ajv({ownProperties: true, allErrors: true, $data: true}) + ajvOP1 = new _Ajv({ownProperties: true, $data: true}) const schema = { required: {$data: "0/req"}, @@ -105,13 +103,13 @@ describe("ownProperties option", () => { }, } - let obj = {a: 1, c: 3} - let proto = {b: 2} + const obj = {a: 1, c: 3} + const proto = {b: 2} test(schema, obj, proto) - obj = {a: 1, b: 2, c: 3} - proto = {d: 4} - test(schema, obj, proto, 1, true) + const obj1 = {a: 1, b: 2, c: 3} + const proto1 = {d: 4} + test(schema, obj1, proto1, 1, true) }) it("should only validate own properties with schema dependencies", () => { @@ -122,13 +120,13 @@ describe("ownProperties option", () => { }, } - let obj = {a: 1, d: 3} - let proto = {b: 2} + const obj = {a: 1, d: 3} + const proto = {b: 2} test(schema, obj, proto) - obj = {a: 1, b: 2} - proto = {d: 4} - test(schema, obj, proto) + const obj1 = {a: 1, b: 2} + const proto1 = {d: 4} + test(schema, obj1, proto1) }) it("should only validate own properties with patternProperties", () => { @@ -153,8 +151,7 @@ describe("ownProperties option", () => { test(schema, obj, proto, 2) }) - function test(schema, obj, proto, errors, reverse) { - errors = errors || 1 + function test(schema, obj, proto, errors = 1, reverse?: boolean) { const validate = ajv.compile(schema) const validateOP = ajvOP.compile(schema) const validateOP1 = ajvOP1.compile(schema) diff --git a/spec/options/removeAdditional.spec.js b/spec/options/removeAdditional.spec.ts similarity index 84% rename from spec/options/removeAdditional.spec.js rename to spec/options/removeAdditional.spec.ts index 180226232d..581e9f2bea 100644 --- a/spec/options/removeAdditional.spec.js +++ b/spec/options/removeAdditional.spec.ts @@ -1,11 +1,9 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" require("../chai").should() describe("removeAdditional option", () => { it("should remove all additional properties", () => { - const ajv = new Ajv({removeAdditional: "all"}) + const ajv = new _Ajv({removeAdditional: "all"}) ajv.addSchema({ $id: "//test/fooBar", @@ -25,7 +23,7 @@ describe("removeAdditional option", () => { }) it("should remove properties that would error when `additionalProperties = false`", () => { - const ajv = new Ajv({removeAdditional: true}) + const ajv = new _Ajv({removeAdditional: true}) ajv.addSchema({ $id: "//test/fooBar", @@ -46,7 +44,7 @@ describe("removeAdditional option", () => { }) it("should remove properties that would error when `additionalProperties = false` (many properties, boolean schema)", () => { - const ajv = new Ajv({removeAdditional: true}) + const ajv = new _Ajv({removeAdditional: true}) const schema = { properties: { @@ -85,7 +83,7 @@ describe("removeAdditional option", () => { }) it("should remove properties that would error when `additionalProperties` is a schema", () => { - const ajv = new Ajv({removeAdditional: "failing"}) + const ajv = new _Ajv({removeAdditional: "failing"}) ajv.addSchema({ $id: "//test/fooBar", @@ -93,7 +91,7 @@ describe("removeAdditional option", () => { additionalProperties: {type: "string"}, }) - let object = { + const object = { foo: "foo", bar: "bar", baz: "baz-to-be-kept", @@ -112,7 +110,7 @@ describe("removeAdditional option", () => { additionalProperties: {type: "string", pattern: "^to-be-", maxLength: 10}, }) - object = { + const object1 = { foo: "foo", bar: "bar", baz: "to-be-kept", @@ -120,10 +118,10 @@ describe("removeAdditional option", () => { fizz: 1000, } - ajv.validate("//test/fooBar2", object).should.equal(true) - object.should.have.property("foo") - object.should.have.property("bar") - object.should.have.property("baz") - object.should.not.have.property("fizz") + ajv.validate("//test/fooBar2", object1).should.equal(true) + object1.should.have.property("foo") + object1.should.have.property("bar") + object1.should.have.property("baz") + object1.should.not.have.property("fizz") }) }) diff --git a/spec/options/schemaId.spec.js b/spec/options/schemaId.spec.ts similarity index 77% rename from spec/options/schemaId.spec.js rename to spec/options/schemaId.spec.ts index feaab2e4ac..a4831d8f50 100644 --- a/spec/options/schemaId.spec.js +++ b/spec/options/schemaId.spec.ts @@ -1,12 +1,10 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("removed schemaId option", () => { it("should use $id and throw exception when id is used", () => { - test(new Ajv({logger: false})) - test(new Ajv({schemaId: "$id", logger: false})) + test(new _Ajv({logger: false})) + test(new _Ajv({schemaId: "$id", logger: false})) function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) @@ -19,8 +17,8 @@ describe("removed schemaId option", () => { }) it("should use $id and ignore id when strict: false", () => { - test(new Ajv({logger: false, strict: false})) - test(new Ajv({schemaId: "$id", logger: false, strict: false})) + test(new _Ajv({logger: false, strict: false})) + test(new _Ajv({schemaId: "$id", logger: false, strict: false})) function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) diff --git a/spec/options/strict.spec.js b/spec/options/strict.spec.ts similarity index 88% rename from spec/options/strict.spec.js rename to spec/options/strict.spec.ts index a170ed5895..a79d52602e 100644 --- a/spec/options/strict.spec.js +++ b/spec/options/strict.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("strict mode", () => { @@ -28,8 +26,8 @@ describe("strict mode", () => { describe('option allowMatchingProperties to allow "properties" matching "patternProperties"', () => { it("should NOT throw an error or log a warning", () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ allowMatchingProperties: true, logger: getLogger(output), }) @@ -47,8 +45,8 @@ function testStrictMode(schema, logPattern) { return () => { describe("strict = false", () => { it("should NOT throw an error or log a warning", () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ strict: false, logger: getLogger(output), }) @@ -59,8 +57,8 @@ function testStrictMode(schema, logPattern) { describe("strict = true or undefined", () => { it("should throw an error", () => { - test(new Ajv({strict: true})) - test(new Ajv()) + test(new _Ajv({strict: true})) + test(new _Ajv()) function test(ajv) { should.throw(() => { @@ -72,8 +70,8 @@ function testStrictMode(schema, logPattern) { describe('strict = "log"', () => { it("should log a warning", () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ strict: "log", logger: getLogger(output), }) diff --git a/spec/options/strictDefaults.spec.js b/spec/options/strictDefaults.spec.ts similarity index 83% rename from spec/options/strictDefaults.spec.js rename to spec/options/strictDefaults.spec.ts index a1f41b5660..b0e67c0e98 100644 --- a/spec/options/strictDefaults.spec.js +++ b/spec/options/strictDefaults.spec.ts @@ -1,14 +1,12 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("strict option with defaults (replaced strictDefaults)", () => { describe("useDefaults = true", () => { describe("strict = false", () => { it("should NOT throw an error or log a warning given an ignored default", () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ useDefaults: true, strict: false, logger: getLogger(output), @@ -23,8 +21,8 @@ describe("strict option with defaults (replaced strictDefaults)", () => { }) it("should NOT throw an error or log a warning given an ignored default #2", () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ useDefaults: true, strict: false, logger: getLogger(output), @@ -49,8 +47,8 @@ describe("strict option with defaults (replaced strictDefaults)", () => { describe("strict = true", () => { it("should throw an error given an ignored default in the schema root when strict is true or undefined", () => { - test(new Ajv({useDefaults: true})) - test(new Ajv({useDefaults: true, strict: true})) + test(new _Ajv({useDefaults: true})) + test(new _Ajv({useDefaults: true, strict: true})) function test(ajv) { const schema = { @@ -64,8 +62,8 @@ describe("strict option with defaults (replaced strictDefaults)", () => { }) it("should throw an error given an ignored default in oneOf when strict is true or undefined", () => { - test(new Ajv({useDefaults: true})) - test(new Ajv({useDefaults: true, strict: true})) + test(new _Ajv({useDefaults: true})) + test(new _Ajv({useDefaults: true, strict: true})) function test(ajv) { const schema = { @@ -89,8 +87,8 @@ describe("strict option with defaults (replaced strictDefaults)", () => { describe('strict = "log"', () => { it('should log a warning given an ignored default in the schema root when strict is "log"', () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ useDefaults: true, strict: "log", logger: getLogger(output), @@ -104,8 +102,8 @@ describe("strict option with defaults (replaced strictDefaults)", () => { }) it('should log a warning given an ignored default in oneOf when strict is "log"', () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ useDefaults: true, strict: "log", logger: getLogger(output), @@ -130,10 +128,10 @@ describe("strict option with defaults (replaced strictDefaults)", () => { describe("useDefaults = false or undefined", () => { it("should NOT throw an error given an ignored default in the schema root when useDefaults is false", () => { - test(new Ajv({useDefaults: false})) - test(new Ajv({useDefaults: false, strict: true})) - test(new Ajv()) - test(new Ajv({strict: true})) + test(new _Ajv({useDefaults: false})) + test(new _Ajv({useDefaults: false, strict: true})) + test(new _Ajv()) + test(new _Ajv({strict: true})) function test(ajv) { const schema = { @@ -147,10 +145,10 @@ describe("strict option with defaults (replaced strictDefaults)", () => { }) it("should NOT throw an error given an ignored default in oneOf when useDefaults is false", () => { - test(new Ajv({useDefaults: false})) - test(new Ajv({useDefaults: false, strict: true})) - test(new Ajv()) - test(new Ajv({strict: true})) + test(new _Ajv({useDefaults: false})) + test(new _Ajv({useDefaults: false, strict: true})) + test(new _Ajv()) + test(new _Ajv({strict: true})) function test(ajv) { const schema = { diff --git a/spec/options/strictKeywords.spec.js b/spec/options/strictKeywords.spec.ts similarity index 87% rename from spec/options/strictKeywords.spec.js rename to spec/options/strictKeywords.spec.ts index 9680576e11..1042a13da5 100644 --- a/spec/options/strictKeywords.spec.js +++ b/spec/options/strictKeywords.spec.ts @@ -1,13 +1,11 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() describe("strict option with keywords (replaced strictKeywords)", () => { describe("strict = false", () => { it("should NOT throw an error or log a warning given an unknown keyword", () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ strict: false, logger: getLogger(output), }) @@ -23,8 +21,8 @@ describe("strict option with keywords (replaced strictKeywords)", () => { describe("strict = true or undefined", () => { it("should throw an error given an unknown keyword in the schema root when strict is true", () => { - test(new Ajv({strict: true})) - test(new Ajv()) + test(new _Ajv({strict: true})) + test(new _Ajv()) function test(ajv) { const schema = { @@ -40,8 +38,8 @@ describe("strict option with keywords (replaced strictKeywords)", () => { describe('strict = "log"', () => { it("should log an error given an unknown keyword in the schema root", () => { - const output = {} - const ajv = new Ajv({ + const output: any = {} + const ajv = new _Ajv({ strict: "log", logger: getLogger(output), }) @@ -56,8 +54,8 @@ describe("strict option with keywords (replaced strictKeywords)", () => { describe("unknown keyword inside schema that has no known keyword in compound keyword", () => { it("should throw an error given an unknown keyword when strict is true or undefined", () => { - test(new Ajv({strict: true})) - test(new Ajv()) + test(new _Ajv({strict: true})) + test(new _Ajv()) function test(ajv) { const schema = { diff --git a/spec/options/strictNumbers.spec.js b/spec/options/strictNumbers.spec.ts similarity index 83% rename from spec/options/strictNumbers.spec.js rename to spec/options/strictNumbers.spec.ts index e8a908409d..391afe4f06 100644 --- a/spec/options/strictNumbers.spec.js +++ b/spec/options/strictNumbers.spec.ts @@ -1,12 +1,10 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" describe("strict option with keywords (replaced structNumbers)", () => { - describe("strict default", testStrict(new Ajv())) - describe("strict = true", testStrict(new Ajv({strict: true}))) - describe('strict = "log"', testStrict(new Ajv({strict: "log"}))) - describe("strict = false", testNotStrict(new Ajv({strict: false}))) + describe("strict default", testStrict(new _Ajv())) + describe("strict = true", testStrict(new _Ajv({strict: true}))) + describe('strict = "log"', testStrict(new _Ajv({strict: "log"}))) + describe("strict = false", testNotStrict(new _Ajv({strict: false}))) }) function testStrict(ajv) { diff --git a/spec/options/unknownFormats.spec.js b/spec/options/unknownFormats.spec.ts similarity index 87% rename from spec/options/unknownFormats.spec.js rename to spec/options/unknownFormats.spec.ts index c85a776755..26035c6f72 100644 --- a/spec/options/unknownFormats.spec.js +++ b/spec/options/unknownFormats.spec.ts @@ -1,14 +1,12 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const should = require("../chai").should() const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("unknownFormats option", () => { describe("= true (default)", () => { it("should fail schema compilation if unknown format is used", () => { - test(new Ajv()) - test(new Ajv({unknownFormats: true})) + test(new _Ajv()) + test(new _Ajv({unknownFormats: true})) function test(ajv) { should.throw(() => { @@ -18,8 +16,8 @@ describe("unknownFormats option", () => { }) it("should fail validation if unknown format is used via $data", () => { - test(new Ajv({$data: true})) - test(new Ajv({$data: true, unknownFormats: true})) + test(new _Ajv({$data: true})) + test(new _Ajv({$data: true, unknownFormats: true})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) @@ -42,7 +40,7 @@ describe("unknownFormats option", () => { describe('= "ignore (default before 5.0.0)"', () => { it("should pass schema compilation and be valid if unknown format is used", () => { - test(new Ajv({unknownFormats: "ignore", logger: false})) + test(new _Ajv({unknownFormats: "ignore", logger: false})) function test(ajv) { const validate = ajv.compile({format: "unknown"}) @@ -51,7 +49,7 @@ describe("unknownFormats option", () => { }) it("should be valid if unknown format is used via $data", () => { - test(new Ajv({$data: true, unknownFormats: "ignore"})) + test(new _Ajv({$data: true, unknownFormats: "ignore"})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) @@ -73,7 +71,7 @@ describe("unknownFormats option", () => { describe("= [String]", () => { it("should pass schema compilation and be valid if allowed unknown format is used", () => { - test(new Ajv({unknownFormats: ["allowed"]})) + test(new _Ajv({unknownFormats: ["allowed"]})) function test(ajv) { const validate = ajv.compile({format: "allowed"}) @@ -86,7 +84,7 @@ describe("unknownFormats option", () => { }) it("should be valid if allowed unknown format is used via $data", () => { - test(new Ajv({$data: true, unknownFormats: ["allowed"]})) + test(new _Ajv({$data: true, unknownFormats: ["allowed"]})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) diff --git a/spec/options/useDefaults.spec.js b/spec/options/useDefaults.spec.ts similarity index 87% rename from spec/options/useDefaults.spec.js rename to spec/options/useDefaults.spec.ts index 38baf11840..e82bd3baee 100644 --- a/spec/options/useDefaults.spec.js +++ b/spec/options/useDefaults.spec.ts @@ -1,6 +1,4 @@ -"use strict" - -const Ajv = require("../ajv") +import _Ajv from "../ajv" const getAjvInstances = require("../ajv_instances") require("../chai").should() @@ -57,8 +55,8 @@ describe("useDefaults option", () => { }) it("should replace undefined item with default value", () => { - test(new Ajv({useDefaults: true})) - test(new Ajv({useDefaults: true, allErrors: true})) + test(new _Ajv({useDefaults: true})) + test(new _Ajv({useDefaults: true, allErrors: true})) function test(ajv) { const schema = { @@ -72,7 +70,7 @@ describe("useDefaults option", () => { const validate = ajv.compile(schema) - let data = [] + let data: any = [] validate(data).should.equal(true) data.should.eql(["abc", 1, false]) @@ -88,8 +86,8 @@ describe("useDefaults option", () => { }) it('should apply default in "then" subschema (issue #635)', () => { - test(new Ajv({useDefaults: true})) - test(new Ajv({useDefaults: true, allErrors: true})) + test(new _Ajv({useDefaults: true})) + test(new _Ajv({useDefaults: true, allErrors: true})) function test(ajv) { const schema = { @@ -120,10 +118,8 @@ describe("useDefaults option", () => { describe("useDefaults: defaults are always passed by value", () => { it("should NOT modify underlying defaults when modifying validated data", () => { - test(new Ajv({useDefaults: true})) - test(new Ajv({useDefaults: true, allErrors: true})) - test(new Ajv({useDefaults: "shared"})) - test(new Ajv({useDefaults: "shared", allErrors: true})) + test(new _Ajv({useDefaults: true})) + test(new _Ajv({useDefaults: true, allErrors: true})) }) function test(ajv) { @@ -138,14 +134,14 @@ describe("useDefaults option", () => { const validate = ajv.compile(schema) - const data = {} + const data: any = {} validate(data).should.equal(true) data.items.should.eql(["a-default"]) data.items.push("another-value") data.items.should.eql(["a-default", "another-value"]) - const data2 = {} + const data2: any = {} validate(data2).should.equal(true) data2.items.should.eql(["a-default"]) @@ -183,8 +179,7 @@ describe("useDefaults option", () => { }) it('should NOT assign defaults when useDefaults is true/"shared"', () => { - test(new Ajv({useDefaults: true})) - test(new Ajv({useDefaults: "shared"})) + test(new _Ajv({useDefaults: true})) function test(ajv) { const validate = ajv.compile(schema) @@ -202,7 +197,7 @@ describe("useDefaults option", () => { }) it('should assign defaults when useDefaults = "empty"', () => { - const ajv = new Ajv({useDefaults: "empty"}) + const ajv = new _Ajv({useDefaults: "empty"}) const validate = ajv.compile(schema) validate(data).should.equal(true) data.should.eql({ diff --git a/spec/resolve.spec.js b/spec/resolve.spec.ts similarity index 97% rename from spec/resolve.spec.js rename to spec/resolve.spec.ts index 6afaf5b55e..946861370f 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.ts @@ -1,8 +1,7 @@ -"use strict" +import getAjvInstances from "./ajv_instances" +import Ajv from "./ajv" -const Ajv = require("./ajv"), - should = require("./chai").should(), - getAjvInstances = require("./ajv_instances") +const should = require("./chai").should() describe("resolve", () => { let instances @@ -283,7 +282,7 @@ describe("resolve", () => { // add schemas and get validator function ajv.addSchema(schemaHeader) ajv.addSchema(schemaMessage) - const v = ajv.getSchema("http://e.com/message.json#") + const v: any = ajv.getSchema("http://e.com/message.json#") v(validMessage).should.equal(true) v.schema.$id.should.equal("http://e.com/message.json#") @@ -321,7 +320,7 @@ describe("resolve", () => { } function testInlined(validate, expectedInlined) { - const inlined = !/refVal/.test(validate.toString()) + const inlined: any = !/refVal/.test(validate.toString()) inlined.should.equal(expectedInlined) } }) diff --git a/spec/schema-tests.spec.js b/spec/schema-tests.spec.ts similarity index 79% rename from spec/schema-tests.spec.js rename to spec/schema-tests.spec.ts index 831de62713..c1455ad108 100644 --- a/spec/schema-tests.spec.js +++ b/spec/schema-tests.spec.ts @@ -1,10 +1,9 @@ -"use strict" +import getAjvInstances from "./ajv_instances" +import jsonSchemaTest from "json-schema-test" +import options from "./ajv_options" +import {afterError, afterEach} from "./after_test" -const jsonSchemaTest = require("json-schema-test"), - addFormats = require("ajv-formats"), - getAjvInstances = require("./ajv_instances"), - options = require("./ajv_options"), - after = require("./after_test") +const addFormats = require("ajv-formats") const instances = getAjvInstances(options, {strict: false, unknownFormats: ["allowedUnknown"]}) @@ -32,10 +31,10 @@ jsonSchemaTest(instances, { suites: {"Schema tests": require("./_json/tests")}, only: [], assert: require("./chai").assert, - afterError: after.error, - afterEach: after.each, + afterError, + afterEach, cwd: __dirname, - timeout: 120000, + timeout: 10000, }) function addRemoteRefsAndFormats(ajv) { diff --git a/spec/security.spec.js b/spec/security.spec.ts similarity index 59% rename from spec/security.spec.js rename to spec/security.spec.ts index 0e628d6d9f..dfadb10fbd 100644 --- a/spec/security.spec.js +++ b/spec/security.spec.ts @@ -1,9 +1,7 @@ -"use strict" - -const jsonSchemaTest = require("json-schema-test"), - getAjvInstances = require("./ajv_instances"), - options = require("./ajv_options"), - after = require("./after_test") +import getAjvInstances from "./ajv_instances" +import jsonSchemaTest from "json-schema-test" +import options from "./ajv_options" +import {afterError, afterEach} from "./after_test" const instances = getAjvInstances(options, { schemas: [require("../dist/refs/json-schema-secure.json")], @@ -14,9 +12,8 @@ jsonSchemaTest(instances, { "Secure schemas tests of " + instances.length + " ajv instances with different options", suites: {security: require("./_json/security")}, assert: require("./chai").assert, - afterError: after.error, - afterEach: after.each, + afterError, + afterEach, cwd: __dirname, hideFolder: "security/", - timeout: 90000, }) diff --git a/spec/tsconfig.json b/spec/tsconfig.json index 20d1d4e305..e4836c51ad 100644 --- a/spec/tsconfig.json +++ b/spec/tsconfig.json @@ -2,7 +2,7 @@ "extends": "..", "include": ["."], "compilerOptions": { - "outDir": ".distspec", + "outDir": "../.distspec", "types": ["node", "mocha"], "noImplicitAny": false } From 56b5177e9a8a4cae7f30e6f4e3bc2801470cd1c2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 8 Sep 2020 12:39:10 +0100 Subject: [PATCH 176/322] global scope of used values [WIP] --- lib/compile/codegen/code.ts | 88 +++++++++ lib/compile/{codegen.ts => codegen/index.ts} | 181 ++----------------- lib/compile/codegen/scope.ts | 118 ++++++++++++ lib/compile/index.ts | 6 +- lib/types.ts | 4 +- 5 files changed, 222 insertions(+), 175 deletions(-) create mode 100644 lib/compile/codegen/code.ts rename lib/compile/{codegen.ts => codegen/index.ts} (63%) create mode 100644 lib/compile/codegen/scope.ts diff --git a/lib/compile/codegen/code.ts b/lib/compile/codegen/code.ts new file mode 100644 index 0000000000..1e6dfc163e --- /dev/null +++ b/lib/compile/codegen/code.ts @@ -0,0 +1,88 @@ +type TemplateArg = SafeExpr | string + +export type Code = _Code | Name + +export type SafeExpr = Code | number | boolean | null + +export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { + // TODO benchmark if loop is faster than reduce + // let res = strs[0] + // for (let i = 0; i < args.length; i++) { + // res += interpolate(args[i]) + strs[i + 1] + // } + // return new _Code(res) + return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) +} + +export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code { + return new _Code( + strs.map(safeStringify).reduce((res, s, i) => { + let aStr = interpolateStr(args[i - 1]) + if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString() + return typeof aStr === "string" + ? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1) + : `${res} + ${aStr} + ${s}` + }) + ) +} + +function interpolate(x: TemplateArg): TemplateArg { + return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null + ? x + : safeStringify(x) +} + +function interpolateStr(x: TemplateArg | string[]): TemplateArg { + if (Array.isArray(x)) x = x.join(",") + return interpolate(x) +} + +export class _Code { + _str: string + + constructor(s: string) { + this._str = s + } + + toString(): string { + return this._str + } + + isQuoted(): boolean { + const len = this._str.length + return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"' + } + + add(c: _Code): void { + this._str += c._str + } +} + +export const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i + +export class Name extends _Code { + constructor(s: string) { + super(s) + if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier") + } + + isQuoted(): boolean { + return false + } + + add(_c: _Code): void { + throw new Error("CodeGen: can't add to Name") + } +} + +export const nil = new _Code("") + +export function stringify(x: unknown): Code { + return new _Code(safeStringify(x)) +} + +function safeStringify(x: unknown): string { + return JSON.stringify(x) + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029") +} diff --git a/lib/compile/codegen.ts b/lib/compile/codegen/index.ts similarity index 63% rename from lib/compile/codegen.ts rename to lib/compile/codegen/index.ts index 872148da4d..8e0e765a9b 100644 --- a/lib/compile/codegen.ts +++ b/lib/compile/codegen/index.ts @@ -1,3 +1,8 @@ +import {_, str, nil, _Code, Code, IDENTIFIER, Name, stringify} from "./code" +import {Scope, NameValue, _Scope} from "./scope" + +export {_, str, nil, Name, Code, _Scope, stringify} + enum BlockKind { If, Else, @@ -9,31 +14,6 @@ export type SafeExpr = Code | number | boolean | null export type Block = Code | (() => void) -export type Code = _Code | Name - -class _Code { - _str: string - - constructor(s: string) { - this._str = s - } - - toString(): string { - return this._str - } - - isQuoted(): boolean { - const len = this._str.length - return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"' - } - - add(c: _Code): void { - this._str += c._str - } -} - -export const nil = new _Code("") - export const operators = { GT: new _Code(">"), GTE: new _Code(">="), @@ -46,23 +26,6 @@ export const operators = { AND: new _Code("&&"), } -const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i - -export class Name extends _Code { - constructor(s: string) { - super(s) - if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier") - } - - isQuoted(): boolean { - return false - } - - add(_c: _Code): void { - throw new Error("CodeGen: can't add to Name") - } -} - export const varKinds = { const: new Name("const"), let: new Name("let"), @@ -80,61 +43,6 @@ interface NameRec { value: NameValue } -type ValueReference = unknown // possibly make CodeGen parameterized type on this type - -export interface NameValue { - ref?: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure - key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used - code?: Code // this is the code creating the value needed for standalone code wit_out closure - can be a primitive value, function or import (`require`) -} - -export interface Scope { - [prefix: string]: ValueReference[] -} - -type TemplateArg = SafeExpr | string - -export class ValueError extends Error { - value: NameValue - constructor(fields: string, {name, value}: NameRec) { - super(`CodeGen: ${fields} for ${name} not defined`) - this.value = value - } -} - -export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { - // TODO benchmark if loop is faster than reduce - // let res = strs[0] - // for (let i = 0; i < args.length; i++) { - // res += interpolate(args[i]) + strs[i + 1] - // } - // return new _Code(res) - return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) -} - -export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code { - return new _Code( - strs.map(safeStringify).reduce((res, s, i) => { - let aStr = interpolateStr(args[i - 1]) - if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString() - return typeof aStr === "string" - ? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1) - : `${res} + ${aStr} + ${s}` - }) - ) -} - -function interpolate(x: TemplateArg): TemplateArg { - return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null - ? x - : safeStringify(x) -} - -function interpolateStr(x: TemplateArg | string[]): TemplateArg { - if (Array.isArray(x)) x = x.join(",") - return interpolate(x) -} - export interface CodeGenOptions { es5?: boolean lines?: boolean @@ -142,6 +50,7 @@ export interface CodeGenOptions { } export class CodeGen { + _scope = new Scope() _names: {[prefix: string]: NameGroup} = {} _valuePrefixes: {[prefix: string]: Name} = {} _out = "" @@ -159,75 +68,17 @@ export class CodeGen { return this._out } - _nameGroup(prefix: string): NameGroup { - let ng = this._names[prefix] - if (!ng) ng = this._names[prefix] = {prefix, index: 0} - return ng - } - - _name(ng: NameGroup): Name { - return new Name(ng.prefix + ng.index++) + _toName(nameOrPrefix: Name | string): Name { + return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) } name(prefix: string): Name { - const ng = this._nameGroup(prefix) - return this._name(ng) + return this._scope.name(prefix) } + // TODO move to global scope value(prefix: string, value: NameValue): Name { - const {ref, key, code} = value - const valueKey = key ?? ref ?? code - if (!valueKey) throw new Error("CodeGen: ref or code must be passed in value") - const ng = this._nameGroup(prefix) - this._valuePrefixes[prefix] = new Name(prefix) - if (!ng.values) { - ng.values = new Map() - } else { - const rec = ng.values.get(valueKey) - if (rec) return rec.name - } - const name = this._name(ng) - ng.values.set(valueKey, {name, value}) - return name - } - - scopeRefs(scopeName: Name, scope: Scope): Code { - return this._reduceValues((rec: NameRec, prefix: string, i: number) => { - const {value: v} = rec - if (v.ref) { - if (!scope[prefix]) scope[prefix] = [] - scope[prefix][i] = v.ref - const prefName = this._valuePrefixes[prefix] - return _`${scopeName}.${prefName}[${i}]` - } - if (v.code) return v.code - throw new ValueError("ref and code", rec) - }) - } - - scopeCode(): Code { - return this._reduceValues((rec: NameRec) => { - const c = rec.value.code - if (c) return c - throw new ValueError("code", rec) - }) - } - - _reduceValues(valueCode: (n: NameRec, pref: string, index: number) => Code): Code { - let code: Code = nil - for (const prefix in this._valuePrefixes) { - let i = 0 - const values = this._names[prefix].values - if (!values) throw new Error("ajv implementation error") - values.forEach((rec: NameRec) => { - code = _`${code}const ${rec.name} = ${valueCode(rec, prefix, i++)};` - }) - } - return code - } - - _toName(nameOrPrefix: Name | string): Name { - return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + return this._scope.value(prefix, value) } _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { @@ -449,16 +300,6 @@ export class CodeGen { } } -export function stringify(x: unknown): Code { - return new _Code(safeStringify(x)) -} - -function safeStringify(x: unknown): string { - return JSON.stringify(x) - .replace(/\u2028/g, "\\u2028") - .replace(/\u2029/g, "\\u2029") -} - export function getProperty(key: Code | string | number): Code { return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]` } diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts new file mode 100644 index 0000000000..5878d81350 --- /dev/null +++ b/lib/compile/codegen/scope.ts @@ -0,0 +1,118 @@ +import {_, nil, Code, Name} from "./code" + +interface NameRec { + name: Name + value: NameValue +} + +interface NameGroup { + prefix: string + index: number + values?: Map // same key as passed in GlobalValue +} + +export interface NameValue { + ref?: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure + key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used + code?: Code // this is the code creating the value needed for standalone code wit_out closure - can be a primitive value, function or import (`require`) +} + +export type ValueReference = unknown // possibly make CodeGen parameterized type on this type + +class ValueError extends Error { + value: NameValue + constructor(fields: string, {name, value}: NameRec) { + super(`CodeGen: ${fields} for ${name} not defined`) + this.value = value + } +} + +export interface _Scope { + [prefix: string]: ValueReference[] +} + +export class Scope { + _names: {[prefix: string]: NameGroup} = {} + _valuePrefixes: {[prefix: string]: Name} = {} + _parent?: Scope + + constructor(parent?: Scope) { + this._parent = parent + } + + _nameGroup(prefix: string): NameGroup { + let ng = this._names[prefix] + if (!ng) ng = this._names[prefix] = {prefix, index: 0} + return ng + } + + _name(ng: NameGroup): Name { + return new Name(ng.prefix + ng.index++) + } + + name(prefix: string): Name { + const ng = this._nameGroup(prefix) + return this._name(ng) + } + + value(prefix: string, value: NameValue): Name { + const {ref, key, code} = value + const valueKey = key ?? ref ?? code + if (!valueKey) throw new Error("CodeGen: ref or code must be passed in value") + const ng = this._nameGroup(prefix) + this._valuePrefixes[prefix] = new Name(prefix) + if (!ng.values) { + ng.values = new Map() + } else { + const rec = ng.values.get(valueKey) + if (rec) return rec.name + } + const name = this._name(ng) + ng.values.set(valueKey, {name, value}) + return name + } + + scopeRefs(scopeName: Name, scope: _Scope): Code { + return this._reduceValues((rec: NameRec, prefix: string, i: number) => { + const {value: v} = rec + if (v.ref) { + if (!scope[prefix]) scope[prefix] = [] + scope[prefix][i] = v.ref + const prefName = this._valuePrefixes[prefix] + return _`${scopeName}.${prefName}[${i}]` + } + if (v.code) return v.code + throw new ValueError("ref and code", rec) + }) + } + + scopeCode(): Code { + return this._reduceValues((rec: NameRec) => { + const c = rec.value.code + if (c) return c + throw new ValueError("code", rec) + }) + } + + _reduceValues(valueCode: (n: NameRec, pref: string, index: number) => Code): Code { + let code: Code = nil + for (const prefix in this._valuePrefixes) { + let i = 0 + const values = this._names[prefix].values + if (!values) throw new Error("ajv implementation error") + values.forEach((rec: NameRec) => { + code = _`${code}const ${rec.name} = ${valueCode(rec, prefix, i++)};` + }) + } + return code + } +} + +// class ParentScope extends Scope { +// _children: Scope[] = [] +// _childrenPrefixes: {[prefix: string]: Name} = {} + +// _freePrefix(prefix?: string) { + +// } +// } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 653089562b..429f5058bf 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,6 @@ import type {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" import type Ajv from "../ajv" -import {CodeGen, _, nil, str, Code, Scope} from "./codegen" +import {CodeGen, _, nil, str, Code, _Scope} from "./codegen" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" @@ -187,7 +187,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW let compilation = getCompilation.call(this, env) if (compilation) return validateWrapper(compilation) - const scope: Scope = {} + const scope: _Scope = {} compilation = env const formats = this._formats @@ -241,7 +241,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW }) let sourceCode = `${vars(refVal, refValCode)} - ${gen.scopeRefs(N.scope, scope)} + ${gen._scope.scopeRefs(N.scope, scope)} // TODO ${gen.toString()}` if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) diff --git a/lib/types.ts b/lib/types.ts index 58cce992bb..bb5d2bc085 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,4 @@ -import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" +import {CodeGen, Code, Name, CodeGenOptions, _Scope} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" import KeywordCtx from "./compile/context" @@ -84,7 +84,7 @@ export interface CacheInterface { interface SourceCode { code: string - scope: Scope + scope: _Scope } export interface ValidateFunction { From d0820fc6309c078a69803f8cd377ad1886ca3b5c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 8 Sep 2020 18:24:55 +0100 Subject: [PATCH 177/322] refactor: use shared external scope for all functions --- lib/ajv.ts | 5 ++ lib/compile/codegen/index.ts | 37 ++++----- lib/compile/codegen/scope.ts | 139 ++++++++++++++++++-------------- lib/compile/context.ts | 2 +- lib/compile/index.ts | 11 ++- lib/compile/validate/keyword.ts | 2 +- lib/types.ts | 4 +- lib/vocabularies/util.ts | 2 +- 8 files changed, 110 insertions(+), 92 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 81418a932a..ca129063c7 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -17,6 +17,7 @@ import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" import {StoredSchema, compileSchemaFragment, compileStoredSchema, Compilation} from "./compile" +import {Scope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" @@ -30,6 +31,7 @@ const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] +const EXT_SCOPE_NAMES = new Set(["keyword", "pattern", "validate$data"]) type CompileAsyncCallback = (err: Error | null, validate?: ValidateFunction) => void @@ -40,6 +42,8 @@ interface IndexableOptions extends Options { export default class Ajv { _opts: IndexableOptions _cache: CacheInterface + // shared external scope values for compiled functions + _scope = new Scope({scope: {}, prefixes: EXT_SCOPE_NAMES}) _schemas: {[key: string]: StoredSchema} = {} _refs: {[ref: string]: StoredSchema | string} = {} _fragments: {[key: string]: StoredSchema} = {} @@ -321,6 +325,7 @@ export default class Ajv { checkKeyword.call(this, keyword, def) if (def) keywordMetaschema.call(this, def) + eachItem(keyword, (kwd) => { eachItem(def?.type, (t) => _addRule.call(this, kwd, t, def)) }) diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index 8e0e765a9b..baf7e07635 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -1,7 +1,7 @@ import {_, str, nil, _Code, Code, IDENTIFIER, Name, stringify} from "./code" -import {Scope, NameValue, _Scope} from "./scope" +import {Scope, ScopeValueSets, NameValue, ScopeStore} from "./scope" -export {_, str, nil, Name, Code, _Scope, stringify} +export {_, str, nil, stringify, Name, Code, Scope, ScopeStore} enum BlockKind { If, @@ -32,17 +32,6 @@ export const varKinds = { var: new Name("var"), } -interface NameGroup { - prefix: string - index: number - values?: Map // same key as passed in GlobalValue -} - -interface NameRec { - name: Name - value: NameValue -} - export interface CodeGenOptions { es5?: boolean lines?: boolean @@ -50,17 +39,19 @@ export interface CodeGenOptions { } export class CodeGen { - _scope = new Scope() - _names: {[prefix: string]: NameGroup} = {} - _valuePrefixes: {[prefix: string]: Name} = {} + _scope: Scope + _extScope: Scope + _values: ScopeValueSets = {} _out = "" _blocks: BlockKind[] = [] _blockStarts: number[] = [] _n = "" opts: CodeGenOptions - constructor(opts: CodeGenOptions = {}) { + constructor(extScope: Scope, opts: CodeGenOptions = {}) { this.opts = opts + this._extScope = extScope + this._scope = new Scope({parent: extScope}) if (opts.lines) this._n = "\n" } @@ -76,9 +67,15 @@ export class CodeGen { return this._scope.name(prefix) } - // TODO move to global scope - value(prefix: string, value: NameValue): Name { - return this._scope.value(prefix, value) + scopeValue(prefix: string, value: NameValue): Name { + const rec = this._extScope.value(prefix, value) + if (!this._values[prefix]) this._values[prefix] = new Set() + this._values[prefix].add(rec) + return rec.name + } + + scopeRefs(scopeName: Name): Code { + return this._extScope.scopeRefs(scopeName, this._values) } _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts index 5878d81350..d852323b06 100644 --- a/lib/compile/codegen/scope.ts +++ b/lib/compile/codegen/scope.ts @@ -1,6 +1,8 @@ import {_, nil, Code, Name} from "./code" -interface NameRec { +export interface NameRec { + prefixName: Name + scopeIndex?: number name: Name value: NameValue } @@ -8,7 +10,6 @@ interface NameRec { interface NameGroup { prefix: string index: number - values?: Map // same key as passed in GlobalValue } export interface NameValue { @@ -21,98 +22,114 @@ export type ValueReference = unknown // possibly make CodeGen parameterized type class ValueError extends Error { value: NameValue - constructor(fields: string, {name, value}: NameRec) { - super(`CodeGen: ${fields} for ${name} not defined`) + constructor({name, value}: NameRec) { + super(`CodeGen: "code" for ${name} not defined`) this.value = value } } -export interface _Scope { - [prefix: string]: ValueReference[] +interface ScopeOptions { + scope?: ScopeStore + prefixes?: Set + parent?: Scope } +export type ScopeStore = Record + +type ScopeValues = {[prefix: string]: Map} + +export type ScopeValueSets = {[prefix: string]: Set} + export class Scope { _names: {[prefix: string]: NameGroup} = {} - _valuePrefixes: {[prefix: string]: Name} = {} + _values: ScopeValues = {} + _prefixes?: Set _parent?: Scope + _scope?: ScopeStore - constructor(parent?: Scope) { + constructor({prefixes, parent, scope}: ScopeOptions = {}) { + this._prefixes = prefixes this._parent = parent + this._scope = scope } - _nameGroup(prefix: string): NameGroup { - let ng = this._names[prefix] - if (!ng) ng = this._names[prefix] = {prefix, index: 0} - return ng - } - - _name(ng: NameGroup): Name { - return new Name(ng.prefix + ng.index++) + get() { + return this._scope } name(prefix: string): Name { - const ng = this._nameGroup(prefix) - return this._name(ng) + let ng = this._names[prefix] + if (!ng) { + checkPrefix.call(this, prefix) + ng = this._names[prefix] = {prefix, index: 0} + } + return new Name(ng.prefix + ng.index++) } - value(prefix: string, value: NameValue): Name { + value(prefix: string, value: NameValue): NameRec { const {ref, key, code} = value const valueKey = key ?? ref ?? code if (!valueKey) throw new Error("CodeGen: ref or code must be passed in value") - const ng = this._nameGroup(prefix) - this._valuePrefixes[prefix] = new Name(prefix) - if (!ng.values) { - ng.values = new Map() + let vs = this._values[prefix] + if (vs) { + const rec = vs.get(valueKey) + if (rec) return rec } else { - const rec = ng.values.get(valueKey) - if (rec) return rec.name + vs = this._values[prefix] = new Map() + } + + const rec: NameRec = { + prefixName: new Name(prefix), + name: this.name(prefix), + value, + } + vs.set(valueKey, rec) + + if (this._scope && value.ref) { + const s = this._scope[prefix] || (this._scope[prefix] = []) + rec.scopeIndex = s.length + s[rec.scopeIndex] = value.ref } - const name = this._name(ng) - ng.values.set(valueKey, {name, value}) - return name + return rec } - scopeRefs(scopeName: Name, scope: _Scope): Code { - return this._reduceValues((rec: NameRec, prefix: string, i: number) => { - const {value: v} = rec - if (v.ref) { - if (!scope[prefix]) scope[prefix] = [] - scope[prefix][i] = v.ref - const prefName = this._valuePrefixes[prefix] - return _`${scopeName}.${prefName}[${i}]` - } - if (v.code) return v.code - throw new ValueError("ref and code", rec) + scopeRefs(scopeName: Name, values?: ScopeValues | ScopeValueSets): Code { + if (!this._scope) { + throw new Error("Codegen: scope has to be passed via options to use scopeRefs") + } + return _reduceValues.call(this, values, (rec: NameRec) => { + const {value, prefixName, scopeIndex} = rec + if (scopeIndex !== undefined) return _`${scopeName}.${prefixName}[${scopeIndex}]` + if (value.code) return value.code + throw new Error("ajv implementation error") }) } - scopeCode(): Code { - return this._reduceValues((rec: NameRec) => { + scopeCode(values?: ScopeValues | ScopeValueSets): Code { + return _reduceValues.call(this, values, (rec: NameRec) => { const c = rec.value.code if (c) return c - throw new ValueError("code", rec) + throw new ValueError(rec) }) } +} - _reduceValues(valueCode: (n: NameRec, pref: string, index: number) => Code): Code { - let code: Code = nil - for (const prefix in this._valuePrefixes) { - let i = 0 - const values = this._names[prefix].values - if (!values) throw new Error("ajv implementation error") - values.forEach((rec: NameRec) => { - code = _`${code}const ${rec.name} = ${valueCode(rec, prefix, i++)};` - }) - } - return code +function checkPrefix(this: Scope, prefix: string) { + if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes?.has(prefix))) { + throw new Error(`Codegen: prefix "${prefix}" is not allowed in this scope`) } } -// class ParentScope extends Scope { -// _children: Scope[] = [] -// _childrenPrefixes: {[prefix: string]: Name} = {} - -// _freePrefix(prefix?: string) { - -// } -// } +function _reduceValues( + this: Scope, + values: ScopeValues | ScopeValueSets = this._values, + valueCode: (n: NameRec) => Code +): Code { + let code: Code = nil + for (const prefix in values) { + values[prefix].forEach((rec: NameRec) => { + code = _`${code}const ${rec.name} = ${valueCode(rec)};` + }) + } + return code +} diff --git a/lib/compile/context.ts b/lib/compile/context.ts index a43fa6d0b9..eac460b393 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -154,7 +154,7 @@ export default class KeywordCtx implements KeywordErrorCtx { function invalid$DataSchema(): Code { if (def.validateSchema) { - const validateSchemaRef = gen.value("validate$data", {ref: def.validateSchema}) // TODO value.code + const validateSchemaRef = gen.scopeValue("validate$data", {ref: def.validateSchema}) // TODO value.code for standalone return _`!${validateSchemaRef}(${schemaCode})` } return nil diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 429f5058bf..042fa6e145 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,6 @@ import type {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" import type Ajv from "../ajv" -import {CodeGen, _, nil, str, Code, _Scope} from "./codegen" +import {CodeGen, _, nil, str, Code} from "./codegen" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" @@ -187,7 +187,6 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW let compilation = getCompilation.call(this, env) if (compilation) return validateWrapper(compilation) - const scope: _Scope = {} compilation = env const formats = this._formats @@ -211,7 +210,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW const $async = typeof _schema == "object" && _schema.$async === true const rootId = getFullPath(_root.schema.$id) - const gen = new CodeGen({...opts.codegen, forInOwn: opts.ownProperties}) + const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties}) validateFunctionCode({ gen, @@ -241,7 +240,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW }) let sourceCode = `${vars(refVal, refValCode)} - ${gen._scope.scopeRefs(N.scope, scope)} // TODO + ${gen.scopeRefs(N.scope)} ${gen.toString()}` if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) @@ -268,7 +267,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW formats, root, refVal, - scope, + self._scope._scope, equal, ucs2length, ValidationError @@ -289,7 +288,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW if (opts.sourceCode === true) { validate.source = { code: sourceCode, - scope, + scope: self._scope, } } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 9581dd492c..0bb5ae83fc 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -132,5 +132,5 @@ function checkAsync(it: SchemaObjCtx, def: FuncKeywordDefinition) { function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name { if (result === undefined) throw new Error(`keyword "${keyword}" failed to compile`) - return gen.value("keyword", {ref: result}) // TODO value.code + return gen.scopeValue("keyword", {ref: result}) // TODO value.code } diff --git a/lib/types.ts b/lib/types.ts index bb5d2bc085..58cce992bb 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,4 @@ -import {CodeGen, Code, Name, CodeGenOptions, _Scope} from "./compile/codegen" +import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" import {ValidationRules} from "./compile/rules" import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" import KeywordCtx from "./compile/context" @@ -84,7 +84,7 @@ export interface CacheInterface { interface SourceCode { code: string - scope: _Scope + scope: Scope } export interface ValidateFunction { diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 6d0b78f8b6..e28126e516 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -74,7 +74,7 @@ export function callValidateCode( } export function usePattern(gen: CodeGen, pattern: string): Name { - return gen.value("pattern", { + return gen.scopeValue("pattern", { key: pattern, ref: new RegExp(pattern), code: _`new RegExp(${pattern})`, From ba791959254ad853ee2832bd2e752ae0142271c3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 8 Sep 2020 19:40:42 +0100 Subject: [PATCH 178/322] refactor: pass external functions and ValidationError via Scope --- lib/ajv.ts | 2 +- lib/compile/errors.ts | 19 +++++----- lib/compile/index.ts | 41 ++++++---------------- lib/compile/validate/index.ts | 4 +-- lib/compile/validate/keyword.ts | 2 +- lib/types.ts | 1 + lib/vocabularies/core/ref.ts | 4 +-- lib/vocabularies/validation/const.ts | 9 ++++- lib/vocabularies/validation/enum.ts | 9 +++-- lib/vocabularies/validation/limitLength.ts | 12 ++++++- lib/vocabularies/validation/uniqueItems.ts | 7 +++- 11 files changed, 61 insertions(+), 49 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index ca129063c7..0702eec8d5 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -31,7 +31,7 @@ const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] -const EXT_SCOPE_NAMES = new Set(["keyword", "pattern", "validate$data"]) +const EXT_SCOPE_NAMES = new Set(["keyword", "pattern", "validate$data", "func", "Error"]) type CompileAsyncCallback = (err: Error | null, validate?: ValidateFunction) => void diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index ae6e1e8f2f..1f490a658e 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,4 +1,4 @@ -import {KeywordErrorCtx, KeywordErrorDefinition} from "../types" +import {KeywordErrorCtx, KeywordErrorDefinition, SchemaCtx} from "../types" import {CodeGen, _, str, Code, Name} from "./codegen" import N from "./names" @@ -18,21 +18,23 @@ export function reportError( error: KeywordErrorDefinition, overrideAllErrors?: boolean ): void { - const {gen, compositeRule, allErrors, async} = cxt.it + const {it} = cxt + const {gen, compositeRule, allErrors} = it const errObj = errorObjectCode(cxt, error) if (overrideAllErrors ?? (compositeRule || allErrors)) { addError(gen, errObj) } else { - returnErrors(gen, async, _`[${errObj}]`) + returnErrors(it, _`[${errObj}]`) } } export function reportExtraError(cxt: KeywordErrorCtx, error: KeywordErrorDefinition): void { - const {gen, compositeRule, allErrors, async} = cxt.it + const {it} = cxt + const {gen, compositeRule, allErrors} = it const errObj = errorObjectCode(cxt, error) addError(gen, errObj) if (!(compositeRule || allErrors)) { - returnErrors(gen, async, N.vErrors) + returnErrors(it, N.vErrors) } } @@ -72,9 +74,10 @@ function addError(gen: CodeGen, errObj: Code): void { gen.code(_`${N.errors}++`) } -function returnErrors(gen: CodeGen, async: boolean, errs: Code): void { - if (async) { - gen.code(_`throw new ValidationError(${errs})`) +function returnErrors(it: SchemaCtx, errs: Code): void { + const {gen} = it + if (it.async) { + gen.code(_`throw new ${it.ValidationError as Name}(${errs})`) } else { gen.assign(_`${N.validate}.errors`, errs) gen.return(false) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 042fa6e145..149046beea 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,22 +1,17 @@ import type {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" import type Ajv from "../ajv" import {CodeGen, _, nil, str, Code} from "./codegen" +import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" import URI = require("uri-js") -const equal = require("fast-deep-equal") -const ucs2length = require("./ucs2length") - /** * Functions below are used inside compiled validations function */ -// this error is thrown by async schemas to return validation errors via exception -const ValidationError = require("./error_classes").ValidationError - interface _StoredSchema extends Compilation { schema: Schema id?: string @@ -211,6 +206,13 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW const rootId = getFullPath(_root.schema.$id) const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties}) + let _ValidationError + if ($async) { + _ValidationError = gen.scopeValue("Error", { + ref: ValidationError, + code: _`require("ajv/dist/compile/error_classes").ValidationError`, + }) + } validateFunctionCode({ gen, @@ -223,6 +225,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW dataLevel: 0, topSchemaRef: _`${N.validate}.schema`, async: $async, + ValidationError: _ValidationError, schema: _schema, isRoot, root: _root, @@ -248,30 +251,8 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW let validate: ValidateFunction try { // TODO refactor to fewer variables - maybe only self and scope - const makeValidate = new Function( - "self", - "RULES", - "formats", - "root", - "refVal", - "scope", - "equal", - "ucs2length", - "ValidationError", - sourceCode - ) - - validate = makeValidate( - self, - self.RULES, - formats, - root, - refVal, - self._scope._scope, - equal, - ucs2length, - ValidationError - ) + const makeValidate = new Function("self", "formats", "root", "refVal", "scope", sourceCode) + validate = makeValidate(self, formats, root, refVal, self._scope.get()) refVal[0] = validate } catch (e) { diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 1e3178be0f..b2ccf0ec03 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -131,12 +131,12 @@ function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: SchemaOb } } -function returnResults({gen, async}: SchemaCtx) { +function returnResults({gen, async, ValidationError}: SchemaCtx) { if (async) { gen.if( _`${N.errors} === 0`, () => gen.return(N.data), - _`throw new ValidationError(${N.vErrors})` + _`throw new ${ValidationError as Name}(${N.vErrors})` ) } else { gen.assign(_`${N.validate}.errors`, N.vErrors) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 0bb5ae83fc..bad7353333 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -79,7 +79,7 @@ function funcKeywordCode(cxt: KeywordCtx, def: FuncKeywordDefinition): void { () => assignValid(_`await `), (e) => gen.assign(valid, false).if( - _`${e} instanceof ValidationError`, + _`${e} instanceof ${it.ValidationError as Name}`, () => gen.assign(ruleErrs, _`${e}.errors`), () => gen.throw(e) ) diff --git a/lib/types.ts b/lib/types.ts index 58cce992bb..16ad79caf2 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -150,6 +150,7 @@ export interface SchemaCtx { dataLevel: number topSchemaRef: Code async: boolean + ValidationError?: Name schema: Schema isRoot: boolean root: SchemaRoot diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index db3fae4d29..2914bd9fee 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -4,7 +4,7 @@ import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" import {callValidateCode} from "../util" -import {_, str, nil, Code} from "../../compile/codegen" +import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" const def: CodeKeywordDefinition = { @@ -68,7 +68,7 @@ const def: CodeKeywordDefinition = { if (!allErrors) gen.assign(valid, true) }, (e) => { - gen.if(_`!(${e} instanceof ValidationError)`, () => gen.throw(e)) + gen.if(_`!(${e} instanceof ${it.ValidationError as Name})`, () => gen.throw(e)) addErrorsFrom(e) if (!allErrors) gen.assign(valid, false) } diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 5653bc0faf..0e638caa3d 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,11 +1,18 @@ import {CodeKeywordDefinition} from "../../types" import KeywordCtx from "../../compile/context" import {_} from "../../compile/codegen" +import equal from "fast-deep-equal" const def: CodeKeywordDefinition = { keyword: "const", $data: true, - code: (cxt: KeywordCtx) => cxt.fail$data(_`!equal(${cxt.data}, ${cxt.schemaCode})`), + code(cxt: KeywordCtx) { + const eql = cxt.gen.scopeValue("func", { + ref: equal, + code: _`require("ajv/dist/compile/equal")`, + }) + cxt.fail$data(_`!${eql}(${cxt.data}, ${cxt.schemaCode})`) + }, error: { message: "should be equal to constant", params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 3390fe6485..a743bfa5d4 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,6 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordCtx from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" +import equal from "fast-deep-equal" const def: CodeKeywordDefinition = { keyword: "enum", @@ -10,6 +11,10 @@ const def: CodeKeywordDefinition = { const {gen, data, $data, schema, schemaCode, it} = cxt if (!$data && schema.length === 0) throw new Error("enum must have non-empty array") const useLoop = typeof it.opts.loopEnum == "number" && schema.length >= it.opts.loopEnum + const eql = cxt.gen.scopeValue("func", { + ref: equal, + code: _`require("ajv/dist/compile/equal")`, + }) let valid: Code if (useLoop || $data) { valid = gen.let("valid") @@ -24,14 +29,14 @@ const def: CodeKeywordDefinition = { function loopEnum(): void { gen.assign(valid, false) gen.forOf("v", schemaCode, (v) => - gen.if(_`equal(${data}, ${v})`, () => gen.assign(valid, true).break()) + gen.if(_`${eql}(${data}, ${v})`, () => gen.assign(valid, true).break()) ) } function equalCode(vSchema: Name, i: number): Code { const sch: string = schema[i] if (sch && typeof sch === "object") { - return _`equal(${data}, ${vSchema}[${i}])` + return _`${eql}(${data}, ${vSchema}[${i}])` } return _`${data} === ${sch}` } diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index ae794f600b..0c7ed5efae 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,6 +1,7 @@ import {CodeKeywordDefinition} from "../../types" import KeywordCtx from "../../compile/context" import {_, str, operators} from "../../compile/codegen" +import ucs2length from "../../compile/ucs2length" const def: CodeKeywordDefinition = { keyword: ["maxLength", "minLength"], @@ -10,7 +11,16 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCtx) { const {keyword, data, schemaCode, it} = cxt const op = keyword === "maxLength" ? operators.GT : operators.LT - const len = it.opts.unicode === false ? _`${data}.length` : _`ucs2length(${data})` + let len + if (it.opts.unicode === false) { + len = _`${data}.length` + } else { + const u2l = cxt.gen.scopeValue("func", { + ref: ucs2length, + code: _`require("ajv/dist/compile/ucs2length")`, + }) + len = _`${u2l}(${data})` + } cxt.fail$data(_`${len} ${op} ${schemaCode}`) }, error: { diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 0a90abe680..a374013e36 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -3,6 +3,7 @@ import KeywordCtx from "../../compile/context" import {getSchemaTypes} from "../../compile/validate/dataType" import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" +import equal from "fast-deep-equal" const def: CodeKeywordDefinition = { keyword: "uniqueItems", @@ -48,10 +49,14 @@ const def: CodeKeywordDefinition = { } function loopN2(i: Name, j: Name): void { + const eql = cxt.gen.scopeValue("func", { + ref: equal, + code: _`require("ajv/dist/compile/equal")`, + }) const outer = gen.name("outer") gen.label(outer).for(_`;${i}--;`, () => gen.for(_`${j} = ${i}; ${j}--;`, () => - gen.if(_`equal(${data}[${i}], ${data}[${j}])`, () => { + gen.if(_`${eql}(${data}[${i}], ${data}[${j}])`, () => { cxt.error() gen.assign(valid, false).break(outer) }) From 9596996836ff4a83101b3b6f940ae1e3cffeea80 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 8 Sep 2020 19:52:16 +0100 Subject: [PATCH 179/322] use Name self instead of hard-coded "self" --- lib/compile/index.ts | 9 ++++++++- lib/compile/validate/index.ts | 2 +- lib/vocabularies/format/format.ts | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 149046beea..f070c47f76 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -251,7 +251,14 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW let validate: ValidateFunction try { // TODO refactor to fewer variables - maybe only self and scope - const makeValidate = new Function("self", "formats", "root", "refVal", "scope", sourceCode) + const makeValidate = new Function( + N.self.toString(), + "formats", + "root", + "refVal", + N.scope.toString(), + sourceCode + ) validate = makeValidate(self, formats, root, refVal, self._scope.get()) refVal[0] = validate diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index b2ccf0ec03..155ca6db89 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -124,7 +124,7 @@ function checkAsync(it: SchemaObjCtx): void { function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: SchemaObjCtx): void { const msg = schema.$comment if ($comment === true) { - gen.code(_`console.log(${msg})`) // should it use logger? + gen.code(_`${N.self}.logger.log(${msg})`) } else if (typeof $comment == "function") { const schemaPath = str`${errSchemaPath}/$comment` gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${N.validate}.root.schema)`) diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index e3875b961d..b0ba77de9b 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,6 +1,7 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" import KeywordCtx from "../../compile/context" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" +import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "format", @@ -31,7 +32,7 @@ const def: CodeKeywordDefinition = { if (opts.unknownFormats === "ignore") return nil let unknown = _`${schemaCode} && !${format}` if (Array.isArray(opts.unknownFormats)) { - unknown = _`${unknown} && !self._opts.unknownFormats.includes(${schemaCode})` + unknown = _`${unknown} && !${N.self}._opts.unknownFormats.includes(${schemaCode})` } return _`(${unknown})` } From 1e4d751ef41a18f3a030530a9b7e3aa8dabd58cf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 8 Sep 2020 19:56:40 +0100 Subject: [PATCH 180/322] move _Code definition --- lib/compile/codegen/code.ts | 78 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/lib/compile/codegen/code.ts b/lib/compile/codegen/code.ts index 1e6dfc163e..6ab73a62f9 100644 --- a/lib/compile/codegen/code.ts +++ b/lib/compile/codegen/code.ts @@ -1,42 +1,3 @@ -type TemplateArg = SafeExpr | string - -export type Code = _Code | Name - -export type SafeExpr = Code | number | boolean | null - -export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { - // TODO benchmark if loop is faster than reduce - // let res = strs[0] - // for (let i = 0; i < args.length; i++) { - // res += interpolate(args[i]) + strs[i + 1] - // } - // return new _Code(res) - return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) -} - -export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code { - return new _Code( - strs.map(safeStringify).reduce((res, s, i) => { - let aStr = interpolateStr(args[i - 1]) - if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString() - return typeof aStr === "string" - ? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1) - : `${res} + ${aStr} + ${s}` - }) - ) -} - -function interpolate(x: TemplateArg): TemplateArg { - return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null - ? x - : safeStringify(x) -} - -function interpolateStr(x: TemplateArg | string[]): TemplateArg { - if (Array.isArray(x)) x = x.join(",") - return interpolate(x) -} - export class _Code { _str: string @@ -75,8 +36,47 @@ export class Name extends _Code { } } +export type Code = _Code | Name + +export type SafeExpr = Code | number | boolean | null + export const nil = new _Code("") +type TemplateArg = SafeExpr | string + +export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { + // TODO benchmark if loop is faster than reduce + // let res = strs[0] + // for (let i = 0; i < args.length; i++) { + // res += interpolate(args[i]) + strs[i + 1] + // } + // return new _Code(res) + return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) +} + +export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code { + return new _Code( + strs.map(safeStringify).reduce((res, s, i) => { + let aStr = interpolateStr(args[i - 1]) + if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString() + return typeof aStr === "string" + ? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1) + : `${res} + ${aStr} + ${s}` + }) + ) +} + +function interpolate(x: TemplateArg): TemplateArg { + return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null + ? x + : safeStringify(x) +} + +function interpolateStr(x: TemplateArg | string[]): TemplateArg { + if (Array.isArray(x)) x = x.join(",") + return interpolate(x) +} + export function stringify(x: unknown): Code { return new _Code(safeStringify(x)) } From 4a0311d1906c026f648d71d1bb5ee35ab4072d8f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 08:51:01 +0100 Subject: [PATCH 181/322] refactor: ScopeValue class, remove "private" methods from CodeGen class --- lib/ajv.ts | 4 +-- lib/compile/codegen/index.ts | 61 ++++++++++++++++++------------------ lib/compile/codegen/scope.ts | 29 ++++++++++------- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 0702eec8d5..779ab17afe 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -17,7 +17,7 @@ import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" import {StoredSchema, compileSchemaFragment, compileStoredSchema, Compilation} from "./compile" -import {Scope} from "./compile/codegen" +import {ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" @@ -43,7 +43,7 @@ export default class Ajv { _opts: IndexableOptions _cache: CacheInterface // shared external scope values for compiled functions - _scope = new Scope({scope: {}, prefixes: EXT_SCOPE_NAMES}) + _scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES}) _schemas: {[key: string]: StoredSchema} = {} _refs: {[ref: string]: StoredSchema | string} = {} _fragments: {[key: string]: StoredSchema} = {} diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index baf7e07635..d49136a8de 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -1,7 +1,7 @@ import {_, str, nil, _Code, Code, IDENTIFIER, Name, stringify} from "./code" -import {Scope, ScopeValueSets, NameValue, ScopeStore} from "./scope" +import {Scope, ScopeValueSets, NameValue, ScopeStore, ValueScope} from "./scope" -export {_, str, nil, stringify, Name, Code, Scope, ScopeStore} +export {_, str, nil, stringify, Name, Code, Scope, ScopeStore, ValueScope} enum BlockKind { If, @@ -40,7 +40,7 @@ export interface CodeGenOptions { export class CodeGen { _scope: Scope - _extScope: Scope + _extScope: ValueScope _values: ScopeValueSets = {} _out = "" _blocks: BlockKind[] = [] @@ -48,7 +48,7 @@ export class CodeGen { _n = "" opts: CodeGenOptions - constructor(extScope: Scope, opts: CodeGenOptions = {}) { + constructor(extScope: ValueScope, opts: CodeGenOptions = {}) { this.opts = opts this._extScope = extScope this._scope = new Scope({parent: extScope}) @@ -59,7 +59,7 @@ export class CodeGen { return this._out } - _toName(nameOrPrefix: Name | string): Name { + toName(nameOrPrefix: Name | string): Name { return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) } @@ -78,24 +78,16 @@ export class CodeGen { return this._extScope.scopeRefs(scopeName, this._values) } - _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - const name = this._toName(nameOrPrefix) - if (this.opts.es5) varKind = varKinds.var - if (rhs === undefined) this._out += `${varKind} ${name};` + this._n - else this._out += `${varKind} ${name} = ${rhs};` + this._n - return name - } - const(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - return this._def(varKinds.const, nameOrPrefix, rhs) + return _def.call(this, varKinds.const, nameOrPrefix, rhs) } let(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - return this._def(varKinds.let, nameOrPrefix, rhs) + return _def.call(this, varKinds.let, nameOrPrefix, rhs) } var(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - return this._def(varKinds.var, nameOrPrefix, rhs) + return _def.call(this, varKinds.var, nameOrPrefix, rhs) } assign(name: Code, rhs: SafeExpr): CodeGen { @@ -163,20 +155,21 @@ export class CodeGen { forBody: (n: Name) => void, varKind: Code = varKinds.const ): CodeGen { - const name = this._toName(nameOrPrefix) + const name = this.toName(nameOrPrefix) if (this.opts.es5) { const i = this.name("_i") - return this._loop( + return _loop.call( + this, new _Code(`for(${varKinds.let} ${i}=0; ${i}<${iterable}.length; ${i}++){`), i, () => { const item = new _Code(`${iterable}[${i}]`) - this._def(varKind, name, item) + _def.call(this, varKind, name, item) forBody(name) } ) } - return this._loop(new _Code(`for(${varKind} ${name} of ${iterable}){`), name, forBody) + return _loop.call(this, new _Code(`for(${varKind} ${name} of ${iterable}){`), name, forBody) } forIn( @@ -189,16 +182,8 @@ export class CodeGen { if (this.opts.forInOwn) { return this.forOf(nameOrPrefix, new _Code(`Object.keys(${obj})`), forBody) } - const name = this._toName(nameOrPrefix) - return this._loop(new _Code(`for(${varKind} ${name} in ${obj}){`), name, forBody) - } - - _loop(forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { - this._blocks.push(BlockKind.For) - this._out += forCode + this._n - forBody(name) - this.endFor() - return this + const name = this.toName(nameOrPrefix) + return _loop.call(this, new _Code(`for(${varKind} ${name} in ${obj}){`), name, forBody) } endFor(): CodeGen { @@ -297,6 +282,22 @@ export class CodeGen { } } +function _def(this: CodeGen, varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { + const name = this.toName(nameOrPrefix) + if (this.opts.es5) varKind = varKinds.var + if (rhs === undefined) this._out += `${varKind} ${name};` + this._n + else this._out += `${varKind} ${name} = ${rhs};` + this._n + return name +} + +function _loop(this: CodeGen, forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { + this._blocks.push(BlockKind.For) + this._out += forCode + this._n + forBody(name) + this.endFor() + return this +} + export function getProperty(key: Code | string | number): Code { return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]` } diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts index d852323b06..da7fd53dee 100644 --- a/lib/compile/codegen/scope.ts +++ b/lib/compile/codegen/scope.ts @@ -42,19 +42,12 @@ export type ScopeValueSets = {[prefix: string]: Set} export class Scope { _names: {[prefix: string]: NameGroup} = {} - _values: ScopeValues = {} _prefixes?: Set _parent?: Scope - _scope?: ScopeStore - constructor({prefixes, parent, scope}: ScopeOptions = {}) { + constructor({prefixes, parent}: ScopeOptions = {}) { this._prefixes = prefixes this._parent = parent - this._scope = scope - } - - get() { - return this._scope } name(prefix: string): Name { @@ -65,6 +58,20 @@ export class Scope { } return new Name(ng.prefix + ng.index++) } +} + +export class ValueScope extends Scope { + _values: ScopeValues = {} + _scope?: ScopeStore + + constructor(opts: ScopeOptions = {}) { + super(opts) + this._scope = opts.scope + } + + get() { + return this._scope + } value(prefix: string, value: NameValue): NameRec { const {ref, key, code} = value @@ -95,7 +102,7 @@ export class Scope { scopeRefs(scopeName: Name, values?: ScopeValues | ScopeValueSets): Code { if (!this._scope) { - throw new Error("Codegen: scope has to be passed via options to use scopeRefs") + throw new Error("CodeGen: scope has to be passed via options to use scopeRefs") } return _reduceValues.call(this, values, (rec: NameRec) => { const {value, prefixName, scopeIndex} = rec @@ -116,12 +123,12 @@ export class Scope { function checkPrefix(this: Scope, prefix: string) { if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes?.has(prefix))) { - throw new Error(`Codegen: prefix "${prefix}" is not allowed in this scope`) + throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`) } } function _reduceValues( - this: Scope, + this: ValueScope, values: ScopeValues | ScopeValueSets = this._values, valueCode: (n: NameRec) => Code ): Code { From b8fe72aad8a616ac4eb820fae0b9c5dea1f66330 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 09:51:08 +0100 Subject: [PATCH 182/322] feat: codegen forRange loop --- lib/compile/codegen/index.ts | 51 ++++++++++--------- lib/compile/errors.ts | 4 +- .../applicator/additionalItems.ts | 3 +- lib/vocabularies/applicator/contains.ts | 3 +- lib/vocabularies/applicator/items.ts | 3 +- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index d49136a8de..a12a45cd3b 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -110,7 +110,7 @@ export class CodeGen { } else if (thenBody) { this.code(thenBody).endIf() } else if (elseBody) { - throw new Error('CodeGen: "else" body wit_out "then" body') + throw new Error('CodeGen: "else" body without "then" body') } return this } @@ -121,13 +121,13 @@ export class CodeGen { } elseIf(condition: Code): CodeGen { - if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" wit_out "if"') + if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"') this._out += `}else if(${condition}){` + this._n return this } else(): CodeGen { - if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" wit_out "if"') + if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" without "if"') this._lastBlock = BlockKind.Else this._out += "}else{" + this._n return this @@ -136,7 +136,7 @@ export class CodeGen { endIf(): CodeGen { // TODO possibly remove empty branches here const b = this._lastBlock - if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" wit_out "if"') + if (b !== BlockKind.If && b !== BlockKind.Else) throw new Error('CodeGen: "endIf" without "if"') this._blocks.pop() this._out += "}" + this._n return this @@ -149,46 +149,51 @@ export class CodeGen { return this } + forRange( + nameOrPrefix: Name | string, + from: SafeExpr, + to: SafeExpr, + forBody: (index: Name) => void, + varKind: Code = varKinds.let + ): CodeGen { + const i = this.toName(nameOrPrefix) + if (this.opts.es5) varKind = varKinds.var + return _loop.call(this, _`for(${varKind} ${i}=${from}; ${i}<${to}; ${i}++){`, i, forBody) + } + forOf( nameOrPrefix: Name | string, iterable: SafeExpr, - forBody: (n: Name) => void, + forBody: (item: Name) => void, varKind: Code = varKinds.const ): CodeGen { const name = this.toName(nameOrPrefix) if (this.opts.es5) { - const i = this.name("_i") - return _loop.call( - this, - new _Code(`for(${varKinds.let} ${i}=0; ${i}<${iterable}.length; ${i}++){`), - i, - () => { - const item = new _Code(`${iterable}[${i}]`) - _def.call(this, varKind, name, item) - forBody(name) - } - ) + const arr = iterable instanceof Name ? iterable : this.var("arr", iterable) + return this.forRange("_i", 0, new _Code(`${arr}.length`), (i) => { + this.var(name, new _Code(`${arr}[${i}]`)) + forBody(name) + }) } - return _loop.call(this, new _Code(`for(${varKind} ${name} of ${iterable}){`), name, forBody) + return _loop.call(this, _`for(${varKind} ${name} of ${iterable}){`, name, forBody) } forIn( nameOrPrefix: Name | string, obj: SafeExpr, - forBody: (n: Name) => void, + forBody: (item: Name) => void, varKind: Code = varKinds.const ): CodeGen { - // TODO define enum for var kinds if (this.opts.forInOwn) { return this.forOf(nameOrPrefix, new _Code(`Object.keys(${obj})`), forBody) } const name = this.toName(nameOrPrefix) - return _loop.call(this, new _Code(`for(${varKind} ${name} in ${obj}){`), name, forBody) + return _loop.call(this, _`for(${varKind} ${name} in ${obj}){`, name, forBody) } endFor(): CodeGen { const b = this._lastBlock - if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" wit_out "for"') + if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"') this._blocks.pop() this._out += "}" + this._n return this @@ -212,7 +217,7 @@ export class CodeGen { } try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen { - if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" wit_out "catch" and "finally"') + if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"') this._out += "try{" + this._n this.code(tryBody) if (catchCode) { @@ -261,7 +266,7 @@ export class CodeGen { endFunc(): CodeGen { const b = this._lastBlock - if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" wit_out "func"') + if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" without "func"') this._blocks.pop() this._out += "}" + this._n return this diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 1f490a658e..cdfa497d30 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -55,8 +55,8 @@ export function extendErrors({ }: KeywordErrorCtx): void { if (errsCount === undefined) throw new Error("ajv implementation error") const err = gen.name("err") - gen.for(_`let i=${errsCount}; i<${N.errors}; i++`, () => { - gen.const(err, _`${N.vErrors}[i]`) + gen.forRange("i", errsCount, N.errors, (i) => { + gen.const(err, _`${N.vErrors}[${i}]`) gen.if( _`${err}.dataPath === undefined`, _`${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 2c9bc8cd0b..27e7de8f47 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -27,8 +27,7 @@ const def: CodeKeywordDefinition = { } function validateItems(valid: Name): void { - const i = gen.name("i") - gen.for(_`let ${i}=${items.length}; ${i}<${len}; ${i}++`, () => { + gen.forRange("i", items.length, len, (i) => { applySubschema(it, {keyword: "additionalItems", dataProp: i, dataPropType: Type.Num}, valid) if (!it.allErrors) gen.ifNot(valid, _`break`) }) diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 1fa65219b4..e8f13907a5 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -19,8 +19,7 @@ const def: CodeKeywordDefinition = { } const valid = gen.name("valid") - const i = gen.name("i") - gen.for(_`let ${i}=0; ${i}<${data}.length; ${i}++`, () => { + gen.forRange("i", 0, _`${data}.length`, (i) => { applySubschema( it, { diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 2bcbe461ac..e435190b63 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -39,8 +39,7 @@ const def: CodeKeywordDefinition = { function validateItems(): void { const valid = gen.name("valid") - const i = gen.name("i") - gen.for(_`let ${i}=0; ${i}<${len}; ${i}++`, () => { + gen.forRange("i", 0, len, (i) => { applySubschema(it, {keyword: "items", dataProp: i, dataPropType: Type.Num}, valid) if (!it.allErrors) gen.ifNot(valid, _`break`) }) From 561bc8b97fee4a4ffbae96a4f82cfbcc2cef6940 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 15:10:13 +0100 Subject: [PATCH 183/322] refactor: compileSchemaFragment --- lib/ajv.ts | 23 +++++++++++------------ lib/compile/index.ts | 24 +++++------------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 779ab17afe..9f4b4990ca 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -16,7 +16,7 @@ import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" -import {StoredSchema, compileSchemaFragment, compileStoredSchema, Compilation} from "./compile" +import {StoredSchema, Compilation, compileStoredSchema, resolveSchema} from "./compile" import {ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" @@ -246,18 +246,17 @@ export default class Ajv { } // Get compiled schema by `key` or `ref`. - getSchema( - keyRef: string // `key` that was passed to `addSchema` or full schema reference (`schema.$id` or resolved id). - ): ValidateFunction | undefined { - const schemaObj = _getSchemaObj.call(this, keyRef) - switch (typeof schemaObj) { - case "object": - return schemaObj.validate || compileStoredSchema.call(this, schemaObj) - case "string": - return this.getSchema(schemaObj) - case "undefined": - return compileSchemaFragment.call(this, keyRef) + // (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id) + getSchema(keyRef: string): ValidateFunction | undefined { + let schemaObj = _getSchemaObj.call(this, keyRef) + if (schemaObj === undefined) { + const root = {schema: {}, refVal: [undefined], refs: {}} + const env = resolveSchema.call(this, root, keyRef) + if (!env) return + schemaObj = this._fragments[keyRef] = new StoredSchema({...env, ref: keyRef, fragment: true}) } + if (typeof schemaObj == "string") return this.getSchema(schemaObj) + return schemaObj.validate || compileStoredSchema.call(this, schemaObj) } // Remove cached schema(s). diff --git a/lib/compile/index.ts b/lib/compile/index.ts index f070c47f76..0bc6502aeb 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -92,11 +92,10 @@ export interface Compilation extends CompileEnv { export function compileStoredSchema( this: Ajv, - schemaObj: StoredSchema, - root?: SchemaRoot + schemaObj: StoredSchema ): ValidateFunction | ValidateWrapper { if (schemaObj.compiling) return validateWrapper(schemaObj) - const v = _tryCompile.call(this, schemaObj, root) + const v = _tryCompile.call(this, schemaObj) schemaObj.validate = v schemaObj.refs = v.refs schemaObj.refVal = v.refVal @@ -104,27 +103,14 @@ export function compileStoredSchema( return v } -export function compileSchemaFragment(this: Ajv, ref: string): ValidateFunction | undefined { - const root: SchemaRoot = {schema: {}, refVal: [undefined], refs: {}} - const env = resolveSchema.call(this, root, ref) - if (!env) return - const validate = compileSchema.call(this, env) - this._fragments[ref] = new StoredSchema({...env, ref, fragment: true, validate}) - return validate -} - -function _tryCompile( - this: Ajv, - schemaObj: StoredSchema, - root?: SchemaRoot -): ValidateFunction | ValidateWrapper { +function _tryCompile(this: Ajv, schemaObj: StoredSchema): ValidateFunction | ValidateWrapper { const currentOpts = this._opts const {meta, schema, localRefs} = schemaObj if (meta) this._opts = this._metaOpts try { schemaObj.compiling = true - const v = compileSchema.call(this, {schema, root, localRefs}) + const v = compileSchema.call(this, {schema, localRefs}) extendValidateWrapper(v, schemaObj, this._opts.sourceCode) return v } catch (e) { @@ -415,7 +401,7 @@ function resolve( } // Resolve schema, its root and baseId -function resolveSchema( +export function resolveSchema( this: Ajv, root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it ref: string // reference to resolve From 310aa054436705f857ee241fa3f7ac1f5cc14aae Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 16:52:52 +0100 Subject: [PATCH 184/322] refactor: compileStoredSchema --- lib/ajv.ts | 4 +- lib/compile/index.ts | 150 ++++++++++++++++++------------------------- 2 files changed, 63 insertions(+), 91 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 9f4b4990ca..125de32cfc 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -16,7 +16,7 @@ import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" -import {StoredSchema, Compilation, compileStoredSchema, resolveSchema} from "./compile" +import {StoredSchema, compileStoredSchema, resolveSchema} from "./compile" import {ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" @@ -48,7 +48,7 @@ export default class Ajv { _refs: {[ref: string]: StoredSchema | string} = {} _fragments: {[key: string]: StoredSchema} = {} _formats: {[name: string]: AddedFormat} = {} - _compilations: Set = new Set() + _compilations: Set = new Set() _loadingSchemas: {[ref: string]: Promise} = {} _metaOpts: IndexableOptions RULES: ValidationRules diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 0bc6502aeb..8b50c0b463 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -12,7 +12,7 @@ import URI = require("uri-js") * Functions below are used inside compiled validations function */ -interface _StoredSchema extends Compilation { +interface _StoredSchema { schema: Schema id?: string ref?: string @@ -74,57 +74,31 @@ export interface SchemaRoot { refs: {[ref: string]: number | undefined} } -export interface CompileEnv { +interface SchemaEnv { schema: Schema - root?: SchemaRoot + root: SchemaRoot localRefs?: LocalRefs baseId?: string } -export interface SchemaEnv extends CompileEnv { - root: SchemaRoot -} - -export interface Compilation extends CompileEnv { - validate?: ValidateFunction - callValidate?: ValidateWrapper -} - export function compileStoredSchema( this: Ajv, - schemaObj: StoredSchema + schObj: StoredSchema ): ValidateFunction | ValidateWrapper { - if (schemaObj.compiling) return validateWrapper(schemaObj) - const v = _tryCompile.call(this, schemaObj) - schemaObj.validate = v - schemaObj.refs = v.refs - schemaObj.refVal = v.refVal - schemaObj.root = v.root - return v -} - -function _tryCompile(this: Ajv, schemaObj: StoredSchema): ValidateFunction | ValidateWrapper { - const currentOpts = this._opts - const {meta, schema, localRefs} = schemaObj - if (meta) this._opts = this._metaOpts - - try { - schemaObj.compiling = true - const v = compileSchema.call(this, {schema, localRefs}) - extendValidateWrapper(v, schemaObj, this._opts.sourceCode) - return v - } catch (e) { - delete schemaObj.validate - delete schemaObj.callValidate - throw e - } finally { - schemaObj.compiling = false - if (meta) this._opts = currentOpts + if (schObj.meta) { + const currentOpts = this._opts + this._opts = this._metaOpts + try { + return compileSchema.call(this, schObj) + } finally { + this._opts = currentOpts + } } + return compileSchema.call(this, schObj) } -function validateWrapper(c: Compilation): ValidateWrapper { - if (!c.callValidate) { +function validateWrapper(sch: StoredSchema): ValidateWrapper { + if (!sch.callValidate) { const wrapper: ValidateWrapper = function (this: Ajv | unknown, ...args) { if (wrapper.validate === undefined) throw new Error("ajv implementation error") const v = wrapper.validate @@ -132,64 +106,62 @@ function validateWrapper(c: Compilation): ValidateWrapper { wrapper.errors = v.errors return valid } - c.callValidate = wrapper + sch.callValidate = wrapper } - c.validate = c.callValidate - return c.callValidate + return sch.callValidate } -function extendValidateWrapper(v: ValidateFunction, c: Compilation, sourceCode?: boolean): void { - const cv = c.callValidate +function extendValidateWrapper(v: ValidateFunction, sch: StoredSchema): void { + const cv = sch.callValidate if (cv) { cv.validate = v - cv.schema = v.schema - cv.errors = null - cv.refs = v.refs - cv.refVal = v.refVal - cv.root = v.root - cv.$async = v.$async - if (sourceCode) cv.source = v.source + Object.assign(cv, v) } } // Compiles schema to validation function -function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateWrapper { - const {schema, root: passedRoot, localRefs} = env +function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | ValidateWrapper { + const _schObj = getCompilingSchema.call(this, sch) as StoredSchema + if (_schObj) return validateWrapper(_schObj) + + const {localRefs} = sch const self = this const opts = this._opts const refVal: (RefVal | undefined)[] = [undefined] const refs: {[ref: string]: number | undefined} = {} - const root: SchemaRoot = passedRoot || { - schema: typeof schema == "boolean" ? {} : schema, - refVal, - refs, + if (sch.root === undefined) { + sch.root = { + schema: typeof sch.schema == "boolean" ? {} : sch.schema, + refVal, + refs, + } } - - let compilation = getCompilation.call(this, env) - if (compilation) return validateWrapper(compilation) - - compilation = env + const root = sch.root const formats = this._formats try { - this._compilations.add(compilation) - const v = localCompile({...env, root}) - extendValidateWrapper(v, compilation) + this._compilations.add(sch) + const v = localCompile(sch as SchemaEnv) + extendValidateWrapper(v, sch) + sch.validate = v + sch.refs = v.refs + sch.refVal = v.refVal + sch.root = v.root return v } finally { - this._compilations.delete(compilation) + this._compilations.delete(sch) } - function localCompile(_env: SchemaEnv): ValidateFunction { - const {schema: _schema, root: _root, baseId} = _env - const isRoot = isRootEnv(_env) - if (_root !== root) { - return compileSchema.call(self, _env) + function localCompile(_sch: StoredSchema): ValidateFunction { + const {schema, baseId} = _sch + if (_sch.root === undefined || _sch.root !== root) { + return compileSchema.call(self, _sch) } + const isRoot = isRootEnv(_sch as SchemaEnv) - const $async = typeof _schema == "object" && _schema.$async === true - const rootId = getFullPath(_root.schema.$id) + const $async = typeof schema == "object" && schema.$async === true + const rootId = getFullPath(_sch.root.schema.$id) const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties}) let _ValidationError @@ -212,9 +184,9 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW topSchemaRef: _`${N.validate}.schema`, async: $async, ValidationError: _ValidationError, - schema: _schema, + schema: schema, isRoot, - root: _root, + root: _sch.root, rootId, baseId: baseId || rootId, schemaPath: nil, @@ -232,7 +204,7 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW ${gen.scopeRefs(N.scope)} ${gen.toString()}` - if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema) + if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) // console.log("\n\n\n *** \n", sourceCode) let validate: ValidateFunction try { @@ -253,11 +225,11 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW throw e } - validate.schema = _schema + validate.schema = schema validate.errors = null validate.refs = refs validate.refVal = refVal - validate.root = isRoot ? root : _root + validate.root = isRoot ? root : _sch.root if ($async) validate.$async = true if (opts.sourceCode === true) { validate.source = { @@ -336,14 +308,14 @@ function compileSchema(this: Ajv, env: CompileEnv): ValidateFunction | ValidateW } // Index of schema compilation in the currently compiled list -function getCompilation(this: Ajv, env: CompileEnv): Compilation | void { - for (const c of this._compilations) { - if (equalEnv(c, env)) return c +function getCompilingSchema(this: Ajv, schObj: StoredSchema): StoredSchema | void { + for (const sch of this._compilations) { + if (sameSchema(sch, schObj)) return sch } } -function equalEnv(e1: CompileEnv, e2: CompileEnv): boolean { - return e1.schema === e2.schema && e1.root === e2.root && e1.baseId === e2.baseId +function sameSchema(s1: StoredSchema, s2: StoredSchema): boolean { + return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId } function refValCode(i: number, refVal: (RefVal | undefined)[]): Code { @@ -375,7 +347,7 @@ function resolve( if (schOrRef instanceof StoredSchema) { return inlineRef(schOrRef.schema, this._opts.inlineRefs) ? schOrRef.schema - : schOrRef.validate || compileStoredSchema.call(this, schOrRef) + : schOrRef.validate || compileSchema.call(this, schOrRef) } const env = resolveSchema.call(this, root, ref) @@ -388,7 +360,7 @@ function resolve( if (schema instanceof StoredSchema) { if (!schema.validate) { - schema.validate = localCompile.call(this, {schema: schema.schema, root, baseId}) + schema.validate = localCompile.call(this, schema as SchemaEnv) } return schema.validate } @@ -416,12 +388,12 @@ export function resolveSchema( return resolveRecursive.call(this, root, schOrRef, p) } if (schOrRef instanceof StoredSchema) { - if (!schOrRef.validate) compileStoredSchema.call(this, schOrRef) + if (!schOrRef.validate) compileSchema.call(this, schOrRef) root = schOrRef } else { schOrRef = this._schemas[id] if (schOrRef instanceof StoredSchema) { - if (!schOrRef.validate) compileStoredSchema.call(this, schOrRef) + if (!schOrRef.validate) compileSchema.call(this, schOrRef) if (id === normalizeId(ref)) { return {schema: schOrRef, root, baseId} } From 5ba494f25a15218aeccc7cec6afb83a21e76d99c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 18:04:19 +0100 Subject: [PATCH 185/322] refactor: remove formats from compilation closure parameters --- lib/ajv.ts | 20 ++++++++++++-------- lib/compile/index.ts | 5 ++--- lib/types.ts | 15 ++++++++++++++- lib/vocabularies/format/format.ts | 19 ++++++++++++++----- lib/vocabularies/validation/enum.ts | 2 +- lib/vocabularies/validation/required.ts | 2 +- 6 files changed, 44 insertions(+), 19 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 125de32cfc..8db8c8d53c 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -4,6 +4,7 @@ import { Vocabulary, KeywordDefinition, Options, + InstanceOptions, ValidateFunction, CacheInterface, Logger, @@ -31,26 +32,29 @@ const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] -const EXT_SCOPE_NAMES = new Set(["keyword", "pattern", "validate$data", "func", "Error"]) +const EXT_SCOPE_NAMES = new Set(["keyword", "pattern", "formats", "validate$data", "func", "Error"]) type CompileAsyncCallback = (err: Error | null, validate?: ValidateFunction) => void -interface IndexableOptions extends Options { - [opt: string]: unknown +const optsDefaults = { + strict: true, + code: {}, + loopRequired: Infinity, + loopEnum: Infinity, } export default class Ajv { - _opts: IndexableOptions + _opts: InstanceOptions _cache: CacheInterface // shared external scope values for compiled functions _scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES}) _schemas: {[key: string]: StoredSchema} = {} _refs: {[ref: string]: StoredSchema | string} = {} _fragments: {[key: string]: StoredSchema} = {} - _formats: {[name: string]: AddedFormat} = {} + formats: {[name: string]: AddedFormat} = {} _compilations: Set = new Set() _loadingSchemas: {[ref: string]: Promise} = {} - _metaOpts: IndexableOptions + _metaOpts: InstanceOptions RULES: ValidationRules logger: Logger errors?: ErrorObject[] | null // errors from the last validation @@ -59,7 +63,7 @@ export default class Ajv { static MissingRefError = MissingRefError constructor(opts: Options = {}) { - opts = this._opts = {strict: true, ...opts} + opts = this._opts = {...optsDefaults, ...opts} this.logger = getLogger(opts.logger) const formatOpt = opts.format opts.format = false @@ -352,7 +356,7 @@ export default class Ajv { // Add format addFormat(name: string, format: Format): Ajv { if (typeof format == "string") format = new RegExp(format) - this._formats[name] = format + this.formats[name] = format return this } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 8b50c0b463..4dc5b93fa4 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -138,7 +138,7 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat } const root = sch.root - const formats = this._formats + const formats = this.formats try { this._compilations.add(sch) @@ -211,13 +211,12 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat // TODO refactor to fewer variables - maybe only self and scope const makeValidate = new Function( N.self.toString(), - "formats", "root", "refVal", N.scope.toString(), sourceCode ) - validate = makeValidate(self, formats, root, refVal, self._scope.get()) + validate = makeValidate(self, root, refVal, self._scope.get()) refVal[0] = validate } catch (e) { diff --git a/lib/types.ts b/lib/types.ts index 16ad79caf2..39a9517bfa 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -48,6 +48,7 @@ export interface CurrentOptions { ownProperties?: boolean multipleOfPrecision?: boolean | number messages?: boolean + code?: CodeOptions sourceCode?: boolean processCode?: (code: string, schema: Schema) => string codegen?: CodeGenOptions @@ -58,6 +59,10 @@ export interface CurrentOptions { allowMatchingProperties?: boolean // disables a strict mode restriction } +export interface CodeOptions { + formats?: Code // code to require (or construct) map of available formats - for standalone code +} + export interface Options extends CurrentOptions { // removed: errorDataPath?: "object" | "property" @@ -69,6 +74,14 @@ export interface Options extends CurrentOptions { unicode?: boolean } +export interface InstanceOptions extends Options { + [opt: string]: unknown + strict: boolean | "log" + code: CodeOptions + loopRequired: number + loopEnum: number +} + export interface Logger { log(...args: unknown[]): unknown warn(...args: unknown[]): unknown @@ -164,7 +177,7 @@ export interface SchemaCtx { createErrors?: boolean // TODO maybe remove later RULES: ValidationRules formats: {[index: string]: AddedFormat} - opts: Options + opts: InstanceOptions resolveRef: (baseId: string, ref: string, isRoot: boolean) => ResolvedRef | void logger: Logger self: Ajv diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index b0ba77de9b..5c462d2373 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -10,14 +10,18 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordCtx, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt - const {formats, opts, logger, errSchemaPath} = it + const {formats, opts, logger, errSchemaPath, self} = it if (opts.format === false) return if ($data) validate$DataFormat() else validateFormat() function validate$DataFormat() { - const fDef = gen.const("fDef", _`formats[${schemaCode}]`) + const fmts = gen.scopeValue("formats", { + ref: self.formats, + code: opts.code?.formats, + }) + const fDef = gen.const("fDef", _`${fmts}[${schemaCode}]`) const fType = gen.let("fType") const format = gen.let("format") // TODO simplify @@ -38,8 +42,9 @@ const def: CodeKeywordDefinition = { } function invalidFmt(): Code { - const fmt = _`${format}(${data})` - const callFormat = it.async ? _`${fDef}.async ? await ${fmt} : ${fmt}` : fmt + const callFormat = it.async + ? _`${fDef}.async ? await ${format}(${data}) : ${format}(${data})` + : _`${format}(${data})` const validData = _`typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` return _`(${format} && ${fType} === ${ruleType} && !(${validData}))` } @@ -68,7 +73,11 @@ const def: CodeKeywordDefinition = { } function getFormat(fmtDef: AddedFormat): [string, FormatValidate, Code] { - const fmt = _`formats${getProperty(schema)}` // TODO use scope for formats? + const fmt = gen.scopeValue("formats", { + key: schema, + ref: fmtDef, + code: opts.code.formats ? _`${opts.code.formats}${getProperty(schema)}` : undefined, + }) if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) { return [fmtDef.type || "string", fmtDef.validate as FormatValidate, _`${fmt}.validate`] } diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index a743bfa5d4..af2bc9ab8f 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCtx) { const {gen, data, $data, schema, schemaCode, it} = cxt if (!$data && schema.length === 0) throw new Error("enum must have non-empty array") - const useLoop = typeof it.opts.loopEnum == "number" && schema.length >= it.opts.loopEnum + const useLoop = schema.length >= it.opts.loopEnum const eql = cxt.gen.scopeValue("func", { ref: equal, code: _`require("ajv/dist/compile/equal")`, diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 9da74972d0..48e33daefc 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -12,7 +12,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCtx) { const {gen, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return - const useLoop = typeof it.opts.loopRequired == "number" && schema.length >= it.opts.loopRequired + const useLoop = schema.length >= it.opts.loopRequired if (it.allErrors) allErrorsMode() else exitOnErrorMode() From 766265f80a13a5cfc52191e657946a58f7f58059 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 18:18:31 +0100 Subject: [PATCH 186/322] refactor: remove logger and RULES from SchemaCtx (compilation context) --- lib/compile/context.ts | 14 +++++++++----- lib/compile/index.ts | 8 +------- lib/compile/util.ts | 11 +++-------- lib/compile/validate/applicability.ts | 4 ++-- lib/compile/validate/dataType.ts | 4 ++-- lib/compile/validate/index.ts | 6 +++--- lib/compile/validate/iterate.ts | 7 ++++--- lib/types.ts | 5 +---- lib/vocabularies/core/ref.ts | 6 +++--- lib/vocabularies/format/format.ts | 4 ++-- lib/vocabularies/util.ts | 11 ++++++----- 11 files changed, 36 insertions(+), 44 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index eac460b393..7802f37b9b 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -174,21 +174,25 @@ function validSchemaType(schema: unknown, schemaType: string | string[]): boolea : typeof schema == schemaType } -function validateKeywordUsage(it: SchemaObjCtx, def: KeywordDefinition, keyword: string): void { +function validateKeywordUsage( + {schema, opts, self}: SchemaObjCtx, + def: KeywordDefinition, + keyword: string +): void { if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { throw new Error("ajv implementation error") } const deps = def.dependencies - if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(it.schema, kwd))) { + if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) { throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) } if (def.validateSchema) { - const valid = def.validateSchema(it.schema[keyword]) + const valid = def.validateSchema(schema[keyword]) if (!valid) { - const msg = "keyword value is invalid: " + it.self.errorsText(def.validateSchema.errors) - if (it.opts.validateSchema === "log") it.logger.error(msg) + const msg = "keyword value is invalid: " + self.errorsText(def.validateSchema.errors) + if (opts.validateSchema === "log") self.logger.error(msg) else throw new Error(msg) } } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 4dc5b93fa4..5a56449b22 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -192,11 +192,9 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat schemaPath: nil, errSchemaPath: "#", errorPath: str``, - RULES: self.RULES, // TODO refactor - it is available on the instance formats, opts, resolveRef, // TODO move to gen.globals - logger: self.logger, self, }) @@ -452,11 +450,7 @@ function getJsonPointer( } } if (schema === undefined) return - if ( - typeof schema != "boolean" && - schema.$ref && - !schemaHasRulesButRef({schema, RULES: this.RULES}) - ) { + if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) { const $ref = resolveUrl(baseId, schema.$ref) const _env = resolveSchema.call(this, root, $ref) if (_env && !isRootEnv(_env)) return _env diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 4f4325e23a..7176115dd5 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -79,18 +79,13 @@ export function schemaHasRules(schema: Schema, rules: {[key: string]: boolean | return false } -export function schemaCtxHasRules({schema, RULES}: SchemaCtx): boolean { +export function schemaCtxHasRules({schema, self}: SchemaCtx): boolean { if (typeof schema == "boolean") return !schema - for (const key in schema) if (RULES.all[key]) return true + for (const key in schema) if (self.RULES.all[key]) return true return false } -interface SchemaAndRules { - schema: Schema - RULES: ValidationRules -} - -export function schemaHasRulesButRef({schema, RULES}: SchemaAndRules): boolean { +export function schemaHasRulesButRef(schema: Schema, RULES: ValidationRules): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (key !== "$ref" && RULES.all[key]) return true return false diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index f69eecde44..ce30a03402 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,8 +1,8 @@ import {SchemaObjCtx, SchemaObject} from "../../types" import {RuleGroup, Rule} from "../rules" -export function schemaHasRulesForType({RULES, schema}: SchemaObjCtx, ty: string): boolean { - const group = RULES.types[ty] +export function schemaHasRulesForType({schema, self}: SchemaObjCtx, ty: string): boolean { + const group = self.RULES.types[ty] return group && group !== true && shouldUseGroup(schema, group) } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 7e26abbd2d..af8eb8ac15 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -6,10 +6,10 @@ import {reportError} from "../errors" import {_, str, Name} from "../codegen" import {ValidationRules} from "../rules" -export function getSchemaTypes({RULES}: SchemaObjCtx, schema: SchemaObject): string[] { +export function getSchemaTypes({self}: SchemaObjCtx, schema: SchemaObject): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] - types.forEach((t) => checkType(t, RULES)) + types.forEach((t) => checkType(t, self.RULES)) const hasNull = types.includes("null") if (hasNull && schema.nullable === false) { throw new Error('{"type": "null"} contradicts {"nullable": "false"}') diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 155ca6db89..0bfede29b1 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -89,12 +89,12 @@ function typeAndKeywords(it: SchemaObjCtx, errsCount?: Name): void { } function checkRefsAndKeywords(it: SchemaObjCtx): void { - const {schema, errSchemaPath, opts, logger} = it - if (schema.$ref && schemaHasRulesButRef(it)) { + const {schema, errSchemaPath, opts, self} = it + if (schema.$ref && schemaHasRulesButRef(schema, self.RULES)) { if (opts.extendRefs === "fail") { throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`) } else if (opts.extendRefs !== true) { - logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) + self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) } } } diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 90aa155a17..517f3ead36 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -14,9 +14,10 @@ export function schemaKeywords( typeErrors: boolean, errsCount?: Name ): void { - const {gen, schema, data, RULES, allErrors, opts} = it - if (schema.$ref && !(opts.extendRefs === true && schemaHasRulesButRef(it))) { - gen.block(() => keywordCode(it, "$ref", (RULES.all.$ref).definition)) // TODO typecast + const {gen, schema, data, allErrors, opts, self} = it + const {RULES} = self + if (schema.$ref && !(opts.extendRefs === true && schemaHasRulesButRef(schema, RULES))) { + gen.block(() => keywordCode(it, "$ref", (RULES.all.$ref as Rule).definition)) // TODO typecast return } gen.block(() => { diff --git a/lib/types.ts b/lib/types.ts index 39a9517bfa..dd80252697 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,4 @@ import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" -import {ValidationRules} from "./compile/rules" import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" import KeywordCtx from "./compile/context" import Ajv from "./ajv" @@ -174,12 +173,10 @@ export interface SchemaCtx { errorPath: Code propertyName?: Name compositeRule?: boolean - createErrors?: boolean // TODO maybe remove later - RULES: ValidationRules + createErrors?: boolean formats: {[index: string]: AddedFormat} opts: InstanceOptions resolveRef: (baseId: string, ref: string, isRoot: boolean) => ResolvedRef | void - logger: Logger self: Ajv } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 2914bd9fee..84a8ab589f 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -12,7 +12,7 @@ const def: CodeKeywordDefinition = { schemaType: "string", code(cxt: KeywordCtx) { const {gen, schema, it} = cxt - const {resolveRef, allErrors, baseId, isRoot, root, opts, logger} = it + const {resolveRef, allErrors, baseId, isRoot, root, opts, self} = it const ref = getRef() const passCxt = opts.passContext ? N.this : nil if (ref === undefined) missingRef() @@ -33,11 +33,11 @@ const def: CodeKeywordDefinition = { const msg = MissingRefError.message(baseId, schema) switch (opts.missingRefs) { case "fail": - logger.error(msg) + self.logger.error(msg) cxt.fail() return case "ignore": - logger.warn(msg) + self.logger.warn(msg) return default: throw new MissingRefError(baseId, schema, msg) diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 5c462d2373..349b0eee34 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordCtx, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt - const {formats, opts, logger, errSchemaPath, self} = it + const {formats, opts, errSchemaPath, self} = it if (opts.format === false) return if ($data) validate$DataFormat() @@ -61,7 +61,7 @@ const def: CodeKeywordDefinition = { function unknownFormat() { if (opts.unknownFormats === "ignore") { - logger.warn(unknownMsg()) + self.logger.warn(unknownMsg()) return } if (Array.isArray(opts.unknownFormats) && opts.unknownFormats.includes(schema)) return diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index e28126e516..b82cbbc75a 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -21,13 +21,14 @@ export function alwaysValidSchema(it: SchemaCtx, schema: Schema): boolean | void if (typeof schema == "boolean") return schema if (Object.keys(schema).length === 0) return true checkUnknownRules(it, schema) - return !schemaHasRules(schema, it.RULES.all) + return !schemaHasRules(schema, it.self.RULES.all) } export function checkUnknownRules(it: SchemaCtx, schema: Schema = it.schema): void { - if (!it.opts.strict) return + const {opts, self} = it + if (!opts.strict) return if (typeof schema === "boolean") return - const rules = it.RULES.keywords + const rules = self.RULES.keywords for (const key in schema) { if (!rules[key]) checkStrictMode(it, `unknown keyword: "${key}"`) } @@ -82,9 +83,9 @@ export function usePattern(gen: CodeGen, pattern: string): Name { } export function checkStrictMode(it: SchemaCtx, msg: string): void { - const {opts, logger} = it + const {opts, self} = it if (opts.strict) { - if (opts.strict === "log") logger.warn(msg) + if (opts.strict === "log") self.logger.warn(msg) else throw new Error(msg) } } From 0f4ecfafdf5d2a716cca375f7b7eddf7b2cc0adc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 19:01:26 +0100 Subject: [PATCH 187/322] refactor: pass validate function Name via SchemaCtx (compilation context) --- lib/compile/errors.ts | 4 ++-- lib/compile/index.ts | 7 +++++-- lib/compile/names.ts | 1 - lib/compile/validate/boolSchema.ts | 4 ++-- lib/compile/validate/index.ts | 16 ++++++++-------- lib/types.ts | 1 + lib/vocabularies/core/ref.ts | 4 ++-- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index cdfa497d30..611497a106 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -75,11 +75,11 @@ function addError(gen: CodeGen, errObj: Code): void { } function returnErrors(it: SchemaCtx, errs: Code): void { - const {gen} = it + const {gen, validateName} = it if (it.async) { gen.code(_`throw new ${it.ValidationError as Name}(${errs})`) } else { - gen.assign(_`${N.validate}.errors`, errs) + gen.assign(_`${validateName}.errors`, errs) gen.return(false) } } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 5a56449b22..c7bdb9fde5 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,6 @@ import type {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" import type Ajv from "../ajv" -import {CodeGen, _, nil, str, Code} from "./codegen" +import {CodeGen, _, nil, str, Code, Name} from "./codegen" import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" @@ -172,6 +172,8 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat }) } + const validateName = new Name("validate") + validateFunctionCode({ gen, allErrors: !!opts.allErrors, @@ -181,8 +183,9 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat dataNames: [N.data], dataPathArr: [nil], dataLevel: 0, - topSchemaRef: _`${N.validate}.schema`, + topSchemaRef: _`${validateName}.schema`, async: $async, + validateName, ValidationError: _ValidationError, schema: schema, isRoot, diff --git a/lib/compile/names.ts b/lib/compile/names.ts index abb05f10d0..b2942974b8 100644 --- a/lib/compile/names.ts +++ b/lib/compile/names.ts @@ -1,7 +1,6 @@ import {Name} from "./codegen" const names = { - validate: new Name("validate"), // validation function name // validation function arguments data: new Name("data"), // data passed to validation function // args passed from referencing schema diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 2f3df0e742..c837d1cd89 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -8,13 +8,13 @@ const boolError: KeywordErrorDefinition = { } export function topBoolOrEmptySchema(it: SchemaCtx): void { - const {gen, schema} = it + const {gen, schema, validateName} = it if (schema === false) { falseSchemaError(it, false) } else if (typeof schema == "object" && schema.$async === true) { gen.return(N.data) } else { - gen.assign(_`${N.validate}.errors`, null) + gen.assign(_`${validateName}.errors`, null) gen.return(true) } } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 0bfede29b1..a47674fe50 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -20,10 +20,10 @@ export function validateFunctionCode(it: SchemaCtx): void { validateFunction(it, () => topBoolOrEmptySchema(it)) } -function validateFunction({gen, schema, async, opts}: SchemaCtx, body: Block) { +function validateFunction({gen, validateName, schema, async, opts}: SchemaCtx, body: Block) { gen.return(() => gen.func( - N.validate, + validateName, _`${N.data}, ${N.dataPath}, ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}`, async, () => gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`).code(body) @@ -121,17 +121,17 @@ function checkAsync(it: SchemaObjCtx): void { if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") } -function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: SchemaObjCtx): void { +function commentKeyword({gen, validateName, schema, errSchemaPath, opts}: SchemaObjCtx): void { const msg = schema.$comment - if ($comment === true) { + if (opts.$comment === true) { gen.code(_`${N.self}.logger.log(${msg})`) - } else if (typeof $comment == "function") { + } else if (typeof opts.$comment == "function") { const schemaPath = str`${errSchemaPath}/$comment` - gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${N.validate}.root.schema)`) + gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${validateName}.root.schema)`) } } -function returnResults({gen, async, ValidationError}: SchemaCtx) { +function returnResults({gen, async, validateName, ValidationError}: SchemaCtx) { if (async) { gen.if( _`${N.errors} === 0`, @@ -139,7 +139,7 @@ function returnResults({gen, async, ValidationError}: SchemaCtx) { _`throw new ${ValidationError as Name}(${N.vErrors})` ) } else { - gen.assign(_`${N.validate}.errors`, N.vErrors) + gen.assign(_`${validateName}.errors`, N.vErrors) gen.return(_`${N.errors} === 0`) } } diff --git a/lib/types.ts b/lib/types.ts index dd80252697..3c274eb0d4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -162,6 +162,7 @@ export interface SchemaCtx { dataLevel: number topSchemaRef: Code async: boolean + validateName: Name ValidationError?: Name schema: Schema isRoot: boolean diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 84a8ab589f..cc4c42d00e 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -12,7 +12,7 @@ const def: CodeKeywordDefinition = { schemaType: "string", code(cxt: KeywordCtx) { const {gen, schema, it} = cxt - const {resolveRef, allErrors, baseId, isRoot, root, opts, self} = it + const {resolveRef, allErrors, baseId, isRoot, root, opts, validateName, self} = it const ref = getRef() const passCxt = opts.passContext ? N.this : nil if (ref === undefined) missingRef() @@ -23,7 +23,7 @@ const def: CodeKeywordDefinition = { function getRef(): ResolvedRef | void { if (schema === "#" || schema === "#/") { return isRoot - ? {code: N.validate, $async: it.async} + ? {code: validateName, $async: it.async} : {code: _`root.refVal[0]`, $async: root.schema.$async === true} } return resolveRef(baseId, schema, isRoot) From ab5ecad337fa691ab55d710e255709b8005d2fee Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 20:32:29 +0100 Subject: [PATCH 188/322] refactor: schema compilation --- lib/compile/index.ts | 159 +++++++++++++++++++------------------------ lib/types.ts | 5 +- 2 files changed, 71 insertions(+), 93 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index c7bdb9fde5..7437ba1a63 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,4 @@ -import type {Schema, SchemaObject, ValidateFunction, ValidateWrapper} from "../types" +import type {Schema, SchemaObject, ValidateFunction} from "../types" import type Ajv from "../ajv" import {CodeGen, _, nil, str, Code, Name} from "./codegen" import {ValidationError} from "./error_classes" @@ -25,8 +25,6 @@ interface _StoredSchema { localRefs?: LocalRefs baseId?: string validate?: ValidateFunction - callValidate?: ValidateWrapper - compiling?: boolean } export class StoredSchema implements _StoredSchema { @@ -42,8 +40,6 @@ export class StoredSchema implements _StoredSchema { localRefs?: LocalRefs baseId?: string validate?: ValidateFunction - callValidate?: ValidateWrapper - compiling?: boolean constructor(obj: _StoredSchema) { this.schema = obj.schema @@ -81,87 +77,68 @@ interface SchemaEnv { baseId?: string } -export function compileStoredSchema( - this: Ajv, - schObj: StoredSchema -): ValidateFunction | ValidateWrapper { - if (schObj.meta) { - const currentOpts = this._opts - this._opts = this._metaOpts - try { - return compileSchema.call(this, schObj) - } finally { - this._opts = currentOpts - } +export function compileStoredSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { + return (schObj.meta ? compileMetaSchema : compileSchema).call(this, schObj) +} + +function compileMetaSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { + const currentOpts = this._opts + this._opts = this._metaOpts + try { + return compileSchema.call(this, schObj) + } finally { + this._opts = currentOpts } - return compileSchema.call(this, schObj) } -function validateWrapper(sch: StoredSchema): ValidateWrapper { - if (!sch.callValidate) { - const wrapper: ValidateWrapper = function (this: Ajv | unknown, ...args) { +function validateWrapper(sch: StoredSchema): ValidateFunction { + if (!sch.validate) { + const wrapper: ValidateFunction = function (this: Ajv | unknown, ...args) { if (wrapper.validate === undefined) throw new Error("ajv implementation error") const v = wrapper.validate const valid = v.apply(this, args) wrapper.errors = v.errors return valid } - sch.callValidate = wrapper + sch.validate = wrapper } - return sch.callValidate + return sch.validate } -function extendValidateWrapper(v: ValidateFunction, sch: StoredSchema): void { - const cv = sch.callValidate - if (cv) { - cv.validate = v - Object.assign(cv, v) - } +function extendWrapper(wrapper: ValidateFunction, v: ValidateFunction): void { + wrapper.validate = v + Object.assign(wrapper, v) } // Compiles schema to validation function -function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | ValidateWrapper { - const _schObj = getCompilingSchema.call(this, sch) as StoredSchema - if (_schObj) return validateWrapper(_schObj) - - const {localRefs} = sch +function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { + const {localRefs} = schObj const self = this const opts = this._opts const refVal: (RefVal | undefined)[] = [undefined] const refs: {[ref: string]: number | undefined} = {} - if (sch.root === undefined) { - sch.root = { - schema: typeof sch.schema == "boolean" ? {} : sch.schema, + if (schObj.root === undefined) { + schObj.root = { + schema: typeof schObj.schema == "boolean" ? {} : schObj.schema, refVal, refs, } } - const root = sch.root - - const formats = this.formats - - try { - this._compilations.add(sch) - const v = localCompile(sch as SchemaEnv) - extendValidateWrapper(v, sch) - sch.validate = v - sch.refs = v.refs - sch.refVal = v.refVal - sch.root = v.root - return v - } finally { - this._compilations.delete(sch) - } - - function localCompile(_sch: StoredSchema): ValidateFunction { - const {schema, baseId} = _sch - if (_sch.root === undefined || _sch.root !== root) { - return compileSchema.call(self, _sch) + const root = schObj.root + return localCompile(schObj as SchemaEnv) + + function localCompile(sch: StoredSchema): ValidateFunction { + // TODO refactor - remove compilations + const _sch = getCompilingSchema.call(self, sch) + if (_sch) return validateWrapper(_sch) + // validateWrapper(sch) + const {schema, baseId} = sch + if (sch.root === undefined || sch.root !== root) { + return compileSchema.call(self, sch) } - const isRoot = isRootEnv(_sch as SchemaEnv) - + const isRoot = isRootEnv(sch as SchemaEnv) const $async = typeof schema == "object" && schema.$async === true - const rootId = getFullPath(_sch.root.schema.$id) + const rootId = getFullPath(sch.root.schema.$id) const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties}) let _ValidationError @@ -174,7 +151,7 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat const validateName = new Name("validate") - validateFunctionCode({ + const schemaCxt = { gen, allErrors: !!opts.allErrors, data: N.data, @@ -189,26 +166,27 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat ValidationError: _ValidationError, schema: schema, isRoot, - root: _sch.root, + root: sch.root, rootId, baseId: baseId || rootId, schemaPath: nil, errSchemaPath: "#", errorPath: str``, - formats, + formats: self.formats, opts, resolveRef, // TODO move to gen.globals self, - }) - - let sourceCode = `${vars(refVal, refValCode)} - ${gen.scopeRefs(N.scope)} - ${gen.toString()}` + } - if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) - // console.log("\n\n\n *** \n", sourceCode) - let validate: ValidateFunction + let sourceCode try { + self._compilations.add(sch) + validateFunctionCode(schemaCxt) + sourceCode = `${vars(refVal, refValCode)} + ${gen.scopeRefs(N.scope)} + ${gen.toString()}` + if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) + // console.log("\n\n\n *** \n", sourceCode) // TODO refactor to fewer variables - maybe only self and scope const makeValidate = new Function( N.self.toString(), @@ -217,28 +195,31 @@ function compileSchema(this: Ajv, sch: StoredSchema): ValidateFunction | Validat N.scope.toString(), sourceCode ) - validate = makeValidate(self, root, refVal, self._scope.get()) + const validate: ValidateFunction = makeValidate(self, root, refVal, self._scope.get()) refVal[0] = validate + validate.schema = schema + validate.errors = null + sch.refs = validate.refs = refs + sch.refVal = validate.refVal = refVal + sch.root = validate.root = isRoot ? root : sch.root + if ($async) validate.$async = true + if (opts.sourceCode === true) { + validate.source = { + code: sourceCode, + scope: self._scope, + } + } + if (sch.validate) extendWrapper(sch.validate, validate) + sch.validate = validate + return validate } catch (e) { - self.logger.error("Error compiling schema, function code:", sourceCode) + delete sch.validate + if (sourceCode) self.logger.error("Error compiling schema, function code:", sourceCode) throw e + } finally { + self._compilations.delete(sch) } - - validate.schema = schema - validate.errors = null - validate.refs = refs - validate.refVal = refVal - validate.root = isRoot ? root : _sch.root - if ($async) validate.$async = true - if (opts.sourceCode === true) { - validate.source = { - code: sourceCode, - scope: self._scope, - } - } - - return validate } function resolveRef(_baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { diff --git a/lib/types.ts b/lib/types.ts index 3c274eb0d4..174d23603f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -115,10 +115,7 @@ export interface ValidateFunction { root?: SchemaRoot $async?: true source?: SourceCode -} - -export interface ValidateWrapper extends ValidateFunction { - validate?: ValidateFunction + validate?: ValidateFunction // it will be only set on wrappers } export interface SchemaValidateFunction { From 7337158529e52989c657aef1e18f88f24dec7f8e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 20:37:57 +0100 Subject: [PATCH 189/322] remove formats from SchemaCtx --- lib/compile/index.ts | 1 - lib/types.ts | 1 - lib/vocabularies/format/format.ts | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 7437ba1a63..5a41eaf8b8 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -172,7 +172,6 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { schemaPath: nil, errSchemaPath: "#", errorPath: str``, - formats: self.formats, opts, resolveRef, // TODO move to gen.globals self, diff --git a/lib/types.ts b/lib/types.ts index 174d23603f..5dbcac8935 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -172,7 +172,6 @@ export interface SchemaCtx { propertyName?: Name compositeRule?: boolean createErrors?: boolean - formats: {[index: string]: AddedFormat} opts: InstanceOptions resolveRef: (baseId: string, ref: string, isRoot: boolean) => ResolvedRef | void self: Ajv diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 349b0eee34..b14a1f0069 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordCtx, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt - const {formats, opts, errSchemaPath, self} = it + const {opts, errSchemaPath, self} = it if (opts.format === false) return if ($data) validate$DataFormat() @@ -51,7 +51,7 @@ const def: CodeKeywordDefinition = { } function validateFormat() { - const formatDef: AddedFormat = formats[schema] + const formatDef: AddedFormat = self.formats[schema] if (!formatDef) { unknownFormat() return From 79c368d2eaf29ffea62e891667c375a35794fa7f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 9 Sep 2020 20:43:08 +0100 Subject: [PATCH 190/322] style: consistent spelling for "cxt" (=context) --- lib/compile/context.ts | 18 +++++----- lib/compile/errors.ts | 12 +++---- lib/compile/subschema.ts | 8 ++--- lib/compile/util.ts | 6 ++-- lib/compile/validate/applicability.ts | 4 +-- lib/compile/validate/boolSchema.ts | 10 +++--- lib/compile/validate/dataType.ts | 14 ++++---- lib/compile/validate/defaults.ts | 6 ++-- lib/compile/validate/index.ts | 36 +++++++++---------- lib/compile/validate/iterate.ts | 6 ++-- lib/compile/validate/keyword.ts | 18 +++++----- lib/types.ts | 24 ++++++------- .../applicator/additionalItems.ts | 4 +-- .../applicator/additionalProperties.ts | 4 +-- lib/vocabularies/applicator/allOf.ts | 4 +-- lib/vocabularies/applicator/anyOf.ts | 4 +-- lib/vocabularies/applicator/contains.ts | 4 +-- lib/vocabularies/applicator/dependencies.ts | 4 +-- lib/vocabularies/applicator/if.ts | 8 ++--- lib/vocabularies/applicator/items.ts | 4 +-- lib/vocabularies/applicator/not.ts | 4 +-- lib/vocabularies/applicator/oneOf.ts | 4 +-- .../applicator/patternProperties.ts | 4 +-- lib/vocabularies/applicator/properties.ts | 6 ++-- lib/vocabularies/applicator/propertyNames.ts | 4 +-- lib/vocabularies/core/ref.ts | 4 +-- lib/vocabularies/format/format.ts | 4 +-- lib/vocabularies/missing.ts | 8 ++--- lib/vocabularies/util.ts | 16 ++++----- lib/vocabularies/validation/const.ts | 4 +-- lib/vocabularies/validation/enum.ts | 4 +-- lib/vocabularies/validation/limit.ts | 4 +-- lib/vocabularies/validation/limitItems.ts | 4 +-- lib/vocabularies/validation/limitLength.ts | 4 +-- .../validation/limitProperties.ts | 4 +-- lib/vocabularies/validation/multipleOf.ts | 4 +-- lib/vocabularies/validation/pattern.ts | 4 +-- lib/vocabularies/validation/required.ts | 4 +-- lib/vocabularies/validation/uniqueItems.ts | 4 +-- 39 files changed, 146 insertions(+), 146 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 7802f37b9b..6d76c3e92d 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -1,8 +1,8 @@ import { KeywordDefinition, - KeywordErrorCtx, - KeywordCtxParams, - SchemaObjCtx, + KeywordErrorCxt, + KeywordCxtParams, + SchemaObjCxt, SchemaObject, } from "../types" import {schemaRefOrVal} from "../vocabularies/util" @@ -17,7 +17,7 @@ import { import {CodeGen, _, nil, or, Code, Name} from "./codegen" import N from "./names" -export default class KeywordCtx implements KeywordErrorCtx { +export default class KeywordCxt implements KeywordErrorCxt { gen: CodeGen allErrors: boolean keyword: string @@ -29,11 +29,11 @@ export default class KeywordCtx implements KeywordErrorCtx { schemaType?: string | string[] parentSchema: SchemaObject errsCount?: Name - params: KeywordCtxParams - it: SchemaObjCtx + params: KeywordCxtParams + it: SchemaObjCxt def: KeywordDefinition - constructor(it: SchemaObjCtx, def: KeywordDefinition, keyword: string) { + constructor(it: SchemaObjCxt, def: KeywordDefinition, keyword: string) { validateKeywordUsage(it, def, keyword) this.gen = it.gen this.allErrors = it.allErrors @@ -114,7 +114,7 @@ export default class KeywordCtx implements KeywordErrorCtx { if (!this.allErrors) this.gen.if(cond) } - setParams(obj: KeywordCtxParams, assign?: true): void { + setParams(obj: KeywordCxtParams, assign?: true): void { if (assign) Object.assign(this.params, obj) else this.params = obj } @@ -175,7 +175,7 @@ function validSchemaType(schema: unknown, schemaType: string | string[]): boolea } function validateKeywordUsage( - {schema, opts, self}: SchemaObjCtx, + {schema, opts, self}: SchemaObjCxt, def: KeywordDefinition, keyword: string ): void { diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 611497a106..bb72ab5411 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,4 +1,4 @@ -import {KeywordErrorCtx, KeywordErrorDefinition, SchemaCtx} from "../types" +import {KeywordErrorCxt, KeywordErrorDefinition, SchemaCxt} from "../types" import {CodeGen, _, str, Code, Name} from "./codegen" import N from "./names" @@ -14,7 +14,7 @@ export const keyword$DataError: KeywordErrorDefinition = { } export function reportError( - cxt: KeywordErrorCtx, + cxt: KeywordErrorCxt, error: KeywordErrorDefinition, overrideAllErrors?: boolean ): void { @@ -28,7 +28,7 @@ export function reportError( } } -export function reportExtraError(cxt: KeywordErrorCtx, error: KeywordErrorDefinition): void { +export function reportExtraError(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): void { const {it} = cxt const {gen, compositeRule, allErrors} = it const errObj = errorObjectCode(cxt, error) @@ -52,7 +52,7 @@ export function extendErrors({ data, errsCount, it, -}: KeywordErrorCtx): void { +}: KeywordErrorCxt): void { if (errsCount === undefined) throw new Error("ajv implementation error") const err = gen.name("err") gen.forRange("i", errsCount, N.errors, (i) => { @@ -74,7 +74,7 @@ function addError(gen: CodeGen, errObj: Code): void { gen.code(_`${N.errors}++`) } -function returnErrors(it: SchemaCtx, errs: Code): void { +function returnErrors(it: SchemaCxt, errs: Code): void { const {gen, validateName} = it if (it.async) { gen.code(_`throw new ${it.ValidationError as Name}(${errs})`) @@ -84,7 +84,7 @@ function returnErrors(it: SchemaCtx, errs: Code): void { } } -function errorObjectCode(cxt: KeywordErrorCtx, error: KeywordErrorDefinition): Code { +function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { const { keyword, data, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 231f334388..b08101d323 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,4 +1,4 @@ -import {Schema, SchemaObjCtx} from "../types" +import {Schema, SchemaObjCxt} from "../types" import {subschemaCode} from "./validate" import {escapeFragment, escapeJsonPointer} from "./util" import {_, str, Code, Name, getProperty} from "./codegen" @@ -45,7 +45,7 @@ interface SubschemaApplicationParams { allErrors: boolean } -export function applySubschema(it: SchemaObjCtx, appl: SubschemaApplication, valid: Name): void { +export function applySubschema(it: SchemaObjCxt, appl: SubschemaApplication, valid: Name): void { const subschema = getSubschema(it, appl) extendSubschemaData(subschema, it, appl) extendSubschemaMode(subschema, appl) @@ -54,7 +54,7 @@ export function applySubschema(it: SchemaObjCtx, appl: SubschemaApplication, val } function getSubschema( - it: SchemaObjCtx, + it: SchemaObjCxt, {keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaApplication ): SubschemaContext { if (keyword !== undefined && schema !== undefined) { @@ -93,7 +93,7 @@ function getSubschema( function extendSubschemaData( subschema: SubschemaContext, - it: SchemaObjCtx, + it: SchemaObjCxt, {dataProp, dataPropType: dpType, data, propertyName}: SubschemaApplication ) { if (data !== undefined && dataProp !== undefined) { diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 7176115dd5..643a2f961a 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,5 +1,5 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" -import {SchemaCtx, Schema} from "../types" +import {SchemaCxt, Schema} from "../types" import N from "./names" import {Rule, ValidationRules} from "./rules" @@ -79,7 +79,7 @@ export function schemaHasRules(schema: Schema, rules: {[key: string]: boolean | return false } -export function schemaCtxHasRules({schema, self}: SchemaCtx): boolean { +export function schemaCxtHasRules({schema, self}: SchemaCxt): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (self.RULES.all[key]) return true return false @@ -95,7 +95,7 @@ const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ export function getData( $data: string, - {dataLevel, dataNames, dataPathArr}: SchemaCtx + {dataLevel, dataNames, dataPathArr}: SchemaCxt ): Code | number { let jsonPointer let data: Code diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index ce30a03402..13e91497d1 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,7 +1,7 @@ -import {SchemaObjCtx, SchemaObject} from "../../types" +import {SchemaObjCxt, SchemaObject} from "../../types" import {RuleGroup, Rule} from "../rules" -export function schemaHasRulesForType({schema, self}: SchemaObjCtx, ty: string): boolean { +export function schemaHasRulesForType({schema, self}: SchemaObjCxt, ty: string): boolean { const group = self.RULES.types[ty] return group && group !== true && shouldUseGroup(schema, group) } diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index c837d1cd89..939b76cc19 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,4 +1,4 @@ -import {KeywordErrorDefinition, SchemaCtx, KeywordErrorCtx} from "../../types" +import {KeywordErrorDefinition, SchemaCxt, KeywordErrorCxt} from "../../types" import {reportError} from "../errors" import {_, Name} from "../codegen" import N from "../names" @@ -7,7 +7,7 @@ const boolError: KeywordErrorDefinition = { message: "boolean schema is false", } -export function topBoolOrEmptySchema(it: SchemaCtx): void { +export function topBoolOrEmptySchema(it: SchemaCxt): void { const {gen, schema, validateName} = it if (schema === false) { falseSchemaError(it, false) @@ -19,7 +19,7 @@ export function topBoolOrEmptySchema(it: SchemaCtx): void { } } -export function boolOrEmptySchema(it: SchemaCtx, valid: Name): void { +export function boolOrEmptySchema(it: SchemaCxt, valid: Name): void { const {gen, schema} = it if (schema === false) { gen.var(valid, false) // TODO var @@ -29,10 +29,10 @@ export function boolOrEmptySchema(it: SchemaCtx, valid: Name): void { } } -function falseSchemaError(it: SchemaCtx, overrideAllErrors?: boolean) { +function falseSchemaError(it: SchemaCxt, overrideAllErrors?: boolean) { const {gen, data} = it // TODO maybe some other interface should be used for non-keyword validation errors... - const cxt: KeywordErrorCtx = { + const cxt: KeywordErrorCxt = { gen, keyword: "false schema", data, diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index af8eb8ac15..db371ad2cd 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,4 +1,4 @@ -import {SchemaObjCtx, KeywordErrorDefinition, KeywordErrorCtx, SchemaObject} from "../../types" +import {SchemaObjCxt, KeywordErrorDefinition, KeywordErrorCxt, SchemaObject} from "../../types" import {toHash, checkDataTypes, DataType} from "../util" import {schemaRefOrVal} from "../../vocabularies/util" import {schemaHasRulesForType} from "./applicability" @@ -6,7 +6,7 @@ import {reportError} from "../errors" import {_, str, Name} from "../codegen" import {ValidationRules} from "../rules" -export function getSchemaTypes({self}: SchemaObjCtx, schema: SchemaObject): string[] { +export function getSchemaTypes({self}: SchemaObjCxt, schema: SchemaObject): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach((t) => checkType(t, self.RULES)) @@ -24,7 +24,7 @@ export function checkType(t: string, RULES: ValidationRules): void { throw new Error('"type" keyword must be allowed string or string[]: ' + t) } -export function coerceAndCheckDataType(it: SchemaObjCtx, types: string[]): boolean { +export function coerceAndCheckDataType(it: SchemaObjCxt, types: string[]): boolean { const {gen, data, opts} = it const coerceTo = coerceToTypes(types, opts.coerceTypes) const checkTypes = @@ -47,7 +47,7 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string : [] } -function coerceData(it: SchemaObjCtx, types: string[], coerceTo: string[]): void { +function coerceData(it: SchemaObjCxt, types: string[], coerceTo: string[]): void { const {gen, data, opts} = it const dataType = gen.let("dataType", _`typeof ${data}`) const coerced = gen.let("coerced", _`undefined`) @@ -122,7 +122,7 @@ function coerceData(it: SchemaObjCtx, types: string[], coerceTo: string[]): void } } -function assignParentData({gen, parentData, parentDataProperty}: SchemaObjCtx, expr: Name): void { +function assignParentData({gen, parentData, parentDataProperty}: SchemaObjCxt, expr: Name): void { // TODO use gen.property gen.if(_`${parentData} !== undefined`, () => gen.assign(_`${parentData}[${parentDataProperty}]`, expr) @@ -135,12 +135,12 @@ const typeError: KeywordErrorDefinition = { typeof schema == "string" ? _`{type: ${schema}}` : _`{type: ${schemaValue}}`, } -export function reportTypeError(it: SchemaObjCtx): void { +export function reportTypeError(it: SchemaObjCxt): void { const cxt = getTypeErrorContext(it) reportError(cxt, typeError) } -function getTypeErrorContext(it: SchemaObjCtx): KeywordErrorCtx { +function getTypeErrorContext(it: SchemaObjCxt): KeywordErrorCxt { const {gen, data, schema} = it const schemaCode = schemaRefOrVal(it, schema, "type") return { diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index d8428c3384..84b7c1ca13 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,8 +1,8 @@ -import {SchemaObjCtx} from "../../types" +import {SchemaObjCxt} from "../../types" import {_, getProperty, stringify} from "../codegen" import {checkStrictMode} from "../../vocabularies/util" -export function assignDefaults(it: SchemaObjCtx, ty?: string): void { +export function assignDefaults(it: SchemaObjCxt, ty?: string): void { const {properties, items} = it.schema if (ty === "object" && properties) { for (const key in properties) { @@ -13,7 +13,7 @@ export function assignDefaults(it: SchemaObjCtx, ty?: string): void { } } -function assignDefault(it: SchemaObjCtx, prop: string | number, defaultValue: unknown): void { +function assignDefault(it: SchemaObjCxt, prop: string | number, defaultValue: unknown): void { const {gen, compositeRule, data, opts} = it if (defaultValue === undefined) return const childData = _`${data}${getProperty(prop)}` diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index a47674fe50..cbe2f6fb0a 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,18 +1,18 @@ -import {Schema, SchemaCtx, SchemaObjCtx, Options} from "../../types" +import {Schema, SchemaCxt, SchemaObjCxt, Options} from "../../types" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" import {CodeGen, _, nil, str, Block, Code, Name} from "../codegen" import N from "../names" import {resolveUrl} from "../resolve" -import {schemaCtxHasRules, schemaHasRulesButRef} from "../util" +import {schemaCxtHasRules, schemaHasRulesButRef} from "../util" import {checkStrictMode, checkUnknownRules} from "../../vocabularies/util" // schema compilation - generates validation function, subschemaCode (below) is used for subschemas -export function validateFunctionCode(it: SchemaCtx): void { +export function validateFunctionCode(it: SchemaCxt): void { if (isSchemaObj(it)) { checkKeywords(it) - if (schemaCtxHasRules(it)) { + if (schemaCxtHasRules(it)) { topSchemaObjCode(it) return } @@ -20,7 +20,7 @@ export function validateFunctionCode(it: SchemaCtx): void { validateFunction(it, () => topBoolOrEmptySchema(it)) } -function validateFunction({gen, validateName, schema, async, opts}: SchemaCtx, body: Block) { +function validateFunction({gen, validateName, schema, async, opts}: SchemaCxt, body: Block) { gen.return(() => gen.func( validateName, @@ -31,7 +31,7 @@ function validateFunction({gen, validateName, schema, async, opts}: SchemaCtx, b ) } -function topSchemaObjCode(it: SchemaObjCtx): void { +function topSchemaObjCode(it: SchemaObjCxt): void { const {schema, opts} = it validateFunction(it, () => { if (opts.$comment && schema.$comment) commentKeyword(it) @@ -50,10 +50,10 @@ function funcSourceUrl(schema: Schema, opts: Options): Code { } // schema compilation - this function is used recursively to generate code for sub-schemas -export function subschemaCode(it: SchemaCtx, valid: Name): void { +export function subschemaCode(it: SchemaCxt, valid: Name): void { if (isSchemaObj(it)) { checkKeywords(it) - if (schemaCtxHasRules(it)) { + if (schemaCxtHasRules(it)) { subSchemaObjCode(it, valid) return } @@ -61,11 +61,11 @@ export function subschemaCode(it: SchemaCtx, valid: Name): void { boolOrEmptySchema(it, valid) } -function isSchemaObj(it: SchemaCtx): it is SchemaObjCtx { +function isSchemaObj(it: SchemaCxt): it is SchemaObjCxt { return typeof it.schema != "boolean" } -function subSchemaObjCode(it: SchemaObjCtx, valid: Name): void { +function subSchemaObjCode(it: SchemaObjCxt, valid: Name): void { const {schema, gen, opts} = it if (opts.$comment && schema.$comment) commentKeyword(it) updateContext(it) @@ -77,18 +77,18 @@ function subSchemaObjCode(it: SchemaObjCtx, valid: Name): void { gen.var(valid, _`${errsCount} === ${N.errors}`) } -function checkKeywords(it: SchemaObjCtx) { +function checkKeywords(it: SchemaObjCxt) { checkUnknownRules(it) checkRefsAndKeywords(it) } -function typeAndKeywords(it: SchemaObjCtx, errsCount?: Name): void { +function typeAndKeywords(it: SchemaObjCxt, errsCount?: Name): void { const types = getSchemaTypes(it, it.schema) const checkedTypes = coerceAndCheckDataType(it, types) schemaKeywords(it, types, !checkedTypes, errsCount) } -function checkRefsAndKeywords(it: SchemaObjCtx): void { +function checkRefsAndKeywords(it: SchemaObjCxt): void { const {schema, errSchemaPath, opts, self} = it if (schema.$ref && schemaHasRulesButRef(schema, self.RULES)) { if (opts.extendRefs === "fail") { @@ -99,7 +99,7 @@ function checkRefsAndKeywords(it: SchemaObjCtx): void { } } -function checkNoDefault(it: SchemaObjCtx): void { +function checkNoDefault(it: SchemaObjCxt): void { const {schema, opts} = it if (schema.default !== undefined && opts.useDefaults && opts.strict) { checkStrictMode(it, "default is ignored in the schema root") @@ -113,15 +113,15 @@ function initializeTop(gen: CodeGen): void { // gen.if(_`${N.dataPath} === undefined`, () => gen.assign(N.dataPath, _`""`)) // TODO maybe add it } -function updateContext(it: SchemaObjCtx): void { +function updateContext(it: SchemaObjCxt): void { if (it.schema.$id) it.baseId = resolveUrl(it.baseId, it.schema.$id) } -function checkAsync(it: SchemaObjCtx): void { +function checkAsync(it: SchemaObjCxt): void { if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") } -function commentKeyword({gen, validateName, schema, errSchemaPath, opts}: SchemaObjCtx): void { +function commentKeyword({gen, validateName, schema, errSchemaPath, opts}: SchemaObjCxt): void { const msg = schema.$comment if (opts.$comment === true) { gen.code(_`${N.self}.logger.log(${msg})`) @@ -131,7 +131,7 @@ function commentKeyword({gen, validateName, schema, errSchemaPath, opts}: Schema } } -function returnResults({gen, async, validateName, ValidationError}: SchemaCtx) { +function returnResults({gen, async, validateName, ValidationError}: SchemaCxt) { if (async) { gen.if( _`${N.errors} === 0`, diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 517f3ead36..7e5e53b13c 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -1,4 +1,4 @@ -import {SchemaObjCtx} from "../../types" +import {SchemaObjCxt} from "../../types" import {shouldUseGroup, shouldUseRule} from "./applicability" import {checkDataType, schemaHasRulesButRef} from "../util" import {keywordCode} from "./keyword" @@ -9,7 +9,7 @@ import {_, Name} from "../codegen" import N from "../names" export function schemaKeywords( - it: SchemaObjCtx, + it: SchemaObjCxt, types: string[], typeErrors: boolean, errsCount?: Name @@ -45,7 +45,7 @@ export function schemaKeywords( } } -function iterateKeywords(it: SchemaObjCtx, group: RuleGroup) { +function iterateKeywords(it: SchemaObjCxt, group: RuleGroup) { const { gen, schema, diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index bad7353333..58edf48194 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -2,10 +2,10 @@ import { KeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, - SchemaObjCtx, + SchemaObjCxt, KeywordCompilationResult, } from "../../types" -import KeywordCtx from "../context" +import KeywordCxt from "../context" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" import {callValidateCode} from "../../vocabularies/util" @@ -13,12 +13,12 @@ import {CodeGen, _, nil, Code, Name} from "../codegen" import N from "../names" export function keywordCode( - it: SchemaObjCtx, + it: SchemaObjCxt, keyword: string, def: KeywordDefinition, ruleType?: string ): void { - const cxt = new KeywordCtx(it, def, keyword) + const cxt = new KeywordCxt(it, def, keyword) if ("code" in def) { def.code(cxt, ruleType) } else if (cxt.$data && def.validate) { @@ -30,7 +30,7 @@ export function keywordCode( } } -function macroKeywordCode(cxt: KeywordCtx, def: MacroKeywordDefinition): void { +function macroKeywordCode(cxt: KeywordCxt, def: MacroKeywordDefinition): void { const {gen, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) const schemaRef = useKeyword(gen, keyword, macroSchema) @@ -51,7 +51,7 @@ function macroKeywordCode(cxt: KeywordCtx, def: MacroKeywordDefinition): void { cxt.pass(valid, () => cxt.error(true)) } -function funcKeywordCode(cxt: KeywordCtx, def: FuncKeywordDefinition): void { +function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void { const {gen, keyword, schema, parentSchema, $data, it} = cxt checkAsync(it, def) const validate = @@ -107,12 +107,12 @@ function funcKeywordCode(cxt: KeywordCtx, def: FuncKeywordDefinition): void { } } -function modifyData(cxt: KeywordCtx) { +function modifyData(cxt: KeywordCxt) { const {gen, data, it} = cxt gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`)) } -function addErrs(cxt: KeywordCtx, errs: Code): void { +function addErrs(cxt: KeywordCxt, errs: Code): void { const {gen} = cxt gen.if( _`Array.isArray(${errs})`, @@ -126,7 +126,7 @@ function addErrs(cxt: KeywordCtx, errs: Code): void { ) } -function checkAsync(it: SchemaObjCtx, def: FuncKeywordDefinition) { +function checkAsync(it: SchemaObjCxt, def: FuncKeywordDefinition) { if (def.async && !it.async) throw new Error("async keyword in sync schema") } diff --git a/lib/types.ts b/lib/types.ts index 5dbcac8935..b805ae616b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,6 @@ import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" -import KeywordCtx from "./compile/context" +import KeywordCxt from "./compile/context" import Ajv from "./ajv" export interface SchemaObject { @@ -148,7 +148,7 @@ export interface ErrorObject { export type KeywordCompilationResult = Schema | SchemaValidateFunction | ValidateFunction -export interface SchemaCtx { +export interface SchemaCxt { gen: CodeGen allErrors: boolean data: Name @@ -177,7 +177,7 @@ export interface SchemaCtx { self: Ajv } -export interface SchemaObjCtx extends SchemaCtx { +export interface SchemaObjCxt extends SchemaCxt { schema: SchemaObject } @@ -196,16 +196,16 @@ interface _KeywordDef { } export interface CodeKeywordDefinition extends _KeywordDef { - code: (cxt: KeywordCtx, ruleType?: string) => void + code: (cxt: KeywordCxt, ruleType?: string) => void trackErrors?: boolean } -export type MacroKeywordFunc = (schema: any, parentSchema: SchemaObject, it: SchemaCtx) => Schema +export type MacroKeywordFunc = (schema: any, parentSchema: SchemaObject, it: SchemaCxt) => Schema export type CompileKeywordFunc = ( schema: any, parentSchema: SchemaObject, - it: SchemaObjCtx + it: SchemaObjCxt ) => ValidateFunction export interface FuncKeywordDefinition extends _KeywordDef { @@ -229,13 +229,13 @@ export type KeywordDefinition = | MacroKeywordDefinition export interface KeywordErrorDefinition { - message: string | ((cxt: KeywordErrorCtx) => Code) - params?: (cxt: KeywordErrorCtx) => Code + message: string | ((cxt: KeywordErrorCxt) => Code) + params?: (cxt: KeywordErrorCxt) => Code } export type Vocabulary = (KeywordDefinition | string)[] -export interface KeywordErrorCtx { +export interface KeywordErrorCxt { gen: CodeGen keyword: string data: Name @@ -246,11 +246,11 @@ export interface KeywordErrorCtx { schemaValue: Code | number | boolean schemaType?: string | string[] errsCount?: Name - params: KeywordCtxParams - it: SchemaCtx + params: KeywordCxtParams + it: SchemaCxt } -export type KeywordCtxParams = {[x: string]: Code | string | number} +export type KeywordCxtParams = {[x: string]: Code | string | number} export type FormatMode = "fast" | "full" diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 27e7de8f47..bc49d68478 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: ["boolean", "object"], before: "uniqueItems", - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, parentSchema, data, it} = cxt const len = gen.const("len", _`${data}.length`) const items = parentSchema.items diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 8c46b81ab4..1f86f23690 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition, KeywordErrorCtx} from "../../types" +import {CodeKeywordDefinition, KeywordErrorCxt} from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" @@ -93,7 +93,7 @@ const def: CodeKeywordDefinition = { }, error: { message: "should NOT have additional properties", - params: ({params}: KeywordErrorCtx): Code => + params: ({params}: KeywordErrorCxt): Code => _`{additionalProperty: ${params.additionalProperty}}`, }, } diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 30702fcb3c..f7f2c37ead 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,12 +1,12 @@ import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" const def: CodeKeywordDefinition = { keyword: "allOf", schemaType: "array", - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, it} = cxt if (!Array.isArray(schema)) throw new Error("ajv implementation error") const valid = gen.name("valid") diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 7b94946fdb..cf5df70299 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", trackErrors: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, it} = cxt if (!Array.isArray(schema)) throw new Error("ajv implementation error") const alwaysValid = schema.some((sch: Schema) => alwaysValidSchema(it, sch)) diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index e8f13907a5..14517c7718 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "boolean"], before: "uniqueItems", trackErrors: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, data, it} = cxt if (alwaysValidSchema(it, schema)) { diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 6b8a1a7733..f7c13a01be 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, SchemaMap} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" @@ -15,7 +15,7 @@ const def: CodeKeywordDefinition = { keyword: "dependencies", type: "object", schemaType: "object", - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, data, it} = cxt const [propDeps, schDeps] = splitDependencies() const valid = gen.name("valid") diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 9aa79fc257..54ae9b94cc 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, SchemaObjCtx} from "../../types" -import KeywordCtx from "../../compile/context" +import {CodeKeywordDefinition, SchemaObjCxt} from "../../types" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], trackErrors: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, parentSchema, it} = cxt if (parentSchema.then === undefined && parentSchema.else === undefined) { checkStrictMode(it, '"if" without "then" and "else" is ignored') @@ -64,7 +64,7 @@ const def: CodeKeywordDefinition = { module.exports = def -function hasSchema(it: SchemaObjCtx, keyword: string): boolean { +function hasSchema(it: SchemaObjCxt, keyword: string): boolean { const schema = it.schema[keyword] return schema !== undefined && !alwaysValidSchema(it, schema) } diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index e435190b63..82db4292d9 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: ["object", "array", "boolean"], before: "uniqueItems", - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, data, it} = cxt const len = gen.const("len", _`${data}.length`) if (Array.isArray(schema)) { diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index bf86a42afc..e986e2c52e 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { keyword: "not", schemaType: ["object", "boolean"], trackErrors: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, it} = cxt if (alwaysValidSchema(it, schema)) { cxt.fail() diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 8f47aba68e..e0d397d097 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", trackErrors: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, it} = cxt if (!Array.isArray(schema)) throw new Error("ajv implementation error") const schArr: Schema[] = schema diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 679077c680..51fe8e2e22 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {schemaProperties, usePattern, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "patternProperties", type: "object", schemaType: "object", - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, data, parentSchema, it} = cxt const patterns = schemaProperties(it, schema) if (patterns.length === 0) return diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index e665240927..88d5e50226 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {schemaProperties, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" import apDef from "./additionalProperties" @@ -8,10 +8,10 @@ const def: CodeKeywordDefinition = { keyword: "properties", type: "object", schemaType: "object", - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, parentSchema, data, it} = cxt if (it.opts.removeAdditional === "all" && parentSchema.additionalProperties === undefined) { - apDef.code(new KeywordCtx(it, apDef, "additionalProperties")) + apDef.code(new KeywordCxt(it, apDef, "additionalProperties")) } const properties = schemaProperties(it, schema) if (properties.length === 0) return diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 138ad2b225..fffdbe33d3 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { keyword: "propertyNames", type: "object", schemaType: ["object", "boolean"], - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, data, it} = cxt if (alwaysValidSchema(it, schema)) return const valid = gen.name("valid") diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index cc4c42d00e..64843656eb 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" @@ -10,7 +10,7 @@ import N from "../../compile/names" const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, it} = cxt const {resolveRef, allErrors, baseId, isRoot, root, opts, validateName, self} = it const ref = getRef() diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index b14a1f0069..8d43c412c2 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" import N from "../../compile/names" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { type: ["number", "string"], schemaType: "string", $data: true, - code(cxt: KeywordCtx, ruleType?: string) { + code(cxt: KeywordCxt, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt const {opts, errSchemaPath, self} = it if (opts.format === false) return diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index 38304b32d7..bd6ca3e0de 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,8 +1,8 @@ -import KeywordCtx from "../compile/context" +import KeywordCxt from "../compile/context" import {noPropertyInData} from "./util" import {_, or, Code, Name} from "../compile/codegen" -export function checkReportMissingProp(cxt: KeywordCtx, prop: string): void { +export function checkReportMissingProp(cxt: KeywordCxt, prop: string): void { const {gen, data, it} = cxt gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => { cxt.setParams({missingProperty: _`${prop}`}, true) @@ -11,7 +11,7 @@ export function checkReportMissingProp(cxt: KeywordCtx, prop: string): void { } export function checkMissingProp( - {data, it: {opts}}: KeywordCtx, + {data, it: {opts}}: KeywordCxt, properties: string[], missing: Name ): Code { @@ -22,7 +22,7 @@ export function checkMissingProp( ) } -export function reportMissingProp(cxt: KeywordCtx, missing: Name): void { +export function reportMissingProp(cxt: KeywordCxt, missing: Name): void { cxt.setParams({missingProperty: missing}, true) cxt.error() } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index b82cbbc75a..88bae13979 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,11 +1,11 @@ import {schemaHasRules} from "../compile/util" -import {Schema, SchemaMap, SchemaCtx, SchemaObjCtx} from "../types" -import KeywordCtx from "../compile/context" +import {Schema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" +import KeywordCxt from "../compile/context" import {CodeGen, _, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" export function schemaRefOrVal( - {topSchemaRef, schemaPath}: SchemaObjCtx, + {topSchemaRef, schemaPath}: SchemaObjCxt, schema: unknown, keyword: string, $data?: string | false @@ -17,14 +17,14 @@ export function schemaRefOrVal( return _`${topSchemaRef}${schemaPath}${getProperty(keyword)}` } -export function alwaysValidSchema(it: SchemaCtx, schema: Schema): boolean | void { +export function alwaysValidSchema(it: SchemaCxt, schema: Schema): boolean | void { if (typeof schema == "boolean") return schema if (Object.keys(schema).length === 0) return true checkUnknownRules(it, schema) return !schemaHasRules(schema, it.self.RULES.all) } -export function checkUnknownRules(it: SchemaCtx, schema: Schema = it.schema): void { +export function checkUnknownRules(it: SchemaCxt, schema: Schema = it.schema): void { const {opts, self} = it if (!opts.strict) return if (typeof schema === "boolean") return @@ -38,7 +38,7 @@ export function allSchemaProperties(schemaMap?: SchemaMap): string[] { return schemaMap ? Object.keys(schemaMap).filter((p) => p !== "__proto__") : [] } -export function schemaProperties(it: SchemaCtx, schemaMap: SchemaMap): string[] { +export function schemaProperties(it: SchemaCxt, schemaMap: SchemaMap): string[] { return allSchemaProperties(schemaMap).filter((p) => !alwaysValidSchema(it, schemaMap[p])) } @@ -61,7 +61,7 @@ export function noPropertyInData( } export function callValidateCode( - {schemaCode, data, it}: KeywordCtx, + {schemaCode, data, it}: KeywordCxt, func: Code, context: Code, passSchema?: boolean @@ -82,7 +82,7 @@ export function usePattern(gen: CodeGen, pattern: string): Name { }) } -export function checkStrictMode(it: SchemaCtx, msg: string): void { +export function checkStrictMode(it: SchemaCxt, msg: string): void { const {opts, self} = it if (opts.strict) { if (opts.strict === "log") self.logger.warn(msg) diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 0e638caa3d..2ac84e0c25 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,12 +1,12 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {_} from "../../compile/codegen" import equal from "fast-deep-equal" const def: CodeKeywordDefinition = { keyword: "const", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const eql = cxt.gen.scopeValue("func", { ref: equal, code: _`require("ajv/dist/compile/equal")`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index af2bc9ab8f..6ea96d9237 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" import equal from "fast-deep-equal" @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { keyword: "enum", schemaType: "array", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, data, $data, schema, schemaCode, it} = cxt if (!$data && schema.length === 0) throw new Error("enum must have non-empty array") const useLoop = schema.length >= it.opts.loopEnum diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 12325c1eb3..8103a6a249 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" // import {bad$DataType} from "../util" import {_, str, operators, Code} from "../../compile/codegen" @@ -17,7 +17,7 @@ const def: CodeKeywordDefinition = { type: "number", schemaType: "number", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {keyword, data, schemaCode} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) cxt.fail$data(_`(${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data}))`) diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 987cd7f659..41802cb680 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: "number", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {keyword, data, schemaCode} = cxt const op = keyword === "maxItems" ? operators.GT : operators.LT cxt.fail$data(_`${data}.length ${op} ${schemaCode}`) diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 0c7ed5efae..2fa735435b 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" import ucs2length from "../../compile/ucs2length" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { type: "string", schemaType: "number", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {keyword, data, schemaCode, it} = cxt const op = keyword === "maxLength" ? operators.GT : operators.LT let len diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index 7f7580ef81..d64be87a56 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { @@ -7,7 +7,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "number", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {keyword, data, schemaCode} = cxt const op = keyword === "maxProperties" ? operators.GT : operators.LT cxt.fail$data(_`Object.keys(${data}).length ${op} ${schemaCode}`) diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 782e68dbb8..8df5f783a3 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" // import {bad$DataType} from "../util" import {_, str} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { type: "number", schemaType: "number", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, data, schemaCode, it} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) const prec = it.opts.multipleOfPrecision diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index bcfcf19b09..bebf4c9fa1 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {usePattern} from "../util" import {_, str} from "../../compile/codegen" @@ -8,7 +8,7 @@ const def: CodeKeywordDefinition = { type: "string", schemaType: "string", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, data, $data, schema, schemaCode} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 48e33daefc..3d367b9f4e 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {_, str, nil, Name} from "../../compile/codegen" @@ -9,7 +9,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "array", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return const useLoop = schema.length >= it.opts.loopRequired diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index a374013e36..1b1644851a 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,5 +1,5 @@ import {CodeKeywordDefinition} from "../../types" -import KeywordCtx from "../../compile/context" +import KeywordCxt from "../../compile/context" import {getSchemaTypes} from "../../compile/validate/dataType" import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { type: "array", schemaType: "boolean", $data: true, - code(cxt: KeywordCtx) { + code(cxt: KeywordCxt) { const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt if (!$data && !schema) return const valid = gen.let("valid") From d1c7600cf407bccece48d148736850204179bde8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 10 Sep 2020 07:55:03 +0100 Subject: [PATCH 191/322] refactor: NameRec interface -> ValueScopeName class --- lib/compile/codegen/index.ts | 6 +-- lib/compile/codegen/scope.ts | 101 ++++++++++++++++++----------------- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index a12a45cd3b..75c3d3ce87 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -68,10 +68,10 @@ export class CodeGen { } scopeValue(prefix: string, value: NameValue): Name { - const rec = this._extScope.value(prefix, value) + const name = this._extScope.value(prefix, value) if (!this._values[prefix]) this._values[prefix] = new Set() - this._values[prefix].add(rec) - return rec.name + this._values[prefix].add(name) + return name } scopeRefs(scopeName: Name): Code { diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts index da7fd53dee..53120c7fd4 100644 --- a/lib/compile/codegen/scope.ts +++ b/lib/compile/codegen/scope.ts @@ -1,12 +1,5 @@ import {_, nil, Code, Name} from "./code" -export interface NameRec { - prefixName: Name - scopeIndex?: number - name: Name - value: NameValue -} - interface NameGroup { prefix: string index: number @@ -21,24 +14,27 @@ export interface NameValue { export type ValueReference = unknown // possibly make CodeGen parameterized type on this type class ValueError extends Error { - value: NameValue - constructor({name, value}: NameRec) { + value?: NameValue + constructor(name: ValueScopeName) { super(`CodeGen: "code" for ${name} not defined`) - this.value = value + this.value = name.value } } interface ScopeOptions { - scope?: ScopeStore prefixes?: Set parent?: Scope } +interface ValueScopeOptions extends ScopeOptions { + scope: ScopeStore +} + export type ScopeStore = Record -type ScopeValues = {[prefix: string]: Map} +type ScopeValues = {[prefix: string]: Map} -export type ScopeValueSets = {[prefix: string]: Set} +export type ScopeValueSets = {[prefix: string]: Set} export class Scope { _names: {[prefix: string]: NameGroup} = {} @@ -51,20 +47,29 @@ export class Scope { } name(prefix: string): Name { - let ng = this._names[prefix] - if (!ng) { - checkPrefix.call(this, prefix) - ng = this._names[prefix] = {prefix, index: 0} - } + const ng = nameGroup.call(this, prefix) return new Name(ng.prefix + ng.index++) } } +export class ValueScopeName extends Name { + prefixName: Name + // index: number + scopeIndex?: number + value: NameValue + + constructor(prefix: string, index: number, value: NameValue) { + super(prefix + index) + this.prefixName = new Name(prefix) + this.value = value + } +} + export class ValueScope extends Scope { _values: ScopeValues = {} - _scope?: ScopeStore + _scope: ScopeStore - constructor(opts: ScopeOptions = {}) { + constructor(opts: ValueScopeOptions) { super(opts) this._scope = opts.scope } @@ -73,39 +78,34 @@ export class ValueScope extends Scope { return this._scope } - value(prefix: string, value: NameValue): NameRec { + value(prefix: string, value: NameValue): ValueScopeName { const {ref, key, code} = value const valueKey = key ?? ref ?? code if (!valueKey) throw new Error("CodeGen: ref or code must be passed in value") let vs = this._values[prefix] if (vs) { - const rec = vs.get(valueKey) - if (rec) return rec + const name = vs.get(valueKey) + if (name) return name } else { vs = this._values[prefix] = new Map() } - const rec: NameRec = { - prefixName: new Name(prefix), - name: this.name(prefix), - value, - } - vs.set(valueKey, rec) + const ng = nameGroup.call(this, prefix) + const name = new ValueScopeName(prefix, ng.index++, value) + + vs.set(valueKey, name) - if (this._scope && value.ref) { + if (value.ref) { const s = this._scope[prefix] || (this._scope[prefix] = []) - rec.scopeIndex = s.length - s[rec.scopeIndex] = value.ref + name.scopeIndex = s.length + s[name.scopeIndex] = value.ref } - return rec + return name } scopeRefs(scopeName: Name, values?: ScopeValues | ScopeValueSets): Code { - if (!this._scope) { - throw new Error("CodeGen: scope has to be passed via options to use scopeRefs") - } - return _reduceValues.call(this, values, (rec: NameRec) => { - const {value, prefixName, scopeIndex} = rec + return reduceValues.call(this, values, (name: ValueScopeName) => { + const {value, prefixName, scopeIndex} = name if (scopeIndex !== undefined) return _`${scopeName}.${prefixName}[${scopeIndex}]` if (value.code) return value.code throw new Error("ajv implementation error") @@ -113,29 +113,34 @@ export class ValueScope extends Scope { } scopeCode(values?: ScopeValues | ScopeValueSets): Code { - return _reduceValues.call(this, values, (rec: NameRec) => { - const c = rec.value.code + return reduceValues.call(this, values, (name: ValueScopeName) => { + const c = name.value.code if (c) return c - throw new ValueError(rec) + throw new ValueError(name) }) } } -function checkPrefix(this: Scope, prefix: string) { - if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes?.has(prefix))) { - throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`) +function nameGroup(this: Scope | ValueScope, prefix: string): NameGroup { + let ng = this._names[prefix] + if (!ng) { + if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes?.has(prefix))) { + throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`) + } + ng = this._names[prefix] = {prefix, index: 0} } + return ng } -function _reduceValues( +function reduceValues( this: ValueScope, values: ScopeValues | ScopeValueSets = this._values, - valueCode: (n: NameRec) => Code + valueCode: (n: ValueScopeName) => Code ): Code { let code: Code = nil for (const prefix in values) { - values[prefix].forEach((rec: NameRec) => { - code = _`${code}const ${rec.name} = ${valueCode(rec)};` + values[prefix].forEach((name: ValueScopeName) => { + code = _`${code}const ${name} = ${valueCode(name)};` }) } return code From 17ee1c0e74f1f953752a602d692dbecad9760857 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 10 Sep 2020 09:22:32 +0100 Subject: [PATCH 192/322] allow referencing validation functions via shared scope --- lib/ajv.ts | 10 +++++- lib/compile/codegen/code.ts | 4 +++ lib/compile/codegen/index.ts | 24 ++++++++------ lib/compile/codegen/scope.ts | 62 +++++++++++++++++++----------------- lib/compile/index.ts | 6 +++- 5 files changed, 65 insertions(+), 41 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 8db8c8d53c..0e5531ad30 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -32,7 +32,15 @@ const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] -const EXT_SCOPE_NAMES = new Set(["keyword", "pattern", "formats", "validate$data", "func", "Error"]) +const EXT_SCOPE_NAMES = new Set([ + "validate", + "keyword", + "pattern", + "formats", + "validate$data", + "func", + "Error", +]) type CompileAsyncCallback = (err: Error | null, validate?: ValidateFunction) => void diff --git a/lib/compile/codegen/code.ts b/lib/compile/codegen/code.ts index 6ab73a62f9..16aeb14112 100644 --- a/lib/compile/codegen/code.ts +++ b/lib/compile/codegen/code.ts @@ -86,3 +86,7 @@ function safeStringify(x: unknown): string { .replace(/\u2028/g, "\\u2028") .replace(/\u2029/g, "\\u2029") } + +export function getProperty(key: Code | string | number): Code { + return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]` +} diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index 75c3d3ce87..e920a77c01 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -1,7 +1,7 @@ -import {_, str, nil, _Code, Code, IDENTIFIER, Name, stringify} from "./code" +import {_, str, nil, _Code, Code, Name, getProperty, stringify} from "./code" import {Scope, ScopeValueSets, NameValue, ScopeStore, ValueScope} from "./scope" -export {_, str, nil, stringify, Name, Code, Scope, ScopeStore, ValueScope} +export {_, str, nil, getProperty, stringify, Name, Code, Scope, ScopeStore, ValueScope} enum BlockKind { If, @@ -67,10 +67,18 @@ export class CodeGen { return this._scope.name(prefix) } - scopeValue(prefix: string, value: NameValue): Name { - const name = this._extScope.value(prefix, value) - if (!this._values[prefix]) this._values[prefix] = new Set() - this._values[prefix].add(name) + scopeValue( + prefix: string, + value: NameValue, + scopeProperty?: string, + itemProperty?: string, + used = true + ): Name { + const name = this._extScope.value(prefix, value, scopeProperty, itemProperty) + if (used) { + const vs = this._values[prefix] || (this._values[prefix] = new Set()) + vs.add(name) + } return name } @@ -303,10 +311,6 @@ function _loop(this: CodeGen, forCode: _Code, name: Name, forBody: (n: Name) => return this } -export function getProperty(key: Code | string | number): Code { - return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]` -} - const andCode = mappend(operators.AND) export function and(...args: Code[]): Code { diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts index 53120c7fd4..312f0217e8 100644 --- a/lib/compile/codegen/scope.ts +++ b/lib/compile/codegen/scope.ts @@ -6,7 +6,7 @@ interface NameGroup { } export interface NameValue { - ref?: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure + ref: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used code?: Code // this is the code creating the value needed for standalone code wit_out closure - can be a primitive value, function or import (`require`) } @@ -47,21 +47,25 @@ export class Scope { } name(prefix: string): Name { - const ng = nameGroup.call(this, prefix) - return new Name(ng.prefix + ng.index++) + return new Name(newName.call(this, prefix)) } } -export class ValueScopeName extends Name { - prefixName: Name - // index: number - scopeIndex?: number +interface ScopePath { + property: string + itemIndex: number + itemProperty?: string +} + +class ValueScopeName extends Name { value: NameValue + scopePath: Code - constructor(prefix: string, index: number, value: NameValue) { - super(prefix + index) - this.prefixName = new Name(prefix) + constructor(nameStr: string, value: NameValue, {property, itemIndex, itemProperty}: ScopePath) { + super(nameStr) this.value = value + this.scopePath = _`.${new Name(property)}[${itemIndex}]` + if (itemProperty) this.scopePath.add(_`.${new Name(itemProperty)}`) } } @@ -78,10 +82,14 @@ export class ValueScope extends Scope { return this._scope } - value(prefix: string, value: NameValue): ValueScopeName { - const {ref, key, code} = value - const valueKey = key ?? ref ?? code - if (!valueKey) throw new Error("CodeGen: ref or code must be passed in value") + value( + prefix: string, + value: NameValue, + scopeProperty = prefix, + itemProperty?: string + ): ValueScopeName { + if (!value.ref) throw new Error("CodeGen: ref must be passed in value") + const valueKey = value.key ?? value.ref let vs = this._values[prefix] if (vs) { const name = vs.get(valueKey) @@ -90,25 +98,21 @@ export class ValueScope extends Scope { vs = this._values[prefix] = new Map() } - const ng = nameGroup.call(this, prefix) - const name = new ValueScopeName(prefix, ng.index++, value) + const s = this._scope[scopeProperty] || (this._scope[scopeProperty] = []) + const itemIndex = s.push(value.ref) - 1 + const name = new ValueScopeName(newName.call(this, prefix), value, { + property: scopeProperty, + itemIndex, + itemProperty, + }) vs.set(valueKey, name) - - if (value.ref) { - const s = this._scope[prefix] || (this._scope[prefix] = []) - name.scopeIndex = s.length - s[name.scopeIndex] = value.ref - } return name } scopeRefs(scopeName: Name, values?: ScopeValues | ScopeValueSets): Code { - return reduceValues.call(this, values, (name: ValueScopeName) => { - const {value, prefixName, scopeIndex} = name - if (scopeIndex !== undefined) return _`${scopeName}.${prefixName}[${scopeIndex}]` - if (value.code) return value.code - throw new Error("ajv implementation error") + return reduceValues.call(this, values, ({scopePath}: ValueScopeName) => { + return _`${scopeName}${scopePath}` }) } @@ -121,7 +125,7 @@ export class ValueScope extends Scope { } } -function nameGroup(this: Scope | ValueScope, prefix: string): NameGroup { +function newName(this: Scope | ValueScope, prefix: string): string { let ng = this._names[prefix] if (!ng) { if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes?.has(prefix))) { @@ -129,7 +133,7 @@ function nameGroup(this: Scope | ValueScope, prefix: string): NameGroup { } ng = this._names[prefix] = {prefix, index: 0} } - return ng + return prefix + ng.index++ } function reduceValues( diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 5a41eaf8b8..7603536b00 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -25,6 +25,7 @@ interface _StoredSchema { localRefs?: LocalRefs baseId?: string validate?: ValidateFunction + validateName?: Name } export class StoredSchema implements _StoredSchema { @@ -40,6 +41,7 @@ export class StoredSchema implements _StoredSchema { localRefs?: LocalRefs baseId?: string validate?: ValidateFunction + validateName?: Name constructor(obj: _StoredSchema) { this.schema = obj.schema @@ -149,7 +151,8 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { }) } - const validateName = new Name("validate") + const validateName = gen.scopeValue("validate", {ref: sch}, "schema", "validate", false) + sch.validateName = validateName const schemaCxt = { gen, @@ -214,6 +217,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { return validate } catch (e) { delete sch.validate + delete sch.validateName if (sourceCode) self.logger.error("Error compiling schema, function code:", sourceCode) throw e } finally { From 33ffbc069c470fda1ac21c77ce5e44187d5b5fe5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 10 Sep 2020 13:42:40 +0100 Subject: [PATCH 193/322] refactor: ref resolution resolve and getJsonPointer --- lib/ajv.ts | 27 ++++++-------- lib/compile/index.ts | 89 ++++++++++++++++---------------------------- 2 files changed, 44 insertions(+), 72 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 0e5531ad30..aed3a8b2a7 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -61,6 +61,7 @@ export default class Ajv { _fragments: {[key: string]: StoredSchema} = {} formats: {[name: string]: AddedFormat} = {} _compilations: Set = new Set() + _compileQueue: StoredSchema[] = [] _loadingSchemas: {[ref: string]: Promise} = {} _metaOpts: InstanceOptions RULES: ValidationRules @@ -176,7 +177,7 @@ export default class Ajv { e: MissingRefError ): Promise { const ref = e.missingSchema - if (added(ref)) { + if (self._refs[ref]) { throw new Error(`Schema ${ref} is loaded but ${e.missingRef} cannot be resolved`) } let schPromise = self._loadingSchemas[ref] @@ -186,20 +187,14 @@ export default class Ajv { } const sch = await schPromise - if (!added(ref)) { - await loadMetaSchemaOf(sch) - if (!added(ref)) self.addSchema(sch, ref, undefined, meta) - } + if (!self._refs[ref]) await loadMetaSchemaOf(sch) + if (!self._refs[ref]) self.addSchema(sch, ref, undefined, meta) return _compileAsync(schemaObj) function removePromise(): void { delete self._loadingSchemas[ref] } } - - function added(ref: string): string | StoredSchema { - return self._refs[ref] || self._schemas[ref] - } } // Adds schema to the instance @@ -404,7 +399,7 @@ export interface ErrorsTextOptions { dataVar?: string } -function checkDeprecatedOptions(this: Ajv, opts: Options) { +function checkDeprecatedOptions(this: Ajv, opts: Options): void { if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath") if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId") if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") @@ -432,7 +427,7 @@ function _removeAllSchemas( this: Ajv, schemas: {[ref: string]: StoredSchema | string}, regex?: RegExp -) { +): void { for (const keyRef in schemas) { const schemaObj = schemas[keyRef] if (!regex || regex.test(keyRef)) { @@ -452,7 +447,7 @@ function _addSchema( skipValidation?: boolean, meta?: boolean, shouldAddSchema?: boolean -) { +): StoredSchema { if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema must be object or boolean") } @@ -518,7 +513,7 @@ function addInitialFormats(this: Ajv): void { } } -function addInitialKeywords(this: Ajv, defs: Vocabulary | {[x: string]: KeywordDefinition}) { +function addInitialKeywords(this: Ajv, defs: Vocabulary | {[x: string]: KeywordDefinition}): void { if (Array.isArray(defs)) { this.addVocabulary(defs) return @@ -531,13 +526,13 @@ function addInitialKeywords(this: Ajv, defs: Vocabulary | {[x: string]: KeywordD } } -function checkUnique(this: Ajv, id: string) { +function checkUnique(this: Ajv, id: string): void { if (this._schemas[id] || this._refs[id]) { throw new Error('schema with key or id "' + id + '" already exists') } } -function getMetaSchemaOptions(this: Ajv) { +function getMetaSchemaOptions(this: Ajv): InstanceOptions { const metaOpts = {...this._opts} for (const opt of META_IGNORE_OPTIONS) delete metaOpts[opt] return metaOpts @@ -556,7 +551,7 @@ function getLogger(logger?: Logger | false): Logger { const KEYWORD_NAME = /^[a-z_$][a-z0-9_$-]*$/i -function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefinition) { +function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefinition): void { /* eslint no-shadow: 0 */ const RULES: ValidationRules = this.RULES eachItem(keyword, (kwd) => { diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 7603536b00..bcc5f9f740 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -83,6 +83,10 @@ export function compileStoredSchema(this: Ajv, schObj: StoredSchema): ValidateFu return (schObj.meta ? compileMetaSchema : compileSchema).call(this, schObj) } +function schemaScopeCode(this: Ajv, sch: StoredSchema): Name { + return this._scope.value("validate", {ref: sch}, "schema", "validate") +} + function compileMetaSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { const currentOpts = this._opts this._opts = this._metaOpts @@ -127,7 +131,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } } const root = schObj.root - return localCompile(schObj as SchemaEnv) + return localCompile(schObj) function localCompile(sch: StoredSchema): ValidateFunction { // TODO refactor - remove compilations @@ -138,7 +142,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { if (sch.root === undefined || sch.root !== root) { return compileSchema.call(self, sch) } - const isRoot = isRootEnv(sch as SchemaEnv) + const isRoot = sch.schema === sch.root.schema const $async = typeof schema == "object" && schema.$async === true const rootId = getFullPath(sch.root.schema.$id) @@ -151,8 +155,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { }) } - const validateName = gen.scopeValue("validate", {ref: sch}, "schema", "validate", false) - sch.validateName = validateName + const validateName = schemaScopeCode.call(self, sch) const schemaCxt = { gen, @@ -163,7 +166,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { dataNames: [N.data], dataPathArr: [nil], dataLevel: 0, - topSchemaRef: _`${validateName}.schema`, + topSchemaRef: _`${validateName}.schema`, // TODO maybe use validateName instead of topSchemaRef? async: $async, validateName, ValidationError: _ValidationError, @@ -231,6 +234,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { if (res) return res const refCode = localRefCode(ref) + let schOrFunc = resolve.call(self, localCompile, root, ref) if (schOrFunc === undefined) { const localSchema = localRefs && localRefs[ref] @@ -317,43 +321,25 @@ function vars( // TODO returns SchemaObject (if the schema can be inlined) or validation function function resolve( this: Ajv, - localCompile: (env: SchemaEnv) => ValidateFunction, // reference to schema compilation function (localCompile) + localCompile: (env: StoredSchema) => ValidateFunction, // reference to schema compilation function (localCompile) root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve -): RefVal | undefined { +): RefVal | undefined | Name | StoredSchema { let schOrRef = this._refs[ref] - if (typeof schOrRef == "string") { - if (this._refs[schOrRef]) schOrRef = this._refs[schOrRef] - else return resolve.call(this, localCompile, root, schOrRef) - } - - schOrRef = schOrRef || this._schemas[ref] - if (schOrRef instanceof StoredSchema) { - return inlineRef(schOrRef.schema, this._opts.inlineRefs) - ? schOrRef.schema - : schOrRef.validate || compileSchema.call(this, schOrRef) - } - - const env = resolveSchema.call(this, root, ref) - let schema, baseId - if (env) { - schema = env.schema - root = env.root - baseId = env.baseId + while (typeof schOrRef == "string") { + ref = schOrRef + schOrRef = this._refs[ref] } + if (schOrRef instanceof StoredSchema) return inlineOrCompile.call(this, schOrRef) + const env = resolveSchema.call(this, root, ref) // TODO why env.schema can be StoredSchema? + if (env) return inlineOrCompile.call(this, env.schema instanceof StoredSchema ? env.schema : env) + return undefined - if (schema instanceof StoredSchema) { - if (!schema.validate) { - schema.validate = localCompile.call(this, schema as SchemaEnv) - } - return schema.validate + function inlineOrCompile(this: Ajv, sch: StoredSchema): RefVal { + return inlineRef(sch.schema, this._opts.inlineRefs) + ? sch.schema + : sch.validate || localCompile.call(this, sch) } - if (schema !== undefined) { - return inlineRef(schema, this._opts.inlineRefs) - ? schema - : localCompile.call(this, {schema, root, baseId}) - } - return undefined } // Resolve schema, its root and baseId @@ -420,33 +406,24 @@ function getJsonPointer( parsedRef: URI.URIComponents, {baseId, schema, root}: SchemaEnv ): SchemaEnv | undefined { - if (typeof schema == "boolean") return - parsedRef.fragment = parsedRef.fragment || "" - if (parsedRef.fragment.slice(0, 1) !== "/") return - const parts = parsedRef.fragment.split("/") - - for (let part of parts) { - if (!part) continue + if (parsedRef.fragment?.[0] !== "/") return + for (const part of parsedRef.fragment.slice(1).split("/")) { if (typeof schema == "boolean") return - part = unescapeFragment(part) - schema = schema[part] + schema = schema[unescapeFragment(part)] if (schema === undefined) return - if (PREVENT_SCOPE_CHANGE[part]) continue - if (typeof schema == "object" && schema.$id) { + // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def? + if (!PREVENT_SCOPE_CHANGE[part] && typeof schema == "object" && schema.$id) { baseId = resolveUrl(baseId, schema.$id) } } - if (schema === undefined) return + let env: SchemaEnv | undefined if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) { const $ref = resolveUrl(baseId, schema.$ref) - const _env = resolveSchema.call(this, root, $ref) - if (_env && !isRootEnv(_env)) return _env + env = resolveSchema.call(this, root, $ref) } - const env = {schema, root, baseId} - if (!isRootEnv(env)) return env + // even though resolution failed we need to return SchemaEnv to throw exception + // so that compileAsync loads missing schema. + env = env || {schema, root, baseId} + if (env.schema !== env.root.schema) return env return undefined } - -function isRootEnv({schema, root}: SchemaEnv): boolean { - return schema === root.schema -} From ad731276bd2e2df6172adf02cbcb2cb8984c19eb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:45:06 +0100 Subject: [PATCH 194/322] refactor: resolve and resolveRef --- lib/compile/index.ts | 46 ++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index bcc5f9f740..03d76f664a 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -172,7 +172,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { ValidationError: _ValidationError, schema: schema, isRoot, - root: sch.root, + root, rootId, baseId: baseId || rootId, schemaPath: nil, @@ -228,26 +228,21 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } } - function resolveRef(_baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { - ref = resolveUrl(_baseId, ref) + function resolveRef(baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { + ref = resolveUrl(baseId, ref) const res = getExistingRef(ref, isRoot) if (res) return res - const refCode = localRefCode(ref) - - let schOrFunc = resolve.call(self, localCompile, root, ref) - if (schOrFunc === undefined) { - const localSchema = localRefs && localRefs[ref] - if (localSchema) { - schOrFunc = inlineRef(localSchema, opts.inlineRefs) - ? localSchema - : compileSchema.call(self, {schema: localSchema, root, localRefs, baseId: _baseId}) - } + let _sch = resolve.call(self, root, ref) + if (_sch === undefined) { + const schema = localRefs && localRefs[ref] + if (schema) _sch = {schema, root, localRefs, baseId} } - if (schOrFunc === undefined) { + if (_sch === undefined) { removeLocalRef(ref) } else { + const schOrFunc = inlineOrCompile.call(self, _sch) replaceLocalRef(ref, schOrFunc) return resolvedRef(schOrFunc, refCode) } @@ -269,6 +264,12 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } } + function inlineOrCompile(this: Ajv, sch: StoredSchema): RefVal { + return inlineRef(sch.schema, this._opts.inlineRefs) + ? sch.schema + : sch.validate || localCompile.call(this, sch) + } + // TODO gen.globals function localRefCode(ref: string, schOrFunc?: RefVal): Code { const refId = refVal.length @@ -321,25 +322,20 @@ function vars( // TODO returns SchemaObject (if the schema can be inlined) or validation function function resolve( this: Ajv, - localCompile: (env: StoredSchema) => ValidateFunction, // reference to schema compilation function (localCompile) root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve -): RefVal | undefined | Name | StoredSchema { +): StoredSchema | undefined { + // ): RefVal | undefined | Name | StoredSchema { let schOrRef = this._refs[ref] while (typeof schOrRef == "string") { ref = schOrRef schOrRef = this._refs[ref] } - if (schOrRef instanceof StoredSchema) return inlineOrCompile.call(this, schOrRef) - const env = resolveSchema.call(this, root, ref) // TODO why env.schema can be StoredSchema? - if (env) return inlineOrCompile.call(this, env.schema instanceof StoredSchema ? env.schema : env) + if (schOrRef instanceof StoredSchema) return schOrRef + const env = resolveSchema.call(this, root, ref) + // TODO why env.schema can be StoredSchema? + if (env) return env.schema instanceof StoredSchema ? env.schema : env return undefined - - function inlineOrCompile(this: Ajv, sch: StoredSchema): RefVal { - return inlineRef(sch.schema, this._opts.inlineRefs) - ? sch.schema - : sch.validate || localCompile.call(this, sch) - } } // Resolve schema, its root and baseId From 9399a9b996506f36b07842df66544086e24e801d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 10 Sep 2020 19:10:06 +0100 Subject: [PATCH 195/322] refactor: replace refVal[0] with localRoot --- lib/ajv.ts | 3 ++- lib/compile/index.ts | 27 +++++++++++++-------------- lib/types.ts | 1 + lib/vocabularies/core/ref.ts | 6 +++--- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index aed3a8b2a7..8be045bad9 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -34,6 +34,7 @@ const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] const EXT_SCOPE_NAMES = new Set([ "validate", + "root", "keyword", "pattern", "formats", @@ -257,7 +258,7 @@ export default class Ajv { getSchema(keyRef: string): ValidateFunction | undefined { let schemaObj = _getSchemaObj.call(this, keyRef) if (schemaObj === undefined) { - const root = {schema: {}, refVal: [undefined], refs: {}} + const root = {schema: {}, localRoot: {}, refVal: [], refs: {}} const env = resolveSchema.call(this, root, keyRef) if (!env) return schemaObj = this._fragments[keyRef] = new StoredSchema({...env, ref: keyRef, fragment: true}) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 03d76f664a..1495f0f8ef 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,4 @@ -import type {Schema, SchemaObject, ValidateFunction} from "../types" +import type {Schema, SchemaObject, ValidateFunction, SchemaCxt} from "../types" import type Ajv from "../ajv" import {CodeGen, _, nil, str, Code, Name} from "./codegen" import {ValidationError} from "./error_classes" @@ -20,6 +20,7 @@ interface _StoredSchema { fragment?: true meta?: boolean root?: SchemaRoot + localRoot?: {validate?: ValidateFunction} refs?: {[ref: string]: number | undefined} refVal?: (RefVal | undefined)[] localRefs?: LocalRefs @@ -36,6 +37,7 @@ export class StoredSchema implements _StoredSchema { fragment?: true meta?: boolean root?: SchemaRoot + localRoot?: {validate?: ValidateFunction} refs?: {[ref: string]: number | undefined} refVal?: (RefVal | undefined)[] localRefs?: LocalRefs @@ -68,6 +70,7 @@ export type RefVal = Schema | ValidateFunction export interface SchemaRoot { schema: SchemaObject + localRoot: {validate?: ValidateFunction} refVal: (RefVal | undefined)[] refs: {[ref: string]: number | undefined} } @@ -121,17 +124,19 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { const {localRefs} = schObj const self = this const opts = this._opts - const refVal: (RefVal | undefined)[] = [undefined] + const refVal: (RefVal | undefined)[] = [] const refs: {[ref: string]: number | undefined} = {} + const localRoot: {validate?: ValidateFunction} = {} if (schObj.root === undefined) { schObj.root = { schema: typeof schObj.schema == "boolean" ? {} : schObj.schema, + localRoot, refVal, refs, } } const root = schObj.root - return localCompile(schObj) + return (localRoot.validate = localCompile(schObj)) function localCompile(sch: StoredSchema): ValidateFunction { // TODO refactor - remove compilations @@ -157,7 +162,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { const validateName = schemaScopeCode.call(self, sch) - const schemaCxt = { + const schemaCxt: SchemaCxt = { gen, allErrors: !!opts.allErrors, data: N.data, @@ -193,20 +198,14 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) // console.log("\n\n\n *** \n", sourceCode) // TODO refactor to fewer variables - maybe only self and scope - const makeValidate = new Function( - N.self.toString(), - "root", - "refVal", - N.scope.toString(), - sourceCode - ) - const validate: ValidateFunction = makeValidate(self, root, refVal, self._scope.get()) - - refVal[0] = validate + const makeValidate = new Function(N.self.toString(), "refVal", N.scope.toString(), sourceCode) + const validate: ValidateFunction = makeValidate(self, refVal, self._scope.get()) + validate.schema = schema validate.errors = null sch.refs = validate.refs = refs sch.refVal = validate.refVal = refVal + sch.localRoot = validate.localRoot = localRoot sch.root = validate.root = isRoot ? root : sch.root if ($async) validate.$async = true if (opts.sourceCode === true) { diff --git a/lib/types.ts b/lib/types.ts index b805ae616b..75efd1949d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -113,6 +113,7 @@ export interface ValidateFunction { refs?: {[ref: string]: number | undefined} refVal?: (RefVal | undefined)[] root?: SchemaRoot + localRoot?: {validate?: ValidateFunction} $async?: true source?: SourceCode validate?: ValidateFunction // it will be only set on wrappers diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 64843656eb..d757f9317a 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -22,9 +22,9 @@ const def: CodeKeywordDefinition = { function getRef(): ResolvedRef | void { if (schema === "#" || schema === "#/") { - return isRoot - ? {code: validateName, $async: it.async} - : {code: _`root.refVal[0]`, $async: root.schema.$async === true} + if (isRoot) return {code: validateName, $async: it.async} + const rootName = gen.scopeValue("root", {ref: root.localRoot}) + return {code: _`${rootName}.validate`, $async: root.schema.$async === true} } return resolveRef(baseId, schema, isRoot) } From 3b38dfc89b1e58fa2dd70b653917350cc968bf4d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 11 Sep 2020 10:24:08 +0100 Subject: [PATCH 196/322] refactor: replace refVal arrays with shared scope --- lib/ajv.ts | 1 + lib/compile/codegen/index.ts | 38 ++++---- lib/compile/codegen/scope.ts | 65 ++++++++------ lib/compile/context.ts | 2 +- lib/compile/index.ts | 132 +++++++--------------------- lib/types.ts | 7 +- lib/vocabularies/core/ref.ts | 11 ++- lib/vocabularies/validation/enum.ts | 2 +- spec/resolve.spec.ts | 13 +-- 9 files changed, 110 insertions(+), 161 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 8be045bad9..1199641bc9 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -35,6 +35,7 @@ const META_SUPPORT_DATA = ["/properties"] const EXT_SCOPE_NAMES = new Set([ "validate", "root", + "schema", "keyword", "pattern", "formats", diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index e920a77c01..2aa0f2cba9 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -1,5 +1,5 @@ import {_, str, nil, _Code, Code, Name, getProperty, stringify} from "./code" -import {Scope, ScopeValueSets, NameValue, ScopeStore, ValueScope} from "./scope" +import {Scope, ScopeValueSets, NameValue, ScopeStore, ValueScope, ValueScopeName} from "./scope" export {_, str, nil, getProperty, stringify, Name, Code, Scope, ScopeStore, ValueScope} @@ -59,29 +59,25 @@ export class CodeGen { return this._out } - toName(nameOrPrefix: Name | string): Name { - return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) - } - name(prefix: string): Name { return this._scope.name(prefix) } - scopeValue( - prefix: string, - value: NameValue, - scopeProperty?: string, - itemProperty?: string, - used = true - ): Name { - const name = this._extScope.value(prefix, value, scopeProperty, itemProperty) - if (used) { - const vs = this._values[prefix] || (this._values[prefix] = new Set()) - vs.add(name) - } + scopeName(prefix: string): ValueScopeName { + return this._extScope.name(prefix) + } + + scopeValue(prefixOrName: ValueScopeName | string, value: NameValue): Name { + const name = this._extScope.value(prefixOrName, value) + const vs = this._values[name.prefix] || (this._values[name.prefix] = new Set()) + vs.add(name) return name } + getScopeValue(prefix: string, keyOrRef: unknown): ValueScopeName | void { + return this._extScope.getValue(prefix, keyOrRef) + } + scopeRefs(scopeName: Name): Code { return this._extScope.scopeRefs(scopeName, this._values) } @@ -164,7 +160,7 @@ export class CodeGen { forBody: (index: Name) => void, varKind: Code = varKinds.let ): CodeGen { - const i = this.toName(nameOrPrefix) + const i = this._scope.toName(nameOrPrefix) if (this.opts.es5) varKind = varKinds.var return _loop.call(this, _`for(${varKind} ${i}=${from}; ${i}<${to}; ${i}++){`, i, forBody) } @@ -175,7 +171,7 @@ export class CodeGen { forBody: (item: Name) => void, varKind: Code = varKinds.const ): CodeGen { - const name = this.toName(nameOrPrefix) + const name = this._scope.toName(nameOrPrefix) if (this.opts.es5) { const arr = iterable instanceof Name ? iterable : this.var("arr", iterable) return this.forRange("_i", 0, new _Code(`${arr}.length`), (i) => { @@ -195,7 +191,7 @@ export class CodeGen { if (this.opts.forInOwn) { return this.forOf(nameOrPrefix, new _Code(`Object.keys(${obj})`), forBody) } - const name = this.toName(nameOrPrefix) + const name = this._scope.toName(nameOrPrefix) return _loop.call(this, _`for(${varKind} ${name} in ${obj}){`, name, forBody) } @@ -296,7 +292,7 @@ export class CodeGen { } function _def(this: CodeGen, varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - const name = this.toName(nameOrPrefix) + const name = this._scope.toName(nameOrPrefix) if (this.opts.es5) varKind = varKinds.var if (rhs === undefined) this._out += `${varKind} ${name};` + this._n else this._out += `${varKind} ${name} = ${rhs};` + this._n diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts index 312f0217e8..b28c1fc00a 100644 --- a/lib/compile/codegen/scope.ts +++ b/lib/compile/codegen/scope.ts @@ -46,6 +46,10 @@ export class Scope { this._parent = parent } + toName(nameOrPrefix: Name | string): Name { + return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix) + } + name(prefix: string): Name { return new Name(newName.call(this, prefix)) } @@ -54,18 +58,21 @@ export class Scope { interface ScopePath { property: string itemIndex: number - itemProperty?: string } -class ValueScopeName extends Name { - value: NameValue - scopePath: Code +export class ValueScopeName extends Name { + prefix: string + value?: NameValue + scopePath?: Code - constructor(nameStr: string, value: NameValue, {property, itemIndex, itemProperty}: ScopePath) { + constructor(prefix: string, nameStr: string) { super(nameStr) + this.prefix = prefix + } + + setValue(value: NameValue, {property, itemIndex}: ScopePath) { this.value = value this.scopePath = _`.${new Name(property)}[${itemIndex}]` - if (itemProperty) this.scopePath.add(_`.${new Name(itemProperty)}`) } } @@ -82,43 +89,47 @@ export class ValueScope extends Scope { return this._scope } - value( - prefix: string, - value: NameValue, - scopeProperty = prefix, - itemProperty?: string - ): ValueScopeName { - if (!value.ref) throw new Error("CodeGen: ref must be passed in value") + name(prefix: string): ValueScopeName { + return new ValueScopeName(prefix, newName.call(this, prefix)) + } + + value(nameOrPrefix: ValueScopeName | string, value: NameValue): ValueScopeName { + if (value.ref === undefined) throw new Error("CodeGen: ref must be passed in value") + const name = this.toName(nameOrPrefix) as ValueScopeName + const prefix = name.prefix const valueKey = value.key ?? value.ref let vs = this._values[prefix] if (vs) { - const name = vs.get(valueKey) - if (name) return name + const _name = vs.get(valueKey) + if (_name) return _name } else { vs = this._values[prefix] = new Map() } - - const s = this._scope[scopeProperty] || (this._scope[scopeProperty] = []) - const itemIndex = s.push(value.ref) - 1 - - const name = new ValueScopeName(newName.call(this, prefix), value, { - property: scopeProperty, - itemIndex, - itemProperty, - }) vs.set(valueKey, name) + + const s = this._scope[prefix] || (this._scope[prefix] = []) + const itemIndex = s.length + s[itemIndex] = value.ref + name.setValue(value, {property: prefix, itemIndex}) return name } + getValue(prefix: string, keyOrRef: unknown): ValueScopeName | void { + const vs = this._values[prefix] + if (!vs) return + return vs.get(keyOrRef) + } + scopeRefs(scopeName: Name, values?: ScopeValues | ScopeValueSets): Code { - return reduceValues.call(this, values, ({scopePath}: ValueScopeName) => { - return _`${scopeName}${scopePath}` + return reduceValues.call(this, values, (name: ValueScopeName) => { + if (name.scopePath === undefined) throw new Error(`CodeGen: name "${name}" has no value`) + return _`${scopeName}${name.scopePath}` }) } scopeCode(values?: ScopeValues | ScopeValueSets): Code { return reduceValues.call(this, values, (name: ValueScopeName) => { - const c = name.value.code + const c = name.value?.code if (c) return c throw new ValueError(name) }) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 6d76c3e92d..9db256e4c7 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -49,7 +49,7 @@ export default class KeywordCxt implements KeywordErrorCxt { this.def = def if (this.$data) { - this.schemaCode = it.gen.const("schema", getData(this.$data, it)) + this.schemaCode = it.gen.const("vSchema", getData(this.$data, it)) } else { this.schemaCode = this.schemaValue if (def.schemaType && !validSchemaType(this.schema, def.schemaType)) { diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 1495f0f8ef..a463cfa88c 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -8,9 +8,7 @@ import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" import URI = require("uri-js") -/** - * Functions below are used inside compiled validations function - */ +export type SchemaRefs = {[ref: string]: Schema | ValidateFunction | undefined} interface _StoredSchema { schema: Schema @@ -21,8 +19,7 @@ interface _StoredSchema { meta?: boolean root?: SchemaRoot localRoot?: {validate?: ValidateFunction} - refs?: {[ref: string]: number | undefined} - refVal?: (RefVal | undefined)[] + refs?: SchemaRefs localRefs?: LocalRefs baseId?: string validate?: ValidateFunction @@ -38,8 +35,7 @@ export class StoredSchema implements _StoredSchema { meta?: boolean root?: SchemaRoot localRoot?: {validate?: ValidateFunction} - refs?: {[ref: string]: number | undefined} - refVal?: (RefVal | undefined)[] + refs?: SchemaRefs localRefs?: LocalRefs baseId?: string validate?: ValidateFunction @@ -65,14 +61,10 @@ interface FuncResolvedRef { inline?: false } -// reference to compiled schema or schema to be inlined -export type RefVal = Schema | ValidateFunction - export interface SchemaRoot { schema: SchemaObject localRoot: {validate?: ValidateFunction} - refVal: (RefVal | undefined)[] - refs: {[ref: string]: number | undefined} + refs: SchemaRefs } interface SchemaEnv { @@ -86,10 +78,6 @@ export function compileStoredSchema(this: Ajv, schObj: StoredSchema): ValidateFu return (schObj.meta ? compileMetaSchema : compileSchema).call(this, schObj) } -function schemaScopeCode(this: Ajv, sch: StoredSchema): Name { - return this._scope.value("validate", {ref: sch}, "schema", "validate") -} - function compileMetaSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { const currentOpts = this._opts this._opts = this._metaOpts @@ -100,7 +88,7 @@ function compileMetaSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } } -function validateWrapper(sch: StoredSchema): ValidateFunction { +function validateWrapper(this: Ajv, sch: StoredSchema): ValidateFunction { if (!sch.validate) { const wrapper: ValidateFunction = function (this: Ajv | unknown, ...args) { if (wrapper.validate === undefined) throw new Error("ajv implementation error") @@ -110,6 +98,7 @@ function validateWrapper(sch: StoredSchema): ValidateFunction { return valid } sch.validate = wrapper + sch.validateName = this._scope.value("validate", {ref: wrapper}) } return sch.validate } @@ -124,14 +113,12 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { const {localRefs} = schObj const self = this const opts = this._opts - const refVal: (RefVal | undefined)[] = [] - const refs: {[ref: string]: number | undefined} = {} + const refs: SchemaRefs = {} const localRoot: {validate?: ValidateFunction} = {} if (schObj.root === undefined) { schObj.root = { schema: typeof schObj.schema == "boolean" ? {} : schObj.schema, localRoot, - refVal, refs, } } @@ -141,8 +128,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { function localCompile(sch: StoredSchema): ValidateFunction { // TODO refactor - remove compilations const _sch = getCompilingSchema.call(self, sch) - if (_sch) return validateWrapper(_sch) - // validateWrapper(sch) + if (_sch) return validateWrapper.call(self, _sch) const {schema, baseId} = sch if (sch.root === undefined || sch.root !== root) { return compileSchema.call(self, sch) @@ -160,7 +146,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { }) } - const validateName = schemaScopeCode.call(self, sch) + const validateName = gen.scopeName("validate") const schemaCxt: SchemaCxt = { gen, @@ -169,13 +155,13 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { parentData: N.parentData, parentDataProperty: N.parentDataProperty, dataNames: [N.data], - dataPathArr: [nil], + dataPathArr: [nil], // TODO can it's lenght be used as dataLevel if nil is removed? dataLevel: 0, - topSchemaRef: _`${validateName}.schema`, // TODO maybe use validateName instead of topSchemaRef? + topSchemaRef: gen.scopeValue("schema", {ref: schema}), async: $async, validateName, ValidationError: _ValidationError, - schema: schema, + schema, isRoot, root, rootId, @@ -184,7 +170,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { errSchemaPath: "#", errorPath: str``, opts, - resolveRef, // TODO move to gen.globals + resolveRef, // TODO move to gen.scopeValue? self, } @@ -192,19 +178,18 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { try { self._compilations.add(sch) validateFunctionCode(schemaCxt) - sourceCode = `${vars(refVal, refValCode)} - ${gen.scopeRefs(N.scope)} + sourceCode = `${gen.scopeRefs(N.scope)} ${gen.toString()}` if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) // console.log("\n\n\n *** \n", sourceCode) - // TODO refactor to fewer variables - maybe only self and scope - const makeValidate = new Function(N.self.toString(), "refVal", N.scope.toString(), sourceCode) - const validate: ValidateFunction = makeValidate(self, refVal, self._scope.get()) + const makeValidate = new Function(N.self.toString(), N.scope.toString(), sourceCode) + const validate: ValidateFunction = makeValidate(self, self._scope.get()) + + gen.scopeValue(validateName, {ref: validate}) validate.schema = schema validate.errors = null sch.refs = validate.refs = refs - sch.refVal = validate.refVal = refVal sch.localRoot = validate.localRoot = localRoot sch.root = validate.root = isRoot ? root : sch.root if ($async) validate.$async = true @@ -216,6 +201,7 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } if (sch.validate) extendWrapper(sch.validate, validate) sch.validate = validate + sch.validateName = validateName return validate } catch (e) { delete sch.validate @@ -227,72 +213,30 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } } - function resolveRef(baseId: string, ref: string, isRoot: boolean): ResolvedRef | void { + function resolveRef( + baseId: string, + ref: string, + isRoot: boolean + ): Schema | ValidateFunction | void { ref = resolveUrl(baseId, ref) - const res = getExistingRef(ref, isRoot) - if (res) return res - const refCode = localRefCode(ref) + // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaRoot) + const schOrFunc = refs[ref] || (!isRoot && root.refs?.[ref]) + if (schOrFunc) return schOrFunc + let _sch = resolve.call(self, root, ref) if (_sch === undefined) { - const schema = localRefs && localRefs[ref] + const schema = localRefs?.[ref] if (schema) _sch = {schema, root, localRefs, baseId} } - if (_sch === undefined) { - removeLocalRef(ref) - } else { - const schOrFunc = inlineOrCompile.call(self, _sch) - replaceLocalRef(ref, schOrFunc) - return resolvedRef(schOrFunc, refCode) - } - } - - function getExistingRef(ref: string, isRoot: boolean): ResolvedRef | void { - const idx = refs[ref] - if (idx !== undefined) { - const schOrFunc = refVal[idx] - return resolvedRef(schOrFunc, _`refVal[${idx}]`) - } - if (!isRoot && root.refs) { - // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaRoot) - const rootIdx = root.refs[ref] - if (rootIdx !== undefined) { - const schOrFunc = root.refVal[rootIdx] - return resolvedRef(schOrFunc, localRefCode(ref, schOrFunc)) - } - } + if (_sch !== undefined) return (refs[ref] = inlineOrCompile.call(self, _sch)) } - function inlineOrCompile(this: Ajv, sch: StoredSchema): RefVal { + function inlineOrCompile(this: Ajv, sch: StoredSchema): Schema | ValidateFunction { return inlineRef(sch.schema, this._opts.inlineRefs) ? sch.schema : sch.validate || localCompile.call(this, sch) } - - // TODO gen.globals - function localRefCode(ref: string, schOrFunc?: RefVal): Code { - const refId = refVal.length - refVal[refId] = schOrFunc - refs[ref] = refId - return _`refVal${refId}` - } - - // TODO gen.globals remove? - function removeLocalRef(ref: string) { - delete refs[ref] - } - - // TODO gen.globals remove? - function replaceLocalRef(ref: string, schOrFunc: RefVal) { - const refId = refs[ref] - if (refId !== undefined) refVal[refId] = schOrFunc - } - - function resolvedRef(schOrFunc: RefVal | undefined, code: Code): ResolvedRef { - return typeof schOrFunc == "object" || typeof schOrFunc == "boolean" - ? {code: code, schema: schOrFunc, inline: true} - : {code: code, $async: schOrFunc && !!schOrFunc.$async} - } } // Index of schema compilation in the currently compiled list @@ -306,17 +250,6 @@ function sameSchema(s1: StoredSchema, s2: StoredSchema): boolean { return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId } -function refValCode(i: number, refVal: (RefVal | undefined)[]): Code { - return refVal[i] === undefined ? nil : _`const refVal${i} = refVal[${i}];` -} - -function vars( - arr: (RefVal | undefined)[], - statement: (i: number, arr: (RefVal | undefined)[]) => Code -): Code { - return arr.map((_el, i) => statement(i, arr)).reduce((res: Code, c: Code) => _`${res}${c}`, nil) -} - // resolve and compile the references ($ref) // TODO returns SchemaObject (if the schema can be inlined) or validation function function resolve( @@ -324,13 +257,12 @@ function resolve( root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve ): StoredSchema | undefined { - // ): RefVal | undefined | Name | StoredSchema { let schOrRef = this._refs[ref] while (typeof schOrRef == "string") { ref = schOrRef schOrRef = this._refs[ref] } - if (schOrRef instanceof StoredSchema) return schOrRef + if (schOrRef) return schOrRef const env = resolveSchema.call(this, root, ref) // TODO why env.schema can be StoredSchema? if (env) return env.schema instanceof StoredSchema ? env.schema : env diff --git a/lib/types.ts b/lib/types.ts index 75efd1949d..554a81aa93 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" -import {RefVal, ResolvedRef, SchemaRoot, StoredSchema} from "./compile" +import {SchemaRoot, SchemaRefs, StoredSchema} from "./compile" import KeywordCxt from "./compile/context" import Ajv from "./ajv" @@ -110,8 +110,7 @@ export interface ValidateFunction { ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] - refs?: {[ref: string]: number | undefined} - refVal?: (RefVal | undefined)[] + refs?: SchemaRefs root?: SchemaRoot localRoot?: {validate?: ValidateFunction} $async?: true @@ -174,7 +173,7 @@ export interface SchemaCxt { compositeRule?: boolean createErrors?: boolean opts: InstanceOptions - resolveRef: (baseId: string, ref: string, isRoot: boolean) => ResolvedRef | void + resolveRef: (baseId: string, ref: string, isRoot: boolean) => Schema | ValidateFunction | void self: Ajv } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index d757f9317a..a9f4ac44af 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -3,6 +3,7 @@ import KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {ResolvedRef, InlineResolvedRef} from "../../compile" +// import {resolveUrl} from "../../compile/resolve" import {callValidateCode} from "../util" import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" @@ -26,7 +27,15 @@ const def: CodeKeywordDefinition = { const rootName = gen.scopeValue("root", {ref: root.localRoot}) return {code: _`${rootName}.validate`, $async: root.schema.$async === true} } - return resolveRef(baseId, schema, isRoot) + + const schOrFunc = resolveRef(baseId, schema, isRoot) + if (typeof schOrFunc == "function") { + const code = gen.scopeValue("validate", {ref: schOrFunc}) + return {code, $async: !!schOrFunc.$async} + } else if (typeof schOrFunc == "boolean" || typeof schOrFunc == "object") { + const code = gen.scopeValue("schema", {ref: schOrFunc}) + return {code, schema: schOrFunc, inline: true} + } } function missingRef(): void { diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 6ea96d9237..fc59201f51 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -21,7 +21,7 @@ const def: CodeKeywordDefinition = { cxt.block$data(valid, loopEnum) } else { if (!Array.isArray(schema)) throw new Error("ajv implementation error") - const vSchema = gen.const("schema", schemaCode) + const vSchema = gen.const("vSchema", schemaCode) valid = or(...schema.map((_x: unknown, i: number) => equalCode(vSchema, i))) } cxt.pass(valid) diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index 946861370f..8e3d5a4555 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -1,5 +1,6 @@ import getAjvInstances from "./ajv_instances" import Ajv from "./ajv" +import {ValidateFunction} from "../dist/types" const should = require("./chai").should() @@ -214,22 +215,22 @@ describe("resolve", () => { ] it("by default should inline schema if it doesn't contain refs", () => { - const ajv = new Ajv({schemas: schemas}) + const ajv = new Ajv({schemas, sourceCode: true}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs == false", () => { - const ajv = new Ajv({schemas: schemas, inlineRefs: false}) + const ajv = new Ajv({schemas, inlineRefs: false, sourceCode: true}) testSchemas(ajv, false) }) it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { - const ajv = new Ajv({schemas: schemas, inlineRefs: 4}) + const ajv = new Ajv({schemas, inlineRefs: 4, sourceCode: true}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { - const ajv = new Ajv({schemas: schemas, inlineRefs: 2}) + const ajv = new Ajv({schemas: schemas, inlineRefs: 2, sourceCode: true}) testSchemas(ajv, false) }) @@ -319,8 +320,8 @@ describe("resolve", () => { validate([{a: 5}]).should.equal(false) } - function testInlined(validate, expectedInlined) { - const inlined: any = !/refVal/.test(validate.toString()) + function testInlined(validate: ValidateFunction, expectedInlined) { + const inlined: any = !/scope\.validate/.test(validate.source?.code || "") inlined.should.equal(expectedInlined) } }) From 4c6d522de8c0cfe095a5068755de4be2f9ed6425 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 11 Sep 2020 12:54:16 +0100 Subject: [PATCH 197/322] refactor: remove SchemaEnv, refactor resolve, resolveSchema --- lib/compile/index.ts | 87 +++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index a463cfa88c..35cbdb72e1 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -28,21 +28,28 @@ interface _StoredSchema { export class StoredSchema implements _StoredSchema { schema: Schema + root?: SchemaRoot + localRoot: {validate?: ValidateFunction} = {} + refs: SchemaRefs = {} id?: string ref?: string cacheKey?: unknown fragment?: true meta?: boolean - root?: SchemaRoot - localRoot?: {validate?: ValidateFunction} - refs?: SchemaRefs localRefs?: LocalRefs baseId?: string validate?: ValidateFunction validateName?: Name constructor(obj: _StoredSchema) { + this.localRoot = {} + this.refs = {} this.schema = obj.schema + // this.root = obj.root || { + // schema: typeof obj.schema == "boolean" ? {} : obj.schema, + // localRoot, + // refs, + // } Object.assign(this, obj) } } @@ -67,13 +74,6 @@ export interface SchemaRoot { refs: SchemaRefs } -interface SchemaEnv { - schema: Schema - root: SchemaRoot - localRefs?: LocalRefs - baseId?: string -} - export function compileStoredSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { return (schObj.meta ? compileMetaSchema : compileSchema).call(this, schObj) } @@ -226,16 +226,16 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { let _sch = resolve.call(self, root, ref) if (_sch === undefined) { const schema = localRefs?.[ref] - if (schema) _sch = {schema, root, localRefs, baseId} + if (schema) _sch = new StoredSchema({schema, root, localRefs, baseId}) } - if (_sch !== undefined) return (refs[ref] = inlineOrCompile.call(self, _sch)) + if (_sch !== undefined) return (refs[ref] = inlineOrCompile(_sch)) } - function inlineOrCompile(this: Ajv, sch: StoredSchema): Schema | ValidateFunction { - return inlineRef(sch.schema, this._opts.inlineRefs) + function inlineOrCompile(sch: StoredSchema): Schema | ValidateFunction { + return inlineRef(sch.schema, self._opts.inlineRefs) ? sch.schema - : sch.validate || localCompile.call(this, sch) + : sch.validate || localCompile(sch) } } @@ -257,16 +257,9 @@ function resolve( root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve ): StoredSchema | undefined { - let schOrRef = this._refs[ref] - while (typeof schOrRef == "string") { - ref = schOrRef - schOrRef = this._refs[ref] - } - if (schOrRef) return schOrRef - const env = resolveSchema.call(this, root, ref) - // TODO why env.schema can be StoredSchema? - if (env) return env.schema instanceof StoredSchema ? env.schema : env - return undefined + let sch + while (typeof (sch = this._refs[ref]) == "string") ref = sch + return sch || this._schemas[ref] || resolveSchema.call(this, root, ref) } // Resolve schema, its root and baseId @@ -274,35 +267,21 @@ export function resolveSchema( this: Ajv, root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it ref: string // reference to resolve -): SchemaEnv | undefined { +): StoredSchema | undefined { const p = URI.parse(ref) const refPath = _getFullPath(p) let baseId = getFullPath(root.schema.$id) if (Object.keys(root.schema).length === 0 || refPath !== baseId) { const id = normalizeId(refPath) - let schOrRef = this._refs[id] - if (typeof schOrRef == "string") { - return resolveRecursive.call(this, root, schOrRef, p) - } - if (schOrRef instanceof StoredSchema) { - if (!schOrRef.validate) compileSchema.call(this, schOrRef) - root = schOrRef - } else { - schOrRef = this._schemas[id] - if (schOrRef instanceof StoredSchema) { - if (!schOrRef.validate) compileSchema.call(this, schOrRef) - if (id === normalizeId(ref)) { - return {schema: schOrRef, root, baseId} - } - root = schOrRef - } else { - return - } - } - if (!root.schema) return + const schOrRef = this._refs[id] || this._schemas[id] + if (typeof schOrRef == "string") return resolveRecursive.call(this, root, schOrRef, p) + if (typeof schOrRef?.schema !== "object") return + if (!schOrRef.validate) compileSchema.call(this, schOrRef) + if (id === normalizeId(ref)) return new StoredSchema({schema: schOrRef.schema, root, baseId}) + root = schOrRef baseId = getFullPath(root.schema.$id) } - return getJsonPointer.call(this, p, {schema: root.schema, root, baseId}) + return getJsonPointer.call(this, p, new StoredSchema({schema: root.schema, root, baseId})) } function resolveRecursive( @@ -310,7 +289,7 @@ function resolveRecursive( root: SchemaRoot, ref: string, parsedRef: URI.URIComponents -): SchemaEnv | undefined { +): StoredSchema | undefined { const env = resolveSchema.call(this, root, ref) if (!env) return const {schema, baseId} = env @@ -331,8 +310,8 @@ const PREVENT_SCOPE_CHANGE = toHash([ function getJsonPointer( this: Ajv, parsedRef: URI.URIComponents, - {baseId, schema, root}: SchemaEnv -): SchemaEnv | undefined { + {baseId, schema, root}: StoredSchema +): StoredSchema | undefined { if (parsedRef.fragment?.[0] !== "/") return for (const part of parsedRef.fragment.slice(1).split("/")) { if (typeof schema == "boolean") return @@ -343,14 +322,14 @@ function getJsonPointer( baseId = resolveUrl(baseId, schema.$id) } } - let env: SchemaEnv | undefined + let env: StoredSchema | undefined if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) { const $ref = resolveUrl(baseId, schema.$ref) - env = resolveSchema.call(this, root, $ref) + env = resolveSchema.call(this, root, $ref) } // even though resolution failed we need to return SchemaEnv to throw exception // so that compileAsync loads missing schema. - env = env || {schema, root, baseId} - if (env.schema !== env.root.schema) return env + env = env || new StoredSchema({schema, root, baseId}) + if (env.schema !== env.root?.schema) return env return undefined } From 99552fe02c31c105dad739ab9b8167edda2da235 Mon Sep 17 00:00:00 2001 From: Victor Villas Date: Fri, 11 Sep 2020 11:26:30 -0300 Subject: [PATCH 198/322] Make compiled validation interface async aware --- lib/ajv.ts | 28 +++++++++++++++ lib/types.ts | 33 +++++++++++++++++ spec/options/async.spec.ts | 73 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 spec/options/async.spec.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 8db8c8d53c..71ee935df0 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -1,11 +1,15 @@ import { Schema, SchemaObject, + SyncSchemaObject, + AsyncSchemaObject, Vocabulary, KeywordDefinition, Options, InstanceOptions, ValidateFunction, + SyncValidateFunction, + AsyncValidateFunction, CacheInterface, Logger, ErrorObject, @@ -111,6 +115,10 @@ export default class Ajv { } // Create validation function for passed schema + compile(s: {$async?: never}, _?: boolean): ValidateFunction + compile(s: SyncSchemaObject, _?: boolean): SyncValidateFunction + compile(s: AsyncSchemaObject, _?: boolean): AsyncValidateFunction + compile(s: Schema, _?: boolean): ValidateFunction compile( schema: Schema, _meta?: boolean // true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. @@ -121,6 +129,26 @@ export default class Ajv { // Creates validating function for passed schema with asynchronous loading of missing schemas. // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. + compileAsync( + s: {$async?: never}, + m?: boolean | CompileAsyncCallback, + c?: CompileAsyncCallback + ): Promise + compileAsync( + s: SyncSchemaObject, + m?: boolean | CompileAsyncCallback, + c?: CompileAsyncCallback + ): Promise + compileAsync( + s: AsyncSchemaObject, + m?: boolean | CompileAsyncCallback, + c?: CompileAsyncCallback + ): Promise + compileAsync( + s: SchemaObject, + m?: boolean | CompileAsyncCallback, + c?: CompileAsyncCallback + ): Promise compileAsync( schema: SchemaObject, metaOrCallback?: boolean | CompileAsyncCallback, // optional true to compile meta-schema; this parameter can be skipped diff --git a/lib/types.ts b/lib/types.ts index b805ae616b..860766bf9a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -5,6 +5,7 @@ import Ajv from "./ajv" export interface SchemaObject { $id?: string + $async?: boolean $schema?: string [x: string]: any // TODO } @@ -118,6 +119,38 @@ export interface ValidateFunction { validate?: ValidateFunction // it will be only set on wrappers } +export interface SyncSchemaObject extends SchemaObject { + $async: false +} + +export interface SyncValidateFunction extends ValidateFunction { + ( + this: Ajv | any, + data: any, + dataPath?: string, + parentData?: Record | any[], + parentDataProperty?: string | number, + rootData?: Record | any[] + ): boolean + $async: undefined +} + +export interface AsyncSchemaObject extends SchemaObject { + $async: true +} + +export interface AsyncValidateFunction extends ValidateFunction { + ( + this: Ajv | any, + data: any, + dataPath?: string, + parentData?: Record | any[], + parentDataProperty?: string | number, + rootData?: Record | any[] + ): Promise + $async: true +} + export interface SchemaValidateFunction { ( schema: any, diff --git a/spec/options/async.spec.ts b/spec/options/async.spec.ts new file mode 100644 index 0000000000..730d654c5e --- /dev/null +++ b/spec/options/async.spec.ts @@ -0,0 +1,73 @@ +import _Ajv from "../ajv" +require("../chai").should() + +describe("$async option", () => { + const ajv = new _Ajv() + + describe("= undefined", () => { + const validate = ajv.compile({}) + it("should return a boolean or promise", async () => { + const result = validate({}) + if (typeof result === "boolean") { + result.should.exist + } else { + await result.then((data) => data.should.exist) + } + }) + }) + + describe("= false", () => { + const validate = ajv.compile({$async: false}) + it("should return a boolean", () => { + const result: boolean = validate({}) + result.should.exist + }) + }) + + describe("= true", () => { + const validate = ajv.compile({$async: true}) + it("should return a promise", async () => { + const result: Promise = validate({}) + await result.then((data) => data.should.exist) + }) + }) + + describe("= boolean", () => { + const schema = {$async: true} + const validate = ajv.compile(schema) + it("should return boolean or promise", async () => { + const result = validate({}) + if (typeof result === "boolean") { + result.should.exist + } else { + await result.then((data) => data.should.exist) + } + }) + }) + + describe("of type unknown", () => { + const schema: Record = {} + const validate = ajv.compile(schema) + it("should return boolean or promise", async () => { + const result = validate({}) + if (typeof result === "boolean") { + result.should.exist + } else { + await result.then((data) => data.should.exist) + } + }) + }) + + describe("of type any", () => { + const schema: any = {} + const validate = ajv.compile(schema) + it("should return boolean or promise", async () => { + const result = validate({}) + if (typeof result === "boolean") { + result.should.exist + } else { + await result.then((data) => data.should.exist) + } + }) + }) +}) From 8daaf7da67f0527388f9d769d2e6b2b94933571c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 11 Sep 2020 16:33:58 +0100 Subject: [PATCH 199/322] tests: update JSON-Schema-Test-Suite tests --- spec/JSON-Schema-Test-Suite | 2 +- spec/json-schema.spec.ts | 40 ++++++++++++++++++++----------------- spec/schema-tests.spec.ts | 2 +- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/spec/JSON-Schema-Test-Suite b/spec/JSON-Schema-Test-Suite index eadeacb042..21555a8540 160000 --- a/spec/JSON-Schema-Test-Suite +++ b/spec/JSON-Schema-Test-Suite @@ -1 +1 @@ -Subproject commit eadeacb04209a18fc81f1a1959e83eef72dcc97a +Subproject commit 21555a8540584447a9f8ea659accd0ce79bd36e5 diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index 4abfa9b6ce..ea0d3362ca 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -7,36 +7,40 @@ import {afterError, afterEach} from "./after_test" const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), "http://localhost:1234/subSchemas.json": require("./JSON-Schema-Test-Suite/remotes/subSchemas.json"), - "http://localhost:1234/folder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json"), + "http://localhost:1234/baseUriChange/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/baseUriChange/folderInteger.json"), + "http://localhost:1234/baseUriChangeFolder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/baseUriChangeFolder/folderInteger.json"), + "http://localhost:1234/baseUriChangeFolderInSubschema/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/baseUriChangeFolderInSubschema/folderInteger.json"), "http://localhost:1234/name.json": require("./JSON-Schema-Test-Suite/remotes/name.json"), } +const SKIP6 = [ + "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here + "optional/non-bmp-regex", + "format", + "optional/format/date", + "optional/format/date-time", + "optional/format/email", + "optional/format/hostname", + "optional/format/ipv4", + "optional/format/ipv6", + "optional/format/json-pointer", + "optional/format/uri", + "optional/format/uri-reference", + "optional/format/uri-template", +] + const SKIP = { - 6: [ - "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here - "optional/format", - ], - 7: [ - "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here + 6: SKIP6, + 7: SKIP6.concat([ "optional/content", - "optional/format/date", - "optional/format/date-time", - "optional/format/email", - "optional/format/hostname", "optional/format/idn-email", "optional/format/idn-hostname", - "optional/format/ipv4", - "optional/format/ipv6", "optional/format/iri", "optional/format/iri-reference", - "optional/format/json-pointer", "optional/format/regex", "optional/format/relative-json-pointer", "optional/format/time", - "optional/format/uri", - "optional/format/uri-reference", - "optional/format/uri-template", - ], + ]), } runTest(getAjvInstances(options, {meta: false, strict: false}), 6, require("./_json/draft6")) diff --git a/spec/schema-tests.spec.ts b/spec/schema-tests.spec.ts index c1455ad108..16aefe0166 100644 --- a/spec/schema-tests.spec.ts +++ b/spec/schema-tests.spec.ts @@ -9,7 +9,7 @@ const instances = getAjvInstances(options, {strict: false, unknownFormats: ["all const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), - "http://localhost:1234/folder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/folder/folderInteger.json"), + "http://localhost:1234/folder/folderInteger.json": require("./JSON-Schema-Test-Suite/remotes/baseUriChange/folderInteger.json"), "http://localhost:1234/name.json": require("./remotes/name.json"), } From 912c3f4ea61524a0d9c372444da556d91d42c1e8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 11 Sep 2020 16:39:37 +0100 Subject: [PATCH 200/322] feat: add unicode flag to regular expressions, passes ecmascript-regex and non-bmp-regex tests --- lib/vocabularies/util.ts | 4 ++-- lib/vocabularies/validation/pattern.ts | 3 +-- spec/json-schema.spec.ts | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 88bae13979..fab2f15822 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -77,8 +77,8 @@ export function callValidateCode( export function usePattern(gen: CodeGen, pattern: string): Name { return gen.scopeValue("pattern", { key: pattern, - ref: new RegExp(pattern), - code: _`new RegExp(${pattern})`, + ref: new RegExp(pattern, "u"), + code: _`new RegExp(${pattern}, "u")`, }) } diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index bebf4c9fa1..251155309f 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -10,8 +10,7 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordCxt) { const {gen, data, $data, schema, schemaCode} = cxt - // const bdt = bad$DataType(schemaCode, def.schemaType, $data) - const regExp = $data ? _`(new RegExp(${schemaCode}))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch + const regExp = $data ? _`(new RegExp(${schemaCode}, "u"))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch cxt.fail$data(_`!${regExp}.test(${data})`) }, error: { diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index ea0d3362ca..22e75b12f5 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -14,8 +14,6 @@ const remoteRefs = { } const SKIP6 = [ - "optional/ecmascript-regex", // TODO only format needs to be skipped, too much is skipped here - "optional/non-bmp-regex", "format", "optional/format/date", "optional/format/date-time", From d0b2d436136edbdf8a56671275abf888d17b2699 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 11 Sep 2020 19:08:35 +0100 Subject: [PATCH 201/322] refactor: resolveRecursive, rename StoredSchema to SchemaEnv --- lib/ajv.ts | 99 ++++++++++++------------ lib/cache.ts | 8 +- lib/compile/index.ts | 142 +++++++++++++++++------------------ lib/types.ts | 8 +- lib/vocabularies/core/ref.ts | 2 +- 5 files changed, 126 insertions(+), 133 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 1199641bc9..4827dff2a0 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -17,7 +17,7 @@ import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" -import {StoredSchema, compileStoredSchema, resolveSchema} from "./compile" +import {SchemaEnv, SchemaObjectEnv, compileSchemaEnv, resolveSchema} from "./compile" import {ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" @@ -58,12 +58,10 @@ export default class Ajv { _cache: CacheInterface // shared external scope values for compiled functions _scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES}) - _schemas: {[key: string]: StoredSchema} = {} - _refs: {[ref: string]: StoredSchema | string} = {} - _fragments: {[key: string]: StoredSchema} = {} + _schemas: {[key: string]: SchemaEnv} = {} + _refs: {[ref: string]: SchemaEnv | string} = {} formats: {[name: string]: AddedFormat} = {} - _compilations: Set = new Set() - _compileQueue: StoredSchema[] = [] + _compilations: Set = new Set() _loadingSchemas: {[ref: string]: Promise} = {} _metaOpts: InstanceOptions RULES: ValidationRules @@ -112,8 +110,8 @@ export default class Ajv { v = this.getSchema(schemaKeyRef) if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"') } else { - const schemaObj = _addSchema.call(this, schemaKeyRef) - v = schemaObj.validate || compileStoredSchema.call(this, schemaObj) + const sch = _addSchema.call(this, schemaKeyRef) + v = sch.validate || compileSchemaEnv.call(this, sch) } const valid = v(data) @@ -126,8 +124,8 @@ export default class Ajv { schema: Schema, _meta?: boolean // true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. ): ValidateFunction { - const schemaObj = _addSchema.call(this, schema, undefined, _meta) - return schemaObj.validate || compileStoredSchema.call(this, schemaObj) + const sch = _addSchema.call(this, schema, undefined, _meta) + return sch.validate || compileSchemaEnv.call(this, sch) } // Creates validating function for passed schema with asynchronous loading of missing schemas. @@ -149,10 +147,10 @@ export default class Ajv { return runCompileAsync(schema, meta, callback) - function runCompileAsync(sch: SchemaObject, _meta?: boolean, cb?: CompileAsyncCallback) { - const p = loadMetaSchemaOf(sch).then(() => { - const schemaObj = _addSchema.call(self, sch, undefined, _meta) - return schemaObj.validate || _compileAsync(schemaObj) + function runCompileAsync(_schema: SchemaObject, _meta?: boolean, cb?: CompileAsyncCallback) { + const p = loadMetaSchemaOf(_schema).then(() => { + const sch = _addSchema.call(self, _schema, undefined, _meta) + return sch.validate || _compileAsync(sch) }) if (cb) p.then((v) => cb(null, v), cb) return p @@ -165,17 +163,17 @@ export default class Ajv { : Promise.resolve() } - function _compileAsync(schemaObj: StoredSchema): ValidateFunction | Promise { + function _compileAsync(sch: SchemaEnv): ValidateFunction | Promise { try { - return compileStoredSchema.call(self, schemaObj) + return compileSchemaEnv.call(self, sch) } catch (e) { - if (e instanceof MissingRefError) return loadMissingSchema(schemaObj, e) + if (e instanceof MissingRefError) return loadMissingSchema(sch, e) throw e } } async function loadMissingSchema( - schemaObj: StoredSchema, + sch: SchemaEnv, e: MissingRefError ): Promise { const ref = e.missingSchema @@ -188,10 +186,10 @@ export default class Ajv { schPromise.then(removePromise, removePromise) } - const sch = await schPromise - if (!self._refs[ref]) await loadMetaSchemaOf(sch) - if (!self._refs[ref]) self.addSchema(sch, ref, undefined, meta) - return _compileAsync(schemaObj) + const _schema = await schPromise + if (!self._refs[ref]) await loadMetaSchemaOf(_schema) + if (!self._refs[ref]) self.addSchema(_schema, ref, undefined, meta) + return _compileAsync(sch) function removePromise(): void { delete self._loadingSchemas[ref] @@ -257,15 +255,15 @@ export default class Ajv { // Get compiled schema by `key` or `ref`. // (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id) getSchema(keyRef: string): ValidateFunction | undefined { - let schemaObj = _getSchemaObj.call(this, keyRef) - if (schemaObj === undefined) { - const root = {schema: {}, localRoot: {}, refVal: [], refs: {}} - const env = resolveSchema.call(this, root, keyRef) - if (!env) return - schemaObj = this._fragments[keyRef] = new StoredSchema({...env, ref: keyRef, fragment: true}) + let sch + while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch + if (sch === undefined) { + const root = new SchemaObjectEnv({schema: {}}) + sch = resolveSchema.call(this, root, keyRef) + if (!sch) return + this._refs[keyRef] = sch } - if (typeof schemaObj == "string") return this.getSchema(schemaObj) - return schemaObj.validate || compileStoredSchema.call(this, schemaObj) + return sch.validate || compileSchemaEnv.call(this, sch) } // Remove cached schema(s). @@ -274,19 +272,19 @@ export default class Ajv { // Even if schema is referenced by other schemas it still can be removed as other schemas have local references. removeSchema(schemaKeyRef: Schema | string | RegExp): Ajv { if (schemaKeyRef instanceof RegExp) { - _removeAllSchemas.call(this, this._schemas, schemaKeyRef) - _removeAllSchemas.call(this, this._refs, schemaKeyRef) + removeAllSchemas.call(this, this._schemas, schemaKeyRef) + removeAllSchemas.call(this, this._refs, schemaKeyRef) return this } switch (typeof schemaKeyRef) { case "undefined": - _removeAllSchemas.call(this, this._schemas) - _removeAllSchemas.call(this, this._refs) + removeAllSchemas.call(this, this._schemas) + removeAllSchemas.call(this, this._refs) this._cache.clear() return this case "string": { - const schemaObj = _getSchemaObj.call(this, schemaKeyRef) - if (schemaObj) this._cache.del(schemaObj.cacheKey) + const sch = getSchEnv.call(this, schemaKeyRef) + if (sch) this._cache.del(sch.cacheKey) delete this._schemas[schemaKeyRef] delete this._refs[schemaKeyRef] return this @@ -420,36 +418,37 @@ function defaultMeta(this: Ajv): string | SchemaObject | undefined { return this._opts.defaultMeta } -function _getSchemaObj(this: Ajv, keyRef: string): StoredSchema | undefined { - keyRef = normalizeId(keyRef) - return this._schemas[keyRef] || this._refs[keyRef] || this._fragments[keyRef] +function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | undefined { + keyRef = normalizeId(keyRef) // TODO tests fail without this line + return this._schemas[keyRef] || this._refs[keyRef] } -function _removeAllSchemas( +function removeAllSchemas( this: Ajv, - schemas: {[ref: string]: StoredSchema | string}, + schemas: {[ref: string]: SchemaEnv | string}, regex?: RegExp ): void { for (const keyRef in schemas) { - const schemaObj = schemas[keyRef] + const sch = schemas[keyRef] if (!regex || regex.test(keyRef)) { - if (typeof schemaObj == "string") { + if (typeof sch == "string") { delete schemas[keyRef] - } else if (!schemaObj.meta) { - this._cache.del(schemaObj.cacheKey) + } else if (!sch.meta) { + this._cache.del(sch.cacheKey) delete schemas[keyRef] } } } } +// TODO refactor function _addSchema( this: Ajv, schema: Schema, skipValidation?: boolean, meta?: boolean, shouldAddSchema?: boolean -): StoredSchema { +): SchemaEnv { if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema must be object or boolean") } @@ -476,14 +475,14 @@ function _addSchema( const localRefs = getSchemaRefs.call(this, schema) - const schemaObj = new StoredSchema({id, schema, localRefs, cacheKey, meta}) + const sch = new SchemaEnv({id, schema, localRefs, cacheKey, meta}) - if (id[0] !== "#" && shouldAddSchema) this._refs[id] = schemaObj - this._cache.put(cacheKey, schemaObj) + if (id[0] !== "#" && shouldAddSchema) this._refs[id] = sch + this._cache.put(cacheKey, sch) if (willValidate && recursiveMeta) this.validateSchema(schema, true) - return schemaObj + return sch } function addDefaultMetaSchema(this: Ajv): void { diff --git a/lib/cache.ts b/lib/cache.ts index ea9c6b29e2..95a2d09fea 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,18 +1,18 @@ -import {StoredSchema} from "./compile" +import {SchemaEnv} from "./compile" import {CacheInterface} from "./types" export default class Cache implements CacheInterface { - _cache: {[key: string]: StoredSchema} + _cache: {[key: string]: SchemaEnv} constructor() { this._cache = {} } - put(key: string, value: StoredSchema): void { + put(key: string, value: SchemaEnv): void { this._cache[key] = value } - get(key: string): StoredSchema { + get(key: string): SchemaEnv { return this._cache[key] } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 35cbdb72e1..624f281cf8 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -10,38 +10,30 @@ import URI = require("uri-js") export type SchemaRefs = {[ref: string]: Schema | ValidateFunction | undefined} -interface _StoredSchema { +interface _SchemaEnv { schema: Schema - id?: string - ref?: string - cacheKey?: unknown - fragment?: true - meta?: boolean root?: SchemaRoot - localRoot?: {validate?: ValidateFunction} - refs?: SchemaRefs - localRefs?: LocalRefs baseId?: string - validate?: ValidateFunction - validateName?: Name + localRefs?: LocalRefs + id?: string + meta?: boolean + cacheKey?: unknown } -export class StoredSchema implements _StoredSchema { +export class SchemaEnv implements _SchemaEnv { schema: Schema root?: SchemaRoot - localRoot: {validate?: ValidateFunction} = {} - refs: SchemaRefs = {} + baseId?: string + localRefs?: LocalRefs id?: string - ref?: string - cacheKey?: unknown - fragment?: true meta?: boolean - localRefs?: LocalRefs - baseId?: string + cacheKey?: unknown + localRoot: {validate?: ValidateFunction} = {} + refs: SchemaRefs = {} validate?: ValidateFunction validateName?: Name - constructor(obj: _StoredSchema) { + constructor(obj: _SchemaEnv) { this.localRoot = {} this.refs = {} this.schema = obj.schema @@ -54,6 +46,19 @@ export class StoredSchema implements _StoredSchema { } } +interface _SchemaObjectEnv extends _SchemaEnv { + schema: SchemaObject +} + +export class SchemaObjectEnv extends SchemaEnv { + schema: SchemaObject + + constructor(obj: _SchemaObjectEnv) { + super(obj) + this.schema = obj.schema + } +} + export type ResolvedRef = InlineResolvedRef | FuncResolvedRef export interface InlineResolvedRef { @@ -74,21 +79,21 @@ export interface SchemaRoot { refs: SchemaRefs } -export function compileStoredSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { - return (schObj.meta ? compileMetaSchema : compileSchema).call(this, schObj) +export function compileSchemaEnv(this: Ajv, sch: SchemaEnv): ValidateFunction { + return (sch.meta ? compileMetaSchema : compileSchema).call(this, sch) } -function compileMetaSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { +function compileMetaSchema(this: Ajv, sch: SchemaEnv): ValidateFunction { const currentOpts = this._opts this._opts = this._metaOpts try { - return compileSchema.call(this, schObj) + return compileSchema.call(this, sch) } finally { this._opts = currentOpts } } -function validateWrapper(this: Ajv, sch: StoredSchema): ValidateFunction { +function validateWrapper(this: Ajv, sch: SchemaEnv): ValidateFunction { if (!sch.validate) { const wrapper: ValidateFunction = function (this: Ajv | unknown, ...args) { if (wrapper.validate === undefined) throw new Error("ajv implementation error") @@ -109,23 +114,23 @@ function extendWrapper(wrapper: ValidateFunction, v: ValidateFunction): void { } // Compiles schema to validation function -function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { - const {localRefs} = schObj +function compileSchema(this: Ajv, schEnv: SchemaEnv): ValidateFunction { + const {localRefs} = schEnv const self = this const opts = this._opts const refs: SchemaRefs = {} const localRoot: {validate?: ValidateFunction} = {} - if (schObj.root === undefined) { - schObj.root = { - schema: typeof schObj.schema == "boolean" ? {} : schObj.schema, + if (schEnv.root === undefined) { + schEnv.root = { + schema: typeof schEnv.schema == "boolean" ? {} : schEnv.schema, localRoot, refs, } } - const root = schObj.root - return (localRoot.validate = localCompile(schObj)) + const root = schEnv.root + return (localRoot.validate = localCompile(schEnv)) - function localCompile(sch: StoredSchema): ValidateFunction { + function localCompile(sch: SchemaEnv): ValidateFunction { // TODO refactor - remove compilations const _sch = getCompilingSchema.call(self, sch) if (_sch) return validateWrapper.call(self, _sch) @@ -213,26 +218,22 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } } - function resolveRef( - baseId: string, - ref: string, - isRoot: boolean - ): Schema | ValidateFunction | void { + function resolveRef(baseId: string, ref: string): Schema | ValidateFunction | void { ref = resolveUrl(baseId, ref) // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaRoot) - const schOrFunc = refs[ref] || (!isRoot && root.refs?.[ref]) + const schOrFunc = refs[ref] || root.refs[ref] if (schOrFunc) return schOrFunc let _sch = resolve.call(self, root, ref) if (_sch === undefined) { const schema = localRefs?.[ref] - if (schema) _sch = new StoredSchema({schema, root, localRefs, baseId}) + if (schema) _sch = new SchemaEnv({schema, root, localRefs, baseId}) } if (_sch !== undefined) return (refs[ref] = inlineOrCompile(_sch)) } - function inlineOrCompile(sch: StoredSchema): Schema | ValidateFunction { + function inlineOrCompile(sch: SchemaEnv): Schema | ValidateFunction { return inlineRef(sch.schema, self._opts.inlineRefs) ? sch.schema : sch.validate || localCompile(sch) @@ -240,13 +241,13 @@ function compileSchema(this: Ajv, schObj: StoredSchema): ValidateFunction { } // Index of schema compilation in the currently compiled list -function getCompilingSchema(this: Ajv, schObj: StoredSchema): StoredSchema | void { +function getCompilingSchema(this: Ajv, schEnv: SchemaEnv): SchemaEnv | void { for (const sch of this._compilations) { - if (sameSchema(sch, schObj)) return sch + if (sameSchemaEnv(sch, schEnv)) return sch } } -function sameSchema(s1: StoredSchema, s2: StoredSchema): boolean { +function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean { return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId } @@ -256,7 +257,7 @@ function resolve( this: Ajv, root: SchemaRoot, // information about the root schema for the current schema ref: string // reference to resolve -): StoredSchema | undefined { +): SchemaEnv | undefined { let sch while (typeof (sch = this._refs[ref]) == "string") ref = sch return sch || this._schemas[ref] || resolveSchema.call(this, root, ref) @@ -265,38 +266,31 @@ function resolve( // Resolve schema, its root and baseId export function resolveSchema( this: Ajv, - root: SchemaRoot, // root object with properties schema, refVal, refs TODO below StoredSchema is assigned to it + root: SchemaRoot, // root object with properties schema, refs TODO below SchemaEnv is assigned to it ref: string // reference to resolve -): StoredSchema | undefined { +): SchemaEnv | undefined { const p = URI.parse(ref) const refPath = _getFullPath(p) let baseId = getFullPath(root.schema.$id) - if (Object.keys(root.schema).length === 0 || refPath !== baseId) { - const id = normalizeId(refPath) - const schOrRef = this._refs[id] || this._schemas[id] - if (typeof schOrRef == "string") return resolveRecursive.call(this, root, schOrRef, p) - if (typeof schOrRef?.schema !== "object") return - if (!schOrRef.validate) compileSchema.call(this, schOrRef) - if (id === normalizeId(ref)) return new StoredSchema({schema: schOrRef.schema, root, baseId}) - root = schOrRef - baseId = getFullPath(root.schema.$id) + if (Object.keys(root.schema).length > 0 && refPath === baseId) { + return getJsonPointer.call(this, p, new SchemaEnv({schema: root.schema, root, baseId})) } - return getJsonPointer.call(this, p, new StoredSchema({schema: root.schema, root, baseId})) -} -function resolveRecursive( - this: Ajv, - root: SchemaRoot, - ref: string, - parsedRef: URI.URIComponents -): StoredSchema | undefined { - const env = resolveSchema.call(this, root, ref) - if (!env) return - const {schema, baseId} = env - if (typeof schema == "object" && schema.$id) { - env.baseId = resolveUrl(baseId, schema.$id) + const id = normalizeId(refPath) + const schOrRef = this._refs[id] || this._schemas[id] + if (typeof schOrRef == "string") { + const sch = resolveSchema.call(this, root, schOrRef) + if (typeof sch?.schema !== "object") return + if (sch.schema.$id) sch.baseId = resolveUrl(sch.baseId, sch.schema.$id) + return getJsonPointer.call(this, p, sch) } - return getJsonPointer.call(this, parsedRef, env) + + if (typeof schOrRef?.schema !== "object") return + if (!schOrRef.validate) compileSchema.call(this, schOrRef) + if (id === normalizeId(ref)) return new SchemaEnv({schema: schOrRef.schema, root, baseId}) + root = schOrRef + baseId = getFullPath(root.schema.$id) + return getJsonPointer.call(this, p, new SchemaEnv({schema: root.schema, root, baseId})) } const PREVENT_SCOPE_CHANGE = toHash([ @@ -310,8 +304,8 @@ const PREVENT_SCOPE_CHANGE = toHash([ function getJsonPointer( this: Ajv, parsedRef: URI.URIComponents, - {baseId, schema, root}: StoredSchema -): StoredSchema | undefined { + {baseId, schema, root}: SchemaEnv +): SchemaEnv | undefined { if (parsedRef.fragment?.[0] !== "/") return for (const part of parsedRef.fragment.slice(1).split("/")) { if (typeof schema == "boolean") return @@ -322,14 +316,14 @@ function getJsonPointer( baseId = resolveUrl(baseId, schema.$id) } } - let env: StoredSchema | undefined + let env: SchemaEnv | undefined if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) { const $ref = resolveUrl(baseId, schema.$ref) env = resolveSchema.call(this, root, $ref) } // even though resolution failed we need to return SchemaEnv to throw exception // so that compileAsync loads missing schema. - env = env || new StoredSchema({schema, root, baseId}) + env = env || new SchemaEnv({schema, root, baseId}) if (env.schema !== env.root?.schema) return env return undefined } diff --git a/lib/types.ts b/lib/types.ts index 554a81aa93..97e91eed16 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" -import {SchemaRoot, SchemaRefs, StoredSchema} from "./compile" +import {SchemaRoot, SchemaRefs, SchemaEnv} from "./compile" import KeywordCxt from "./compile/context" import Ajv from "./ajv" @@ -88,8 +88,8 @@ export interface Logger { } export interface CacheInterface { - put(key: unknown, value: StoredSchema): void - get(key: unknown): StoredSchema + put(key: unknown, value: SchemaEnv): void + get(key: unknown): SchemaEnv del(key: unknown): void clear(): void } @@ -173,7 +173,7 @@ export interface SchemaCxt { compositeRule?: boolean createErrors?: boolean opts: InstanceOptions - resolveRef: (baseId: string, ref: string, isRoot: boolean) => Schema | ValidateFunction | void + resolveRef: (baseId: string, ref: string) => Schema | ValidateFunction | void self: Ajv } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index a9f4ac44af..5061ff1731 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -28,7 +28,7 @@ const def: CodeKeywordDefinition = { return {code: _`${rootName}.validate`, $async: root.schema.$async === true} } - const schOrFunc = resolveRef(baseId, schema, isRoot) + const schOrFunc = resolveRef(baseId, schema) if (typeof schOrFunc == "function") { const code = gen.scopeValue("validate", {ref: schOrFunc}) return {code, $async: !!schOrFunc.$async} From cca486bc724657809b8aa9101c456fd6c07b4e43 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 11 Sep 2020 21:02:55 +0100 Subject: [PATCH 202/322] refactor: SchemaRoot to SchemaEnv --- lib/ajv.ts | 6 +- lib/compile/context.ts | 2 +- lib/compile/index.ts | 126 +++++++++++------------------------ lib/types.ts | 10 ++- lib/vocabularies/core/ref.ts | 26 ++++++-- 5 files changed, 69 insertions(+), 101 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 4827dff2a0..26a7955e5a 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -17,7 +17,7 @@ import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" -import {SchemaEnv, SchemaObjectEnv, compileSchemaEnv, resolveSchema} from "./compile" +import {SchemaEnv, compileSchemaEnv, resolveSchema} from "./compile" import {ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" @@ -258,7 +258,7 @@ export default class Ajv { let sch while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch if (sch === undefined) { - const root = new SchemaObjectEnv({schema: {}}) + const root = new SchemaEnv({schema: {}}) sch = resolveSchema.call(this, root, keyRef) if (!sch) return this._refs[keyRef] = sch @@ -475,7 +475,7 @@ function _addSchema( const localRefs = getSchemaRefs.call(this, schema) - const sch = new SchemaEnv({id, schema, localRefs, cacheKey, meta}) + const sch = new SchemaEnv({schema, baseId: id, localRefs, cacheKey, meta}) if (id[0] !== "#" && shouldAddSchema) this._refs[id] = sch this._cache.put(cacheKey, sch) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 9db256e4c7..2594918a63 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -19,7 +19,7 @@ import N from "./names" export default class KeywordCxt implements KeywordErrorCxt { gen: CodeGen - allErrors: boolean + allErrors?: boolean keyword: string data: Name $data?: string | false diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 624f281cf8..07940c714c 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,6 +1,6 @@ import type {Schema, SchemaObject, ValidateFunction, SchemaCxt} from "../types" import type Ajv from "../ajv" -import {CodeGen, _, nil, str, Code, Name} from "./codegen" +import {CodeGen, _, nil, str, Name} from "./codegen" import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" @@ -10,75 +10,43 @@ import URI = require("uri-js") export type SchemaRefs = {[ref: string]: Schema | ValidateFunction | undefined} -interface _SchemaEnv { +interface SchemaEnvArgs { schema: Schema - root?: SchemaRoot + root?: SchemaEnv baseId?: string localRefs?: LocalRefs - id?: string meta?: boolean cacheKey?: unknown } -export class SchemaEnv implements _SchemaEnv { +export class SchemaEnv implements SchemaEnvArgs { schema: Schema - root?: SchemaRoot - baseId?: string + root: SchemaEnv + baseId: string localRefs?: LocalRefs - id?: string meta?: boolean cacheKey?: unknown + $async?: boolean localRoot: {validate?: ValidateFunction} = {} refs: SchemaRefs = {} validate?: ValidateFunction validateName?: Name - constructor(obj: _SchemaEnv) { + constructor(env: SchemaEnvArgs) { + let schema: SchemaObject | undefined + if (typeof env.schema == "object") schema = env.schema + this.schema = env.schema + this.root = env.root || this + this.baseId = env.baseId ?? normalizeId(schema?.$id) + this.localRefs = env.localRefs + this.meta = env.meta + this.cacheKey = env.cacheKey + this.$async = schema?.$async this.localRoot = {} this.refs = {} - this.schema = obj.schema - // this.root = obj.root || { - // schema: typeof obj.schema == "boolean" ? {} : obj.schema, - // localRoot, - // refs, - // } - Object.assign(this, obj) } } -interface _SchemaObjectEnv extends _SchemaEnv { - schema: SchemaObject -} - -export class SchemaObjectEnv extends SchemaEnv { - schema: SchemaObject - - constructor(obj: _SchemaObjectEnv) { - super(obj) - this.schema = obj.schema - } -} - -export type ResolvedRef = InlineResolvedRef | FuncResolvedRef - -export interface InlineResolvedRef { - code: Code - schema: Schema - inline: true -} - -interface FuncResolvedRef { - code: Code - $async?: boolean - inline?: false -} - -export interface SchemaRoot { - schema: SchemaObject - localRoot: {validate?: ValidateFunction} - refs: SchemaRefs -} - export function compileSchemaEnv(this: Ajv, sch: SchemaEnv): ValidateFunction { return (sch.meta ? compileMetaSchema : compileSchema).call(this, sch) } @@ -114,33 +82,21 @@ function extendWrapper(wrapper: ValidateFunction, v: ValidateFunction): void { } // Compiles schema to validation function -function compileSchema(this: Ajv, schEnv: SchemaEnv): ValidateFunction { - const {localRefs} = schEnv +function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { const self = this const opts = this._opts - const refs: SchemaRefs = {} - const localRoot: {validate?: ValidateFunction} = {} - if (schEnv.root === undefined) { - schEnv.root = { - schema: typeof schEnv.schema == "boolean" ? {} : schEnv.schema, - localRoot, - refs, - } - } - const root = schEnv.root - return (localRoot.validate = localCompile(schEnv)) + return (env.localRoot.validate = localCompile(env)) function localCompile(sch: SchemaEnv): ValidateFunction { // TODO refactor - remove compilations const _sch = getCompilingSchema.call(self, sch) if (_sch) return validateWrapper.call(self, _sch) const {schema, baseId} = sch - if (sch.root === undefined || sch.root !== root) { - return compileSchema.call(self, sch) - } + if (sch.root !== env.root) return compileSchema.call(self, sch) + const isRoot = sch.schema === sch.root.schema const $async = typeof schema == "object" && schema.$async === true - const rootId = getFullPath(sch.root.schema.$id) + const rootId = getFullPath(sch.root.baseId) const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties}) let _ValidationError @@ -155,7 +111,7 @@ function compileSchema(this: Ajv, schEnv: SchemaEnv): ValidateFunction { const schemaCxt: SchemaCxt = { gen, - allErrors: !!opts.allErrors, + allErrors: opts.allErrors, data: N.data, parentData: N.parentData, parentDataProperty: N.parentDataProperty, @@ -168,7 +124,7 @@ function compileSchema(this: Ajv, schEnv: SchemaEnv): ValidateFunction { ValidationError: _ValidationError, schema, isRoot, - root, + root: env.root, rootId, baseId: baseId || rootId, schemaPath: nil, @@ -194,9 +150,7 @@ function compileSchema(this: Ajv, schEnv: SchemaEnv): ValidateFunction { validate.schema = schema validate.errors = null - sch.refs = validate.refs = refs - sch.localRoot = validate.localRoot = localRoot - sch.root = validate.root = isRoot ? root : sch.root + validate.root = env.root // TODO remove - only used by $comment keyword if ($async) validate.$async = true if (opts.sourceCode === true) { validate.source = { @@ -220,17 +174,17 @@ function compileSchema(this: Ajv, schEnv: SchemaEnv): ValidateFunction { function resolveRef(baseId: string, ref: string): Schema | ValidateFunction | void { ref = resolveUrl(baseId, ref) - // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaRoot) - const schOrFunc = refs[ref] || root.refs[ref] + // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaEnv) + const schOrFunc = env.refs[ref] || env.root.refs[ref] if (schOrFunc) return schOrFunc - let _sch = resolve.call(self, root, ref) + let _sch = resolve.call(self, env.root, ref) if (_sch === undefined) { - const schema = localRefs?.[ref] - if (schema) _sch = new SchemaEnv({schema, root, localRefs, baseId}) + const schema = env.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv + if (schema) _sch = new SchemaEnv({schema, root: env.root, localRefs: env.localRefs, baseId}) } - if (_sch !== undefined) return (refs[ref] = inlineOrCompile(_sch)) + if (_sch !== undefined) return (env.refs[ref] = inlineOrCompile(_sch)) } function inlineOrCompile(sch: SchemaEnv): Schema | ValidateFunction { @@ -255,7 +209,7 @@ function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean { // TODO returns SchemaObject (if the schema can be inlined) or validation function function resolve( this: Ajv, - root: SchemaRoot, // information about the root schema for the current schema + root: SchemaEnv, // information about the root schema for the current schema ref: string // reference to resolve ): SchemaEnv | undefined { let sch @@ -266,14 +220,15 @@ function resolve( // Resolve schema, its root and baseId export function resolveSchema( this: Ajv, - root: SchemaRoot, // root object with properties schema, refs TODO below SchemaEnv is assigned to it + root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it ref: string // reference to resolve ): SchemaEnv | undefined { const p = URI.parse(ref) const refPath = _getFullPath(p) - let baseId = getFullPath(root.schema.$id) + const baseId = getFullPath(root.baseId) + // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests if (Object.keys(root.schema).length > 0 && refPath === baseId) { - return getJsonPointer.call(this, p, new SchemaEnv({schema: root.schema, root, baseId})) + return getJsonPointer.call(this, p, root) } const id = normalizeId(refPath) @@ -281,6 +236,7 @@ export function resolveSchema( if (typeof schOrRef == "string") { const sch = resolveSchema.call(this, root, schOrRef) if (typeof sch?.schema !== "object") return + // TODO review - most of the time sch.baseId == normalizeId(sch.schema.$id) if (sch.schema.$id) sch.baseId = resolveUrl(sch.baseId, sch.schema.$id) return getJsonPointer.call(this, p, sch) } @@ -288,9 +244,7 @@ export function resolveSchema( if (typeof schOrRef?.schema !== "object") return if (!schOrRef.validate) compileSchema.call(this, schOrRef) if (id === normalizeId(ref)) return new SchemaEnv({schema: schOrRef.schema, root, baseId}) - root = schOrRef - baseId = getFullPath(root.schema.$id) - return getJsonPointer.call(this, p, new SchemaEnv({schema: root.schema, root, baseId})) + return getJsonPointer.call(this, p, schOrRef) } const PREVENT_SCOPE_CHANGE = toHash([ @@ -319,11 +273,11 @@ function getJsonPointer( let env: SchemaEnv | undefined if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) { const $ref = resolveUrl(baseId, schema.$ref) - env = resolveSchema.call(this, root, $ref) + env = resolveSchema.call(this, root, $ref) } // even though resolution failed we need to return SchemaEnv to throw exception // so that compileAsync loads missing schema. env = env || new SchemaEnv({schema, root, baseId}) - if (env.schema !== env.root?.schema) return env + if (env.schema !== env.root.schema) return env return undefined } diff --git a/lib/types.ts b/lib/types.ts index 97e91eed16..c74160576f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" -import {SchemaRoot, SchemaRefs, SchemaEnv} from "./compile" +import {SchemaEnv} from "./compile" import KeywordCxt from "./compile/context" import Ajv from "./ajv" @@ -110,9 +110,7 @@ export interface ValidateFunction { ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] - refs?: SchemaRefs - root?: SchemaRoot - localRoot?: {validate?: ValidateFunction} + root?: SchemaEnv $async?: true source?: SourceCode validate?: ValidateFunction // it will be only set on wrappers @@ -150,7 +148,7 @@ export type KeywordCompilationResult = Schema | SchemaValidateFunction | Validat export interface SchemaCxt { gen: CodeGen - allErrors: boolean + allErrors?: boolean data: Name parentData: Name parentDataProperty: Code | number @@ -163,7 +161,7 @@ export interface SchemaCxt { ValidationError?: Name schema: Schema isRoot: boolean - root: SchemaRoot + root: SchemaEnv rootId: string // TODO ? baseId: string schemaPath: Code diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 5061ff1731..f5c9879da0 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,13 +1,26 @@ -import {CodeKeywordDefinition} from "../../types" +import {CodeKeywordDefinition, Schema} from "../../types" import KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" -import {ResolvedRef, InlineResolvedRef} from "../../compile" -// import {resolveUrl} from "../../compile/resolve" import {callValidateCode} from "../util" import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" +// TODO remove these interfaces +type ResolvedRef = InlineResolvedRef | FuncResolvedRef + +interface InlineResolvedRef { + code: Code + schema: Schema + inline: true +} + +interface FuncResolvedRef { + code: Code + $async?: boolean + inline?: false +} + const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", @@ -25,13 +38,16 @@ const def: CodeKeywordDefinition = { if (schema === "#" || schema === "#/") { if (isRoot) return {code: validateName, $async: it.async} const rootName = gen.scopeValue("root", {ref: root.localRoot}) - return {code: _`${rootName}.validate`, $async: root.schema.$async === true} + return { + code: _`${rootName}.validate`, + $async: typeof root.schema == "object" && root.schema.$async === true, + } } const schOrFunc = resolveRef(baseId, schema) if (typeof schOrFunc == "function") { const code = gen.scopeValue("validate", {ref: schOrFunc}) - return {code, $async: !!schOrFunc.$async} + return {code, $async: schOrFunc.$async} } else if (typeof schOrFunc == "boolean" || typeof schOrFunc == "object") { const code = gen.scopeValue("schema", {ref: schOrFunc}) return {code, schema: schOrFunc, inline: true} From 2d7fd9cc8c3fe3bbfaf0ae626a8ade37fdd31020 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 12 Sep 2020 14:14:14 +0100 Subject: [PATCH 203/322] refactor: eslint rules, classes --- .eslintrc.js | 30 +++ .eslintrc.yml | 70 ----- lib/ajv.ts | 244 +++++++++--------- lib/cache.ts | 4 +- lib/compile/codegen/code.ts | 6 +- lib/compile/codegen/index.ts | 66 ++--- lib/compile/codegen/scope.ts | 82 +++--- lib/compile/context.ts | 5 +- lib/compile/errors.ts | 1 - lib/compile/index.ts | 20 +- lib/compile/resolve.ts | 21 +- lib/compile/rules.ts | 10 +- lib/compile/subschema.ts | 12 +- lib/compile/util.ts | 12 +- lib/compile/validate/applicability.ts | 5 +- lib/compile/validate/boolSchema.ts | 2 +- lib/compile/validate/index.ts | 6 +- lib/compile/validate/iterate.ts | 2 +- lib/compile/validate/keyword.ts | 6 +- lib/types.ts | 12 +- .../applicator/additionalItems.ts | 2 +- lib/vocabularies/applicator/dependencies.ts | 4 +- lib/vocabularies/applicator/oneOf.ts | 2 +- .../applicator/patternProperties.ts | 4 +- lib/vocabularies/applicator/properties.ts | 2 +- lib/vocabularies/core/ref.ts | 3 +- lib/vocabularies/format/format.ts | 14 +- lib/vocabularies/util.ts | 4 +- lib/vocabularies/validation/uniqueItems.ts | 2 +- package.json | 10 +- spec/.eslintrc.yml | 27 +- spec/after_test.ts | 4 +- spec/async_schemas.spec.ts | 2 +- spec/async_validate.spec.ts | 8 +- tsconfig.json | 3 +- 35 files changed, 335 insertions(+), 372 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 .eslintrc.yml diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..aa561d9133 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,30 @@ +const jsConfig = require("@ajv-validator/config/.eslintrc_js") +const tsConfig = require("@ajv-validator/config/.eslintrc") + +module.exports = { + env: { + es6: true, + node: true, + }, + overrides: [ + jsConfig, + { + ...tsConfig, + files: ["*.ts"], + rules: { + ...tsConfig.rules, + complexity: ["error", 17], + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-implied-eval": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unnecessary-condition": "warn", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "no-invalid-this": "off", + }, + }, + ], +} diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index d7e6521c56..0000000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,70 +0,0 @@ -env: - es6: true - node: true - browser: true -extends: - - "eslint:recommended" - - prettier -parserOptions: - ecmaVersion: 2018 - sourceType: module -overrides: - - files: ["*.ts"] - extends: - - "eslint:recommended" - - "plugin:@typescript-eslint/recommended" - - "plugin:@typescript-eslint/recommended-requiring-type-checking" - - "prettier/@typescript-eslint" - parser: "@typescript-eslint/parser" - parserOptions: - project: ["./tsconfig.json"] - plugins: ["@typescript-eslint"] - rules: - "@typescript-eslint/restrict-template-expressions": off - "@typescript-eslint/no-unsafe-member-access": off - "@typescript-eslint/no-unsafe-assignment": off - "@typescript-eslint/restrict-plus-operands": off - "@typescript-eslint/no-var-requires": off - "@typescript-eslint/no-empty-function": off - "@typescript-eslint/no-this-alias": off - "@typescript-eslint/no-implied-eval": off - "@typescript-eslint/no-floating-promises": off - "@typescript-eslint/no-explicit-any": off - no-invalid-this: off -rules: - block-scoped-var: error - callback-return: error - complexity: [error, 18] - curly: [error, multi-line, consistent] - dot-location: [error, property] - dot-notation: error - eqeqeq: [error, smart] - id-match: error - linebreak-style: [error, unix] - new-cap: error - no-console: [error, allow: [warn, error]] - no-debugger: error - no-duplicate-imports: error - no-else-return: error - no-eq-null: error - no-eval: error - no-fallthrough: error - no-invalid-this: error - no-new-wrappers: error - no-path-concat: error - no-redeclare: error - no-return-assign: error - no-sequences: error - no-shadow: warn - no-template-curly-in-string: error - no-trailing-spaces: error - no-undef-init: error - no-use-before-define: [error, nofunc] - prefer-arrow-callback: error - prefer-const: error - radix: error - semi: 0 - valid-jsdoc: [error, requireReturn: false] - no-useless-escape: error - no-void: error - no-var: error diff --git a/lib/ajv.ts b/lib/ajv.ts index 26a7955e5a..088f714944 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -11,13 +11,12 @@ import { ErrorObject, Format, AddedFormat, - LoadSchemaFunction, } from "./types" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" -import rules, {ValidationRules, Rule, RuleGroup} from "./compile/rules" +import {getRules, ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" -import {SchemaEnv, compileSchemaEnv, resolveSchema} from "./compile" +import {SchemaEnv, compileSchema, resolveSchema} from "./compile" import {ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" @@ -27,6 +26,8 @@ import formatVocabulary from "./vocabularies/format" import {metadataVocabulary, contentVocabulary} from "./vocabularies/metadata" import stableStringify from "fast-json-stable-stringify" import {eachItem} from "./compile/util" +import $dataRefSchema from "./refs/data.json" +import draft7MetaSchema from "./refs/json-schema-draft-07.json" const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" @@ -55,18 +56,18 @@ const optsDefaults = { export default class Ajv { _opts: InstanceOptions - _cache: CacheInterface // shared external scope values for compiled functions _scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES}) - _schemas: {[key: string]: SchemaEnv} = {} - _refs: {[ref: string]: SchemaEnv | string} = {} - formats: {[name: string]: AddedFormat} = {} + _schemas: {[key: string]: SchemaEnv | undefined} = {} + _refs: {[ref: string]: SchemaEnv | string | undefined} = {} + formats: {[name: string]: AddedFormat | undefined} = {} _compilations: Set = new Set() - _loadingSchemas: {[ref: string]: Promise} = {} - _metaOpts: InstanceOptions RULES: ValidationRules logger: Logger errors?: ErrorObject[] | null // errors from the last validation + private _loadingSchemas: {[ref: string]: Promise | undefined} = {} + private readonly _cache: CacheInterface + private readonly _metaOpts: InstanceOptions static ValidationError = ValidationError static MissingRefError = MissingRefError @@ -78,7 +79,7 @@ export default class Ajv { opts.format = false this._cache = opts.cache || new Cache() - this.RULES = rules() + this.RULES = getRules() checkDeprecatedOptions.call(this, opts) if (opts.serialize === undefined) opts.serialize = stableStringify this._metaOpts = getMetaSchemaOptions.call(this) @@ -110,8 +111,8 @@ export default class Ajv { v = this.getSchema(schemaKeyRef) if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"') } else { - const sch = _addSchema.call(this, schemaKeyRef) - v = sch.validate || compileSchemaEnv.call(this, sch) + const sch = this._addSchema(schemaKeyRef) + v = sch.validate || this._compileSchemaEnv(sch) } const valid = v(data) @@ -124,8 +125,8 @@ export default class Ajv { schema: Schema, _meta?: boolean // true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. ): ValidateFunction { - const sch = _addSchema.call(this, schema, undefined, _meta) - return sch.validate || compileSchemaEnv.call(this, sch) + const sch = this._addSchema(schema, undefined, _meta) + return sch.validate || this._compileSchemaEnv(sch) } // Creates validating function for passed schema with asynchronous loading of missing schemas. @@ -135,21 +136,24 @@ export default class Ajv { metaOrCallback?: boolean | CompileAsyncCallback, // optional true to compile meta-schema; this parameter can be skipped callback?: CompileAsyncCallback ): Promise { - /* eslint no-shadow: 0 */ const self = this if (typeof this._opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") } - const loadSchema: LoadSchemaFunction = this._opts.loadSchema + const {loadSchema} = this._opts let meta: boolean | undefined if (typeof metaOrCallback == "function") callback = metaOrCallback else meta = metaOrCallback return runCompileAsync(schema, meta, callback) - function runCompileAsync(_schema: SchemaObject, _meta?: boolean, cb?: CompileAsyncCallback) { + function runCompileAsync( + _schema: SchemaObject, + _meta?: boolean, + cb?: CompileAsyncCallback + ): Promise { const p = loadMetaSchemaOf(_schema).then(() => { - const sch = _addSchema.call(self, _schema, undefined, _meta) + const sch = self._addSchema(_schema, undefined, _meta) return sch.validate || _compileAsync(sch) }) if (cb) p.then((v) => cb(null, v), cb) @@ -157,7 +161,7 @@ export default class Ajv { } function loadMetaSchemaOf(sch: SchemaObject): Promise { - const $schema = sch.$schema + const {$schema} = sch return $schema && !self.getSchema($schema) ? runCompileAsync({$ref: $schema}, true) : Promise.resolve() @@ -165,7 +169,7 @@ export default class Ajv { function _compileAsync(sch: SchemaEnv): ValidateFunction | Promise { try { - return compileSchemaEnv.call(self, sch) + return self._compileSchemaEnv(sch) } catch (e) { if (e instanceof MissingRefError) return loadMissingSchema(sch, e) throw e @@ -215,7 +219,7 @@ export default class Ajv { } key = normalizeId(key || id) checkUnique.call(this, key) - this._schemas[key] = _addSchema.call(this, schema, _skipValidation, _meta, true) + this._schemas[key] = this._addSchema(schema, _skipValidation, _meta, true) return this } @@ -233,7 +237,8 @@ export default class Ajv { // Validate schema against its meta-schema validateSchema(schema: Schema, throwOrLogError?: boolean): boolean | Promise { if (typeof schema == "boolean") return true - let $schema: string | SchemaObject | undefined = schema.$schema + let $schema: string | SchemaObject | undefined + $schema = schema.$schema if ($schema !== undefined && typeof $schema != "string") { throw new Error("$schema must be a string") } @@ -263,7 +268,7 @@ export default class Ajv { if (!sch) return this._refs[keyRef] = sch } - return sch.validate || compileSchemaEnv.call(this, sch) + return sch.validate || this._compileSchemaEnv(sch) } // Remove cached schema(s). @@ -272,25 +277,25 @@ export default class Ajv { // Even if schema is referenced by other schemas it still can be removed as other schemas have local references. removeSchema(schemaKeyRef: Schema | string | RegExp): Ajv { if (schemaKeyRef instanceof RegExp) { - removeAllSchemas.call(this, this._schemas, schemaKeyRef) - removeAllSchemas.call(this, this._refs, schemaKeyRef) + this._removeAllSchemas(this._schemas, schemaKeyRef) + this._removeAllSchemas(this._refs, schemaKeyRef) return this } switch (typeof schemaKeyRef) { case "undefined": - removeAllSchemas.call(this, this._schemas) - removeAllSchemas.call(this, this._refs) + this._removeAllSchemas(this._schemas) + this._removeAllSchemas(this._refs) this._cache.clear() return this case "string": { const sch = getSchEnv.call(this, schemaKeyRef) - if (sch) this._cache.del(sch.cacheKey) + if (typeof sch == "object") this._cache.del(sch.cacheKey) delete this._schemas[schemaKeyRef] delete this._refs[schemaKeyRef] return this } case "object": { - const serialize = this._opts.serialize + const {serialize} = this._opts const cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef this._cache.del(cacheKey) let id = schemaKeyRef.$id @@ -346,7 +351,7 @@ export default class Ajv { // Remove keyword removeKeyword(keyword: string): Ajv { // TODO return type should be Ajv - const RULES: ValidationRules = this.RULES + const {RULES} = this delete RULES.keywords[keyword] delete RULES.all[keyword] for (const group of RULES.rules) { @@ -379,19 +384,93 @@ export default class Ajv { metaSchema = JSON.parse(JSON.stringify(metaSchema)) const segments = jsonPointer.split("/").slice(1) // first segment is an empty string let keywords = metaSchema - for (const seg of segments) keywords = keywords[seg] + for (const seg of segments) keywords = keywords[seg] as SchemaObject for (const key in rules) { const rule = rules[key] if (typeof rule != "object") continue - const $data = rule.definition?.$data - const schema = keywords[key] + const {$data} = rule.definition + const schema = keywords[key] as SchemaObject | undefined if ($data && schema) keywords[key] = schemaOrData(schema) } } return metaSchema } + + private _removeAllSchemas( + schemas: {[ref: string]: SchemaEnv | string | undefined}, + regex?: RegExp + ): void { + for (const keyRef in schemas) { + const sch = schemas[keyRef] + if (!regex || regex.test(keyRef)) { + if (typeof sch == "string") { + delete schemas[keyRef] + } else if (sch && !sch.meta) { + this._cache.del(sch.cacheKey) + delete schemas[keyRef] + } + } + } + } + + // TODO refactor + private _addSchema( + schema: Schema, + skipValidation?: boolean, + meta?: boolean, + shouldAddSchema?: boolean + ): SchemaEnv { + if (typeof schema != "object" && typeof schema != "boolean") { + throw new Error("schema must be object or boolean") + } + const {serialize} = this._opts + const cacheKey = serialize ? serialize(schema) : schema + const cached = this._cache.get(cacheKey) + if (cached) return cached + + shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false + + let $id, $schema + if (typeof schema == "object") { + $id = schema.$id + $schema = schema.$schema + } + const id = normalizeId($id) + if (id && shouldAddSchema) checkUnique.call(this, id) + + const willValidate = this._opts.validateSchema !== false && !skipValidation + let recursiveMeta + if (willValidate && !(recursiveMeta = id && id === normalizeId($schema))) { + this.validateSchema(schema, true) + } + + const localRefs = getSchemaRefs.call(this, schema) + + const sch = new SchemaEnv({schema, baseId: id, localRefs, cacheKey, meta}) + + if (id[0] !== "#" && shouldAddSchema) this._refs[id] = sch + this._cache.put(cacheKey, sch) + + if (willValidate && recursiveMeta) this.validateSchema(schema, true) + + return sch + } + + private _compileSchemaEnv(sch: SchemaEnv): ValidateFunction { + return sch.meta ? this._compileMetaSchema(sch) : compileSchema.call(this, sch) + } + + private _compileMetaSchema(sch: SchemaEnv): ValidateFunction { + const currentOpts = this._opts + this._opts = this._metaOpts + try { + return compileSchema.call(this, sch) + } finally { + this._opts = currentOpts + } + } } export interface ErrorsTextOptions { @@ -408,7 +487,7 @@ function checkDeprecatedOptions(this: Ajv, opts: Options): void { } function defaultMeta(this: Ajv): string | SchemaObject | undefined { - const meta = this._opts.meta + const {meta} = this._opts this._opts.defaultMeta = typeof meta == "object" ? meta.$id || meta @@ -418,84 +497,18 @@ function defaultMeta(this: Ajv): string | SchemaObject | undefined { return this._opts.defaultMeta } -function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | undefined { +function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | string | undefined { keyRef = normalizeId(keyRef) // TODO tests fail without this line return this._schemas[keyRef] || this._refs[keyRef] } -function removeAllSchemas( - this: Ajv, - schemas: {[ref: string]: SchemaEnv | string}, - regex?: RegExp -): void { - for (const keyRef in schemas) { - const sch = schemas[keyRef] - if (!regex || regex.test(keyRef)) { - if (typeof sch == "string") { - delete schemas[keyRef] - } else if (!sch.meta) { - this._cache.del(sch.cacheKey) - delete schemas[keyRef] - } - } - } -} - -// TODO refactor -function _addSchema( - this: Ajv, - schema: Schema, - skipValidation?: boolean, - meta?: boolean, - shouldAddSchema?: boolean -): SchemaEnv { - if (typeof schema != "object" && typeof schema != "boolean") { - throw new Error("schema must be object or boolean") - } - const serialize = this._opts.serialize - const cacheKey = serialize ? serialize(schema) : schema - const cached = this._cache.get(cacheKey) - if (cached) return cached - - shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false - - let $id, $schema - if (typeof schema == "object") { - $id = schema.$id - $schema = schema.$schema - } - const id = normalizeId($id) - if (id && shouldAddSchema) checkUnique.call(this, id) - - const willValidate = this._opts.validateSchema !== false && !skipValidation - let recursiveMeta - if (willValidate && !(recursiveMeta = id && id === normalizeId($schema))) { - this.validateSchema(schema, true) - } - - const localRefs = getSchemaRefs.call(this, schema) - - const sch = new SchemaEnv({schema, baseId: id, localRefs, cacheKey, meta}) - - if (id[0] !== "#" && shouldAddSchema) this._refs[id] = sch - this._cache.put(cacheKey, sch) - - if (willValidate && recursiveMeta) this.validateSchema(schema, true) - - return sch -} - function addDefaultMetaSchema(this: Ajv): void { - let $dataSchema - if (this._opts.$data) { - $dataSchema = require("./refs/data.json") - this.addMetaSchema($dataSchema, $dataSchema.$id, true) - } - if (this._opts.meta === false) return - let metaSchema = require("./refs/json-schema-draft-07.json") - if (this._opts.$data) { - metaSchema = this.$dataMetaSchema(metaSchema, META_SUPPORT_DATA) - } + const {$data, meta} = this._opts + if ($data) this.addMetaSchema($dataRefSchema, $dataRefSchema.$id, true) + if (meta === false) return + const metaSchema = $data + ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) + : draft7MetaSchema this.addMetaSchema(metaSchema, META_SCHEMA_ID, true) this._refs["http://json-schema.org/schema"] = META_SCHEMA_ID } @@ -541,20 +554,17 @@ function getMetaSchemaOptions(this: Ajv): InstanceOptions { const noLogs = {log() {}, warn() {}, error() {}} -function getLogger(logger?: Logger | false): Logger { +function getLogger(logger?: Partial | false): Logger { if (logger === false) return noLogs if (logger === undefined) return console - if (!(typeof logger == "object" && logger.log && logger.warn && logger.error)) { - throw new Error("logger must implement log, warn and error methods") - } - return logger + if (logger.log && logger.warn && logger.error) return logger as Logger + throw new Error("logger must implement log, warn and error methods") } const KEYWORD_NAME = /^[a-z_$][a-z0-9_$-]*$/i function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefinition): void { - /* eslint no-shadow: 0 */ - const RULES: ValidationRules = this.RULES + const {RULES} = this eachItem(keyword, (kwd) => { if (RULES.keywords[kwd]) throw new Error(`Keyword ${kwd} is already defined`) if (!KEYWORD_NAME.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`) @@ -572,7 +582,7 @@ function _addRule( dataType?: string, definition?: KeywordDefinition ): void { - const RULES: ValidationRules = this.RULES + const {RULES} = this let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) if (!ruleGroup) { ruleGroup = {type: dataType, rules: []} @@ -589,7 +599,7 @@ function _addRule( } function _addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: string): void { - const i = ruleGroup.rules.findIndex((rule) => rule.keyword === before) + const i = ruleGroup.rules.findIndex((_rule) => _rule.keyword === before) if (i >= 0) { ruleGroup.rules.splice(i, 0, rule) } else { @@ -599,7 +609,7 @@ function _addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: str } function keywordMetaschema(this: Ajv, def: KeywordDefinition): void { - let metaSchema = def.metaSchema + let {metaSchema} = def if (metaSchema === undefined) return if (def.$data && this._opts.$data) metaSchema = schemaOrData(metaSchema) def.validateSchema = this.compile(metaSchema, true) diff --git a/lib/cache.ts b/lib/cache.ts index 95a2d09fea..8783c9b6fd 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -2,7 +2,7 @@ import {SchemaEnv} from "./compile" import {CacheInterface} from "./types" export default class Cache implements CacheInterface { - _cache: {[key: string]: SchemaEnv} + private _cache: {[key: string]: SchemaEnv | undefined} constructor() { this._cache = {} @@ -12,7 +12,7 @@ export default class Cache implements CacheInterface { this._cache[key] = value } - get(key: string): SchemaEnv { + get(key: string): SchemaEnv | undefined { return this._cache[key] } diff --git a/lib/compile/codegen/code.ts b/lib/compile/codegen/code.ts index 16aeb14112..746e874b08 100644 --- a/lib/compile/codegen/code.ts +++ b/lib/compile/codegen/code.ts @@ -1,5 +1,5 @@ export class _Code { - _str: string + private _str: string constructor(s: string) { this._str = s @@ -42,7 +42,7 @@ export type SafeExpr = Code | number | boolean | null export const nil = new _Code("") -type TemplateArg = SafeExpr | string +type TemplateArg = SafeExpr | string | undefined export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { // TODO benchmark if loop is faster than reduce @@ -51,7 +51,7 @@ export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { // res += interpolate(args[i]) + strs[i + 1] // } // return new _Code(res) - return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s)) + return new _Code(strs.reduce((res, s, i) => `${res}${interpolate(args[i - 1])}${s}`)) } export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code { diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index 2aa0f2cba9..a40d69aa0b 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -39,20 +39,20 @@ export interface CodeGenOptions { } export class CodeGen { - _scope: Scope - _extScope: ValueScope - _values: ScopeValueSets = {} - _out = "" - _blocks: BlockKind[] = [] - _blockStarts: number[] = [] - _n = "" - opts: CodeGenOptions + private readonly _scope: Scope + private readonly _extScope: ValueScope + private readonly _values: ScopeValueSets = {} + private readonly _blocks: BlockKind[] = [] + private readonly _blockStarts: number[] = [] + private readonly opts: CodeGenOptions + private readonly _n: string + private _out = "" constructor(extScope: ValueScope, opts: CodeGenOptions = {}) { this.opts = opts this._extScope = extScope this._scope = new Scope({parent: extScope}) - if (opts.lines) this._n = "\n" + this._n = opts.lines ? "\n" : "" } toString(): string { @@ -82,16 +82,24 @@ export class CodeGen { return this._extScope.scopeRefs(scopeName, this._values) } + private _def(varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { + const name = this._scope.toName(nameOrPrefix) + if (this.opts.es5) varKind = varKinds.var + if (rhs === undefined) this._out += `${varKind} ${name};` + this._n + else this._out += `${varKind} ${name} = ${rhs};` + this._n + return name + } + const(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - return _def.call(this, varKinds.const, nameOrPrefix, rhs) + return this._def(varKinds.const, nameOrPrefix, rhs) } let(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - return _def.call(this, varKinds.let, nameOrPrefix, rhs) + return this._def(varKinds.let, nameOrPrefix, rhs) } var(nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - return _def.call(this, varKinds.var, nameOrPrefix, rhs) + return this._def(varKinds.var, nameOrPrefix, rhs) } assign(name: Code, rhs: SafeExpr): CodeGen { @@ -101,7 +109,7 @@ export class CodeGen { code(c: Block | SafeExpr): CodeGen { if (typeof c == "function") c() - else this._out += c + ";" + this._n + else this._out += `${c};${this._n}` return this } @@ -162,7 +170,7 @@ export class CodeGen { ): CodeGen { const i = this._scope.toName(nameOrPrefix) if (this.opts.es5) varKind = varKinds.var - return _loop.call(this, _`for(${varKind} ${i}=${from}; ${i}<${to}; ${i}++){`, i, forBody) + return this._loop(_`for(${varKind} ${i}=${from}; ${i}<${to}; ${i}++){`, i, forBody) } forOf( @@ -179,7 +187,7 @@ export class CodeGen { forBody(name) }) } - return _loop.call(this, _`for(${varKind} ${name} of ${iterable}){`, name, forBody) + return this._loop(_`for(${varKind} ${name} of ${iterable}){`, name, forBody) } forIn( @@ -192,7 +200,15 @@ export class CodeGen { return this.forOf(nameOrPrefix, new _Code(`Object.keys(${obj})`), forBody) } const name = this._scope.toName(nameOrPrefix) - return _loop.call(this, _`for(${varKind} ${name} in ${obj}){`, name, forBody) + return this._loop(_`for(${varKind} ${name} in ${obj}){`, name, forBody) + } + + private _loop(forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { + this._blocks.push(BlockKind.For) + this._out += `${forCode}${this._n}` + forBody(name) + this.endFor() + return this } endFor(): CodeGen { @@ -204,7 +220,7 @@ export class CodeGen { } label(label?: Code): CodeGen { - this._out += label + ":" + this._n + this._out += `${label}:${this._n}` return this } @@ -291,22 +307,6 @@ export class CodeGen { } } -function _def(this: CodeGen, varKind: Name, nameOrPrefix: Name | string, rhs?: SafeExpr): Name { - const name = this._scope.toName(nameOrPrefix) - if (this.opts.es5) varKind = varKinds.var - if (rhs === undefined) this._out += `${varKind} ${name};` + this._n - else this._out += `${varKind} ${name} = ${rhs};` + this._n - return name -} - -function _loop(this: CodeGen, forCode: _Code, name: Name, forBody: (n: Name) => void): CodeGen { - this._blocks.push(BlockKind.For) - this._out += forCode + this._n - forBody(name) - this.endFor() - return this -} - const andCode = mappend(operators.AND) export function and(...args: Code[]): Code { diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts index b28c1fc00a..3353c9477f 100644 --- a/lib/compile/codegen/scope.ts +++ b/lib/compile/codegen/scope.ts @@ -30,16 +30,20 @@ interface ValueScopeOptions extends ScopeOptions { scope: ScopeStore } -export type ScopeStore = Record +export type ScopeStore = Record -type ScopeValues = {[prefix: string]: Map} +interface ScopeValues { + [prefix: string]: Map | undefined +} -export type ScopeValueSets = {[prefix: string]: Set} +export interface ScopeValueSets { + [prefix: string]: Set | undefined +} export class Scope { - _names: {[prefix: string]: NameGroup} = {} - _prefixes?: Set - _parent?: Scope + protected _names: {[prefix: string]: NameGroup | undefined} = {} + protected _prefixes?: Set + protected _parent?: Scope constructor({prefixes, parent}: ScopeOptions = {}) { this._prefixes = prefixes @@ -51,7 +55,19 @@ export class Scope { } name(prefix: string): Name { - return new Name(newName.call(this, prefix)) + return new Name(this._newName(prefix)) + } + + protected _newName(prefix: string): string { + const ng = this._names[prefix] || this._nameGroup(prefix) + return `${prefix}${ng.index++}` + } + + private _nameGroup(prefix: string): NameGroup { + if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes.has(prefix))) { + throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`) + } + return (this._names[prefix] = {prefix, index: 0}) } } @@ -70,33 +86,33 @@ export class ValueScopeName extends Name { this.prefix = prefix } - setValue(value: NameValue, {property, itemIndex}: ScopePath) { + setValue(value: NameValue, {property, itemIndex}: ScopePath): void { this.value = value this.scopePath = _`.${new Name(property)}[${itemIndex}]` } } export class ValueScope extends Scope { - _values: ScopeValues = {} - _scope: ScopeStore + protected _values: ScopeValues = {} + protected _scope: ScopeStore constructor(opts: ValueScopeOptions) { super(opts) this._scope = opts.scope } - get() { + get(): ScopeStore { return this._scope } name(prefix: string): ValueScopeName { - return new ValueScopeName(prefix, newName.call(this, prefix)) + return new ValueScopeName(prefix, this._newName(prefix)) } value(nameOrPrefix: ValueScopeName | string, value: NameValue): ValueScopeName { if (value.ref === undefined) throw new Error("CodeGen: ref must be passed in value") const name = this.toName(nameOrPrefix) as ValueScopeName - const prefix = name.prefix + const {prefix} = name const valueKey = value.key ?? value.ref let vs = this._values[prefix] if (vs) { @@ -120,43 +136,31 @@ export class ValueScope extends Scope { return vs.get(keyOrRef) } - scopeRefs(scopeName: Name, values?: ScopeValues | ScopeValueSets): Code { - return reduceValues.call(this, values, (name: ValueScopeName) => { + scopeRefs(scopeName: Name, values: ScopeValues | ScopeValueSets = this._values): Code { + return this._reduceValues(values, (name: ValueScopeName) => { if (name.scopePath === undefined) throw new Error(`CodeGen: name "${name}" has no value`) return _`${scopeName}${name.scopePath}` }) } - scopeCode(values?: ScopeValues | ScopeValueSets): Code { - return reduceValues.call(this, values, (name: ValueScopeName) => { + scopeCode(values: ScopeValues | ScopeValueSets = this._values): Code { + return this._reduceValues(values, (name: ValueScopeName) => { const c = name.value?.code if (c) return c throw new ValueError(name) }) } -} -function newName(this: Scope | ValueScope, prefix: string): string { - let ng = this._names[prefix] - if (!ng) { - if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes?.has(prefix))) { - throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`) + private _reduceValues( + values: ScopeValues | ScopeValueSets, + valueCode: (n: ValueScopeName) => Code + ): Code { + let code: Code = nil + for (const prefix in values) { + values[prefix]?.forEach((name: ValueScopeName) => { + code = _`${code}const ${name} = ${valueCode(name)};` + }) } - ng = this._names[prefix] = {prefix, index: 0} - } - return prefix + ng.index++ -} - -function reduceValues( - this: ValueScope, - values: ScopeValues | ScopeValueSets = this._values, - valueCode: (n: ValueScopeName) => Code -): Code { - let code: Code = nil - for (const prefix in values) { - values[prefix].forEach((name: ValueScopeName) => { - code = _`${code}const ${name} = ${valueCode(name)};` - }) + return code } - return code } diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 2594918a63..cb355862e1 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -64,7 +64,8 @@ export default class KeywordCxt implements KeywordErrorCxt { result(condition: Code, successAction?: () => void, failAction?: () => void): void { this.gen.ifNot(condition) - failAction ? failAction() : this.error() + if (failAction) failAction() + else this.error() if (successAction) { this.gen.else() successAction() @@ -119,7 +120,7 @@ export default class KeywordCxt implements KeywordErrorCxt { else this.params = obj } - block$data(valid: Name = nil, codeBlock: () => void, $dataValid: Code = nil): void { + block$data(valid: Name, codeBlock: () => void, $dataValid: Code = nil): void { this.gen.block(() => { this.check$data(valid, $dataValid) codeBlock() diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index bb72ab5411..b1957d6a11 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -92,7 +92,6 @@ function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): C it: {createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, } = cxt if (createErrors === false) return _`{}` - if (!error) throw new Error('keyword definition must have "error" property') const {params, message} = error const msg = typeof message == "string" ? message : message(cxt) const par = params ? params(cxt) : _`{}` diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 07940c714c..de3b607f01 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -8,7 +8,9 @@ import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" import URI = require("uri-js") -export type SchemaRefs = {[ref: string]: Schema | ValidateFunction | undefined} +export interface SchemaRefs { + [ref: string]: Schema | ValidateFunction | undefined +} interface SchemaEnvArgs { schema: Schema @@ -47,20 +49,6 @@ export class SchemaEnv implements SchemaEnvArgs { } } -export function compileSchemaEnv(this: Ajv, sch: SchemaEnv): ValidateFunction { - return (sch.meta ? compileMetaSchema : compileSchema).call(this, sch) -} - -function compileMetaSchema(this: Ajv, sch: SchemaEnv): ValidateFunction { - const currentOpts = this._opts - this._opts = this._metaOpts - try { - return compileSchema.call(this, sch) - } finally { - this._opts = currentOpts - } -} - function validateWrapper(this: Ajv, sch: SchemaEnv): ValidateFunction { if (!sch.validate) { const wrapper: ValidateFunction = function (this: Ajv | unknown, ...args) { @@ -82,7 +70,7 @@ function extendWrapper(wrapper: ValidateFunction, v: ValidateFunction): void { } // Compiles schema to validation function -function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { +export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { const self = this const opts = this._opts return (env.localRoot.validate = localCompile(env)) diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 141935bf3b..f517d580c9 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -7,7 +7,7 @@ import URI = require("uri-js") // the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution export interface LocalRefs { - [ref: string]: SchemaObject + [ref: string]: SchemaObject | undefined } // TODO refactor to use keyword definitions @@ -75,7 +75,7 @@ export function normalizeId(id: string | undefined): string { return id ? id.replace(TRAILING_SLASH_HASH, "") : "" } -export function resolveUrl(baseId = "", id: string): string { +export function resolveUrl(baseId: string, id: string): string { id = normalizeId(id) return URI.resolve(baseId, id) } @@ -86,7 +86,6 @@ export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { const baseIds: {[jsonPtr: string]: string} = {"": schemaId} const pathPrefix = getFullPath(schemaId, false) const localRefs: LocalRefs = {} - const self = this traverse(schema, {allKeys: true}, (sch, jsonPtr, _, parentJsonPtr) => { if (parentJsonPtr === undefined) return @@ -95,16 +94,16 @@ export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { let baseId = baseIds[parentJsonPtr] if (typeof id == "string") { id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id) - let schOrRef = self._refs[id] - if (typeof schOrRef == "string") schOrRef = self._refs[schOrRef] - if (typeof schOrRef == "object" && schOrRef.schema) { + let schOrRef = this._refs[id] + if (typeof schOrRef == "string") schOrRef = this._refs[schOrRef] + if (typeof schOrRef == "object") { checkAmbiguosId(sch, schOrRef.schema, id) } else if (id !== normalizeId(fullPath)) { if (id[0] === "#") { - if (localRefs[id]) checkAmbiguosId(sch, localRefs[id], id) + checkAmbiguosId(sch, localRefs[id], id) localRefs[id] = sch } else { - self._refs[id] = fullPath + this._refs[id] = fullPath } } } @@ -113,7 +112,9 @@ export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { return localRefs - function checkAmbiguosId(sch1: Schema, sch2: Schema, id: string) { - if (!equal(sch1, sch2)) throw new Error(`id "${id}" resolves to more than one schema`) + function checkAmbiguosId(sch1: Schema, sch2: Schema | undefined, id: string): void { + if (sch2 !== undefined && !equal(sch1, sch2)) { + throw new Error(`id "${id}" resolves to more than one schema`) + } } } diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index bc7d449c69..cbadc81e62 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -1,12 +1,14 @@ import {toHash} from "./util" import {KeywordDefinition} from "../types" -type ValidationTypes = {[key: string]: boolean | RuleGroup} +interface ValidationTypes { + [key: string]: boolean | RuleGroup | undefined +} export interface ValidationRules { rules: RuleGroup[] - all: {[key: string]: boolean | Rule} // rules that have to be validated - keywords: {[key: string]: boolean} // all known keywords (superset of "all") + all: {[key: string]: boolean | Rule | undefined} // rules that have to be validated + keywords: {[key: string]: boolean | undefined} // all known keywords (superset of "all") types: ValidationTypes } @@ -23,7 +25,7 @@ export interface Rule { const ALL = ["type", "$comment"] -export default function rules(): ValidationRules { +export function getRules(): ValidationRules { const groups = { number: {type: "number", rules: []}, string: {type: "string", rules: []}, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index b08101d323..b8f7b4391f 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -72,7 +72,7 @@ function getSubschema( : { schema: sch[schemaProp], schemaPath: _`${it.schemaPath}${getProperty(keyword)}${getProperty(schemaProp)}`, - errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment("" + schemaProp)}`, + errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment(schemaProp)}`, } } @@ -95,7 +95,7 @@ function extendSubschemaData( subschema: SubschemaContext, it: SchemaObjCxt, {dataProp, dataPropType: dpType, data, propertyName}: SubschemaApplication -) { +): void { if (data !== undefined && dataProp !== undefined) { throw new Error('both "data" and "dataProp" passed, only one allowed') } @@ -118,7 +118,7 @@ function extendSubschemaData( // TODO something is possibly wrong here with not changing parentDataProperty and not appending dataPathArr } - function dataContextProps(_nextData: Name) { + function dataContextProps(_nextData: Name): void { subschema.data = _nextData subschema.dataLevel = it.dataLevel + 1 subschema.parentData = it.data @@ -129,7 +129,7 @@ function extendSubschemaData( function extendSubschemaMode( subschema: SubschemaContext, {compositeRule, createErrors, allErrors}: SubschemaApplication -) { +): void { if (compositeRule !== undefined) subschema.compositeRule = compositeRule if (createErrors !== undefined) subschema.createErrors = createErrors if (allErrors !== undefined) subschema.allErrors = allErrors @@ -151,7 +151,5 @@ function getErrorPath( ? _`"/" + ${dataProp}` : _`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")` // TODO maybe use global escapePointer } - return jsPropertySyntax - ? getProperty(dataProp).toString() - : "/" + (typeof dataProp == "number" ? dataProp : escapeJsonPointer(dataProp)) + return jsPropertySyntax ? getProperty(dataProp).toString() : "/" + escapeJsonPointer(dataProp) } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 643a2f961a..86126343c8 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -67,13 +67,16 @@ export function checkDataTypes( } // TODO refactor to use Set -export function toHash(arr: string[]): {[key: string]: true} { +export function toHash(arr: string[]): {[key: string]: true | undefined} { const hash: {[key: string]: true} = {} for (const item of arr) hash[item] = true return hash } -export function schemaHasRules(schema: Schema, rules: {[key: string]: boolean | Rule}): boolean { +export function schemaHasRules( + schema: Schema, + rules: {[key: string]: boolean | Rule | undefined} +): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (rules[key]) return true return false @@ -137,11 +140,12 @@ export function unescapeFragment(str: string): string { return unescapeJsonPointer(decodeURIComponent(str)) } -export function escapeFragment(str: string): string { +export function escapeFragment(str: string | number): string { return encodeURIComponent(escapeJsonPointer(str)) } -export function escapeJsonPointer(str: string): string { +export function escapeJsonPointer(str: string | number): string { + if (typeof str == "number") return `${str}` return str.replace(/~/g, "~0").replace(/\//g, "~1") } diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 13e91497d1..dd715f28ec 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,7 +1,10 @@ import {SchemaObjCxt, SchemaObject} from "../../types" import {RuleGroup, Rule} from "../rules" -export function schemaHasRulesForType({schema, self}: SchemaObjCxt, ty: string): boolean { +export function schemaHasRulesForType( + {schema, self}: SchemaObjCxt, + ty: string +): boolean | undefined { const group = self.RULES.types[ty] return group && group !== true && shouldUseGroup(schema, group) } diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 939b76cc19..ead473ba63 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -29,7 +29,7 @@ export function boolOrEmptySchema(it: SchemaCxt, valid: Name): void { } } -function falseSchemaError(it: SchemaCxt, overrideAllErrors?: boolean) { +function falseSchemaError(it: SchemaCxt, overrideAllErrors?: boolean): void { const {gen, data} = it // TODO maybe some other interface should be used for non-keyword validation errors... const cxt: KeywordErrorCxt = { diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index cbe2f6fb0a..559d075390 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -20,7 +20,7 @@ export function validateFunctionCode(it: SchemaCxt): void { validateFunction(it, () => topBoolOrEmptySchema(it)) } -function validateFunction({gen, validateName, schema, async, opts}: SchemaCxt, body: Block) { +function validateFunction({gen, validateName, schema, async, opts}: SchemaCxt, body: Block): void { gen.return(() => gen.func( validateName, @@ -77,7 +77,7 @@ function subSchemaObjCode(it: SchemaObjCxt, valid: Name): void { gen.var(valid, _`${errsCount} === ${N.errors}`) } -function checkKeywords(it: SchemaObjCxt) { +function checkKeywords(it: SchemaObjCxt): void { checkUnknownRules(it) checkRefsAndKeywords(it) } @@ -131,7 +131,7 @@ function commentKeyword({gen, validateName, schema, errSchemaPath, opts}: Schema } } -function returnResults({gen, async, validateName, ValidationError}: SchemaCxt) { +function returnResults({gen, async, validateName, ValidationError}: SchemaCxt): void { if (async) { gen.if( _`${N.errors} === 0`, diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 7e5e53b13c..3a82e98d3b 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -45,7 +45,7 @@ export function schemaKeywords( } } -function iterateKeywords(it: SchemaObjCxt, group: RuleGroup) { +function iterateKeywords(it: SchemaObjCxt, group: RuleGroup): void { const { gen, schema, diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 58edf48194..b76e690c0f 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -61,7 +61,7 @@ function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void { cxt.block$data(valid, validateKeyword) cxt.ok(def.valid ?? valid) - function validateKeyword() { + function validateKeyword(): void { if (def.errors === false) { assignValid() if (def.modifying) modifyData(cxt) @@ -107,7 +107,7 @@ function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void { } } -function modifyData(cxt: KeywordCxt) { +function modifyData(cxt: KeywordCxt): void { const {gen, data, it} = cxt gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`)) } @@ -126,7 +126,7 @@ function addErrs(cxt: KeywordCxt, errs: Code): void { ) } -function checkAsync(it: SchemaObjCxt, def: FuncKeywordDefinition) { +function checkAsync(it: SchemaObjCxt, def: FuncKeywordDefinition): void { if (def.async && !it.async) throw new Error("async keyword in sync schema") } diff --git a/lib/types.ts b/lib/types.ts index c74160576f..939f150cf4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -12,7 +12,7 @@ export interface SchemaObject { export type Schema = SchemaObject | boolean export interface SchemaMap { - [key: string]: Schema + [key: string]: Schema | undefined } export type LoadSchemaFunction = ( @@ -89,7 +89,7 @@ export interface Logger { export interface CacheInterface { put(key: unknown, value: SchemaEnv): void - get(key: unknown): SchemaEnv + get(key: unknown): SchemaEnv | undefined del(key: unknown): void clear(): void } @@ -248,7 +248,9 @@ export interface KeywordErrorCxt { it: SchemaCxt } -export type KeywordCxtParams = {[x: string]: Code | string | number} +export interface KeywordCxtParams { + [x: string]: Code | string | number | undefined +} export type FormatMode = "fast" | "full" @@ -261,14 +263,14 @@ export type FormatCompare = (data1: T, data2: T) => boolean export type AsyncFormatValidator = (data: T) => Promise export interface FormatDefinition { - type: T extends string ? "string" : "number" + type?: T extends string ? "string" : "number" validate: FormatValidator | (T extends string ? string | RegExp : never) async?: false | undefined compare?: FormatCompare } export interface AsyncFormatDefinition { - type: T extends string ? "string" : "number" + type?: T extends string ? "string" : "number" validate: AsyncFormatValidator async: true compare?: FormatCompare diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index bc49d68478..72428c629c 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -12,7 +12,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCxt) { const {gen, schema, parentSchema, data, it} = cxt const len = gen.const("len", _`${data}.length`) - const items = parentSchema.items + const {items} = parentSchema if (!Array.isArray(items)) { checkStrictMode(it, '"additionalItems" without "items" is ignored') return diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index f7c13a01be..4b1f1a67d3 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition, SchemaMap} from "../../types" +import {CodeKeywordDefinition, SchemaMap, Schema} from "../../types" import KeywordCxt from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" @@ -61,7 +61,7 @@ const def: CodeKeywordDefinition = { function validateSchemaDeps(schemaDeps: SchemaMap): void { for (const prop in schemaDeps) { - if (alwaysValidSchema(it, schemaDeps[prop])) continue + if (alwaysValidSchema(it, schemaDeps[prop] as Schema)) continue gen.if( propertyInData(data, prop, it.opts.ownProperties), () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index e0d397d097..ed4289d982 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -26,7 +26,7 @@ const def: CodeKeywordDefinition = { () => cxt.error(true) ) - function validateOneOf() { + function validateOneOf(): void { schArr.forEach((sch: Schema, i: number) => { if (alwaysValidSchema(it, sch)) { gen.var(schValid, true) diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 51fe8e2e22..cdef59873e 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -17,7 +17,7 @@ const def: CodeKeywordDefinition = { const valid = gen.name("valid") validatePatternProperties() - function validatePatternProperties() { + function validatePatternProperties(): void { for (const pat of patterns) { if (checkProperties) checkMatchingProperties(pat) if (it.allErrors) { @@ -41,7 +41,7 @@ const def: CodeKeywordDefinition = { } } - function validateProperties(pat: string) { + function validateProperties(pat: string): void { gen.forIn("key", data, (key) => { gen.if(_`${usePattern(gen, pat)}.test(${key})`, () => { applySubschema( diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 88d5e50226..cf0cb89ce7 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -33,7 +33,7 @@ const def: CodeKeywordDefinition = { return it.opts.useDefaults && !it.compositeRule && schema[prop].default !== undefined } - function applyPropertySchema(prop: string) { + function applyPropertySchema(prop: string): void { applySubschema( it, { diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index f5c9879da0..52d9fb5a9d 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -34,7 +34,7 @@ const def: CodeKeywordDefinition = { else if (ref.$async || it.async) validateAsyncRef(ref.code) else validateRef(ref.code) - function getRef(): ResolvedRef | void { + function getRef(): ResolvedRef | undefined { if (schema === "#" || schema === "#/") { if (isRoot) return {code: validateName, $async: it.async} const rootName = gen.scopeValue("root", {ref: root.localRoot}) @@ -52,6 +52,7 @@ const def: CodeKeywordDefinition = { const code = gen.scopeValue("schema", {ref: schOrFunc}) return {code, schema: schOrFunc, inline: true} } + return undefined } function missingRef(): void { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 8d43c412c2..f2229dc95d 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -16,10 +16,10 @@ const def: CodeKeywordDefinition = { if ($data) validate$DataFormat() else validateFormat() - function validate$DataFormat() { + function validate$DataFormat(): void { const fmts = gen.scopeValue("formats", { ref: self.formats, - code: opts.code?.formats, + code: opts.code.formats, }) const fDef = gen.const("fDef", _`${fmts}[${schemaCode}]`) const fType = gen.let("fType") @@ -46,12 +46,12 @@ const def: CodeKeywordDefinition = { ? _`${fDef}.async ? await ${format}(${data}) : ${format}(${data})` : _`${format}(${data})` const validData = _`typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` - return _`(${format} && ${fType} === ${ruleType} && !(${validData}))` + return _`(${format} && ${fType} === ${ruleType as string} && !(${validData}))` } } - function validateFormat() { - const formatDef: AddedFormat = self.formats[schema] + function validateFormat(): void { + const formatDef: AddedFormat | undefined = self.formats[schema] if (!formatDef) { unknownFormat() return @@ -59,7 +59,7 @@ const def: CodeKeywordDefinition = { const [fmtType, format, fmtRef] = getFormat(formatDef) if (fmtType === ruleType) cxt.pass(validCondition()) - function unknownFormat() { + function unknownFormat(): void { if (opts.unknownFormats === "ignore") { self.logger.warn(unknownMsg()) return @@ -68,7 +68,7 @@ const def: CodeKeywordDefinition = { throw new Error(unknownMsg()) function unknownMsg(): string { - return `unknown format "${schema}" ignored in schema at path "${errSchemaPath}"` + return `unknown format "${schema as string}" ignored in schema at path "${errSchemaPath}"` } } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index fab2f15822..819d63d1a5 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -39,7 +39,9 @@ export function allSchemaProperties(schemaMap?: SchemaMap): string[] { } export function schemaProperties(it: SchemaCxt, schemaMap: SchemaMap): string[] { - return allSchemaProperties(schemaMap).filter((p) => !alwaysValidSchema(it, schemaMap[p])) + return allSchemaProperties(schemaMap).filter( + (p) => !alwaysValidSchema(it, schemaMap[p] as Schema) + ) } function isOwnProperty(data: Name, property: Name | string): Code { diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 1b1644851a..80e129f8db 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -18,7 +18,7 @@ const def: CodeKeywordDefinition = { cxt.block$data(valid, validateUniqueItems, _`${schemaCode} === false`) cxt.ok(valid) - function validateUniqueItems() { + function validateUniqueItems(): void { const i = gen.let("i", _`${data}.length`) const j = gen.let("j") cxt.setParams({i, j}) diff --git a/package.json b/package.json index 8fc4695357..7ebdb6d550 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,14 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint lib/{**/,}*.ts spec/{**/,}*.*s scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint 'lib/**/*.ts' 'spec/**/*.*s' scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", - "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register spec/{**/,}*.spec.ts -R dot", + "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register 'spec/**/*.spec.ts' -R dot", "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", "bundle": "rm -rf bundle && node ./scripts/bundle.js", - "build": "rm -rf dist && tsc && cp -r lib/refs dist/refs", + "build": "rm -rf dist && tsc && cp -r lib/refs dist", "json-tests": "rm -rf spec/_json/*.js && node scripts/jsontests", "test-karma": "karma start", "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && karma start", @@ -67,7 +67,7 @@ "uri-js": "^4.2.2" }, "devDependencies": { - "@ajv-validator/config": "^0.1.1", + "@ajv-validator/config": "^0.2.0", "@types/chai": "^4.2.12", "@types/mocha": "^8.0.3", "@types/node": "^14.0.27", @@ -78,7 +78,7 @@ "chai": "^4.0.1", "coveralls": "^3.0.1", "cross-env": "^7.0.2", - "eslint": "^7.3.1", + "eslint": "^7.8.1", "eslint-config-prettier": "^6.11.0", "gh-pages-generator": "^0.2.3", "glob": "^7.0.0", diff --git a/spec/.eslintrc.yml b/spec/.eslintrc.yml index c479fb62d6..c31cffef6a 100644 --- a/spec/.eslintrc.yml +++ b/spec/.eslintrc.yml @@ -1,10 +1,8 @@ -parserOptions: - sourceType: script +env: + browser: true, rules: - no-console: 0 - no-empty: [2, allowEmptyCatch: true] - quotes: 0 - no-invalid-this: 0 + no-console: off + no-invalid-this: off globals: describe: false it: false @@ -13,22 +11,15 @@ globals: afterEach: false overrides: - files: ["*.ts"] - extends: - - "eslint:recommended" - - "plugin:@typescript-eslint/recommended" - - "plugin:@typescript-eslint/recommended-requiring-type-checking" - - "prettier/@typescript-eslint" - parser: "@typescript-eslint/parser" parserOptions: project: ["./spec/tsconfig.json"] - plugins: ["@typescript-eslint"] rules: - "@typescript-eslint/no-unsafe-member-access": off + "@typescript-eslint/explicit-function-return-type": off + "@typescript-eslint/explicit-member-accessibility": off + "@typescript-eslint/restrict-plus-operands": off + "@typescript-eslint/no-explicit-any": off "@typescript-eslint/no-unsafe-assignment": off "@typescript-eslint/no-unsafe-call": off + "@typescript-eslint/no-unsafe-member-access": off "@typescript-eslint/no-unsafe-return": off - "@typescript-eslint/restrict-plus-operands": off "@typescript-eslint/no-var-requires": off - "@typescript-eslint/no-empty-function": off - "@typescript-eslint/no-explicit-any": off - no-invalid-this: off diff --git a/spec/after_test.ts b/spec/after_test.ts index 24175e3914..eeecf40dee 100644 --- a/spec/after_test.ts +++ b/spec/after_test.ts @@ -16,8 +16,8 @@ export function afterEach(res): void { should.equal(res.errors, null) } else { res.errors.should.be.an("array") - for (let i = 0; i < res.errors.length; i++) { - res.errors[i].should.be.an("object") + for (const err of res.errors) { + err.should.be.an("object") } } } diff --git a/spec/async_schemas.spec.ts b/spec/async_schemas.spec.ts index fea359999a..aff2a2f0ad 100644 --- a/spec/async_schemas.spec.ts +++ b/spec/async_schemas.spec.ts @@ -77,7 +77,7 @@ function checkIdExists(schema, data) { } function checkIdExistsWithError(schema, data) { - const table = schema.table + const {table} = schema switch (table) { case "users": return check(table, [1, 5, 8]) diff --git a/spec/async_validate.spec.ts b/spec/async_validate.spec.ts index f7c494587b..8762f35ef3 100644 --- a/spec/async_validate.spec.ts +++ b/spec/async_validate.spec.ts @@ -161,7 +161,7 @@ describe("async schemas, formats and keywords", function () { } function checkIdExistsWithError(schema, data) { - const table = schema.table + const {table} = schema switch (table) { case "users": return check(table, [1, 5, 8]) @@ -357,11 +357,7 @@ describe("async schemas, formats and keywords", function () { return repeat(() => { return Promise.all( instances.map((_ajv) => { - if (refSchema) { - try { - _ajv.addSchema(refSchema) - } catch (e) {} - } + if (refSchema) _ajv.addSchema(refSchema) const validate = _ajv.compile(schema) let data diff --git a/tsconfig.json b/tsconfig.json index 868a90fef5..56e8b8a2a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "lib": ["ES2018", "DOM"], "types": ["node"], "allowJs": true, - "target": "ES2018" + "target": "ES2018", + "resolveJsonModule": true } } From 58bc64dee9503378f6e78b279aed45843f6ba871 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 12 Sep 2020 17:42:30 +0100 Subject: [PATCH 204/322] refactor: $ref keyword --- lib/ajv.ts | 4 +- lib/compile/index.ts | 3 +- lib/types.ts | 2 +- lib/vocabularies/core/ref.ts | 91 ++++++++++++++---------------------- spec/resolve.spec.ts | 2 +- 5 files changed, 43 insertions(+), 59 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 088f714944..31eda98605 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -304,9 +304,11 @@ export default class Ajv { delete this._schemas[id] delete this._refs[id] } + return this } + default: + throw new Error("ajv.removeSchema: invalid parameter") } - return this } // add "vocabulary" - a collection of keywords diff --git a/lib/compile/index.ts b/lib/compile/index.ts index de3b607f01..6c133ad592 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -160,7 +160,7 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { } } - function resolveRef(baseId: string, ref: string): Schema | ValidateFunction | void { + function resolveRef(baseId: string, ref: string): Schema | ValidateFunction | undefined { ref = resolveUrl(baseId, ref) // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaEnv) const schOrFunc = env.refs[ref] || env.root.refs[ref] @@ -173,6 +173,7 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { } if (_sch !== undefined) return (env.refs[ref] = inlineOrCompile(_sch)) + return } function inlineOrCompile(sch: SchemaEnv): Schema | ValidateFunction { diff --git a/lib/types.ts b/lib/types.ts index 939f150cf4..21114b299c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -171,7 +171,7 @@ export interface SchemaCxt { compositeRule?: boolean createErrors?: boolean opts: InstanceOptions - resolveRef: (baseId: string, ref: string) => Schema | ValidateFunction | void + resolveRef: (baseId: string, ref: string) => Schema | ValidateFunction | undefined self: Ajv } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 52d9fb5a9d..b260d1cbb8 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition, Schema} from "../../types" +import {CodeKeywordDefinition, Schema, ValidateFunction} from "../../types" import KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" @@ -6,53 +6,49 @@ import {callValidateCode} from "../util" import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" -// TODO remove these interfaces -type ResolvedRef = InlineResolvedRef | FuncResolvedRef - -interface InlineResolvedRef { - code: Code - schema: Schema - inline: true -} - -interface FuncResolvedRef { - code: Code - $async?: boolean - inline?: false -} - const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", code(cxt: KeywordCxt) { const {gen, schema, it} = cxt const {resolveRef, allErrors, baseId, isRoot, root, opts, validateName, self} = it - const ref = getRef() const passCxt = opts.passContext ? N.this : nil - if (ref === undefined) missingRef() - else if (ref.inline) applyRefSchema(ref) - else if (ref.$async || it.async) validateAsyncRef(ref.code) - else validateRef(ref.code) + if (schema === "#" || schema === "#/") return callRootRef() + const schOrFunc = resolveRef(baseId, schema) + if (schOrFunc === undefined) return missingRef() + if (typeof schOrFunc == "function") return callCompiledRef(schOrFunc) + return inlineRefSchema(schOrFunc) - function getRef(): ResolvedRef | undefined { - if (schema === "#" || schema === "#/") { - if (isRoot) return {code: validateName, $async: it.async} - const rootName = gen.scopeValue("root", {ref: root.localRoot}) - return { - code: _`${rootName}.validate`, - $async: typeof root.schema == "object" && root.schema.$async === true, - } - } + function callRootRef(): void { + if (isRoot) return callRef(validateName, it.async) + const rootName = gen.scopeValue("root", {ref: root.localRoot}) + return callRef(_`${rootName}.validate`, root.$async) + } - const schOrFunc = resolveRef(baseId, schema) - if (typeof schOrFunc == "function") { - const code = gen.scopeValue("validate", {ref: schOrFunc}) - return {code, $async: schOrFunc.$async} - } else if (typeof schOrFunc == "boolean" || typeof schOrFunc == "object") { - const code = gen.scopeValue("schema", {ref: schOrFunc}) - return {code, schema: schOrFunc, inline: true} - } - return undefined + function callCompiledRef(func: ValidateFunction): void { + const v = gen.scopeValue("validate", {ref: func}) + return callRef(v, func.$async) + } + + function callRef(v: Code, $async?: boolean): void { + if ($async || it.async) validateAsyncRef(v) + else validateSyncRef(v) + } + + function inlineRefSchema(sch: Schema): void { + const schName = gen.scopeValue("schema", {ref: sch}) + const valid = gen.name("valid") + applySubschema( + it, + { + schema: sch, + schemaPath: nil, + topSchemaRef: schName, + errSchemaPath: schema, + }, + valid + ) + cxt.ok(valid) } function missingRef(): void { @@ -70,21 +66,6 @@ const def: CodeKeywordDefinition = { } } - function applyRefSchema(inlineRef: InlineResolvedRef): void { - const valid = gen.name("valid") - applySubschema( - it, - { - schema: inlineRef.schema, - schemaPath: nil, - topSchemaRef: inlineRef.code, - errSchemaPath: schema, - }, - valid - ) - cxt.ok(valid) - } - function validateAsyncRef(v: Code): void { if (!it.async) throw new Error("async schema referenced by sync schema") const valid = gen.let("valid") @@ -102,7 +83,7 @@ const def: CodeKeywordDefinition = { cxt.ok(valid) } - function validateRef(v: Code): void { + function validateSyncRef(v: Code): void { cxt.pass(callValidateCode(cxt, v, passCxt), () => addErrorsFrom(v)) } diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index 8e3d5a4555..8c93b8e63b 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -321,7 +321,7 @@ describe("resolve", () => { } function testInlined(validate: ValidateFunction, expectedInlined) { - const inlined: any = !/scope\.validate/.test(validate.source?.code || "") + const inlined: any = !validate.source?.code.includes("scope.validate") inlined.should.equal(expectedInlined) } }) From 4bbb97f0f3c4608f49da45e300746d1785e0546a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 12 Sep 2020 20:04:28 +0100 Subject: [PATCH 205/322] refactor: remove validate wrapper function (closure) to improve performance for cyclic schema refs --- lib/ajv.ts | 10 ++++-- lib/compile/index.ts | 68 ++++++++++++++---------------------- lib/types.ts | 5 +-- lib/vocabularies/core/ref.ts | 29 +++++++++------ 4 files changed, 55 insertions(+), 57 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 31eda98605..507e1e181f 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -35,6 +35,7 @@ const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] const EXT_SCOPE_NAMES = new Set([ "validate", + "wrapper", "root", "schema", "keyword", @@ -461,14 +462,17 @@ export default class Ajv { } private _compileSchemaEnv(sch: SchemaEnv): ValidateFunction { - return sch.meta ? this._compileMetaSchema(sch) : compileSchema.call(this, sch) + if (sch.meta) this._compileMetaSchema(sch) + else compileSchema.call(this, sch) + if (!sch.validate) throw new Error("ajv implementation error") + return sch.validate } - private _compileMetaSchema(sch: SchemaEnv): ValidateFunction { + private _compileMetaSchema(sch: SchemaEnv): void { const currentOpts = this._opts this._opts = this._metaOpts try { - return compileSchema.call(this, sch) + compileSchema.call(this, sch) } finally { this._opts = currentOpts } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 6c133ad592..7f2ac8fda5 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -29,7 +29,7 @@ export class SchemaEnv implements SchemaEnvArgs { meta?: boolean cacheKey?: unknown $async?: boolean - localRoot: {validate?: ValidateFunction} = {} + localRoot: {validate?: ValidateFunction} refs: SchemaRefs = {} validate?: ValidateFunction validateName?: Name @@ -49,46 +49,29 @@ export class SchemaEnv implements SchemaEnvArgs { } } -function validateWrapper(this: Ajv, sch: SchemaEnv): ValidateFunction { - if (!sch.validate) { - const wrapper: ValidateFunction = function (this: Ajv | unknown, ...args) { - if (wrapper.validate === undefined) throw new Error("ajv implementation error") - const v = wrapper.validate - const valid = v.apply(this, args) - wrapper.errors = v.errors - return valid - } - sch.validate = wrapper - sch.validateName = this._scope.value("validate", {ref: wrapper}) - } - return sch.validate -} - -function extendWrapper(wrapper: ValidateFunction, v: ValidateFunction): void { - wrapper.validate = v - Object.assign(wrapper, v) -} - -// Compiles schema to validation function -export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { +// Compiles schema in SchemaEnv +export function compileSchema(this: Ajv, env: SchemaEnv): void { const self = this const opts = this._opts - return (env.localRoot.validate = localCompile(env)) + localCompile(env) + if (env.validate) env.localRoot.validate = env.validate - function localCompile(sch: SchemaEnv): ValidateFunction { + function localCompile(sch: SchemaEnv): SchemaEnv { // TODO refactor - remove compilations const _sch = getCompilingSchema.call(self, sch) - if (_sch) return validateWrapper.call(self, _sch) + if (_sch) return _sch const {schema, baseId} = sch - if (sch.root !== env.root) return compileSchema.call(self, sch) + if (sch.root !== env.root) { + compileSchema.call(self, sch) + return sch + } const isRoot = sch.schema === sch.root.schema - const $async = typeof schema == "object" && schema.$async === true const rootId = getFullPath(sch.root.baseId) const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties}) let _ValidationError - if ($async) { + if (sch.$async) { _ValidationError = gen.scopeValue("Error", { ref: ValidationError, code: _`require("ajv/dist/compile/error_classes").ValidationError`, @@ -96,6 +79,7 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { } const validateName = gen.scopeName("validate") + sch.validateName = validateName const schemaCxt: SchemaCxt = { gen, @@ -107,12 +91,12 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { dataPathArr: [nil], // TODO can it's lenght be used as dataLevel if nil is removed? dataLevel: 0, topSchemaRef: gen.scopeValue("schema", {ref: schema}), - async: $async, + async: sch.$async, validateName, ValidationError: _ValidationError, schema, isRoot, - root: env.root, + root: sch.root, rootId, baseId: baseId || rootId, schemaPath: nil, @@ -138,18 +122,17 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { validate.schema = schema validate.errors = null - validate.root = env.root // TODO remove - only used by $comment keyword - if ($async) validate.$async = true + validate.root = sch.root // TODO remove - only used by $comment keyword + validate.env = sch + if (sch.$async) validate.$async = true if (opts.sourceCode === true) { validate.source = { code: sourceCode, scope: self._scope, } } - if (sch.validate) extendWrapper(sch.validate, validate) sch.validate = validate - sch.validateName = validateName - return validate + return sch } catch (e) { delete sch.validate delete sch.validateName @@ -160,9 +143,11 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { } } - function resolveRef(baseId: string, ref: string): Schema | ValidateFunction | undefined { + function resolveRef( + baseId: string, + ref: string + ): Schema | ValidateFunction | SchemaEnv | undefined { ref = resolveUrl(baseId, ref) - // TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaEnv) const schOrFunc = env.refs[ref] || env.root.refs[ref] if (schOrFunc) return schOrFunc @@ -176,10 +161,9 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction { return } - function inlineOrCompile(sch: SchemaEnv): Schema | ValidateFunction { - return inlineRef(sch.schema, self._opts.inlineRefs) - ? sch.schema - : sch.validate || localCompile(sch) + function inlineOrCompile(sch: SchemaEnv): Schema | SchemaEnv { + if (inlineRef(sch.schema, self._opts.inlineRefs)) return sch.schema + return sch.validate ? sch : localCompile(sch) } } diff --git a/lib/types.ts b/lib/types.ts index 21114b299c..cc07e972fe 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -110,6 +110,7 @@ export interface ValidateFunction { ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] + env?: SchemaEnv root?: SchemaEnv $async?: true source?: SourceCode @@ -156,7 +157,7 @@ export interface SchemaCxt { dataPathArr: (Code | number)[] dataLevel: number topSchemaRef: Code - async: boolean + async?: boolean validateName: Name ValidationError?: Name schema: Schema @@ -171,7 +172,7 @@ export interface SchemaCxt { compositeRule?: boolean createErrors?: boolean opts: InstanceOptions - resolveRef: (baseId: string, ref: string) => Schema | ValidateFunction | undefined + resolveRef: (baseId: string, ref: string) => Schema | SchemaEnv | undefined self: Ajv } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index b260d1cbb8..7a39aa1b36 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,10 +1,11 @@ -import {CodeKeywordDefinition, Schema, ValidateFunction} from "../../types" +import {CodeKeywordDefinition, Schema} from "../../types" import KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {callValidateCode} from "../util" import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" +import {SchemaEnv} from "../../compile" const def: CodeKeywordDefinition = { keyword: "$ref", @@ -16,23 +17,31 @@ const def: CodeKeywordDefinition = { if (schema === "#" || schema === "#/") return callRootRef() const schOrFunc = resolveRef(baseId, schema) if (schOrFunc === undefined) return missingRef() - if (typeof schOrFunc == "function") return callCompiledRef(schOrFunc) + if (schOrFunc instanceof SchemaEnv) return callValidate(schOrFunc) return inlineRefSchema(schOrFunc) function callRootRef(): void { if (isRoot) return callRef(validateName, it.async) + // TODO use the same name as compiled function, so it can be dropped in shared scope const rootName = gen.scopeValue("root", {ref: root.localRoot}) - return callRef(_`${rootName}.validate`, root.$async) + return callRef(_`${rootName}.validate`, root.$async || it.async) } - function callCompiledRef(func: ValidateFunction): void { - const v = gen.scopeValue("validate", {ref: func}) - return callRef(v, func.$async) + function callValidate(sch: SchemaEnv): void { + let v: Code + if (sch.validate) { + v = gen.scopeValue("validate", {ref: sch.validate}) + } else { + const code = _`{validate: ${sch.validateName}}` + const wrapper = gen.scopeValue("wrapper", {ref: sch, code}) + v = _`${wrapper}.validate` + } + callRef(v, sch.$async) } function callRef(v: Code, $async?: boolean): void { - if ($async || it.async) validateAsyncRef(v) - else validateSyncRef(v) + if ($async) callAsyncRef(v) + else callSyncRef(v) } function inlineRefSchema(sch: Schema): void { @@ -66,7 +75,7 @@ const def: CodeKeywordDefinition = { } } - function validateAsyncRef(v: Code): void { + function callAsyncRef(v: Code): void { if (!it.async) throw new Error("async schema referenced by sync schema") const valid = gen.let("valid") gen.try( @@ -83,7 +92,7 @@ const def: CodeKeywordDefinition = { cxt.ok(valid) } - function validateSyncRef(v: Code): void { + function callSyncRef(v: Code): void { cxt.pass(callValidateCode(cxt, v, passCxt), () => addErrorsFrom(v)) } From 0e4c2d88e2db08aa208aaf952292172ebd9d1a9d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 13 Sep 2020 09:02:37 +0100 Subject: [PATCH 206/322] refactor: remove localCompile, remove resolveRef from SchemaCxt compilation context --- lib/compile/index.ts | 202 +++++++++++++++++------------------ lib/types.ts | 1 - lib/vocabularies/core/ref.ts | 6 +- 3 files changed, 100 insertions(+), 109 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 7f2ac8fda5..1daacd082d 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -50,121 +50,113 @@ export class SchemaEnv implements SchemaEnvArgs { } // Compiles schema in SchemaEnv -export function compileSchema(this: Ajv, env: SchemaEnv): void { - const self = this +export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { + // TODO refactor - remove compilations + const _sch = getCompilingSchema.call(this, sch) + if (_sch) return _sch const opts = this._opts - localCompile(env) - if (env.validate) env.localRoot.validate = env.validate - - function localCompile(sch: SchemaEnv): SchemaEnv { - // TODO refactor - remove compilations - const _sch = getCompilingSchema.call(self, sch) - if (_sch) return _sch - const {schema, baseId} = sch - if (sch.root !== env.root) { - compileSchema.call(self, sch) - return sch - } - - const isRoot = sch.schema === sch.root.schema - const rootId = getFullPath(sch.root.baseId) - - const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties}) - let _ValidationError - if (sch.$async) { - _ValidationError = gen.scopeValue("Error", { - ref: ValidationError, - code: _`require("ajv/dist/compile/error_classes").ValidationError`, - }) - } - - const validateName = gen.scopeName("validate") - sch.validateName = validateName + const {schema, baseId} = sch + + const isRoot = sch.schema === sch.root.schema + const rootId = getFullPath(sch.root.baseId) // TODO remove getFullPath, 1 tests fails + + const gen = new CodeGen(this._scope, {...opts.codegen, forInOwn: opts.ownProperties}) + let _ValidationError + if (sch.$async) { + _ValidationError = gen.scopeValue("Error", { + ref: ValidationError, + code: _`require("ajv/dist/compile/error_classes").ValidationError`, + }) + } - const schemaCxt: SchemaCxt = { - gen, - allErrors: opts.allErrors, - data: N.data, - parentData: N.parentData, - parentDataProperty: N.parentDataProperty, - dataNames: [N.data], - dataPathArr: [nil], // TODO can it's lenght be used as dataLevel if nil is removed? - dataLevel: 0, - topSchemaRef: gen.scopeValue("schema", {ref: schema}), - async: sch.$async, - validateName, - ValidationError: _ValidationError, - schema, - isRoot, - root: sch.root, - rootId, - baseId: baseId || rootId, - schemaPath: nil, - errSchemaPath: "#", - errorPath: str``, - opts, - resolveRef, // TODO move to gen.scopeValue? - self, - } + const validateName = gen.scopeName("validate") + sch.validateName = validateName + + const schemaCxt: SchemaCxt = { + gen, + allErrors: opts.allErrors, + data: N.data, + parentData: N.parentData, + parentDataProperty: N.parentDataProperty, + dataNames: [N.data], + dataPathArr: [nil], // TODO can it's lenght be used as dataLevel if nil is removed? + dataLevel: 0, + topSchemaRef: gen.scopeValue("schema", {ref: schema}), + async: sch.$async, + validateName, + ValidationError: _ValidationError, + schema, + isRoot, + root: sch.root, + rootId, + baseId: baseId || rootId, + schemaPath: nil, + errSchemaPath: "#", + errorPath: str``, + opts, + self: this, + } - let sourceCode - try { - self._compilations.add(sch) - validateFunctionCode(schemaCxt) - sourceCode = `${gen.scopeRefs(N.scope)} + let sourceCode + try { + this._compilations.add(sch) + validateFunctionCode(schemaCxt) + sourceCode = `${gen.scopeRefs(N.scope)} ${gen.toString()}` - if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) - // console.log("\n\n\n *** \n", sourceCode) - const makeValidate = new Function(N.self.toString(), N.scope.toString(), sourceCode) - const validate: ValidateFunction = makeValidate(self, self._scope.get()) - - gen.scopeValue(validateName, {ref: validate}) - - validate.schema = schema - validate.errors = null - validate.root = sch.root // TODO remove - only used by $comment keyword - validate.env = sch - if (sch.$async) validate.$async = true - if (opts.sourceCode === true) { - validate.source = { - code: sourceCode, - scope: self._scope, - } + if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) + // console.log("\n\n\n *** \n", sourceCode) + const makeValidate = new Function(N.self.toString(), N.scope.toString(), sourceCode) + const validate: ValidateFunction = makeValidate(this, this._scope.get()) + + gen.scopeValue(validateName, {ref: validate}) + + validate.schema = schema + validate.errors = null + validate.root = sch.root // TODO remove - only used by $comment keyword + validate.env = sch + if (sch.$async) validate.$async = true + if (opts.sourceCode === true) { + validate.source = { + code: sourceCode, + scope: this._scope, } - sch.validate = validate - return sch - } catch (e) { - delete sch.validate - delete sch.validateName - if (sourceCode) self.logger.error("Error compiling schema, function code:", sourceCode) - throw e - } finally { - self._compilations.delete(sch) } + sch.validate = validate + if (isRoot) sch.root.localRoot.validate = validate + return sch + } catch (e) { + delete sch.validate + delete sch.validateName + if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode) + throw e + } finally { + this._compilations.delete(sch) } +} - function resolveRef( - baseId: string, - ref: string - ): Schema | ValidateFunction | SchemaEnv | undefined { - ref = resolveUrl(baseId, ref) - const schOrFunc = env.refs[ref] || env.root.refs[ref] - if (schOrFunc) return schOrFunc - - let _sch = resolve.call(self, env.root, ref) - if (_sch === undefined) { - const schema = env.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv - if (schema) _sch = new SchemaEnv({schema, root: env.root, localRefs: env.localRefs, baseId}) - } - - if (_sch !== undefined) return (env.refs[ref] = inlineOrCompile(_sch)) - return +export function resolveRef( + this: Ajv, + root: SchemaEnv, + baseId: string, + ref: string +): Schema | ValidateFunction | SchemaEnv | undefined { + ref = resolveUrl(baseId, ref) + const schOrFunc = root.refs[ref] + if (schOrFunc) return schOrFunc + + let _sch = resolve.call(this, root, ref) + if (_sch === undefined) { + const schema = root.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv + if (schema) _sch = new SchemaEnv({schema, root, baseId}) } - function inlineOrCompile(sch: SchemaEnv): Schema | SchemaEnv { - if (inlineRef(sch.schema, self._opts.inlineRefs)) return sch.schema - return sch.validate ? sch : localCompile(sch) - } + if (_sch === undefined) return + return (root.refs[ref] = inlineOrCompile.call(this, _sch)) +} + +function inlineOrCompile(this: Ajv, sch: SchemaEnv): Schema | SchemaEnv { + if (inlineRef(sch.schema, this._opts.inlineRefs)) return sch.schema + return sch.validate ? sch : compileSchema.call(this, sch) } // Index of schema compilation in the currently compiled list diff --git a/lib/types.ts b/lib/types.ts index cc07e972fe..d69d8b5120 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -172,7 +172,6 @@ export interface SchemaCxt { compositeRule?: boolean createErrors?: boolean opts: InstanceOptions - resolveRef: (baseId: string, ref: string) => Schema | SchemaEnv | undefined self: Ajv } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 7a39aa1b36..b07df3b0af 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -5,17 +5,17 @@ import {applySubschema} from "../../compile/subschema" import {callValidateCode} from "../util" import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" -import {SchemaEnv} from "../../compile" +import {SchemaEnv, resolveRef} from "../../compile" const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", code(cxt: KeywordCxt) { const {gen, schema, it} = cxt - const {resolveRef, allErrors, baseId, isRoot, root, opts, validateName, self} = it + const {allErrors, baseId, isRoot, root, opts, validateName, self} = it const passCxt = opts.passContext ? N.this : nil if (schema === "#" || schema === "#/") return callRootRef() - const schOrFunc = resolveRef(baseId, schema) + const schOrFunc = resolveRef.call(self, root, baseId, schema) if (schOrFunc === undefined) return missingRef() if (schOrFunc instanceof SchemaEnv) return callValidate(schOrFunc) return inlineRefSchema(schOrFunc) From a4d115d5ddb3141f51151fce4c35ca7e79ddb48c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 13 Sep 2020 09:12:10 +0100 Subject: [PATCH 207/322] refactor: compileAsync --- .eslintrc.js | 3 +- lib/ajv.ts | 80 +++++++++++++++++++++++++--------------------------- package.json | 2 +- 3 files changed, 41 insertions(+), 44 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index aa561d9133..05ff963184 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,12 +18,11 @@ module.exports = { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-implied-eval": "off", - "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-invalid-this": "off", "@typescript-eslint/no-unnecessary-condition": "warn", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/restrict-template-expressions": "off", - "no-invalid-this": "off", }, }, ], diff --git a/lib/ajv.ts b/lib/ajv.ts index 507e1e181f..7d134c0510 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -66,7 +66,7 @@ export default class Ajv { RULES: ValidationRules logger: Logger errors?: ErrorObject[] | null // errors from the last validation - private _loadingSchemas: {[ref: string]: Promise | undefined} = {} + private _loading: {[ref: string]: Promise | undefined} = {} private readonly _cache: CacheInterface private readonly _metaOpts: InstanceOptions @@ -132,72 +132,70 @@ export default class Ajv { // Creates validating function for passed schema with asynchronous loading of missing schemas. // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. + // TODO allow passing schema URI compileAsync( schema: SchemaObject, metaOrCallback?: boolean | CompileAsyncCallback, // optional true to compile meta-schema; this parameter can be skipped - callback?: CompileAsyncCallback + cb?: CompileAsyncCallback // deprecated ): Promise { - const self = this if (typeof this._opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") } const {loadSchema} = this._opts let meta: boolean | undefined - if (typeof metaOrCallback == "function") callback = metaOrCallback + if (typeof metaOrCallback == "function") cb = metaOrCallback else meta = metaOrCallback - return runCompileAsync(schema, meta, callback) + const vp = runCompileAsync.call(this, schema, meta) + if (cb) vp.then((v) => cb?.(null, v), cb) + return vp - function runCompileAsync( + async function runCompileAsync( + this: Ajv, _schema: SchemaObject, - _meta?: boolean, - cb?: CompileAsyncCallback + _meta?: boolean ): Promise { - const p = loadMetaSchemaOf(_schema).then(() => { - const sch = self._addSchema(_schema, undefined, _meta) - return sch.validate || _compileAsync(sch) - }) - if (cb) p.then((v) => cb(null, v), cb) - return p + await loadMetaSchema.call(this, _schema.$schema) + const sch = this._addSchema(_schema, undefined, _meta) + return sch.validate || _compileAsync.call(this, sch) } - function loadMetaSchemaOf(sch: SchemaObject): Promise { - const {$schema} = sch - return $schema && !self.getSchema($schema) - ? runCompileAsync({$ref: $schema}, true) - : Promise.resolve() + async function loadMetaSchema(this: Ajv, $ref?: string): Promise { + if ($ref && !this.getSchema($ref)) { + await runCompileAsync.call(this, {$ref}, true) + } } - function _compileAsync(sch: SchemaEnv): ValidateFunction | Promise { + async function _compileAsync(this: Ajv, sch: SchemaEnv): Promise { try { - return self._compileSchemaEnv(sch) + return this._compileSchemaEnv(sch) } catch (e) { - if (e instanceof MissingRefError) return loadMissingSchema(sch, e) - throw e + if (!(e instanceof MissingRefError)) throw e + checkLoaded.call(this, e) + await loadMissingSchema.call(this, e.missingSchema) + return _compileAsync.call(this, sch) } } - async function loadMissingSchema( - sch: SchemaEnv, - e: MissingRefError - ): Promise { - const ref = e.missingSchema - if (self._refs[ref]) { - throw new Error(`Schema ${ref} is loaded but ${e.missingRef} cannot be resolved`) - } - let schPromise = self._loadingSchemas[ref] - if (schPromise === undefined) { - schPromise = self._loadingSchemas[ref] = loadSchema(ref) - schPromise.then(removePromise, removePromise) + function checkLoaded(this: Ajv, {missingSchema: ref, missingRef}: MissingRefError): void { + if (this._refs[ref]) { + throw new Error(`Schema ${ref} is loaded but ${missingRef} cannot be resolved`) } + } - const _schema = await schPromise - if (!self._refs[ref]) await loadMetaSchemaOf(_schema) - if (!self._refs[ref]) self.addSchema(_schema, ref, undefined, meta) - return _compileAsync(sch) + async function loadMissingSchema(this: Ajv, ref: string): Promise { + const _schema = await _loadSchema.call(this, ref) + if (!this._refs[ref]) await loadMetaSchema.call(this, _schema.$schema) + if (!this._refs[ref]) this.addSchema(_schema, ref, undefined, meta) + } - function removePromise(): void { - delete self._loadingSchemas[ref] + async function _loadSchema(this: Ajv, ref: string): Promise { + const p = this._loading[ref] + if (p) return p + try { + return await (this._loading[ref] = loadSchema(ref)) + } finally { + delete this._loading[ref] } } } diff --git a/package.json b/package.json index 7ebdb6d550..7516195951 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "uri-js": "^4.2.2" }, "devDependencies": { - "@ajv-validator/config": "^0.2.0", + "@ajv-validator/config": "^0.2.2", "@types/chai": "^4.2.12", "@types/mocha": "^8.0.3", "@types/node": "^14.0.27", From 945190d4748ccb77cd925d10ca938b08f2aa1172 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 13 Sep 2020 09:43:08 +0100 Subject: [PATCH 208/322] feat: remove support for callback parameter from compileAsync --- lib/ajv.ts | 13 +----- spec/async.spec.ts | 99 ++++++++-------------------------------------- 2 files changed, 19 insertions(+), 93 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 7d134c0510..d56ea35b70 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -46,8 +46,6 @@ const EXT_SCOPE_NAMES = new Set([ "Error", ]) -type CompileAsyncCallback = (err: Error | null, validate?: ValidateFunction) => void - const optsDefaults = { strict: true, code: {}, @@ -135,20 +133,13 @@ export default class Ajv { // TODO allow passing schema URI compileAsync( schema: SchemaObject, - metaOrCallback?: boolean | CompileAsyncCallback, // optional true to compile meta-schema; this parameter can be skipped - cb?: CompileAsyncCallback // deprecated + meta?: boolean // optional true to compile meta-schema ): Promise { if (typeof this._opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") } const {loadSchema} = this._opts - let meta: boolean | undefined - if (typeof metaOrCallback == "function") cb = metaOrCallback - else meta = metaOrCallback - - const vp = runCompileAsync.call(this, schema, meta) - if (cb) vp.then((v) => cb?.(null, v), cb) - return vp + return runCompileAsync.call(this, schema, meta) async function runCompileAsync( this: Ajv, diff --git a/spec/async.spec.ts b/spec/async.spec.ts index f6ef037ba6..2748e6014d 100644 --- a/spec/async.spec.ts +++ b/spec/async.spec.ts @@ -1,5 +1,5 @@ import _Ajv from "./ajv" -import type {SchemaObject} from "../dist/types" +import type {SchemaObject, ValidateFunction} from "../dist/types" const should = require("./chai").should() @@ -99,20 +99,18 @@ describe("compileAsync method", () => { }) }) - it("should compile schemas loading missing schemas and return function via callback", (done) => { + it("should compile schemas loading missing schemas and return promise with function", () => { const schema = { $id: "http://example.com/parent.json", properties: { a: {$ref: "object.json"}, }, } - ajv.compileAsync(schema, (err, validate) => { + return ajv.compileAsync(schema).then((validate) => { should.equal(loadCallCount, 2) - should.not.exist(err) validate.should.be.a("function") validate({a: {b: 2}}).should.equal(true) validate({a: {b: 1}}).should.equal(false) - done() }) }) @@ -263,7 +261,7 @@ describe("compileAsync method", () => { } }) - it("should throw exception if loadSchema is not passed", (done) => { + it("should throw exception if loadSchema is not passed", () => { const schema = { $id: "http://example.com/int2plus.json", type: "integer", @@ -271,77 +269,11 @@ describe("compileAsync method", () => { } ajv = new _Ajv() should.throw(() => { - ajv.compileAsync(schema, () => { - done(new Error("it should have thrown exception")) - }) + ajv.compileAsync(schema).then(expextedSyncError, expextedSyncError) }) - setTimeout(() => { - // function is needed for the test to pass in Firefox 4 - done() - }) - }) - describe("should return error via callback", () => { - it("if passed schema is invalid", (done) => { - const invalidSchema = { - $id: "http://example.com/int2plus.json", - type: "integer", - minimum: "invalid", - } - ajv.compileAsync(invalidSchema, shouldFail(done)) - }) - - it("if loaded schema is invalid", (done) => { - const schema = { - $id: "http://example.com/parent.json", - properties: { - a: {$ref: "invalid.json"}, - }, - } - ajv.compileAsync(schema, shouldFail(done)) - }) - - it("if required schema is loaded but the reference cannot be resolved", (done) => { - const schema = { - $id: "http://example.com/parent.json", - properties: { - a: {$ref: "object.json#/definitions/not_found"}, - }, - } - ajv.compileAsync(schema, shouldFail(done)) - }) - - it("if loadSchema returned error", (done) => { - const schema = { - $id: "http://example.com/parent.json", - properties: { - a: {$ref: "object.json"}, - }, - } - ajv = new _Ajv({loadSchema: badLoadSchema}) - ajv.compileAsync(schema, shouldFail(done)) - - function badLoadSchema() { - return Promise.reject(new Error("cant load")) - } - }) - - it("if schema compilation throws some other exception", (done) => { - ajv.addKeyword({keyword: "badkeyword", compile: badCompile}) - const schema = {badkeyword: true} - ajv.compileAsync(schema, shouldFail(done)) - - function badCompile(/* schema */) { - throw new Error("cant compile keyword schema") - } - }) - - function shouldFail(done) { - return (err, validate) => { - should.exist(err) - should.not.exist(validate) - done() - } + function expextedSyncError() { + throw new Error("it should have thrown exception") } }) @@ -352,7 +284,7 @@ describe("compileAsync method", () => { type: "integer", minimum: "invalid", } - return shouldReject(ajv.compileAsync(invalidSchema)) + return shouldReject(ajv.compileAsync(invalidSchema), /schema is invalid/) }) it("if loaded schema is invalid", () => { @@ -362,7 +294,7 @@ describe("compileAsync method", () => { a: {$ref: "invalid.json"}, }, } - return shouldReject(ajv.compileAsync(schema)) + return shouldReject(ajv.compileAsync(schema), /schema is invalid/) }) it("if required schema is loaded but the reference cannot be resolved", () => { @@ -372,7 +304,7 @@ describe("compileAsync method", () => { a: {$ref: "object.json#/definitions/not_found"}, }, } - return shouldReject(ajv.compileAsync(schema)) + return shouldReject(ajv.compileAsync(schema), /is loaded but/) }) it("if loadSchema returned error", () => { @@ -383,7 +315,7 @@ describe("compileAsync method", () => { }, } ajv = new _Ajv({loadSchema: badLoadSchema}) - return shouldReject(ajv.compileAsync(schema)) + return shouldReject(ajv.compileAsync(schema), /cant load/) function badLoadSchema() { return Promise.reject(new Error("cant load")) @@ -393,20 +325,23 @@ describe("compileAsync method", () => { it("if schema compilation throws some other exception", () => { ajv.addKeyword({keyword: "badkeyword", compile: badCompile}) const schema = {badkeyword: true} - return shouldReject(ajv.compileAsync(schema)) + return shouldReject(ajv.compileAsync(schema), /cant compile keyword schema/) function badCompile(/* schema */) { throw new Error("cant compile keyword schema") } }) - function shouldReject(p) { + function shouldReject(p: Promise, rx: RegExp) { return p.then( (validate) => { should.not.exist(validate) throw new Error("Promise has resolved; it should have rejected") }, - (err) => should.exist(err) + (err) => { + should.exist(err) + err.message.should.match(rx) + } ) } }) From ab91c3c5f0c6503313763d6bb2094f7f387a182b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 13 Sep 2020 10:52:39 +0100 Subject: [PATCH 209/322] feat: processCode now receives SchemaEnv as 2nd arg; remove localRoot, replace root, isRoot, async in SchemaCxt with schemaEnv --- lib/compile/errors.ts | 4 ++-- lib/compile/index.ts | 34 ++++++++++--------------------- lib/compile/validate/index.ts | 18 +++++++++------- lib/compile/validate/keyword.ts | 4 ++-- lib/types.ts | 10 +++------ lib/vocabularies/core/ref.ts | 12 +++++------ lib/vocabularies/format/format.ts | 6 +++--- 7 files changed, 38 insertions(+), 50 deletions(-) diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index b1957d6a11..4b2bd74615 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -75,8 +75,8 @@ function addError(gen: CodeGen, errObj: Code): void { } function returnErrors(it: SchemaCxt, errs: Code): void { - const {gen, validateName} = it - if (it.async) { + const {gen, validateName, schemaEnv} = it + if (schemaEnv.$async) { gen.code(_`throw new ${it.ValidationError as Name}(${errs})`) } else { gen.assign(_`${validateName}.errors`, errs) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 1daacd082d..29648f9ee0 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -29,7 +29,6 @@ export class SchemaEnv implements SchemaEnvArgs { meta?: boolean cacheKey?: unknown $async?: boolean - localRoot: {validate?: ValidateFunction} refs: SchemaRefs = {} validate?: ValidateFunction validateName?: Name @@ -44,7 +43,6 @@ export class SchemaEnv implements SchemaEnvArgs { this.meta = env.meta this.cacheKey = env.cacheKey this.$async = schema?.$async - this.localRoot = {} this.refs = {} } } @@ -55,11 +53,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { const _sch = getCompilingSchema.call(this, sch) if (_sch) return _sch const opts = this._opts - const {schema, baseId} = sch - - const isRoot = sch.schema === sch.root.schema - const rootId = getFullPath(sch.root.baseId) // TODO remove getFullPath, 1 tests fails - + const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails const gen = new CodeGen(this._scope, {...opts.codegen, forInOwn: opts.ownProperties}) let _ValidationError if (sch.$async) { @@ -79,17 +73,15 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { parentData: N.parentData, parentDataProperty: N.parentDataProperty, dataNames: [N.data], - dataPathArr: [nil], // TODO can it's lenght be used as dataLevel if nil is removed? + dataPathArr: [nil], // TODO can its lenght be used as dataLevel if nil is removed? dataLevel: 0, - topSchemaRef: gen.scopeValue("schema", {ref: schema}), - async: sch.$async, + topSchemaRef: gen.scopeValue("schema", {ref: sch.schema}), validateName, ValidationError: _ValidationError, - schema, - isRoot, - root: sch.root, + schema: sch.schema, + schemaEnv: sch, rootId, - baseId: baseId || rootId, + baseId: sch.baseId || rootId, schemaPath: nil, errSchemaPath: "#", errorPath: str``, @@ -101,19 +93,16 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { try { this._compilations.add(sch) validateFunctionCode(schemaCxt) - sourceCode = `${gen.scopeRefs(N.scope)} - ${gen.toString()}` - if (opts.processCode) sourceCode = opts.processCode(sourceCode, schema) + sourceCode = `${gen.scopeRefs(N.scope)}${gen}` + if (opts.processCode) sourceCode = opts.processCode(sourceCode, sch) // console.log("\n\n\n *** \n", sourceCode) - const makeValidate = new Function(N.self.toString(), N.scope.toString(), sourceCode) + const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode) const validate: ValidateFunction = makeValidate(this, this._scope.get()) - gen.scopeValue(validateName, {ref: validate}) - validate.schema = schema validate.errors = null - validate.root = sch.root // TODO remove - only used by $comment keyword - validate.env = sch + validate.schema = sch.schema + validate.schemaEnv = sch if (sch.$async) validate.$async = true if (opts.sourceCode === true) { validate.source = { @@ -122,7 +111,6 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { } } sch.validate = validate - if (isRoot) sch.root.localRoot.validate = validate return sch } catch (e) { delete sch.validate diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 559d075390..f3b56b5fed 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -20,12 +20,15 @@ export function validateFunctionCode(it: SchemaCxt): void { validateFunction(it, () => topBoolOrEmptySchema(it)) } -function validateFunction({gen, validateName, schema, async, opts}: SchemaCxt, body: Block): void { +function validateFunction( + {gen, validateName, schema, schemaEnv, opts}: SchemaCxt, + body: Block +): void { gen.return(() => gen.func( validateName, _`${N.data}, ${N.dataPath}, ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}`, - async, + schemaEnv.$async, () => gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`).code(body) ) ) @@ -118,21 +121,22 @@ function updateContext(it: SchemaObjCxt): void { } function checkAsync(it: SchemaObjCxt): void { - if (it.schema.$async && !it.async) throw new Error("async schema in sync schema") + if (it.schema.$async && !it.schemaEnv.$async) throw new Error("async schema in sync schema") } -function commentKeyword({gen, validateName, schema, errSchemaPath, opts}: SchemaObjCxt): void { +function commentKeyword({gen, schemaEnv, schema, errSchemaPath, opts}: SchemaObjCxt): void { const msg = schema.$comment if (opts.$comment === true) { gen.code(_`${N.self}.logger.log(${msg})`) } else if (typeof opts.$comment == "function") { const schemaPath = str`${errSchemaPath}/$comment` - gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${validateName}.root.schema)`) + const rootName = gen.scopeValue("root", {ref: schemaEnv.root}) + gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`) } } -function returnResults({gen, async, validateName, ValidationError}: SchemaCxt): void { - if (async) { +function returnResults({gen, schemaEnv, validateName, ValidationError}: SchemaCxt): void { + if (schemaEnv.$async) { gen.if( _`${N.errors} === 0`, () => gen.return(N.data), diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index b76e690c0f..54279d9484 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -126,8 +126,8 @@ function addErrs(cxt: KeywordCxt, errs: Code): void { ) } -function checkAsync(it: SchemaObjCxt, def: FuncKeywordDefinition): void { - if (def.async && !it.async) throw new Error("async keyword in sync schema") +function checkAsync({schemaEnv}: SchemaObjCxt, def: FuncKeywordDefinition): void { + if (def.async && !schemaEnv.$async) throw new Error("async keyword in sync schema") } function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name { diff --git a/lib/types.ts b/lib/types.ts index d69d8b5120..07b07c25cc 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -49,7 +49,7 @@ export interface CurrentOptions { messages?: boolean code?: CodeOptions sourceCode?: boolean - processCode?: (code: string, schema: Schema) => string + processCode?: (code: string, schema?: SchemaEnv) => string codegen?: CodeGenOptions cache?: CacheInterface logger?: Logger | false @@ -110,11 +110,9 @@ export interface ValidateFunction { ): boolean | Promise schema?: Schema errors?: null | ErrorObject[] - env?: SchemaEnv - root?: SchemaEnv + schemaEnv?: SchemaEnv $async?: true source?: SourceCode - validate?: ValidateFunction // it will be only set on wrappers } export interface SchemaValidateFunction { @@ -157,12 +155,10 @@ export interface SchemaCxt { dataPathArr: (Code | number)[] dataLevel: number topSchemaRef: Code - async?: boolean validateName: Name ValidationError?: Name schema: Schema - isRoot: boolean - root: SchemaEnv + schemaEnv: SchemaEnv rootId: string // TODO ? baseId: string schemaPath: Code diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index b07df3b0af..852c8374f2 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -12,19 +12,19 @@ const def: CodeKeywordDefinition = { schemaType: "string", code(cxt: KeywordCxt) { const {gen, schema, it} = cxt - const {allErrors, baseId, isRoot, root, opts, validateName, self} = it + const {allErrors, baseId, schemaEnv: env, opts, validateName, self} = it const passCxt = opts.passContext ? N.this : nil if (schema === "#" || schema === "#/") return callRootRef() - const schOrFunc = resolveRef.call(self, root, baseId, schema) + const schOrFunc = resolveRef.call(self, env.root, baseId, schema) if (schOrFunc === undefined) return missingRef() if (schOrFunc instanceof SchemaEnv) return callValidate(schOrFunc) return inlineRefSchema(schOrFunc) function callRootRef(): void { - if (isRoot) return callRef(validateName, it.async) + if (env === env.root) return callRef(validateName, env.$async) // TODO use the same name as compiled function, so it can be dropped in shared scope - const rootName = gen.scopeValue("root", {ref: root.localRoot}) - return callRef(_`${rootName}.validate`, root.$async || it.async) + const rootName = gen.scopeValue("root", {ref: env.root}) + return callRef(_`${rootName}.validate`, env.root.$async) } function callValidate(sch: SchemaEnv): void { @@ -76,7 +76,7 @@ const def: CodeKeywordDefinition = { } function callAsyncRef(v: Code): void { - if (!it.async) throw new Error("async schema referenced by sync schema") + if (!env.$async) throw new Error("async schema referenced by sync schema") const valid = gen.let("valid") gen.try( () => { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index f2229dc95d..6d554bf6aa 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { $data: true, code(cxt: KeywordCxt, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt - const {opts, errSchemaPath, self} = it + const {opts, errSchemaPath, schemaEnv, self} = it if (opts.format === false) return if ($data) validate$DataFormat() @@ -42,7 +42,7 @@ const def: CodeKeywordDefinition = { } function invalidFmt(): Code { - const callFormat = it.async + const callFormat = schemaEnv.$async ? _`${fDef}.async ? await ${format}(${data}) : ${format}(${data})` : _`${format}(${data})` const validData = _`typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` @@ -87,7 +87,7 @@ const def: CodeKeywordDefinition = { function validCondition(): Code { if (typeof formatDef == "object" && !(formatDef instanceof RegExp) && formatDef.async) { - if (!it.async) throw new Error("async format in sync schema") + if (!schemaEnv.$async) throw new Error("async format in sync schema") return _`await ${fmtRef}(${data})` } return typeof format == "function" ? _`${fmtRef}(${data})` : _`${fmtRef}.test(${data})` From f38f755bb3e79a7d1910c179ac399c64d868af81 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 13 Sep 2020 11:30:28 +0100 Subject: [PATCH 210/322] rename Ajv class members (remove _), make class members readonly when they are not changed --- lib/ajv.ts | 104 +++++++++++++++--------------- lib/compile/codegen/index.ts | 6 +- lib/compile/codegen/scope.ts | 14 ++-- lib/compile/context.ts | 26 ++++---- lib/compile/error_classes.ts | 10 +-- lib/compile/index.ts | 39 ++++++----- lib/compile/resolve.ts | 6 +- lib/compile/validate/index.ts | 2 +- lib/vocabularies/format/format.ts | 2 +- package.json | 2 +- spec/after_test.ts | 2 +- spec/errors.spec.ts | 17 ++--- spec/json-schema.spec.ts | 2 +- spec/keyword.spec.ts | 2 +- 14 files changed, 114 insertions(+), 120 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index d56ea35b70..c970fae213 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -54,17 +54,17 @@ const optsDefaults = { } export default class Ajv { - _opts: InstanceOptions - // shared external scope values for compiled functions - _scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES}) - _schemas: {[key: string]: SchemaEnv | undefined} = {} - _refs: {[ref: string]: SchemaEnv | string | undefined} = {} - formats: {[name: string]: AddedFormat | undefined} = {} - _compilations: Set = new Set() - RULES: ValidationRules - logger: Logger + opts: InstanceOptions errors?: ErrorObject[] | null // errors from the last validation - private _loading: {[ref: string]: Promise | undefined} = {} + logger: Logger + // shared external scope values for compiled functions + readonly scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES}) + readonly schemas: {[key: string]: SchemaEnv | undefined} = {} + readonly refs: {[ref: string]: SchemaEnv | string | undefined} = {} + readonly formats: {[name: string]: AddedFormat | undefined} = {} + readonly RULES: ValidationRules + readonly _compilations: Set = new Set() + private readonly _loading: {[ref: string]: Promise | undefined} = {} private readonly _cache: CacheInterface private readonly _metaOpts: InstanceOptions @@ -72,7 +72,7 @@ export default class Ajv { static MissingRefError = MissingRefError constructor(opts: Options = {}) { - opts = this._opts = {...optsDefaults, ...opts} + opts = this.opts = {...optsDefaults, ...opts} this.logger = getLogger(opts.logger) const formatOpt = opts.format opts.format = false @@ -135,10 +135,10 @@ export default class Ajv { schema: SchemaObject, meta?: boolean // optional true to compile meta-schema ): Promise { - if (typeof this._opts.loadSchema != "function") { + if (typeof this.opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") } - const {loadSchema} = this._opts + const {loadSchema} = this.opts return runCompileAsync.call(this, schema, meta) async function runCompileAsync( @@ -169,15 +169,15 @@ export default class Ajv { } function checkLoaded(this: Ajv, {missingSchema: ref, missingRef}: MissingRefError): void { - if (this._refs[ref]) { + if (this.refs[ref]) { throw new Error(`Schema ${ref} is loaded but ${missingRef} cannot be resolved`) } } async function loadMissingSchema(this: Ajv, ref: string): Promise { const _schema = await _loadSchema.call(this, ref) - if (!this._refs[ref]) await loadMetaSchema.call(this, _schema.$schema) - if (!this._refs[ref]) this.addSchema(_schema, ref, undefined, meta) + if (!this.refs[ref]) await loadMetaSchema.call(this, _schema.$schema) + if (!this.refs[ref]) this.addSchema(_schema, ref, undefined, meta) } async function _loadSchema(this: Ajv, ref: string): Promise { @@ -209,7 +209,7 @@ export default class Ajv { } key = normalizeId(key || id) checkUnique.call(this, key) - this._schemas[key] = this._addSchema(schema, _skipValidation, _meta, true) + this.schemas[key] = this._addSchema(schema, _skipValidation, _meta, true) return this } @@ -232,7 +232,7 @@ export default class Ajv { if ($schema !== undefined && typeof $schema != "string") { throw new Error("$schema must be a string") } - $schema = $schema || this._opts.defaultMeta || defaultMeta.call(this) + $schema = $schema || this.opts.defaultMeta || defaultMeta.call(this) if (!$schema) { this.logger.warn("meta-schema not available") this.errors = null @@ -241,7 +241,7 @@ export default class Ajv { const valid = this.validate($schema, schema) if (!valid && throwOrLogError) { const message = "schema is invalid: " + this.errorsText() - if (this._opts.validateSchema === "log") this.logger.error(message) + if (this.opts.validateSchema === "log") this.logger.error(message) else throw new Error(message) } return valid @@ -256,7 +256,7 @@ export default class Ajv { const root = new SchemaEnv({schema: {}}) sch = resolveSchema.call(this, root, keyRef) if (!sch) return - this._refs[keyRef] = sch + this.refs[keyRef] = sch } return sch.validate || this._compileSchemaEnv(sch) } @@ -267,32 +267,32 @@ export default class Ajv { // Even if schema is referenced by other schemas it still can be removed as other schemas have local references. removeSchema(schemaKeyRef: Schema | string | RegExp): Ajv { if (schemaKeyRef instanceof RegExp) { - this._removeAllSchemas(this._schemas, schemaKeyRef) - this._removeAllSchemas(this._refs, schemaKeyRef) + this._removeAllSchemas(this.schemas, schemaKeyRef) + this._removeAllSchemas(this.refs, schemaKeyRef) return this } switch (typeof schemaKeyRef) { case "undefined": - this._removeAllSchemas(this._schemas) - this._removeAllSchemas(this._refs) + this._removeAllSchemas(this.schemas) + this._removeAllSchemas(this.refs) this._cache.clear() return this case "string": { const sch = getSchEnv.call(this, schemaKeyRef) if (typeof sch == "object") this._cache.del(sch.cacheKey) - delete this._schemas[schemaKeyRef] - delete this._refs[schemaKeyRef] + delete this.schemas[schemaKeyRef] + delete this.refs[schemaKeyRef] return this } case "object": { - const {serialize} = this._opts + const {serialize} = this.opts const cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef this._cache.del(cacheKey) let id = schemaKeyRef.$id if (id) { id = normalizeId(id) - delete this._schemas[id] - delete this._refs[id] + delete this.schemas[id] + delete this.refs[id] } return this } @@ -330,7 +330,7 @@ export default class Ajv { if (def) keywordMetaschema.call(this, def) eachItem(keyword, (kwd) => { - eachItem(def?.type, (t) => _addRule.call(this, kwd, t, def)) + eachItem(def?.type, (t) => addRule.call(this, kwd, t, def)) }) return this } @@ -417,12 +417,12 @@ export default class Ajv { if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema must be object or boolean") } - const {serialize} = this._opts + const {serialize} = this.opts const cacheKey = serialize ? serialize(schema) : schema const cached = this._cache.get(cacheKey) if (cached) return cached - shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false + shouldAddSchema = shouldAddSchema || this.opts.addUsedSchema !== false let $id, $schema if (typeof schema == "object") { @@ -432,7 +432,7 @@ export default class Ajv { const id = normalizeId($id) if (id && shouldAddSchema) checkUnique.call(this, id) - const willValidate = this._opts.validateSchema !== false && !skipValidation + const willValidate = this.opts.validateSchema !== false && !skipValidation let recursiveMeta if (willValidate && !(recursiveMeta = id && id === normalizeId($schema))) { this.validateSchema(schema, true) @@ -442,7 +442,7 @@ export default class Ajv { const sch = new SchemaEnv({schema, baseId: id, localRefs, cacheKey, meta}) - if (id[0] !== "#" && shouldAddSchema) this._refs[id] = sch + if (id[0] !== "#" && shouldAddSchema) this.refs[id] = sch this._cache.put(cacheKey, sch) if (willValidate && recursiveMeta) this.validateSchema(schema, true) @@ -458,12 +458,12 @@ export default class Ajv { } private _compileMetaSchema(sch: SchemaEnv): void { - const currentOpts = this._opts - this._opts = this._metaOpts + const currentOpts = this.opts + this.opts = this._metaOpts try { compileSchema.call(this, sch) } finally { - this._opts = currentOpts + this.opts = currentOpts } } } @@ -482,42 +482,42 @@ function checkDeprecatedOptions(this: Ajv, opts: Options): void { } function defaultMeta(this: Ajv): string | SchemaObject | undefined { - const {meta} = this._opts - this._opts.defaultMeta = + const {meta} = this.opts + this.opts.defaultMeta = typeof meta == "object" ? meta.$id || meta : this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined - return this._opts.defaultMeta + return this.opts.defaultMeta } function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | string | undefined { keyRef = normalizeId(keyRef) // TODO tests fail without this line - return this._schemas[keyRef] || this._refs[keyRef] + return this.schemas[keyRef] || this.refs[keyRef] } function addDefaultMetaSchema(this: Ajv): void { - const {$data, meta} = this._opts + const {$data, meta} = this.opts if ($data) this.addMetaSchema($dataRefSchema, $dataRefSchema.$id, true) if (meta === false) return const metaSchema = $data ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) : draft7MetaSchema this.addMetaSchema(metaSchema, META_SCHEMA_ID, true) - this._refs["http://json-schema.org/schema"] = META_SCHEMA_ID + this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID } function addInitialSchemas(this: Ajv): void { - const optsSchemas = this._opts.schemas + const optsSchemas = this.opts.schemas if (!optsSchemas) return if (Array.isArray(optsSchemas)) this.addSchema(optsSchemas) else for (const key in optsSchemas) this.addSchema(optsSchemas[key], key) } function addInitialFormats(this: Ajv): void { - for (const name in this._opts.formats) { - const format = this._opts.formats[name] + for (const name in this.opts.formats) { + const format = this.opts.formats[name] this.addFormat(name, format) } } @@ -536,13 +536,13 @@ function addInitialKeywords(this: Ajv, defs: Vocabulary | {[x: string]: KeywordD } function checkUnique(this: Ajv, id: string): void { - if (this._schemas[id] || this._refs[id]) { + if (this.schemas[id] || this.refs[id]) { throw new Error('schema with key or id "' + id + '" already exists') } } function getMetaSchemaOptions(this: Ajv): InstanceOptions { - const metaOpts = {...this._opts} + const metaOpts = {...this.opts} for (const opt of META_IGNORE_OPTIONS) delete metaOpts[opt] return metaOpts } @@ -571,7 +571,7 @@ function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefini } } -function _addRule( +function addRule( this: Ajv, keyword: string, dataType?: string, @@ -587,13 +587,13 @@ function _addRule( if (!definition) return const rule: Rule = {keyword, definition} - if (definition.before) _addBeforeRule.call(this, ruleGroup, rule, definition.before) + if (definition.before) addBeforeRule.call(this, ruleGroup, rule, definition.before) else ruleGroup.rules.push(rule) RULES.all[keyword] = rule definition.implements?.forEach((kwd) => this.addKeyword(kwd)) } -function _addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: string): void { +function addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: string): void { const i = ruleGroup.rules.findIndex((_rule) => _rule.keyword === before) if (i >= 0) { ruleGroup.rules.splice(i, 0, rule) @@ -606,7 +606,7 @@ function _addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: str function keywordMetaschema(this: Ajv, def: KeywordDefinition): void { let {metaSchema} = def if (metaSchema === undefined) return - if (def.$data && this._opts.$data) metaSchema = schemaOrData(metaSchema) + if (def.$data && this.opts.$data) metaSchema = schemaOrData(metaSchema) def.validateSchema = this.compile(metaSchema, true) } diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index a40d69aa0b..f4163bb695 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -39,9 +39,9 @@ export interface CodeGenOptions { } export class CodeGen { - private readonly _scope: Scope - private readonly _extScope: ValueScope - private readonly _values: ScopeValueSets = {} + readonly _scope: Scope + readonly _extScope: ValueScope + readonly _values: ScopeValueSets = {} private readonly _blocks: BlockKind[] = [] private readonly _blockStarts: number[] = [] private readonly opts: CodeGenOptions diff --git a/lib/compile/codegen/scope.ts b/lib/compile/codegen/scope.ts index 3353c9477f..5aaf3b0375 100644 --- a/lib/compile/codegen/scope.ts +++ b/lib/compile/codegen/scope.ts @@ -14,7 +14,7 @@ export interface NameValue { export type ValueReference = unknown // possibly make CodeGen parameterized type on this type class ValueError extends Error { - value?: NameValue + readonly value?: NameValue constructor(name: ValueScopeName) { super(`CodeGen: "code" for ${name} not defined`) this.value = name.value @@ -41,9 +41,9 @@ export interface ScopeValueSets { } export class Scope { - protected _names: {[prefix: string]: NameGroup | undefined} = {} - protected _prefixes?: Set - protected _parent?: Scope + protected readonly _names: {[prefix: string]: NameGroup | undefined} = {} + protected readonly _prefixes?: Set + protected readonly _parent?: Scope constructor({prefixes, parent}: ScopeOptions = {}) { this._prefixes = prefixes @@ -77,7 +77,7 @@ interface ScopePath { } export class ValueScopeName extends Name { - prefix: string + readonly prefix: string value?: NameValue scopePath?: Code @@ -93,8 +93,8 @@ export class ValueScopeName extends Name { } export class ValueScope extends Scope { - protected _values: ScopeValues = {} - protected _scope: ScopeStore + protected readonly _values: ScopeValues = {} + protected readonly _scope: ScopeStore constructor(opts: ValueScopeOptions) { super(opts) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index cb355862e1..a0487e4bc5 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -18,20 +18,20 @@ import {CodeGen, _, nil, or, Code, Name} from "./codegen" import N from "./names" export default class KeywordCxt implements KeywordErrorCxt { - gen: CodeGen - allErrors?: boolean - keyword: string - data: Name - $data?: string | false - schema: any - schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value - schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) - schemaType?: string | string[] - parentSchema: SchemaObject - errsCount?: Name + readonly gen: CodeGen + readonly allErrors?: boolean + readonly keyword: string + readonly data: Name + readonly $data?: string | false + readonly schema: any + readonly schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value + readonly schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) + readonly schemaType?: string | string[] + readonly parentSchema: SchemaObject + readonly errsCount?: Name params: KeywordCxtParams - it: SchemaObjCxt - def: KeywordDefinition + readonly it: SchemaObjCxt + readonly def: KeywordDefinition constructor(it: SchemaObjCxt, def: KeywordDefinition, keyword: string) { validateKeywordUsage(it, def, keyword) diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts index e1da3682ed..1b72350f2c 100644 --- a/lib/compile/error_classes.ts +++ b/lib/compile/error_classes.ts @@ -2,9 +2,9 @@ import {ErrorObject} from "../types" import {resolveUrl, normalizeId, getFullPath} from "./resolve" export class ValidationError extends Error { - errors: Partial[] - ajv: true - validation: true + readonly errors: Partial[] + readonly ajv: true + readonly validation: true constructor(errors: Partial[]) { super("validation failed") @@ -14,8 +14,8 @@ export class ValidationError extends Error { } export class MissingRefError extends Error { - missingRef: string - missingSchema: string + readonly missingRef: string + readonly missingSchema: string static message(baseId: string, ref: string): string { return `can't resolve reference ${ref} from id ${baseId}` diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 29648f9ee0..2362308114 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -22,14 +22,14 @@ interface SchemaEnvArgs { } export class SchemaEnv implements SchemaEnvArgs { - schema: Schema - root: SchemaEnv - baseId: string - localRefs?: LocalRefs - meta?: boolean - cacheKey?: unknown - $async?: boolean - refs: SchemaRefs = {} + readonly schema: Schema + readonly root: SchemaEnv + baseId: string // TODO possibly, it should be readonly + readonly localRefs?: LocalRefs + readonly meta?: boolean + readonly cacheKey?: unknown + readonly $async?: boolean + readonly refs: SchemaRefs = {} validate?: ValidateFunction validateName?: Name @@ -52,9 +52,8 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { // TODO refactor - remove compilations const _sch = getCompilingSchema.call(this, sch) if (_sch) return _sch - const opts = this._opts const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails - const gen = new CodeGen(this._scope, {...opts.codegen, forInOwn: opts.ownProperties}) + const gen = new CodeGen(this.scope, {...this.opts.codegen, forInOwn: this.opts.ownProperties}) let _ValidationError if (sch.$async) { _ValidationError = gen.scopeValue("Error", { @@ -68,7 +67,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { const schemaCxt: SchemaCxt = { gen, - allErrors: opts.allErrors, + allErrors: this.opts.allErrors, data: N.data, parentData: N.parentData, parentDataProperty: N.parentDataProperty, @@ -85,7 +84,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { schemaPath: nil, errSchemaPath: "#", errorPath: str``, - opts, + opts: this.opts, self: this, } @@ -94,20 +93,20 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { this._compilations.add(sch) validateFunctionCode(schemaCxt) sourceCode = `${gen.scopeRefs(N.scope)}${gen}` - if (opts.processCode) sourceCode = opts.processCode(sourceCode, sch) + if (this.opts.processCode) sourceCode = this.opts.processCode(sourceCode, sch) // console.log("\n\n\n *** \n", sourceCode) const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode) - const validate: ValidateFunction = makeValidate(this, this._scope.get()) + const validate: ValidateFunction = makeValidate(this, this.scope.get()) gen.scopeValue(validateName, {ref: validate}) validate.errors = null validate.schema = sch.schema validate.schemaEnv = sch if (sch.$async) validate.$async = true - if (opts.sourceCode === true) { + if (this.opts.sourceCode === true) { validate.source = { code: sourceCode, - scope: this._scope, + scope: this.scope, } } sch.validate = validate @@ -143,7 +142,7 @@ export function resolveRef( } function inlineOrCompile(this: Ajv, sch: SchemaEnv): Schema | SchemaEnv { - if (inlineRef(sch.schema, this._opts.inlineRefs)) return sch.schema + if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema return sch.validate ? sch : compileSchema.call(this, sch) } @@ -166,8 +165,8 @@ function resolve( ref: string // reference to resolve ): SchemaEnv | undefined { let sch - while (typeof (sch = this._refs[ref]) == "string") ref = sch - return sch || this._schemas[ref] || resolveSchema.call(this, root, ref) + while (typeof (sch = this.refs[ref]) == "string") ref = sch + return sch || this.schemas[ref] || resolveSchema.call(this, root, ref) } // Resolve schema, its root and baseId @@ -185,7 +184,7 @@ export function resolveSchema( } const id = normalizeId(refPath) - const schOrRef = this._refs[id] || this._schemas[id] + const schOrRef = this.refs[id] || this.schemas[id] if (typeof schOrRef == "string") { const sch = resolveSchema.call(this, root, schOrRef) if (typeof sch?.schema !== "object") return diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index f517d580c9..311fd6cd3c 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -94,8 +94,8 @@ export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { let baseId = baseIds[parentJsonPtr] if (typeof id == "string") { id = baseId = normalizeId(baseId ? URI.resolve(baseId, id) : id) - let schOrRef = this._refs[id] - if (typeof schOrRef == "string") schOrRef = this._refs[schOrRef] + let schOrRef = this.refs[id] + if (typeof schOrRef == "string") schOrRef = this.refs[schOrRef] if (typeof schOrRef == "object") { checkAmbiguosId(sch, schOrRef.schema, id) } else if (id !== normalizeId(fullPath)) { @@ -103,7 +103,7 @@ export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { checkAmbiguosId(sch, localRefs[id], id) localRefs[id] = sch } else { - this._refs[id] = fullPath + this.refs[id] = fullPath } } } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index f3b56b5fed..0ff0b92d8f 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -131,7 +131,7 @@ function commentKeyword({gen, schemaEnv, schema, errSchemaPath, opts}: SchemaObj } else if (typeof opts.$comment == "function") { const schemaPath = str`${errSchemaPath}/$comment` const rootName = gen.scopeValue("root", {ref: schemaEnv.root}) - gen.code(_`${N.self}._opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`) + gen.code(_`${N.self}.opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`) } } diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 6d554bf6aa..cc9e18510e 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -36,7 +36,7 @@ const def: CodeKeywordDefinition = { if (opts.unknownFormats === "ignore") return nil let unknown = _`${schemaCode} && !${format}` if (Array.isArray(opts.unknownFormats)) { - unknown = _`${unknown} && !${N.self}._opts.unknownFormats.includes(${schemaCode})` + unknown = _`${unknown} && !${N.self}.opts.unknownFormats.includes(${schemaCode})` } return _`(${unknown})` } diff --git a/package.json b/package.json index 7516195951..ac721d8169 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@types/node": "^14.0.27", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", - "ajv-formats": "^0.1.0", + "ajv-formats": "^0.2.0", "browserify": "^16.2.0", "chai": "^4.0.1", "coveralls": "^3.0.1", diff --git a/spec/after_test.ts b/spec/after_test.ts index eeecf40dee..c1104c508d 100644 --- a/spec/after_test.ts +++ b/spec/after_test.ts @@ -6,7 +6,7 @@ module.exports = { } export function afterError(res): void { - console.log("ajv options:", res.validator._opts) + console.log("ajv options:", res.validator.opts) } export function afterEach(res): void { diff --git a/spec/errors.spec.ts b/spec/errors.spec.ts index 7b90976304..77a57f7918 100644 --- a/spec/errors.spec.ts +++ b/spec/errors.spec.ts @@ -808,7 +808,7 @@ describe("Validation errors", () => { err, "exclusiveMaximum", "#/properties/smaller/exclusiveMaximum", - _ajv._opts.jsPropertySyntax ? ".smaller" : "/smaller", + _ajv.opts.jsPropertySyntax ? ".smaller" : "/smaller", "should be < 4", {comparison: "<", limit: 4} ) @@ -876,7 +876,7 @@ describe("Validation errors", () => { function prepareTest(_ajv, schema) { validate = _ajv.compile(schema) - numErrors = _ajv._opts.allErrors ? 2 : 1 + numErrors = _ajv.opts.allErrors ? 2 : 1 } function testIfError(ifClause, multipleOf) { @@ -920,10 +920,10 @@ describe("Validation errors", () => { {i: 1, j: 2} ) - const expectedErrors = _ajv._opts.allErrors ? 2 : 1 + const expectedErrors = _ajv.opts.allErrors ? 2 : 1 shouldBeInvalid(validate, [1, "2", "2", 2], expectedErrors) - testTypeError(0, _ajv._opts.jsPropertySyntax ? "[1]" : "/1") - if (expectedErrors === 2) testTypeError(1, _ajv._opts.jsPropertySyntax ? "[2]" : "/2") + testTypeError(0, _ajv.opts.jsPropertySyntax ? "[1]" : "/1") + if (expectedErrors === 2) testTypeError(1, _ajv.opts.jsPropertySyntax ? "[2]" : "/2") function testTypeError(i, dataPath) { const err = validate.errors[i] @@ -960,12 +960,7 @@ describe("Validation errors", () => { const validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError( - validate.errors[0], - "type", - schPath, - _ajv._opts.jsPropertySyntax ? ".foo" : "/foo" - ) + shouldBeError(validate.errors[0], "type", schPath, _ajv.opts.jsPropertySyntax ? ".foo" : "/foo") } function shouldBeValid(validate, data) { diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index 22e75b12f5..003a40b111 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -49,7 +49,7 @@ function runTest(instances: Ajv[], draft: number, tests) { switch (draft) { case 6: ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) - ajv._opts.defaultMeta = "http://json-schema.org/draft-06/schema#" + ajv.opts.defaultMeta = "http://json-schema.org/draft-06/schema#" break } for (const id in remoteRefs) ajv.addSchema(remoteRefs[id], id) diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index 13b94d7889..c4d3257893 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -422,7 +422,7 @@ describe("User-defined keywords", () => { } const validate = _ajv.compile(schema) - const numErrors = _ajv._opts.allErrors ? 4 : 2 + const numErrors = _ajv.opts.allErrors ? 4 : 2 shouldBeInvalid(validate, 2, 2) shouldBeInvalid(validate, 3, numErrors) From ddfae0266312187e026a77f4c94d39a1cd8aac25 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 13 Sep 2020 14:06:26 +0100 Subject: [PATCH 211/322] refactor: sync/$async schemas validate function types --- lib/ajv.ts | 42 ++++------------- lib/types.ts | 28 +++-------- spec/async_types.spec.ts | 95 ++++++++++++++++++++++++++++++++++++++ spec/options/async.spec.ts | 73 ----------------------------- 4 files changed, 112 insertions(+), 126 deletions(-) create mode 100644 spec/async_types.spec.ts delete mode 100644 spec/options/async.spec.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 2bf46ca027..f771b1e830 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -124,14 +124,11 @@ export default class Ajv { } // Create validation function for passed schema - compile(s: {$async?: never}, _?: boolean): ValidateFunction - compile(s: SyncSchemaObject, _?: boolean): SyncValidateFunction - compile(s: AsyncSchemaObject, _?: boolean): AsyncValidateFunction - compile(s: Schema, _?: boolean): ValidateFunction - compile( - schema: Schema, - _meta?: boolean // true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. - ): ValidateFunction { + // _meta: true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. + compile(schema: SyncSchemaObject | boolean, _meta?: boolean): SyncValidateFunction + compile(schema: AsyncSchemaObject, _meta?: boolean): AsyncValidateFunction + compile(schema: Schema, _meta?: boolean): ValidateFunction + compile(schema: Schema, _meta?: boolean): ValidateFunction { const sch = this._addSchema(schema, undefined, _meta) return sch.validate || this._compileSchemaEnv(sch) } @@ -139,30 +136,11 @@ export default class Ajv { // Creates validating function for passed schema with asynchronous loading of missing schemas. // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. // TODO allow passing schema URI - compileAsync( - s: {$async?: never}, - m?: boolean | CompileAsyncCallback, - c?: CompileAsyncCallback - ): Promise - compileAsync( - s: SyncSchemaObject, - m?: boolean | CompileAsyncCallback, - c?: CompileAsyncCallback - ): Promise - compileAsync( - s: AsyncSchemaObject, - m?: boolean | CompileAsyncCallback, - c?: CompileAsyncCallback - ): Promise - compileAsync( - s: SchemaObject, - m?: boolean | CompileAsyncCallback, - c?: CompileAsyncCallback - ): Promise - compileAsync( - schema: SchemaObject, - meta?: boolean // optional true to compile meta-schema - ): Promise { + // meta - optional true to compile meta-schema + compileAsync(schema: SyncSchemaObject, meta?: boolean): Promise + compileAsync(schema: AsyncSchemaObject, meta?: boolean): Promise + compileAsync(schema: SchemaObject, meta?: boolean): Promise + compileAsync(schema: SchemaObject, meta?: boolean): Promise { if (typeof this.opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") } diff --git a/lib/types.ts b/lib/types.ts index 065a45044e..92fc7b015e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -100,7 +100,7 @@ interface SourceCode { scope: Scope } -export interface ValidateFunction { +export interface _ValidateFunction> { ( this: Ajv | any, data: any, @@ -108,7 +108,7 @@ export interface ValidateFunction { parentData?: Record | any[], parentDataProperty?: string | number, rootData?: Record | any[] - ): boolean | Promise + ): T schema?: Schema errors?: null | ErrorObject[] schemaEnv?: SchemaEnv @@ -116,19 +116,13 @@ export interface ValidateFunction { source?: SourceCode } +export type ValidateFunction = _ValidateFunction> + export interface SyncSchemaObject extends SchemaObject { - $async: false + $async?: false | undefined } -export interface SyncValidateFunction extends ValidateFunction { - ( - this: Ajv | any, - data: any, - dataPath?: string, - parentData?: Record | any[], - parentDataProperty?: string | number, - rootData?: Record | any[] - ): boolean +export interface SyncValidateFunction extends _ValidateFunction { $async: undefined } @@ -136,15 +130,7 @@ export interface AsyncSchemaObject extends SchemaObject { $async: true } -export interface AsyncValidateFunction extends ValidateFunction { - ( - this: Ajv | any, - data: any, - dataPath?: string, - parentData?: Record | any[], - parentDataProperty?: string | number, - rootData?: Record | any[] - ): Promise +export interface AsyncValidateFunction extends _ValidateFunction> { $async: true } diff --git a/spec/async_types.spec.ts b/spec/async_types.spec.ts new file mode 100644 index 0000000000..67b3d0d61e --- /dev/null +++ b/spec/async_types.spec.ts @@ -0,0 +1,95 @@ +import type {SchemaObject, SyncSchemaObject, AsyncSchemaObject} from "../dist/types" +import _Ajv from "./ajv" +const should = require("./chai").should() + +describe("validate function type depends on $async property type", () => { + const ajv = new _Ajv() + + describe("$async: undefined", () => { + const validate = ajv.compile({}) + it("should return boolean", () => { + const result: boolean = validate({}) + should.exist(result) + }) + }) + + describe("$async: false", () => { + it("should return boolean 1", () => { + const validate = ajv.compile({$async: false}) + const result: boolean = validate({}) + should.exist(result) + }) + + it("should return boolean 2", () => { + const schema: SyncSchemaObject = {$async: false} + const validate = ajv.compile(schema) + const result: boolean = validate({}) + should.exist(result) + }) + }) + + describe("$async: true", () => { + it("should return promise 1", async () => { + const validate = ajv.compile({$async: true}) + const result: Promise = validate({}) + await result.then((data) => data.should.exist) + }) + + it("should return promise 2", async () => { + const schema: AsyncSchemaObject = {$async: true} + const validate = ajv.compile(schema) + const result: Promise = validate({}) + await result.then((data) => data.should.exist) + }) + }) + + describe("$async: boolean", () => { + it("should return promise", async () => { + const schema = {$async: true} + const validate = ajv.compile(schema) + const result = validate({}) + if (typeof result === "boolean") { + throw new Error("should return promise") + } else { + await result.then((data) => data.should.exist) + } + }) + + it("should return boolean", () => { + const schema = {$async: false} + const validate = ajv.compile(schema) + const result = validate({}) + if (typeof result === "boolean") { + result.should.equal(true) + } else { + throw new Error("should return boolean") + } + }) + }) + + describe("$async: unknown", () => { + const schema: Record = {} + const validate = ajv.compile(schema) + it("should return boolean", () => { + const result = validate({}) + if (typeof result === "boolean") { + should.exist(result) + } else { + throw new Error("should return boolean") + } + }) + }) + + describe("= any", () => { + const schema: SchemaObject = {} + const validate = ajv.compile(schema) + it("should return boolean", () => { + const result = validate({}) + if (typeof result === "boolean") { + should.exist(result) + } else { + throw new Error("should return boolean") + } + }) + }) +}) diff --git a/spec/options/async.spec.ts b/spec/options/async.spec.ts deleted file mode 100644 index 730d654c5e..0000000000 --- a/spec/options/async.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import _Ajv from "../ajv" -require("../chai").should() - -describe("$async option", () => { - const ajv = new _Ajv() - - describe("= undefined", () => { - const validate = ajv.compile({}) - it("should return a boolean or promise", async () => { - const result = validate({}) - if (typeof result === "boolean") { - result.should.exist - } else { - await result.then((data) => data.should.exist) - } - }) - }) - - describe("= false", () => { - const validate = ajv.compile({$async: false}) - it("should return a boolean", () => { - const result: boolean = validate({}) - result.should.exist - }) - }) - - describe("= true", () => { - const validate = ajv.compile({$async: true}) - it("should return a promise", async () => { - const result: Promise = validate({}) - await result.then((data) => data.should.exist) - }) - }) - - describe("= boolean", () => { - const schema = {$async: true} - const validate = ajv.compile(schema) - it("should return boolean or promise", async () => { - const result = validate({}) - if (typeof result === "boolean") { - result.should.exist - } else { - await result.then((data) => data.should.exist) - } - }) - }) - - describe("of type unknown", () => { - const schema: Record = {} - const validate = ajv.compile(schema) - it("should return boolean or promise", async () => { - const result = validate({}) - if (typeof result === "boolean") { - result.should.exist - } else { - await result.then((data) => data.should.exist) - } - }) - }) - - describe("of type any", () => { - const schema: any = {} - const validate = ajv.compile(schema) - it("should return boolean or promise", async () => { - const result = validate({}) - if (typeof result === "boolean") { - result.should.exist - } else { - await result.then((data) => data.should.exist) - } - }) - }) -}) From 4f41529819292545e3e59f561514d1136198b902 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 09:02:26 +0100 Subject: [PATCH 212/322] test: $async validation result types --- spec/async_types.spec.ts | 47 ++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/spec/async_types.spec.ts b/spec/async_types.spec.ts index 67b3d0d61e..8bf0995c2c 100644 --- a/spec/async_types.spec.ts +++ b/spec/async_types.spec.ts @@ -2,25 +2,25 @@ import type {SchemaObject, SyncSchemaObject, AsyncSchemaObject} from "../dist/ty import _Ajv from "./ajv" const should = require("./chai").should() -describe("validate function type depends on $async property type", () => { +describe("validate function result type depends on $async property type", () => { const ajv = new _Ajv() describe("$async: undefined", () => { const validate = ajv.compile({}) - it("should return boolean", () => { + it("should have result type boolean", () => { const result: boolean = validate({}) should.exist(result) }) }) describe("$async: false", () => { - it("should return boolean 1", () => { + it("should have result type boolean 1", () => { const validate = ajv.compile({$async: false}) const result: boolean = validate({}) should.exist(result) }) - it("should return boolean 2", () => { + it("should have result type boolean 2", () => { const schema: SyncSchemaObject = {$async: false} const validate = ajv.compile(schema) const result: boolean = validate({}) @@ -29,13 +29,13 @@ describe("validate function type depends on $async property type", () => { }) describe("$async: true", () => { - it("should return promise 1", async () => { + it("should have result type promise 1", async () => { const validate = ajv.compile({$async: true}) const result: Promise = validate({}) await result.then((data) => data.should.exist) }) - it("should return promise 2", async () => { + it("should have result type promise 2", async () => { const schema: AsyncSchemaObject = {$async: true} const validate = ajv.compile(schema) const result: Promise = validate({}) @@ -44,25 +44,38 @@ describe("validate function type depends on $async property type", () => { }) describe("$async: boolean", () => { - it("should return promise", async () => { + it("should have result type boolean | promise", async () => { const schema = {$async: true} const validate = ajv.compile(schema) const result = validate({}) if (typeof result === "boolean") { - throw new Error("should return promise") + result.should.equal(true) } else { await result.then((data) => data.should.exist) } }) - it("should return boolean", () => { + it("should have result type boolean | promise", async () => { const schema = {$async: false} const validate = ajv.compile(schema) const result = validate({}) if (typeof result === "boolean") { result.should.equal(true) } else { - throw new Error("should return boolean") + await result.then((data) => data.should.exist) + } + }) + }) + + describe("= boolean", () => { + const schema: SchemaObject = {} + const validate = ajv.compile(schema) + it("should have result type boolean | promise", async () => { + const result = validate({}) + if (typeof result === "boolean") { + should.exist(result) + } else { + await result.then((data) => data.should.exist) } }) }) @@ -70,25 +83,27 @@ describe("validate function type depends on $async property type", () => { describe("$async: unknown", () => { const schema: Record = {} const validate = ajv.compile(schema) - it("should return boolean", () => { + it("should have result type boolean | promise", async () => { const result = validate({}) if (typeof result === "boolean") { should.exist(result) } else { - throw new Error("should return boolean") + // await result.then((data) => data.should.exist) + await Promise.resolve() } }) }) - describe("= any", () => { - const schema: SchemaObject = {} + describe("$async: any", () => { + const schema: any = {} const validate = ajv.compile(schema) - it("should return boolean", () => { + it("should have result type boolean | promise", async () => { const result = validate({}) if (typeof result === "boolean") { should.exist(result) } else { - throw new Error("should return boolean") + // await result.then((data) => data.should.exist) + await Promise.resolve() } }) }) From 9885226beb7691230df41163f7c98348970d0632 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 12:33:28 +0100 Subject: [PATCH 213/322] validation as type guard, close #736, close #1279 --- lib/ajv.ts | 17 +++ lib/types.ts | 13 ++- lib/types/json-schema.ts | 76 ++++++++++++ .../async-validate.spec.ts} | 22 ++-- spec/types/json-schema.spec.ts | 109 ++++++++++++++++++ 5 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 lib/types/json-schema.ts rename spec/{async_types.spec.ts => types/async-validate.spec.ts} (86%) create mode 100644 spec/types/json-schema.spec.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index f771b1e830..1c00166664 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -8,6 +8,7 @@ import { Options, InstanceOptions, ValidateFunction, + ValidateGuard, SyncValidateFunction, AsyncValidateFunction, CacheInterface, @@ -16,6 +17,7 @@ import { Format, AddedFormat, } from "./types" +import {JSONSchemaType} from "./types/json-schema" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import {getRules, ValidationRules, Rule, RuleGroup} from "./compile/rules" @@ -105,6 +107,12 @@ export default class Ajv { // Validate data using schema // Schema will be compiled and cached using as a key JSON serialized with // [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) + validate(schema: {$async?: never}, data: unknown): boolean | Promise + validate(schema: SyncSchemaObject | boolean, data: unknown): boolean + validate(schema: SyncSchemaObject | JSONSchemaType, data: unknown): data is T + validate(schema: AsyncSchemaObject, data: unknown): Promise + // eslint-disable-next-line @typescript-eslint/unified-signatures + validate(schemaKeyRef: Schema | string, data: unknown): boolean | Promise validate( schemaKeyRef: Schema | string, // key, ref or schema object data: unknown // to be validated @@ -125,8 +133,11 @@ export default class Ajv { // Create validation function for passed schema // _meta: true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. + compile(schema: {$async?: never}, _meta?: boolean): ValidateFunction compile(schema: SyncSchemaObject | boolean, _meta?: boolean): SyncValidateFunction + compile(schema: SyncSchemaObject | JSONSchemaType, _meta?: boolean): ValidateGuard compile(schema: AsyncSchemaObject, _meta?: boolean): AsyncValidateFunction + // eslint-disable-next-line @typescript-eslint/unified-signatures compile(schema: Schema, _meta?: boolean): ValidateFunction compile(schema: Schema, _meta?: boolean): ValidateFunction { const sch = this._addSchema(schema, undefined, _meta) @@ -137,8 +148,14 @@ export default class Ajv { // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. // TODO allow passing schema URI // meta - optional true to compile meta-schema + compileAsync(schema: {$async?: never}, _meta?: boolean): Promise compileAsync(schema: SyncSchemaObject, meta?: boolean): Promise + compileAsync( + schema: SyncSchemaObject | JSONSchemaType, + _meta?: boolean + ): Promise> compileAsync(schema: AsyncSchemaObject, meta?: boolean): Promise + // eslint-disable-next-line @typescript-eslint/unified-signatures compileAsync(schema: SchemaObject, meta?: boolean): Promise compileAsync(schema: SchemaObject, meta?: boolean): Promise { if (typeof this.opts.loadSchema != "function") { diff --git a/lib/types.ts b/lib/types.ts index 92fc7b015e..2852fe812b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -100,7 +100,7 @@ interface SourceCode { scope: Scope } -export interface _ValidateFunction> { +export interface ValidateGuard extends _ValidateFuncProps { ( this: Ajv | any, data: any, @@ -108,11 +108,18 @@ export interface _ValidateFunction> { parentData?: Record | any[], parentDataProperty?: string | number, rootData?: Record | any[] - ): T + ): data is T +} + +interface _ValidateFunction> extends _ValidateFuncProps { + (...args: Parameters>): T + $async?: true +} + +interface _ValidateFuncProps { schema?: Schema errors?: null | ErrorObject[] schemaEnv?: SchemaEnv - $async?: true source?: SourceCode } diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts new file mode 100644 index 0000000000..76d8962866 --- /dev/null +++ b/lib/types/json-schema.ts @@ -0,0 +1,76 @@ +export type JSONSchemaType = (T extends number + ? { + type: "number" | "integer" + minimum?: number + maximum?: number + exclusiveMinimum?: number + exclusiveMaximum?: number + multipleOf?: number + format?: string + } + : T extends string + ? { + type: "string" + minLength?: number + maxLength?: number + pattern?: string + format?: string + } + : T extends boolean + ? { + type: "boolean" + } + : T extends [any, ...any[]] + ? { + type: "array" + items: { + [K in keyof T]-?: JSONSchemaType & Nullable + } & {length: T["length"]} + minItems: T["length"] + } & ({maxItems: T["length"]} | {additionalItems: false}) + : T extends any[] + ? { + type: "array" + items: JSONSchemaType + minItems?: number + maxItems?: number + uniqueItems?: true + additionalItems?: never + } + : T extends Record + ? { + type: "object" + // "required" type does not guarantee that all required properties are listed + // it only asserts that optional cannot be listed + required: RequiredMembers[] + additionalProperties: boolean | JSONSchemaType + properties?: { + [K in keyof T]-?: JSONSchemaType & Nullable + } + patternProperties?: { + [pattern: string]: JSONSchemaType + } + propertyNames?: JSONSchemaType + minProperties?: number + maxProperties?: number + } + : never) & { + [keyword: string]: any +} + +type RequiredMembers = { + [K in keyof T]-?: undefined extends T[K] ? never : K +}[keyof T] + +type Nullable = undefined extends T + ? { + nullable: true + const?: null // any other value here would make `null` fail + enum?: (T | null)[] // `null` must be explicitely included in "enum" + default?: T | null + } + : { + const?: T + enum?: T[] + default?: T + } diff --git a/spec/async_types.spec.ts b/spec/types/async-validate.spec.ts similarity index 86% rename from spec/async_types.spec.ts rename to spec/types/async-validate.spec.ts index 8bf0995c2c..1cdf5067a4 100644 --- a/spec/async_types.spec.ts +++ b/spec/types/async-validate.spec.ts @@ -1,15 +1,19 @@ -import type {SchemaObject, SyncSchemaObject, AsyncSchemaObject} from "../dist/types" -import _Ajv from "./ajv" -const should = require("./chai").should() +import type {SchemaObject, SyncSchemaObject, AsyncSchemaObject} from "../../dist/types" +import _Ajv from "../ajv" +const should = require("../chai").should() describe("validate function result type depends on $async property type", () => { const ajv = new _Ajv() describe("$async: undefined", () => { const validate = ajv.compile({}) - it("should have result type boolean", () => { - const result: boolean = validate({}) - should.exist(result) + it("should have result type boolean | promise", async () => { + const result = validate({}) + if (typeof result === "boolean") { + should.exist(result) + } else { + await result.then((data) => data.should.exist) + } }) }) @@ -88,8 +92,7 @@ describe("validate function result type depends on $async property type", () => if (typeof result === "boolean") { should.exist(result) } else { - // await result.then((data) => data.should.exist) - await Promise.resolve() + await result.then((data) => data.should.exist) } }) }) @@ -102,8 +105,7 @@ describe("validate function result type depends on $async property type", () => if (typeof result === "boolean") { should.exist(result) } else { - // await result.then((data) => data.should.exist) - await Promise.resolve() + await result.then((data) => data.should.exist) } }) }) diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts new file mode 100644 index 0000000000..dec1eecd52 --- /dev/null +++ b/spec/types/json-schema.spec.ts @@ -0,0 +1,109 @@ +import _Ajv from "../ajv" +import type {JSONSchemaType} from "../../dist/types/json-schema" +import type {SyncSchemaObject} from "../../dist/types" + +interface MyData { + foo: string + bar?: number + baz: { + quux: "quux" + [x: string]: string + } + boo?: boolean + arr: {id: number}[] + tuple?: [number, string] + map: {[x: string]: number} +} + +const mySchema: JSONSchemaType = { + type: "object", + properties: { + foo: {type: "string"}, + bar: {type: "number", nullable: true}, + baz: { + type: "object", + properties: { + quux: {type: "string", const: "quux"}, + }, + patternProperties: { + abc: {type: "string"}, + }, + additionalProperties: false, + required: [], + }, + boo: {type: "boolean", nullable: true}, + arr: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "integer", + }, + }, + additionalProperties: false, + required: ["id"], + }, + uniqueItems: true, + }, + tuple: { + type: "array", + items: [{type: "number"}, {type: "string"}], + minItems: 2, + additionalItems: false, + nullable: true, + }, + map: { + type: "object", + required: [], + additionalProperties: {type: "number"}, + }, + }, + additionalProperties: false, + required: ["foo", "baz", "arr", "map"], // any other property added here won't typecheck +} + +describe("JSONSchemaType type and validation as a type guard", () => { + const ajv = new _Ajv() + + const validData: unknown = { + foo: "foo", + bar: 1, + baz: { + quux: "quux", + abc: "abc", + }, + boo: true, + arr: [{id: 1}, {id: 2}], + tuple: [1, "abc"], + map: { + a: 1, + b: 2, + }, + } + + describe("schema has type JSONSchemaType", () => { + it("should prove the type of validated data", () => { + const validate = ajv.compile(mySchema) + if (validate(validData)) { + validData.foo.should.equal("foo") + } + if (ajv.validate(mySchema, validData)) { + validData.foo.should.equal("foo") + } + }) + }) + + describe("schema has type SyncSchemaObject", () => { + it("should prove the type of validated data", () => { + const schema = mySchema as SyncSchemaObject + const validate = ajv.compile(schema) + if (validate(validData)) { + validData.foo.should.equal("foo") + } + if (ajv.validate(schema, validData)) { + validData.foo.should.equal("foo") + } + }) + }) +}) From 650af5bbefbea1747980334b4a3c1cc5f611fb79 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 12:47:01 +0100 Subject: [PATCH 214/322] schema type for const and enum with nullable --- lib/types/json-schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts index 76d8962866..bc9931f771 100644 --- a/lib/types/json-schema.ts +++ b/lib/types/json-schema.ts @@ -65,8 +65,8 @@ type RequiredMembers = { type Nullable = undefined extends T ? { nullable: true - const?: null // any other value here would make `null` fail - enum?: (T | null)[] // `null` must be explicitely included in "enum" + const?: never // any non-null value would fail `null`, `null` would fail any other value + enum?: (T | null)[] // `null` must be explicitely included in "enum" for `null` to pass default?: T | null } : { From 4ff2135636d558dc4a254dda4047c5faf5b54ba2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 15:24:45 +0100 Subject: [PATCH 215/322] add remaining keywords to JSONSchemaType --- lib/types/json-schema.ts | 30 ++++++++++++++++++++++++++++-- spec/types/json-schema.spec.ts | 16 +++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts index bc9931f771..1a9cb7d0a6 100644 --- a/lib/types/json-schema.ts +++ b/lib/types/json-schema.ts @@ -1,4 +1,4 @@ -export type JSONSchemaType = (T extends number +export type JSONSchemaType = (T extends number ? { type: "number" | "integer" minimum?: number @@ -22,6 +22,7 @@ export type JSONSchemaType = (T extends number } : T extends [any, ...any[]] ? { + // JSON Schema for tuple type: "array" items: { [K in keyof T]-?: JSONSchemaType & Nullable @@ -32,6 +33,7 @@ export type JSONSchemaType = (T extends number ? { type: "array" items: JSONSchemaType + contains?: PartialSchema minItems?: number maxItems?: number uniqueItems?: true @@ -39,10 +41,14 @@ export type JSONSchemaType = (T extends number } : T extends Record ? { + // JSON Schema for records and dicitonaries + // "required" and "additionalProperties" are not optional because they are often forgotten + // "properties" are optional for more concise dicitonary schemas + // "patternProperties" and can be only used with interfaces that have string index type: "object" // "required" type does not guarantee that all required properties are listed // it only asserts that optional cannot be listed - required: RequiredMembers[] + required: Partial extends true ? (keyof T)[] : RequiredMembers[] additionalProperties: boolean | JSONSchemaType properties?: { [K in keyof T]-?: JSONSchemaType & Nullable @@ -51,13 +57,33 @@ export type JSONSchemaType = (T extends number [pattern: string]: JSONSchemaType } propertyNames?: JSONSchemaType + dependencies?: { + [K in keyof T]?: (keyof T)[] | PartialSchema | undefined + } minProperties?: number maxProperties?: number } + : T extends null + ? { + nullable: true + } : never) & { [keyword: string]: any + $id?: string + $ref?: string + $defs?: {[key: string]: JSONSchemaType} + definitions?: {[key: string]: JSONSchemaType} + allOf?: PartialSchema[] + anyOf?: PartialSchema[] + oneOf?: PartialSchema[] + if?: PartialSchema + then?: PartialSchema + else?: PartialSchema + not?: PartialSchema } +type PartialSchema = Partial> + type RequiredMembers = { [K in keyof T]-?: undefined extends T[K] ? never : K }[keyof T] diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts index dec1eecd52..a1d615abf6 100644 --- a/spec/types/json-schema.spec.ts +++ b/spec/types/json-schema.spec.ts @@ -4,19 +4,24 @@ import type {SyncSchemaObject} from "../../dist/types" interface MyData { foo: string - bar?: number + bar?: number // should be present if "boo" is present baz: { quux: "quux" [x: string]: string } - boo?: boolean + boo?: true arr: {id: number}[] tuple?: [number, string] map: {[x: string]: number} + notBoo?: string // should not be present if "boo" is present } const mySchema: JSONSchemaType = { type: "object", + dependencies: { + bar: ["boo"], + boo: {not: {required: ["notBoo"]}}, + }, properties: { foo: {type: "string"}, bar: {type: "number", nullable: true}, @@ -31,7 +36,11 @@ const mySchema: JSONSchemaType = { additionalProperties: false, required: [], }, - boo: {type: "boolean", nullable: true}, + boo: { + type: "boolean", + nullable: true, + enum: [true, null], + }, arr: { type: "array", items: { @@ -58,6 +67,7 @@ const mySchema: JSONSchemaType = { required: [], additionalProperties: {type: "number"}, }, + notBoo: {type: "string", nullable: true}, }, additionalProperties: false, required: ["foo", "baz", "arr", "map"], // any other property added here won't typecheck From ca74224ea15d9e0f18202558389e97d64626b70b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 16:27:58 +0100 Subject: [PATCH 216/322] JSONSchemaType for definitions --- lib/types/json-schema.ts | 19 ++++++++-- spec/types/json-schema.spec.ts | 69 ++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts index 1a9cb7d0a6..17e3e2a9c6 100644 --- a/lib/types/json-schema.ts +++ b/lib/types/json-schema.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ +export type SomeJSONSchema = JSONSchemaType + +export type PartialSchema = Partial> + export type JSONSchemaType = (T extends number ? { type: "number" | "integer" @@ -51,7 +56,7 @@ export type JSONSchemaType = (T extends number required: Partial extends true ? (keyof T)[] : RequiredMembers[] additionalProperties: boolean | JSONSchemaType properties?: { - [K in keyof T]-?: JSONSchemaType & Nullable + [K in keyof T]-?: (JSONSchemaType & Nullable) | {$ref: string} } patternProperties?: { [pattern: string]: JSONSchemaType @@ -71,8 +76,12 @@ export type JSONSchemaType = (T extends number [keyword: string]: any $id?: string $ref?: string - $defs?: {[key: string]: JSONSchemaType} - definitions?: {[key: string]: JSONSchemaType} + $defs?: { + [key: string]: JSONSchemaType + } + definitions?: { + [key: string]: JSONSchemaType + } allOf?: PartialSchema[] anyOf?: PartialSchema[] oneOf?: PartialSchema[] @@ -82,7 +91,9 @@ export type JSONSchemaType = (T extends number not?: PartialSchema } -type PartialSchema = Partial> +type Known = KnownRecord | [Known, ...Known[]] | Known[] | number | string | boolean | null + +interface KnownRecord extends Record {} type RequiredMembers = { [K in keyof T]-?: undefined extends T[K] ? never : K diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts index a1d615abf6..ad6cf2dd61 100644 --- a/spec/types/json-schema.spec.ts +++ b/spec/types/json-schema.spec.ts @@ -10,22 +10,37 @@ interface MyData { [x: string]: string } boo?: true - arr: {id: number}[] tuple?: [number, string] + arr: {id: number}[] map: {[x: string]: number} notBoo?: string // should not be present if "boo" is present } -const mySchema: JSONSchemaType = { - type: "object", - dependencies: { - bar: ["boo"], - boo: {not: {required: ["notBoo"]}}, +const arrSchema: JSONSchemaType = { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "integer", + }, + }, + additionalProperties: false, + required: ["id"], }, - properties: { - foo: {type: "string"}, - bar: {type: "number", nullable: true}, + uniqueItems: true, +} + +const mySchema: JSONSchemaType & { + definitions: { + baz: JSONSchemaType + tuple: JSONSchemaType + } +} = { + type: "object", + definitions: { baz: { + // schema type is checked here ... type: "object", properties: { quux: {type: "string", const: "quux"}, @@ -36,32 +51,30 @@ const mySchema: JSONSchemaType = { additionalProperties: false, required: [], }, - boo: { - type: "boolean", - nullable: true, - enum: [true, null], - }, - arr: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "integer", - }, - }, - additionalProperties: false, - required: ["id"], - }, - uniqueItems: true, - }, tuple: { + // ... and here ... type: "array", items: [{type: "number"}, {type: "string"}], minItems: 2, additionalItems: false, nullable: true, }, + }, + dependencies: { + bar: ["boo"], + boo: {not: {required: ["notBoo"]}}, // optional properties can be cheched in required in PartialSchema + }, + properties: { + foo: {type: "string"}, + bar: {type: "number", nullable: true}, + baz: {$ref: "#/definitions/baz"}, // ... but it does not check type here, ... + boo: { + type: "boolean", + nullable: true, + enum: [true, null], + }, + tuple: {$ref: "#/definitions/tuple"}, // ... nor here. + arr: arrSchema, // ... The alternative is to define it externally - here it checks type map: { type: "object", required: [], From 0451facf35b4c8455fbe6c805c51d5e0c626a62c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 19:03:01 +0100 Subject: [PATCH 217/322] refactor: _addSchema (schema is now always added before validation, not only in recursive cases, for consistency) --- lib/ajv.ts | 92 ++++++++++++++------------------ lib/compile/index.ts | 2 +- lib/{types.ts => types/index.ts} | 11 ++-- lib/vocabularies/core/ref.ts | 1 - 4 files changed, 48 insertions(+), 58 deletions(-) rename lib/{types.ts => types/index.ts} (96%) diff --git a/lib/ajv.ts b/lib/ajv.ts index 1c00166664..d3777e8c72 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -57,6 +57,7 @@ const optsDefaults = { code: {}, loopRequired: Infinity, loopEnum: Infinity, + addUsedSchema: true, } export default class Ajv { @@ -78,7 +79,13 @@ export default class Ajv { static MissingRefError = MissingRefError constructor(opts: Options = {}) { - opts = this.opts = {...optsDefaults, ...opts} + opts = this.opts = { + ...optsDefaults, + ...opts, + serialize: opts.serialize === false ? (x) => x : opts.serialize ?? stableStringify, + addUsedSchema: opts.addUsedSchema ?? true, + validateSchema: opts.validateSchema ?? true, + } this.logger = getLogger(opts.logger) const formatOpt = opts.format opts.format = false @@ -86,7 +93,6 @@ export default class Ajv { this._cache = opts.cache || new Cache() this.RULES = getRules() checkDeprecatedOptions.call(this, opts) - if (opts.serialize === undefined) opts.serialize = stableStringify this._metaOpts = getMetaSchemaOptions.call(this) if (opts.formats) addInitialFormats.call(this) @@ -140,7 +146,7 @@ export default class Ajv { // eslint-disable-next-line @typescript-eslint/unified-signatures compile(schema: Schema, _meta?: boolean): ValidateFunction compile(schema: Schema, _meta?: boolean): ValidateFunction { - const sch = this._addSchema(schema, undefined, _meta) + const sch = this._addSchema(schema, _meta) return sch.validate || this._compileSchemaEnv(sch) } @@ -170,7 +176,7 @@ export default class Ajv { _meta?: boolean ): Promise { await loadMetaSchema.call(this, _schema.$schema) - const sch = this._addSchema(_schema, undefined, _meta) + const sch = this._addSchema(_schema, _meta) return sch.validate || _compileAsync.call(this, sch) } @@ -200,7 +206,7 @@ export default class Ajv { async function loadMissingSchema(this: Ajv, ref: string): Promise { const _schema = await _loadSchema.call(this, ref) if (!this.refs[ref]) await loadMetaSchema.call(this, _schema.$schema) - if (!this.refs[ref]) this.addSchema(_schema, ref, undefined, meta) + if (!this.refs[ref]) this.addSchema(_schema, ref, meta) } async function _loadSchema(this: Ajv, ref: string): Promise { @@ -218,11 +224,11 @@ export default class Ajv { addSchema( schema: Schema | Schema[], // If array is passed, `key` will be ignored key?: string, // Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. - _skipValidation?: boolean, //true to skip schema validation. Used internally, option validateSchema should be used instead. - _meta?: boolean // true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. + _meta?: boolean, // true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. + _validateSchema = this.opts.validateSchema // false to skip schema validation. Used internally, option validateSchema should be used instead. ): Ajv { if (Array.isArray(schema)) { - for (const sch of schema) this.addSchema(sch, undefined, _skipValidation, _meta) + for (const sch of schema) this.addSchema(sch, undefined, _meta, _validateSchema) return this } let id: string | undefined @@ -231,8 +237,8 @@ export default class Ajv { if (id !== undefined && typeof id != "string") throw new Error("schema id must be string") } key = normalizeId(key || id) - checkUnique.call(this, key) - this.schemas[key] = this._addSchema(schema, _skipValidation, _meta, true) + this._checkUnique(key) + this.schemas[key] = this._addSchema(schema, _meta, _validateSchema, true) return this } @@ -241,9 +247,9 @@ export default class Ajv { addMetaSchema( schema: SchemaObject, key?: string, // schema key - skipValidation?: boolean // true to skip schema validation, can be used to override validateSchema option for meta-schema + _validateSchema = this.opts.validateSchema // false to skip schema validation, can be used to override validateSchema option for meta-schema ): Ajv { - this.addSchema(schema, key, skipValidation, true) + this.addSchema(schema, key, true, _validateSchema) return this } @@ -308,8 +314,7 @@ export default class Ajv { return this } case "object": { - const {serialize} = this.opts - const cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef + const cacheKey = this.opts.serialize(schemaKeyRef) this._cache.del(cacheKey) let id = schemaKeyRef.$id if (id) { @@ -430,47 +435,36 @@ export default class Ajv { } } - // TODO refactor private _addSchema( schema: Schema, - skipValidation?: boolean, meta?: boolean, - shouldAddSchema?: boolean + validateSchema = this.opts.validateSchema, + addSchema = this.opts.addUsedSchema ): SchemaEnv { if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema must be object or boolean") } - const {serialize} = this.opts - const cacheKey = serialize ? serialize(schema) : schema - const cached = this._cache.get(cacheKey) - if (cached) return cached - - shouldAddSchema = shouldAddSchema || this.opts.addUsedSchema !== false + const cacheKey = this.opts.serialize(schema) + let sch = this._cache.get(cacheKey) + if (sch) return sch - let $id, $schema - if (typeof schema == "object") { - $id = schema.$id - $schema = schema.$schema + const localRefs = getSchemaRefs.call(this, schema) + sch = new SchemaEnv({schema, cacheKey, meta, localRefs}) + this._cache.put(sch.cacheKey, sch) + const id = sch.baseId + if (addSchema && !id.startsWith("#")) { + // TODO atm it is allowed to overwrite schemas without id (instead of not adding them) + if (id) this._checkUnique(id) + this.refs[id] = sch } - const id = normalizeId($id) - if (id && shouldAddSchema) checkUnique.call(this, id) + if (validateSchema) this.validateSchema(schema, true) + return sch + } - const willValidate = this.opts.validateSchema !== false && !skipValidation - let recursiveMeta - if (willValidate && !(recursiveMeta = id && id === normalizeId($schema))) { - this.validateSchema(schema, true) + private _checkUnique(id: string): void { + if (this.schemas[id] || this.refs[id]) { + throw new Error(`schema with key or id "${id}" already exists`) } - - const localRefs = getSchemaRefs.call(this, schema) - - const sch = new SchemaEnv({schema, baseId: id, localRefs, cacheKey, meta}) - - if (id[0] !== "#" && shouldAddSchema) this.refs[id] = sch - this._cache.put(cacheKey, sch) - - if (willValidate && recursiveMeta) this.validateSchema(schema, true) - - return sch } private _compileSchemaEnv(sch: SchemaEnv): ValidateFunction { @@ -522,12 +516,12 @@ function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | string | undefined { function addDefaultMetaSchema(this: Ajv): void { const {$data, meta} = this.opts - if ($data) this.addMetaSchema($dataRefSchema, $dataRefSchema.$id, true) + if ($data) this.addMetaSchema($dataRefSchema, $dataRefSchema.$id, false) if (meta === false) return const metaSchema = $data ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) : draft7MetaSchema - this.addMetaSchema(metaSchema, META_SCHEMA_ID, true) + this.addMetaSchema(metaSchema, META_SCHEMA_ID, false) this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID } @@ -558,12 +552,6 @@ function addInitialKeywords(this: Ajv, defs: Vocabulary | {[x: string]: KeywordD } } -function checkUnique(this: Ajv, id: string): void { - if (this.schemas[id] || this.refs[id]) { - throw new Error('schema with key or id "' + id + '" already exists') - } -} - function getMetaSchemaOptions(this: Ajv): InstanceOptions { const metaOpts = {...this.opts} for (const opt of META_IGNORE_OPTIONS) delete metaOpts[opt] diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 2362308114..6ede1ec5b9 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -25,7 +25,7 @@ export class SchemaEnv implements SchemaEnvArgs { readonly schema: Schema readonly root: SchemaEnv baseId: string // TODO possibly, it should be readonly - readonly localRefs?: LocalRefs + localRefs?: LocalRefs readonly meta?: boolean readonly cacheKey?: unknown readonly $async?: boolean diff --git a/lib/types.ts b/lib/types/index.ts similarity index 96% rename from lib/types.ts rename to lib/types/index.ts index 2852fe812b..484871d55e 100644 --- a/lib/types.ts +++ b/lib/types/index.ts @@ -1,7 +1,7 @@ -import {CodeGen, Code, Name, CodeGenOptions, Scope} from "./compile/codegen" -import {SchemaEnv} from "./compile" -import KeywordCxt from "./compile/context" -import Ajv from "./ajv" +import type {CodeGen, Code, Name, CodeGenOptions, Scope} from "../compile/codegen" +import type {SchemaEnv} from "../compile" +import type KeywordCxt from "../compile/context" +import type Ajv from "../ajv" export interface SchemaObject { $id?: string @@ -80,6 +80,9 @@ export interface InstanceOptions extends Options { code: CodeOptions loopRequired: number loopEnum: number + serialize: (schema: Schema) => unknown + addUsedSchema: boolean + validateSchema: boolean | "log" } export interface Logger { diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 852c8374f2..7481cfd5b8 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -22,7 +22,6 @@ const def: CodeKeywordDefinition = { function callRootRef(): void { if (env === env.root) return callRef(validateName, env.$async) - // TODO use the same name as compiled function, so it can be dropped in shared scope const rootName = gen.scopeValue("root", {ref: env.root}) return callRef(_`${rootName}.validate`, env.root.$async) } From 71420861d2ea4dbfe99892adf0315252274c95d0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 19:19:49 +0100 Subject: [PATCH 218/322] rename type parameter in JSONSchemaType --- lib/types/json-schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts index 17e3e2a9c6..33875c5ca7 100644 --- a/lib/types/json-schema.ts +++ b/lib/types/json-schema.ts @@ -3,7 +3,7 @@ export type SomeJSONSchema = JSONSchemaType export type PartialSchema = Partial> -export type JSONSchemaType = (T extends number +export type JSONSchemaType = (T extends number ? { type: "number" | "integer" minimum?: number @@ -53,7 +53,7 @@ export type JSONSchemaType = (T extends number type: "object" // "required" type does not guarantee that all required properties are listed // it only asserts that optional cannot be listed - required: Partial extends true ? (keyof T)[] : RequiredMembers[] + required: _allRequired extends true ? (keyof T)[] : RequiredMembers[] additionalProperties: boolean | JSONSchemaType properties?: { [K in keyof T]-?: (JSONSchemaType & Nullable) | {$ref: string} From 0e30a749753464e90ae8e6423bd1da31591c304b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Sep 2020 19:55:07 +0100 Subject: [PATCH 219/322] JSONSchemaType: allow partial properties in partial schemas --- lib/types/json-schema.ts | 12 +++++++----- spec/types/json-schema.spec.ts | 22 ++++++++++++++++++++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts index 33875c5ca7..7e34ff5d78 100644 --- a/lib/types/json-schema.ts +++ b/lib/types/json-schema.ts @@ -3,7 +3,7 @@ export type SomeJSONSchema = JSONSchemaType export type PartialSchema = Partial> -export type JSONSchemaType = (T extends number +export type JSONSchemaType = (T extends number ? { type: "number" | "integer" minimum?: number @@ -53,11 +53,9 @@ export type JSONSchemaType = (T extends number type: "object" // "required" type does not guarantee that all required properties are listed // it only asserts that optional cannot be listed - required: _allRequired extends true ? (keyof T)[] : RequiredMembers[] + required: _partial extends true ? (keyof T)[] : RequiredMembers[] additionalProperties: boolean | JSONSchemaType - properties?: { - [K in keyof T]-?: (JSONSchemaType & Nullable) | {$ref: string} - } + properties?: _partial extends true ? Partial> : PropertiesSchema patternProperties?: { [pattern: string]: JSONSchemaType } @@ -95,6 +93,10 @@ type Known = KnownRecord | [Known, ...Known[]] | Known[] | number | string | boo interface KnownRecord extends Record {} +type PropertiesSchema = { + [K in keyof T]-?: (JSONSchemaType & Nullable) | {$ref: string} +} + type RequiredMembers = { [K in keyof T]-?: undefined extends T[K] ? never : K }[keyof T] diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts index ad6cf2dd61..59229a363c 100644 --- a/spec/types/json-schema.spec.ts +++ b/spec/types/json-schema.spec.ts @@ -1,10 +1,12 @@ import _Ajv from "../ajv" import type {JSONSchemaType} from "../../dist/types/json-schema" import type {SyncSchemaObject} from "../../dist/types" +import chai from "../chai" +const should = chai.should() interface MyData { foo: string - bar?: number // should be present if "boo" is present + bar?: number // "boo" should be present if "bar" is present baz: { quux: "quux" [x: string]: string @@ -14,6 +16,7 @@ interface MyData { arr: {id: number}[] map: {[x: string]: number} notBoo?: string // should not be present if "boo" is present + negativeIfBoo?: number // should be negative if "boo" is present } const arrSchema: JSONSchemaType = { @@ -62,7 +65,14 @@ const mySchema: JSONSchemaType & { }, dependencies: { bar: ["boo"], - boo: {not: {required: ["notBoo"]}}, // optional properties can be cheched in required in PartialSchema + boo: { + not: {required: ["notBoo"]}, // optional properties can be cheched in "required" in PartialSchema + required: ["negativeIfBoo"], + properties: { + // partial properties can be used in partial schemas + negativeIfBoo: {type: "number", nullable: true, exclusiveMaximum: 0}, + }, + }, }, properties: { foo: {type: "string"}, @@ -81,6 +91,7 @@ const mySchema: JSONSchemaType & { additionalProperties: {type: "number"}, }, notBoo: {type: "string", nullable: true}, + negativeIfBoo: {type: "number", nullable: true}, }, additionalProperties: false, required: ["foo", "baz", "arr", "map"], // any other property added here won't typecheck @@ -103,6 +114,7 @@ describe("JSONSchemaType type and validation as a type guard", () => { a: 1, b: 2, }, + negativeIfBoo: -1, } describe("schema has type JSONSchemaType", () => { @@ -111,9 +123,12 @@ describe("JSONSchemaType type and validation as a type guard", () => { if (validate(validData)) { validData.foo.should.equal("foo") } + should.not.exist(validate.errors) + if (ajv.validate(mySchema, validData)) { validData.foo.should.equal("foo") } + should.not.exist(ajv.errors) }) }) @@ -124,9 +139,12 @@ describe("JSONSchemaType type and validation as a type guard", () => { if (validate(validData)) { validData.foo.should.equal("foo") } + should.not.exist(validate.errors) + if (ajv.validate(schema, validData)) { validData.foo.should.equal("foo") } + should.not.exist(ajv.errors) }) }) }) From 80f6f5dcfd83774e585a7c85f4173e7645d60037 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 15 Sep 2020 15:51:59 +0100 Subject: [PATCH 220/322] 7.0.0-alpha.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac721d8169..90d93a00a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv", - "version": "6.12.4", + "version": "7.0.0-alpha.0", "description": "Another JSON Schema Validator", "main": "dist/ajv.js", "typings": "lib/ajv.d.ts", From ad5881e57b65e5bce64c968e894a6063acf2bc61 Mon Sep 17 00:00:00 2001 From: Eric Wyne Date: Tue, 15 Sep 2020 08:37:27 -0700 Subject: [PATCH 221/322] export Typescript types ajv.d.ts is in dist not lib in npm package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90d93a00a2..a244552c7d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "7.0.0-alpha.0", "description": "Another JSON Schema Validator", "main": "dist/ajv.js", - "typings": "lib/ajv.d.ts", + "types": "./dist/ajv.d.ts", "files": [ "lib/", "dist/", From b35e72ca3177b89a1d8fac2a3722213826b9e73b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 08:35:48 +0100 Subject: [PATCH 222/322] test: run format tests from JSON-Schema-Test-Suite with ajv-formats, use "import type" --- lib/ajv.ts | 20 +++++++- lib/compile/codegen/index.ts | 8 ++-- lib/compile/context.ts | 2 +- lib/compile/error_classes.ts | 2 +- lib/compile/errors.ts | 2 +- lib/compile/resolve.ts | 6 +-- lib/compile/rules.ts | 2 +- lib/compile/subschema.ts | 2 +- lib/compile/util.ts | 4 +- lib/compile/validate/applicability.ts | 4 +- lib/compile/validate/boolSchema.ts | 2 +- lib/compile/validate/dataType.ts | 6 +-- lib/compile/validate/defaults.ts | 2 +- lib/compile/validate/index.ts | 2 +- lib/compile/validate/iterate.ts | 4 +- lib/compile/validate/keyword.ts | 8 +++- lib/types/index.ts | 30 +++++------- .../applicator/additionalItems.ts | 4 +- .../applicator/additionalProperties.ts | 2 +- lib/vocabularies/applicator/allOf.ts | 4 +- lib/vocabularies/applicator/anyOf.ts | 4 +- lib/vocabularies/applicator/contains.ts | 4 +- lib/vocabularies/applicator/dependencies.ts | 4 +- lib/vocabularies/applicator/if.ts | 4 +- lib/vocabularies/applicator/index.ts | 2 +- lib/vocabularies/applicator/items.ts | 4 +- lib/vocabularies/applicator/not.ts | 4 +- lib/vocabularies/applicator/oneOf.ts | 4 +- .../applicator/patternProperties.ts | 4 +- lib/vocabularies/applicator/properties.ts | 2 +- lib/vocabularies/applicator/propertyNames.ts | 4 +- lib/vocabularies/applicator/thenElse.ts | 5 +- lib/vocabularies/core/index.ts | 2 +- lib/vocabularies/core/ref.ts | 4 +- lib/vocabularies/format/format.ts | 19 ++++++-- lib/vocabularies/format/index.ts | 2 +- lib/vocabularies/metadata.ts | 2 +- lib/vocabularies/missing.ts | 2 +- lib/vocabularies/util.ts | 4 +- lib/vocabularies/validation/const.ts | 4 +- lib/vocabularies/validation/enum.ts | 4 +- lib/vocabularies/validation/index.ts | 2 +- lib/vocabularies/validation/limit.ts | 5 +- lib/vocabularies/validation/limitItems.ts | 4 +- lib/vocabularies/validation/limitLength.ts | 4 +- .../validation/limitProperties.ts | 4 +- lib/vocabularies/validation/multipleOf.ts | 5 +- lib/vocabularies/validation/pattern.ts | 4 +- lib/vocabularies/validation/required.ts | 4 +- lib/vocabularies/validation/uniqueItems.ts | 4 +- package.json | 2 +- spec/json-schema.spec.ts | 46 ++++++++----------- spec/schema-tests.spec.ts | 3 +- 53 files changed, 154 insertions(+), 137 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index d3777e8c72..6432042165 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -1,4 +1,19 @@ -import { +export { + Format, + FormatDefinition, + AsyncFormatDefinition, + KeywordDefinition, + Vocabulary, +} from "./types" +export interface Plugin { + (ajv: Ajv, options?: Opts): Ajv + [prop: string]: any +} + +import KeywordCxt from "./compile/context" +export {KeywordCxt} + +import type { Schema, SchemaObject, SyncSchemaObject, @@ -17,7 +32,7 @@ import { Format, AddedFormat, } from "./types" -import {JSONSchemaType} from "./types/json-schema" +import type {JSONSchemaType} from "./types/json-schema" import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import {getRules, ValidationRules, Rule, RuleGroup} from "./compile/rules" @@ -630,3 +645,4 @@ function schemaOrData(schema: Schema): SchemaObject { } module.exports = Ajv +module.exports.default = Ajv diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index f4163bb695..117795bf2d 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -1,7 +1,9 @@ -import {_, str, nil, _Code, Code, Name, getProperty, stringify} from "./code" -import {Scope, ScopeValueSets, NameValue, ScopeStore, ValueScope, ValueScopeName} from "./scope" +import type {ScopeValueSets, NameValue, ValueScope, ValueScopeName} from "./scope" +import {_, nil, _Code, Code, Name} from "./code" +import {Scope} from "./scope" -export {_, str, nil, getProperty, stringify, Name, Code, Scope, ScopeStore, ValueScope} +export {_, str, nil, getProperty, stringify, Name, Code} from "./code" +export {Scope, ScopeStore, ValueScope} from "./scope" enum BlockKind { If, diff --git a/lib/compile/context.ts b/lib/compile/context.ts index a0487e4bc5..2ba9f318c9 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -1,4 +1,4 @@ -import { +import type { KeywordDefinition, KeywordErrorCxt, KeywordCxtParams, diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts index 1b72350f2c..5b842ef389 100644 --- a/lib/compile/error_classes.ts +++ b/lib/compile/error_classes.ts @@ -1,4 +1,4 @@ -import {ErrorObject} from "../types" +import type {ErrorObject} from "../types" import {resolveUrl, normalizeId, getFullPath} from "./resolve" export class ValidationError extends Error { diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 4b2bd74615..0ccd54845e 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,4 +1,4 @@ -import {KeywordErrorCxt, KeywordErrorDefinition, SchemaCxt} from "../types" +import type {KeywordErrorCxt, KeywordErrorDefinition, SchemaCxt} from "../types" import {CodeGen, _, str, Code, Name} from "./codegen" import N from "./names" diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 311fd6cd3c..5d5087e563 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,8 +1,8 @@ -import {Schema, SchemaObject} from "../types" +import type {Schema, SchemaObject} from "../types" +import type Ajv from "../ajv" import {eachItem, toHash} from "./util" -import Ajv from "../ajv" import equal from "fast-deep-equal" -import traverse = require("json-schema-traverse") +import traverse from "json-schema-traverse" import URI = require("uri-js") // the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index cbadc81e62..53ba254ab9 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -1,5 +1,5 @@ +import type {KeywordDefinition} from "../types" import {toHash} from "./util" -import {KeywordDefinition} from "../types" interface ValidationTypes { [key: string]: boolean | RuleGroup | undefined diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index b8f7b4391f..9986d83979 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,4 +1,4 @@ -import {Schema, SchemaObjCxt} from "../types" +import type {Schema, SchemaObjCxt} from "../types" import {subschemaCode} from "./validate" import {escapeFragment, escapeJsonPointer} from "./util" import {_, str, Code, Name, getProperty} from "./codegen" diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 86126343c8..d7318ebb91 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,7 +1,7 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" -import {SchemaCxt, Schema} from "../types" +import type {SchemaCxt, Schema} from "../types" +import type {Rule, ValidationRules} from "./rules" import N from "./names" -import {Rule, ValidationRules} from "./rules" export enum DataType { Correct, diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index dd715f28ec..e8704809cb 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,5 +1,5 @@ -import {SchemaObjCxt, SchemaObject} from "../../types" -import {RuleGroup, Rule} from "../rules" +import type {SchemaObjCxt, SchemaObject} from "../../types" +import type {RuleGroup, Rule} from "../rules" export function schemaHasRulesForType( {schema, self}: SchemaObjCxt, diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index ead473ba63..879fcba4e0 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,4 +1,4 @@ -import {KeywordErrorDefinition, SchemaCxt, KeywordErrorCxt} from "../../types" +import type {KeywordErrorDefinition, SchemaCxt, KeywordErrorCxt} from "../../types" import {reportError} from "../errors" import {_, Name} from "../codegen" import N from "../names" diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index db371ad2cd..6ed95408a4 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,10 +1,10 @@ -import {SchemaObjCxt, KeywordErrorDefinition, KeywordErrorCxt, SchemaObject} from "../../types" +import type {SchemaObjCxt, KeywordErrorDefinition, KeywordErrorCxt, SchemaObject} from "../../types" +import type {ValidationRules} from "../rules" +import {schemaHasRulesForType} from "./applicability" import {toHash, checkDataTypes, DataType} from "../util" import {schemaRefOrVal} from "../../vocabularies/util" -import {schemaHasRulesForType} from "./applicability" import {reportError} from "../errors" import {_, str, Name} from "../codegen" -import {ValidationRules} from "../rules" export function getSchemaTypes({self}: SchemaObjCxt, schema: SchemaObject): string[] { const st: undefined | string | string[] = schema.type diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 84b7c1ca13..2a30cab817 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,4 +1,4 @@ -import {SchemaObjCxt} from "../../types" +import type {SchemaObjCxt} from "../../types" import {_, getProperty, stringify} from "../codegen" import {checkStrictMode} from "../../vocabularies/util" diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 0ff0b92d8f..f5b674ac9b 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,4 +1,4 @@ -import {Schema, SchemaCxt, SchemaObjCxt, Options} from "../../types" +import type {Schema, SchemaCxt, SchemaObjCxt, Options} from "../../types" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 3a82e98d3b..2283eb02e1 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -1,10 +1,10 @@ -import {SchemaObjCxt} from "../../types" +import type {SchemaObjCxt} from "../../types" +import type {Rule, RuleGroup} from "../rules" import {shouldUseGroup, shouldUseRule} from "./applicability" import {checkDataType, schemaHasRulesButRef} from "../util" import {keywordCode} from "./keyword" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" -import {Rule, RuleGroup} from "../rules" import {_, Name} from "../codegen" import N from "../names" diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 54279d9484..290f980947 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -1,9 +1,11 @@ -import { +import type { KeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, + Schema, SchemaObjCxt, - KeywordCompilationResult, + SchemaValidateFunction, + ValidateFunction, } from "../../types" import KeywordCxt from "../context" import {applySubschema} from "../subschema" @@ -12,6 +14,8 @@ import {callValidateCode} from "../../vocabularies/util" import {CodeGen, _, nil, Code, Name} from "../codegen" import N from "../names" +type KeywordCompilationResult = Schema | SchemaValidateFunction | ValidateFunction + export function keywordCode( it: SchemaObjCxt, keyword: string, diff --git a/lib/types/index.ts b/lib/types/index.ts index 484871d55e..dc338cff89 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -172,8 +172,6 @@ export interface ErrorObject { data?: unknown } -export type KeywordCompilationResult = Schema | SchemaValidateFunction | ValidateFunction - export interface SchemaCxt { gen: CodeGen allErrors?: boolean @@ -277,36 +275,32 @@ export interface KeywordCxtParams { [x: string]: Code | string | number | undefined } -export type FormatMode = "fast" | "full" - -type SN = string | number +export type FormatValidator = (data: T) => boolean -export type FormatValidator = (data: T) => boolean +export type FormatCompare = (data1: T, data2: T) => boolean -export type FormatCompare = (data1: T, data2: T) => boolean +export type AsyncFormatValidator = (data: T) => Promise -export type AsyncFormatValidator = (data: T) => Promise - -export interface FormatDefinition { - type?: T extends string ? "string" : "number" +export interface FormatDefinition { + type: T extends string ? "string" | undefined : "number" validate: FormatValidator | (T extends string ? string | RegExp : never) async?: false | undefined compare?: FormatCompare } -export interface AsyncFormatDefinition { - type?: T extends string ? "string" : "number" +export interface AsyncFormatDefinition { + type: T extends string ? "string" | undefined : "number" validate: AsyncFormatValidator async: true compare?: FormatCompare } -export type FormatValidate = FormatValidator | AsyncFormatValidator | RegExp - export type AddedFormat = | RegExp - | FormatValidator // TODO should be string, not SN - | FormatDefinition - | AsyncFormatDefinition + | FormatValidator + | FormatDefinition + | FormatDefinition + | AsyncFormatDefinition + | AsyncFormatDefinition export type Format = AddedFormat | string diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 72428c629c..fc79b6b9bd 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 1f86f23690..109fe658a1 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition, KeywordErrorCxt} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorCxt} from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index f7f2c37ead..6b86d20035 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition, Schema} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index cf5df70299..ddc73e62fb 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition, Schema} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index 14517c7718..e4b0aa28bd 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 4b1f1a67d3..7075d6970a 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, SchemaMap, Schema} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition, SchemaMap, Schema} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 54ae9b94cc..af8997c208 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, SchemaObjCxt} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition, SchemaObjCxt} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 6abc401b18..086c1ad13f 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -1,4 +1,4 @@ -import {Vocabulary} from "../../types" +import type {Vocabulary} from "../../types" const applicator: Vocabulary = [ // array diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 82db4292d9..30ffaadac8 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition, Schema} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index e986e2c52e..bb183fa776 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index ed4289d982..fc58cae7d2 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition, Schema} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index cdef59873e..7d11ea4cc3 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {schemaProperties, usePattern, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index cf0cb89ce7..493af3b1c9 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -1,4 +1,4 @@ -import {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition} from "../../types" import KeywordCxt from "../../compile/context" import {schemaProperties, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index fffdbe33d3..42e8638df5 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" diff --git a/lib/vocabularies/applicator/thenElse.ts b/lib/vocabularies/applicator/thenElse.ts index 5e9d1e58e0..ba0541a197 100644 --- a/lib/vocabularies/applicator/thenElse.ts +++ b/lib/vocabularies/applicator/thenElse.ts @@ -1,10 +1,11 @@ -import {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {checkStrictMode} from "../util" const def: CodeKeywordDefinition = { keyword: ["then", "else"], schemaType: ["object", "boolean"], - code({keyword, parentSchema, it}) { + code({keyword, parentSchema, it}: KeywordCxt) { if (parentSchema.if === undefined) checkStrictMode(it, `"${keyword}" without "if" is ignored`) }, } diff --git a/lib/vocabularies/core/index.ts b/lib/vocabularies/core/index.ts index 5f01ad9fc9..5845d0384b 100644 --- a/lib/vocabularies/core/index.ts +++ b/lib/vocabularies/core/index.ts @@ -1,4 +1,4 @@ -import {Vocabulary} from "../../types" +import type {Vocabulary} from "../../types" const core: Vocabulary = ["$schema", "$id", "$defs", "definitions", require("./ref")] diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 7481cfd5b8..49c9a1b1bd 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition, Schema} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition, Schema} from "../../types" +import type KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {callValidateCode} from "../util" diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index cc9e18510e..22b6e4ca29 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -1,8 +1,21 @@ -import {CodeKeywordDefinition, AddedFormat, FormatValidate} from "../../types" -import KeywordCxt from "../../compile/context" +import type { + AddedFormat, + FormatValidator, + AsyncFormatValidator, + CodeKeywordDefinition, +} from "../../types" +import type KeywordCxt from "../../compile/context" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" import N from "../../compile/names" +type FormatValidate = + | FormatValidator + | FormatValidator + | AsyncFormatValidator + | AsyncFormatValidator + | RegExp + | string + const def: CodeKeywordDefinition = { keyword: "format", type: ["number", "string"], @@ -79,7 +92,7 @@ const def: CodeKeywordDefinition = { code: opts.code.formats ? _`${opts.code.formats}${getProperty(schema)}` : undefined, }) if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) { - return [fmtDef.type || "string", fmtDef.validate as FormatValidate, _`${fmt}.validate`] + return [fmtDef.type || "string", fmtDef.validate, _`${fmt}.validate`] } return ["string", fmtDef, fmt] diff --git a/lib/vocabularies/format/index.ts b/lib/vocabularies/format/index.ts index 06fa12881c..9763393d67 100644 --- a/lib/vocabularies/format/index.ts +++ b/lib/vocabularies/format/index.ts @@ -1,4 +1,4 @@ -import {Vocabulary} from "../../types" +import type {Vocabulary} from "../../types" const format: Vocabulary = [require("./format")] diff --git a/lib/vocabularies/metadata.ts b/lib/vocabularies/metadata.ts index 81216f3af4..b9d5af85fe 100644 --- a/lib/vocabularies/metadata.ts +++ b/lib/vocabularies/metadata.ts @@ -1,4 +1,4 @@ -import {Vocabulary} from "../types" +import type {Vocabulary} from "../types" export const metadataVocabulary: Vocabulary = [ "title", diff --git a/lib/vocabularies/missing.ts b/lib/vocabularies/missing.ts index bd6ca3e0de..08e650cd28 100644 --- a/lib/vocabularies/missing.ts +++ b/lib/vocabularies/missing.ts @@ -1,4 +1,4 @@ -import KeywordCxt from "../compile/context" +import type KeywordCxt from "../compile/context" import {noPropertyInData} from "./util" import {_, or, Code, Name} from "../compile/codegen" diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 819d63d1a5..2482b92183 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,6 +1,6 @@ +import type {Schema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" +import type KeywordCxt from "../compile/context" import {schemaHasRules} from "../compile/util" -import {Schema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" -import KeywordCxt from "../compile/context" import {CodeGen, _, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 2ac84e0c25..648ae97a6e 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {_} from "../../compile/codegen" import equal from "fast-deep-equal" diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index fc59201f51..84e973e85a 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" import equal from "fast-deep-equal" diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 5a30422de0..116ce17590 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -1,4 +1,4 @@ -import {Vocabulary} from "../../types" +import type {Vocabulary} from "../../types" const validation: Vocabulary = [ // number diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 8103a6a249..a5bf82de29 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,6 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" -// import {bad$DataType} from "../util" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {_, str, operators, Code} from "../../compile/codegen" const ops = operators diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index 41802cb680..e7615fb3a5 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 2fa735435b..448b63dc15 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" import ucs2length from "../../compile/ucs2length" diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index d64be87a56..04e7e8ba8c 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" const def: CodeKeywordDefinition = { diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 8df5f783a3..c4298dfc72 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,6 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" -// import {bad$DataType} from "../util" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {_, str} from "../../compile/codegen" const def: CodeKeywordDefinition = { diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 251155309f..e4ceefce9c 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {usePattern} from "../util" import {_, str} from "../../compile/codegen" diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 3d367b9f4e..85a3b85286 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {_, str, nil, Name} from "../../compile/codegen" diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 80e129f8db..02da608607 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,5 +1,5 @@ -import {CodeKeywordDefinition} from "../../types" -import KeywordCxt from "../../compile/context" +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {getSchemaTypes} from "../../compile/validate/dataType" import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" diff --git a/package.json b/package.json index 90d93a00a2..0b3b739381 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "7.0.0-alpha.0", "description": "Another JSON Schema Validator", "main": "dist/ajv.js", - "typings": "lib/ajv.d.ts", + "types": "dist/ajv.d.ts", "files": [ "lib/", "dist/", diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index 003a40b111..100134cb98 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -3,6 +3,7 @@ import getAjvInstances from "./ajv_instances" import jsonSchemaTest from "json-schema-test" import options from "./ajv_options" import {afterError, afterEach} from "./after_test" +import addFormats from "ajv-formats" const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), @@ -13,47 +14,36 @@ const remoteRefs = { "http://localhost:1234/name.json": require("./JSON-Schema-Test-Suite/remotes/name.json"), } -const SKIP6 = [ - "format", - "optional/format/date", - "optional/format/date-time", - "optional/format/email", - "optional/format/hostname", - "optional/format/ipv4", - "optional/format/ipv6", - "optional/format/json-pointer", - "optional/format/uri", - "optional/format/uri-reference", - "optional/format/uri-template", -] - const SKIP = { - 6: SKIP6, - 7: SKIP6.concat([ + 6: [], + 7: [ "optional/content", "optional/format/idn-email", "optional/format/idn-hostname", "optional/format/iri", "optional/format/iri-reference", - "optional/format/regex", - "optional/format/relative-json-pointer", - "optional/format/time", - ]), + ], } runTest(getAjvInstances(options, {meta: false, strict: false}), 6, require("./_json/draft6")) -runTest(getAjvInstances(options, {strict: false}), 7, require("./_json/draft7")) +runTest( + getAjvInstances(options, { + strict: false, + unknownFormats: ["idn-email", "idn-hostname", "iri", "iri-reference"], + }), + 7, + require("./_json/draft7") +) function runTest(instances: Ajv[], draft: number, tests) { - instances.forEach((ajv: Ajv) => { - switch (draft) { - case 6: - ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) - ajv.opts.defaultMeta = "http://json-schema.org/draft-06/schema#" - break + for (const ajv of instances) { + if (draft === 6) { + ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) + ajv.opts.defaultMeta = "http://json-schema.org/draft-06/schema#" } for (const id in remoteRefs) ajv.addSchema(remoteRefs[id], id) - }) + addFormats(ajv) + } jsonSchemaTest(instances, { description: `JSON-Schema Test Suite draft-0${draft}: ${instances.length} ajv instances with different options`, diff --git a/spec/schema-tests.spec.ts b/spec/schema-tests.spec.ts index 16aefe0166..3d70c60714 100644 --- a/spec/schema-tests.spec.ts +++ b/spec/schema-tests.spec.ts @@ -2,8 +2,7 @@ import getAjvInstances from "./ajv_instances" import jsonSchemaTest from "json-schema-test" import options from "./ajv_options" import {afterError, afterEach} from "./after_test" - -const addFormats = require("ajv-formats") +import addFormats from "ajv-formats" const instances = getAjvInstances(options, {strict: false, unknownFormats: ["allowedUnknown"]}) From 07fd0c05886ca52e3d825950dd76c6862f1f4924 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 14:32:40 +0100 Subject: [PATCH 223/322] types for async validation (with $async tag in schema) --- lib/ajv.ts | 96 +++++++-------- lib/compile/context.ts | 4 +- lib/compile/index.ts | 28 +++-- lib/compile/resolve.ts | 14 +-- lib/compile/subschema.ts | 6 +- lib/compile/util.ts | 6 +- lib/compile/validate/applicability.ts | 6 +- lib/compile/validate/dataType.ts | 9 +- lib/compile/validate/index.ts | 4 +- lib/compile/validate/keyword.ts | 6 +- lib/refs/data.json | 2 +- lib/refs/json-schema-secure.json | 2 +- lib/types/index.ts | 124 ++++++++++--------- lib/types/json-schema.ts | 4 +- lib/vocabularies/applicator/allOf.ts | 4 +- lib/vocabularies/applicator/anyOf.ts | 6 +- lib/vocabularies/applicator/dependencies.ts | 4 +- lib/vocabularies/applicator/items.ts | 6 +- lib/vocabularies/applicator/oneOf.ts | 6 +- lib/vocabularies/core/ref.ts | 4 +- lib/vocabularies/util.ts | 8 +- package.json | 4 +- spec/async.spec.ts | 4 +- spec/resolve.spec.ts | 4 +- spec/types/async-validate.spec.ts | 127 ++++++++++++-------- spec/types/json-schema.spec.ts | 6 +- 26 files changed, 262 insertions(+), 232 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 6432042165..9f3c9e00bd 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -15,16 +15,16 @@ export {KeywordCxt} import type { Schema, + AnySchema, + AnySchemaObject, SchemaObject, - SyncSchemaObject, - AsyncSchemaObject, + AsyncSchema, Vocabulary, KeywordDefinition, Options, InstanceOptions, + AnyValidateFunction, ValidateFunction, - ValidateGuard, - SyncValidateFunction, AsyncValidateFunction, CacheInterface, Logger, @@ -86,7 +86,7 @@ export default class Ajv { readonly formats: {[name: string]: AddedFormat | undefined} = {} readonly RULES: ValidationRules readonly _compilations: Set = new Set() - private readonly _loading: {[ref: string]: Promise | undefined} = {} + private readonly _loading: {[ref: string]: Promise | undefined} = {} private readonly _cache: CacheInterface private readonly _metaOpts: InstanceOptions @@ -126,19 +126,16 @@ export default class Ajv { } // Validate data using schema - // Schema will be compiled and cached using as a key JSON serialized with + // AnySchema will be compiled and cached using as a key JSON serialized with // [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) - validate(schema: {$async?: never}, data: unknown): boolean | Promise - validate(schema: SyncSchemaObject | boolean, data: unknown): boolean - validate(schema: SyncSchemaObject | JSONSchemaType, data: unknown): data is T - validate(schema: AsyncSchemaObject, data: unknown): Promise - // eslint-disable-next-line @typescript-eslint/unified-signatures - validate(schemaKeyRef: Schema | string, data: unknown): boolean | Promise - validate( - schemaKeyRef: Schema | string, // key, ref or schema object + validate(schema: Schema | JSONSchemaType | string, data: unknown): data is T + validate(schema: AsyncSchema, data: unknown): Promise + validate(schemaKeyRef: AnySchema | string, data: unknown): data is T | Promise + validate( + schemaKeyRef: AnySchema | string, // key, ref or schema object data: unknown // to be validated - ): boolean | Promise { - let v: ValidateFunction | undefined + ): boolean | Promise { + let v: AnyValidateFunction | undefined if (typeof schemaKeyRef == "string") { v = this.getSchema(schemaKeyRef) if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"') @@ -148,37 +145,32 @@ export default class Ajv { } const valid = v(data) - if (v.$async !== true) this.errors = v.errors + if (!("$async" in v)) this.errors = v.errors return valid } // Create validation function for passed schema // _meta: true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. - compile(schema: {$async?: never}, _meta?: boolean): ValidateFunction - compile(schema: SyncSchemaObject | boolean, _meta?: boolean): SyncValidateFunction - compile(schema: SyncSchemaObject | JSONSchemaType, _meta?: boolean): ValidateGuard - compile(schema: AsyncSchemaObject, _meta?: boolean): AsyncValidateFunction - // eslint-disable-next-line @typescript-eslint/unified-signatures - compile(schema: Schema, _meta?: boolean): ValidateFunction - compile(schema: Schema, _meta?: boolean): ValidateFunction { + compile(schema: Schema | JSONSchemaType, _meta?: boolean): ValidateFunction + compile(schema: AsyncSchema, _meta?: boolean): AsyncValidateFunction + compile(schema: AnySchema, _meta?: boolean): AnyValidateFunction + compile(schema: AnySchema, _meta?: boolean): AnyValidateFunction { const sch = this._addSchema(schema, _meta) - return sch.validate || this._compileSchemaEnv(sch) + return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction } // Creates validating function for passed schema with asynchronous loading of missing schemas. // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. // TODO allow passing schema URI // meta - optional true to compile meta-schema - compileAsync(schema: {$async?: never}, _meta?: boolean): Promise - compileAsync(schema: SyncSchemaObject, meta?: boolean): Promise - compileAsync( - schema: SyncSchemaObject | JSONSchemaType, + compileAsync( + schema: SchemaObject | JSONSchemaType, _meta?: boolean - ): Promise> - compileAsync(schema: AsyncSchemaObject, meta?: boolean): Promise + ): Promise> + compileAsync(schema: AsyncSchema, meta?: boolean): Promise> // eslint-disable-next-line @typescript-eslint/unified-signatures - compileAsync(schema: SchemaObject, meta?: boolean): Promise - compileAsync(schema: SchemaObject, meta?: boolean): Promise { + compileAsync(schema: AnySchemaObject, meta?: boolean): Promise> + compileAsync(schema: AnySchemaObject, meta?: boolean): Promise> { if (typeof this.opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") } @@ -187,9 +179,9 @@ export default class Ajv { async function runCompileAsync( this: Ajv, - _schema: SchemaObject, + _schema: AnySchemaObject, _meta?: boolean - ): Promise { + ): Promise { await loadMetaSchema.call(this, _schema.$schema) const sch = this._addSchema(_schema, _meta) return sch.validate || _compileAsync.call(this, sch) @@ -201,7 +193,7 @@ export default class Ajv { } } - async function _compileAsync(this: Ajv, sch: SchemaEnv): Promise { + async function _compileAsync(this: Ajv, sch: SchemaEnv): Promise { try { return this._compileSchemaEnv(sch) } catch (e) { @@ -214,7 +206,7 @@ export default class Ajv { function checkLoaded(this: Ajv, {missingSchema: ref, missingRef}: MissingRefError): void { if (this.refs[ref]) { - throw new Error(`Schema ${ref} is loaded but ${missingRef} cannot be resolved`) + throw new Error(`AnySchema ${ref} is loaded but ${missingRef} cannot be resolved`) } } @@ -224,7 +216,7 @@ export default class Ajv { if (!this.refs[ref]) this.addSchema(_schema, ref, meta) } - async function _loadSchema(this: Ajv, ref: string): Promise { + async function _loadSchema(this: Ajv, ref: string): Promise { const p = this._loading[ref] if (p) return p try { @@ -237,7 +229,7 @@ export default class Ajv { // Adds schema to the instance addSchema( - schema: Schema | Schema[], // If array is passed, `key` will be ignored + schema: AnySchema | AnySchema[], // If array is passed, `key` will be ignored key?: string, // Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. _meta?: boolean, // true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. _validateSchema = this.opts.validateSchema // false to skip schema validation. Used internally, option validateSchema should be used instead. @@ -260,7 +252,7 @@ export default class Ajv { // Add schema that will be used to validate other schemas // options in META_IGNORE_OPTIONS are alway set to false addMetaSchema( - schema: SchemaObject, + schema: AnySchemaObject, key?: string, // schema key _validateSchema = this.opts.validateSchema // false to skip schema validation, can be used to override validateSchema option for meta-schema ): Ajv { @@ -269,9 +261,9 @@ export default class Ajv { } // Validate schema against its meta-schema - validateSchema(schema: Schema, throwOrLogError?: boolean): boolean | Promise { + validateSchema(schema: AnySchema, throwOrLogError?: boolean): boolean | Promise { if (typeof schema == "boolean") return true - let $schema: string | SchemaObject | undefined + let $schema: string | AnySchemaObject | undefined $schema = schema.$schema if ($schema !== undefined && typeof $schema != "string") { throw new Error("$schema must be a string") @@ -293,7 +285,7 @@ export default class Ajv { // Get compiled schema by `key` or `ref`. // (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id) - getSchema(keyRef: string): ValidateFunction | undefined { + getSchema(keyRef: string): AnyValidateFunction | undefined { let sch while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch if (sch === undefined) { @@ -302,14 +294,14 @@ export default class Ajv { if (!sch) return this.refs[keyRef] = sch } - return sch.validate || this._compileSchemaEnv(sch) + return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction | undefined } // Remove cached schema(s). // If no parameter is passed all schemas but meta-schemas are removed. // If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. // Even if schema is referenced by other schemas it still can be removed as other schemas have local references. - removeSchema(schemaKeyRef: Schema | string | RegExp): Ajv { + removeSchema(schemaKeyRef: AnySchema | string | RegExp): Ajv { if (schemaKeyRef instanceof RegExp) { this._removeAllSchemas(this.schemas, schemaKeyRef) this._removeAllSchemas(this.refs, schemaKeyRef) @@ -413,19 +405,19 @@ export default class Ajv { .reduce((text, msg) => text + msg + separator) } - $dataMetaSchema(metaSchema: SchemaObject, keywordsJsonPointers: string[]): SchemaObject { + $dataMetaSchema(metaSchema: AnySchemaObject, keywordsJsonPointers: string[]): AnySchemaObject { const rules = this.RULES.all for (const jsonPointer of keywordsJsonPointers) { metaSchema = JSON.parse(JSON.stringify(metaSchema)) const segments = jsonPointer.split("/").slice(1) // first segment is an empty string let keywords = metaSchema - for (const seg of segments) keywords = keywords[seg] as SchemaObject + for (const seg of segments) keywords = keywords[seg] as AnySchemaObject for (const key in rules) { const rule = rules[key] if (typeof rule != "object") continue const {$data} = rule.definition - const schema = keywords[key] as SchemaObject | undefined + const schema = keywords[key] as AnySchemaObject | undefined if ($data && schema) keywords[key] = schemaOrData(schema) } } @@ -451,7 +443,7 @@ export default class Ajv { } private _addSchema( - schema: Schema, + schema: AnySchema, meta?: boolean, validateSchema = this.opts.validateSchema, addSchema = this.opts.addUsedSchema @@ -482,7 +474,7 @@ export default class Ajv { } } - private _compileSchemaEnv(sch: SchemaEnv): ValidateFunction { + private _compileSchemaEnv(sch: SchemaEnv): AnyValidateFunction { if (sch.meta) this._compileMetaSchema(sch) else compileSchema.call(this, sch) if (!sch.validate) throw new Error("ajv implementation error") @@ -513,7 +505,7 @@ function checkDeprecatedOptions(this: Ajv, opts: Options): void { if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") } -function defaultMeta(this: Ajv): string | SchemaObject | undefined { +function defaultMeta(this: Ajv): string | AnySchemaObject | undefined { const {meta} = this.opts this.opts.defaultMeta = typeof meta == "object" @@ -640,7 +632,7 @@ const $dataRef = { $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", } -function schemaOrData(schema: Schema): SchemaObject { +function schemaOrData(schema: AnySchema): AnySchemaObject { return {anyOf: [schema, $dataRef]} } diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 2ba9f318c9..bdc0ab2a6b 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -3,7 +3,7 @@ import type { KeywordErrorCxt, KeywordCxtParams, SchemaObjCxt, - SchemaObject, + AnySchemaObject, } from "../types" import {schemaRefOrVal} from "../vocabularies/util" import {getData, checkDataTypes, DataType} from "./util" @@ -27,7 +27,7 @@ export default class KeywordCxt implements KeywordErrorCxt { readonly schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value readonly schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) readonly schemaType?: string | string[] - readonly parentSchema: SchemaObject + readonly parentSchema: AnySchemaObject readonly errsCount?: Name params: KeywordCxtParams readonly it: SchemaObjCxt diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 6ede1ec5b9..18f09db02a 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,4 +1,10 @@ -import type {Schema, SchemaObject, ValidateFunction, SchemaCxt} from "../types" +import type { + AnySchema, + AnySchemaObject, + AnyValidateFunction, + AsyncValidateFunction, + SchemaCxt, +} from "../types" import type Ajv from "../ajv" import {CodeGen, _, nil, str, Name} from "./codegen" import {ValidationError} from "./error_classes" @@ -9,11 +15,11 @@ import {validateFunctionCode} from "./validate" import URI = require("uri-js") export interface SchemaRefs { - [ref: string]: Schema | ValidateFunction | undefined + [ref: string]: SchemaEnv | AnySchema | undefined } interface SchemaEnvArgs { - schema: Schema + schema: AnySchema root?: SchemaEnv baseId?: string localRefs?: LocalRefs @@ -22,7 +28,7 @@ interface SchemaEnvArgs { } export class SchemaEnv implements SchemaEnvArgs { - readonly schema: Schema + readonly schema: AnySchema readonly root: SchemaEnv baseId: string // TODO possibly, it should be readonly localRefs?: LocalRefs @@ -30,11 +36,11 @@ export class SchemaEnv implements SchemaEnvArgs { readonly cacheKey?: unknown readonly $async?: boolean readonly refs: SchemaRefs = {} - validate?: ValidateFunction + validate?: AnyValidateFunction validateName?: Name constructor(env: SchemaEnvArgs) { - let schema: SchemaObject | undefined + let schema: AnySchemaObject | undefined if (typeof env.schema == "object") schema = env.schema this.schema = env.schema this.root = env.root || this @@ -96,13 +102,13 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { if (this.opts.processCode) sourceCode = this.opts.processCode(sourceCode, sch) // console.log("\n\n\n *** \n", sourceCode) const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode) - const validate: ValidateFunction = makeValidate(this, this.scope.get()) + const validate: AnyValidateFunction = makeValidate(this, this.scope.get()) gen.scopeValue(validateName, {ref: validate}) validate.errors = null validate.schema = sch.schema validate.schemaEnv = sch - if (sch.$async) validate.$async = true + if (sch.$async) (validate as AsyncValidateFunction).$async = true if (this.opts.sourceCode === true) { validate.source = { code: sourceCode, @@ -126,7 +132,7 @@ export function resolveRef( root: SchemaEnv, baseId: string, ref: string -): Schema | ValidateFunction | SchemaEnv | undefined { +): AnySchema | AnyValidateFunction | SchemaEnv | undefined { ref = resolveUrl(baseId, ref) const schOrFunc = root.refs[ref] if (schOrFunc) return schOrFunc @@ -141,7 +147,7 @@ export function resolveRef( return (root.refs[ref] = inlineOrCompile.call(this, _sch)) } -function inlineOrCompile(this: Ajv, sch: SchemaEnv): Schema | SchemaEnv { +function inlineOrCompile(this: Ajv, sch: SchemaEnv): AnySchema | SchemaEnv { if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema return sch.validate ? sch : compileSchema.call(this, sch) } @@ -158,7 +164,7 @@ function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean { } // resolve and compile the references ($ref) -// TODO returns SchemaObject (if the schema can be inlined) or validation function +// TODO returns AnySchemaObject (if the schema can be inlined) or validation function function resolve( this: Ajv, root: SchemaEnv, // information about the root schema for the current schema diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 5d5087e563..72ecf3e25a 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,4 +1,4 @@ -import type {Schema, SchemaObject} from "../types" +import type {AnySchema, AnySchemaObject} from "../types" import type Ajv from "../ajv" import {eachItem, toHash} from "./util" import equal from "fast-deep-equal" @@ -7,7 +7,7 @@ import URI = require("uri-js") // the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution export interface LocalRefs { - [ref: string]: SchemaObject | undefined + [ref: string]: AnySchemaObject | undefined } // TODO refactor to use keyword definitions @@ -29,14 +29,14 @@ const SIMPLE_INLINED = toHash([ "enum", "const", ]) -export function inlineRef(schema: Schema, limit: boolean | number = true): boolean { +export function inlineRef(schema: AnySchema, limit: boolean | number = true): boolean { if (typeof schema == "boolean") return true if (limit === true) return !hasRef(schema) if (!limit) return false return countKeys(schema) <= limit } -function hasRef(schema: SchemaObject): boolean { +function hasRef(schema: AnySchemaObject): boolean { for (const key in schema) { if (key === "$ref") return true const sch = schema[key] @@ -46,7 +46,7 @@ function hasRef(schema: SchemaObject): boolean { return false } -function countKeys(schema: SchemaObject): number { +function countKeys(schema: AnySchemaObject): number { let count = 0 for (const key in schema) { if (key === "$ref") return Infinity @@ -80,7 +80,7 @@ export function resolveUrl(baseId: string, id: string): string { return URI.resolve(baseId, id) } -export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { +export function getSchemaRefs(this: Ajv, schema: AnySchema): LocalRefs { if (typeof schema == "boolean") return {} const schemaId = normalizeId(schema.$id) const baseIds: {[jsonPtr: string]: string} = {"": schemaId} @@ -112,7 +112,7 @@ export function getSchemaRefs(this: Ajv, schema: Schema): LocalRefs { return localRefs - function checkAmbiguosId(sch1: Schema, sch2: Schema | undefined, id: string): void { + function checkAmbiguosId(sch1: AnySchema, sch2: AnySchema | undefined, id: string): void { if (sch2 !== undefined && !equal(sch1, sch2)) { throw new Error(`id "${id}" resolves to more than one schema`) } diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 9986d83979..67b36e1d8a 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,11 +1,11 @@ -import type {Schema, SchemaObjCxt} from "../types" +import type {AnySchema, SchemaObjCxt} from "../types" import {subschemaCode} from "./validate" import {escapeFragment, escapeJsonPointer} from "./util" import {_, str, Code, Name, getProperty} from "./codegen" export interface SubschemaContext { // TODO use Optional? - schema: Schema + schema: AnySchema schemaPath: Code errSchemaPath: string topSchemaRef?: Code @@ -32,7 +32,7 @@ export type SubschemaApplication = Partial interface SubschemaApplicationParams { keyword: string schemaProp: string | number - schema: Schema + schema: AnySchema schemaPath: Code errSchemaPath: string topSchemaRef: Code diff --git a/lib/compile/util.ts b/lib/compile/util.ts index d7318ebb91..606a51bd60 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,5 +1,5 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" -import type {SchemaCxt, Schema} from "../types" +import type {SchemaCxt, AnySchema} from "../types" import type {Rule, ValidationRules} from "./rules" import N from "./names" @@ -74,7 +74,7 @@ export function toHash(arr: string[]): {[key: string]: true | undefined} { } export function schemaHasRules( - schema: Schema, + schema: AnySchema, rules: {[key: string]: boolean | Rule | undefined} ): boolean { if (typeof schema == "boolean") return !schema @@ -88,7 +88,7 @@ export function schemaCxtHasRules({schema, self}: SchemaCxt): boolean { return false } -export function schemaHasRulesButRef(schema: Schema, RULES: ValidationRules): boolean { +export function schemaHasRulesButRef(schema: AnySchema, RULES: ValidationRules): boolean { if (typeof schema == "boolean") return !schema for (const key in schema) if (key !== "$ref" && RULES.all[key]) return true return false diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index e8704809cb..839b74e077 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,4 +1,4 @@ -import type {SchemaObjCxt, SchemaObject} from "../../types" +import type {SchemaObjCxt, AnySchemaObject} from "../../types" import type {RuleGroup, Rule} from "../rules" export function schemaHasRulesForType( @@ -9,11 +9,11 @@ export function schemaHasRulesForType( return group && group !== true && shouldUseGroup(schema, group) } -export function shouldUseGroup(schema: SchemaObject, group: RuleGroup): boolean { +export function shouldUseGroup(schema: AnySchemaObject, group: RuleGroup): boolean { return group.rules.some((rule) => shouldUseRule(schema, rule)) } -export function shouldUseRule(schema: SchemaObject, rule: Rule): boolean | undefined { +export function shouldUseRule(schema: AnySchemaObject, rule: Rule): boolean | undefined { return ( schema[rule.keyword] !== undefined || rule.definition.implements?.some((kwd) => schema[kwd] !== undefined) diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 6ed95408a4..f45206fd08 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,4 +1,9 @@ -import type {SchemaObjCxt, KeywordErrorDefinition, KeywordErrorCxt, SchemaObject} from "../../types" +import type { + SchemaObjCxt, + KeywordErrorDefinition, + KeywordErrorCxt, + AnySchemaObject, +} from "../../types" import type {ValidationRules} from "../rules" import {schemaHasRulesForType} from "./applicability" import {toHash, checkDataTypes, DataType} from "../util" @@ -6,7 +11,7 @@ import {schemaRefOrVal} from "../../vocabularies/util" import {reportError} from "../errors" import {_, str, Name} from "../codegen" -export function getSchemaTypes({self}: SchemaObjCxt, schema: SchemaObject): string[] { +export function getSchemaTypes({self}: SchemaObjCxt, schema: AnySchemaObject): string[] { const st: undefined | string | string[] = schema.type const types: string[] = Array.isArray(st) ? st : st ? [st] : [] types.forEach((t) => checkType(t, self.RULES)) diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index f5b674ac9b..3e74c9b1cd 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,4 +1,4 @@ -import type {Schema, SchemaCxt, SchemaObjCxt, Options} from "../../types" +import type {AnySchema, SchemaCxt, SchemaObjCxt, Options} from "../../types" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" @@ -46,7 +46,7 @@ function topSchemaObjCode(it: SchemaObjCxt): void { return } -function funcSourceUrl(schema: Schema, opts: Options): Code { +function funcSourceUrl(schema: AnySchema, opts: Options): Code { return typeof schema == "object" && schema.$id && (opts.sourceCode || opts.processCode) ? _`/*# sourceURL=${schema.$id} */` : nil diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index 290f980947..c4c916fb34 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -2,10 +2,10 @@ import type { KeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, - Schema, + AnySchema, SchemaObjCxt, SchemaValidateFunction, - ValidateFunction, + AnyValidateFunction, } from "../../types" import KeywordCxt from "../context" import {applySubschema} from "../subschema" @@ -14,7 +14,7 @@ import {callValidateCode} from "../../vocabularies/util" import {CodeGen, _, nil, Code, Name} from "../codegen" import N from "../names" -type KeywordCompilationResult = Schema | SchemaValidateFunction | ValidateFunction +type KeywordCompilationResult = AnySchema | SchemaValidateFunction | AnyValidateFunction export function keywordCode( it: SchemaObjCxt, diff --git a/lib/refs/data.json b/lib/refs/data.json index 52b5f40700..1338b815bf 100644 --- a/lib/refs/data.json +++ b/lib/refs/data.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#", - "description": "Meta-schema for $data reference (JSON Schema extension proposal)", + "description": "Meta-schema for $data reference (JSON AnySchema extension proposal)", "type": "object", "required": ["$data"], "properties": { diff --git a/lib/refs/json-schema-secure.json b/lib/refs/json-schema-secure.json index 51f9a7cc06..3968abd5d9 100644 --- a/lib/refs/json-schema-secure.json +++ b/lib/refs/json-schema-secure.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#", "title": "Meta-schema for the security assessment of JSON Schemas", - "description": "If a JSON Schema fails validation against this meta-schema, it may be unsafe to validate untrusted data", + "description": "If a JSON AnySchema fails validation against this meta-schema, it may be unsafe to validate untrusted data", "definitions": { "schemaArray": { "type": "array", diff --git a/lib/types/index.ts b/lib/types/index.ts index dc338cff89..14adce9148 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -3,23 +3,37 @@ import type {SchemaEnv} from "../compile" import type KeywordCxt from "../compile/context" import type Ajv from "../ajv" -export interface SchemaObject { +interface _SchemaObject { $id?: string - $async?: boolean $schema?: string [x: string]: any // TODO } +export interface SchemaObject extends _SchemaObject { + $id?: string + $schema?: string + $async?: false + [x: string]: any // TODO +} + +export interface AsyncSchema extends _SchemaObject { + $async: true +} + +export type AnySchemaObject = SchemaObject | AsyncSchema + export type Schema = SchemaObject | boolean +export type AnySchema = Schema | AsyncSchema + export interface SchemaMap { - [key: string]: Schema | undefined + [key: string]: AnySchema | undefined } export type LoadSchemaFunction = ( uri: string, - cb?: (err: Error | null, schema?: SchemaObject) => void -) => Promise + cb?: (err: Error | null, schema?: AnySchemaObject) => void +) => Promise export interface CurrentOptions { strict?: boolean | "log" @@ -30,15 +44,15 @@ export interface CurrentOptions { formats?: {[name: string]: Format} keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated unknownFormats?: true | string[] | "ignore" - schemas?: Schema[] | {[key: string]: Schema} + schemas?: AnySchema[] | {[key: string]: AnySchema} missingRefs?: true | "ignore" | "fail" extendRefs?: true | "ignore" | "fail" loadSchema?: LoadSchemaFunction removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" - meta?: SchemaObject | boolean - defaultMeta?: string | SchemaObject + meta?: AnySchemaObject | boolean + defaultMeta?: string | AnySchemaObject validateSchema?: boolean | "log" addUsedSchema?: boolean inlineRefs?: boolean | number @@ -54,8 +68,10 @@ export interface CurrentOptions { codegen?: CodeGenOptions cache?: CacheInterface logger?: Logger | false - serialize?: false | ((schema: Schema) => unknown) - $comment?: true | ((comment: string, schemaPath?: string, rootSchema?: SchemaObject) => unknown) + serialize?: false | ((schema: AnySchema) => unknown) + $comment?: + | true + | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) allowMatchingProperties?: boolean // disables a strict mode restriction } @@ -80,7 +96,7 @@ export interface InstanceOptions extends Options { code: CodeOptions loopRequired: number loopEnum: number - serialize: (schema: Schema) => unknown + serialize: (schema: AnySchema) => unknown addUsedSchema: boolean validateSchema: boolean | "log" } @@ -103,7 +119,7 @@ interface SourceCode { scope: Scope } -export interface ValidateGuard extends _ValidateFuncProps { +export interface ValidateFunction { ( this: Ajv | any, data: any, @@ -112,50 +128,18 @@ export interface ValidateGuard extends _ValidateFuncProps { parentDataProperty?: string | number, rootData?: Record | any[] ): data is T -} - -interface _ValidateFunction> extends _ValidateFuncProps { - (...args: Parameters>): T - $async?: true -} - -interface _ValidateFuncProps { - schema?: Schema errors?: null | ErrorObject[] + schema?: AnySchema schemaEnv?: SchemaEnv source?: SourceCode } -export type ValidateFunction = _ValidateFunction> - -export interface SyncSchemaObject extends SchemaObject { - $async?: false | undefined -} - -export interface SyncValidateFunction extends _ValidateFunction { - $async: undefined -} - -export interface AsyncSchemaObject extends SchemaObject { +export interface AsyncValidateFunction extends ValidateFunction { + (...args: Parameters>): Promise $async: true } -export interface AsyncValidateFunction extends _ValidateFunction> { - $async: true -} - -export interface SchemaValidateFunction { - ( - schema: any, - data: any, - parentSchema?: SchemaObject, - dataPath?: string, - parentData?: Record | any[], - parentDataProperty?: string | number, - rootData?: Record | any[] - ): boolean | Promise - errors?: Partial[] -} +export type AnyValidateFunction = ValidateFunction | AsyncValidateFunction export interface ErrorObject { keyword: string @@ -168,7 +152,7 @@ export interface ErrorObject { message?: string // These are added with the `verbose` option. schema?: unknown - parentSchema?: SchemaObject + parentSchema?: AnySchemaObject data?: unknown } @@ -184,7 +168,7 @@ export interface SchemaCxt { topSchemaRef: Code validateName: Name ValidationError?: Name - schema: Schema + schema: AnySchema schemaEnv: SchemaEnv rootId: string // TODO ? baseId: string @@ -199,7 +183,7 @@ export interface SchemaCxt { } export interface SchemaObjCxt extends SchemaCxt { - schema: SchemaObject + schema: AnySchemaObject } interface _KeywordDef { @@ -209,8 +193,8 @@ interface _KeywordDef { $data?: boolean implements?: string[] before?: string - metaSchema?: SchemaObject - validateSchema?: ValidateFunction // compiled keyword metaSchema - should not be passed + metaSchema?: AnySchemaObject + validateSchema?: AnyValidateFunction // compiled keyword metaSchema - should not be passed dependencies?: string[] // keywords that must be present in the same schema error?: KeywordErrorDefinition $dataError?: KeywordErrorDefinition @@ -221,18 +205,40 @@ export interface CodeKeywordDefinition extends _KeywordDef { trackErrors?: boolean } -export type MacroKeywordFunc = (schema: any, parentSchema: SchemaObject, it: SchemaCxt) => Schema +export type MacroKeywordFunc = ( + schema: any, + parentSchema: AnySchemaObject, + it: SchemaCxt +) => AnySchema export type CompileKeywordFunc = ( schema: any, - parentSchema: SchemaObject, + parentSchema: AnySchemaObject, it: SchemaObjCxt -) => ValidateFunction +) => DataValidateFunction + +export interface DataValidateFunction { + (...args: Parameters): boolean | Promise + errors?: Partial[] +} + +export interface SchemaValidateFunction { + ( + schema: any, + data: any, + parentSchema?: AnySchemaObject, + dataPath?: string, + parentData?: Record | any[], + parentDataProperty?: string | number, + rootData?: Record | any[] + ): boolean | Promise + errors?: Partial[] +} export interface FuncKeywordDefinition extends _KeywordDef { - validate?: SchemaValidateFunction | ValidateFunction + validate?: SchemaValidateFunction | DataValidateFunction compile?: CompileKeywordFunc - // schema: false makes validate not to expect schema (ValidateFunction) + // schema: false makes validate not to expect schema (DataValidateFunction) schema?: boolean // requires "validate" modifying?: boolean async?: boolean @@ -262,7 +268,7 @@ export interface KeywordErrorCxt { data: Name $data?: string | false schema: any // TODO - parentSchema?: SchemaObject + parentSchema?: AnySchemaObject schemaCode: Code | number | boolean schemaValue: Code | number | boolean schemaType?: string | string[] diff --git a/lib/types/json-schema.ts b/lib/types/json-schema.ts index 7e34ff5d78..348ece4ad9 100644 --- a/lib/types/json-schema.ts +++ b/lib/types/json-schema.ts @@ -27,7 +27,7 @@ export type JSONSchemaType = (T extends number } : T extends [any, ...any[]] ? { - // JSON Schema for tuple + // JSON AnySchema for tuple type: "array" items: { [K in keyof T]-?: JSONSchemaType & Nullable @@ -46,7 +46,7 @@ export type JSONSchemaType = (T extends number } : T extends Record ? { - // JSON Schema for records and dicitonaries + // JSON AnySchema for records and dicitonaries // "required" and "additionalProperties" are not optional because they are often forgotten // "properties" are optional for more concise dicitonary schemas // "patternProperties" and can be only used with interfaces that have string index diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 6b86d20035..33aa3cc7c0 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, Schema} from "../../types" +import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { const {gen, schema, it} = cxt if (!Array.isArray(schema)) throw new Error("ajv implementation error") const valid = gen.name("valid") - schema.forEach((sch: Schema, i: number) => { + schema.forEach((sch: AnySchema, i: number) => { if (alwaysValidSchema(it, sch)) return applySubschema(it, {keyword: "allOf", schemaProp: i}, valid) cxt.ok(valid) diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index ddc73e62fb..a7f3a6ce06 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, Schema} from "../../types" +import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -11,14 +11,14 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCxt) { const {gen, schema, it} = cxt if (!Array.isArray(schema)) throw new Error("ajv implementation error") - const alwaysValid = schema.some((sch: Schema) => alwaysValidSchema(it, sch)) + const alwaysValid = schema.some((sch: AnySchema) => alwaysValidSchema(it, sch)) if (alwaysValid) return const valid = gen.let("valid", false) const schValid = gen.name("_valid") gen.block(() => { - schema.forEach((_sch: Schema, i: number) => { + schema.forEach((_sch: AnySchema, i: number) => { applySubschema( it, { diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 7075d6970a..5efb26782c 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, SchemaMap, Schema} from "../../types" +import type {CodeKeywordDefinition, SchemaMap, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" @@ -61,7 +61,7 @@ const def: CodeKeywordDefinition = { function validateSchemaDeps(schemaDeps: SchemaMap): void { for (const prop in schemaDeps) { - if (alwaysValidSchema(it, schemaDeps[prop] as Schema)) continue + if (alwaysValidSchema(it, schemaDeps[prop] as AnySchema)) continue gen.if( propertyInData(data, prop, it.opts.ownProperties), () => applySubschema(it, {keyword: "dependencies", schemaProp: prop}, valid), diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 30ffaadac8..15a89db1e6 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, Schema} from "../../types" +import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema, Type} from "../../compile/subschema" @@ -18,9 +18,9 @@ const def: CodeKeywordDefinition = { validateItems() } - function validateDefinedItems(schArr: Schema[]): void { + function validateDefinedItems(schArr: AnySchema[]): void { const valid = gen.name("valid") - schArr.forEach((sch: Schema, i: number) => { + schArr.forEach((sch: AnySchema, i: number) => { if (alwaysValidSchema(it, sch)) return gen.if(_`${len} > ${i}`, () => applySubschema( diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index fc58cae7d2..8c17f6bfc5 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, Schema} from "../../types" +import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" @@ -11,7 +11,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCxt) { const {gen, schema, it} = cxt if (!Array.isArray(schema)) throw new Error("ajv implementation error") - const schArr: Schema[] = schema + const schArr: AnySchema[] = schema const valid = gen.let("valid", false) const passing = gen.let("passing", null) const schValid = gen.name("_valid") @@ -27,7 +27,7 @@ const def: CodeKeywordDefinition = { ) function validateOneOf(): void { - schArr.forEach((sch: Schema, i: number) => { + schArr.forEach((sch: AnySchema, i: number) => { if (alwaysValidSchema(it, sch)) { gen.var(schValid, true) } else { diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 49c9a1b1bd..d76b5ad890 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, Schema} from "../../types" +import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" @@ -43,7 +43,7 @@ const def: CodeKeywordDefinition = { else callSyncRef(v) } - function inlineRefSchema(sch: Schema): void { + function inlineRefSchema(sch: AnySchema): void { const schName = gen.scopeValue("schema", {ref: sch}) const valid = gen.name("valid") applySubschema( diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 2482b92183..fe7759c8be 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,4 +1,4 @@ -import type {Schema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" +import type {AnySchema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" import type KeywordCxt from "../compile/context" import {schemaHasRules} from "../compile/util" import {CodeGen, _, nil, Code, Name, getProperty} from "../compile/codegen" @@ -17,14 +17,14 @@ export function schemaRefOrVal( return _`${topSchemaRef}${schemaPath}${getProperty(keyword)}` } -export function alwaysValidSchema(it: SchemaCxt, schema: Schema): boolean | void { +export function alwaysValidSchema(it: SchemaCxt, schema: AnySchema): boolean | void { if (typeof schema == "boolean") return schema if (Object.keys(schema).length === 0) return true checkUnknownRules(it, schema) return !schemaHasRules(schema, it.self.RULES.all) } -export function checkUnknownRules(it: SchemaCxt, schema: Schema = it.schema): void { +export function checkUnknownRules(it: SchemaCxt, schema: AnySchema = it.schema): void { const {opts, self} = it if (!opts.strict) return if (typeof schema === "boolean") return @@ -40,7 +40,7 @@ export function allSchemaProperties(schemaMap?: SchemaMap): string[] { export function schemaProperties(it: SchemaCxt, schemaMap: SchemaMap): string[] { return allSchemaProperties(schemaMap).filter( - (p) => !alwaysValidSchema(it, schemaMap[p] as Schema) + (p) => !alwaysValidSchema(it, schemaMap[p] as AnySchema) ) } diff --git a/package.json b/package.json index 0b3b739381..b0aace6442 100644 --- a/package.json +++ b/package.json @@ -67,13 +67,13 @@ "uri-js": "^4.2.2" }, "devDependencies": { - "@ajv-validator/config": "^0.2.2", + "@ajv-validator/config": "^0.2.3", "@types/chai": "^4.2.12", "@types/mocha": "^8.0.3", "@types/node": "^14.0.27", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", - "ajv-formats": "^0.2.0", + "ajv-formats": "^0.3.0", "browserify": "^16.2.0", "chai": "^4.0.1", "coveralls": "^3.0.1", diff --git a/spec/async.spec.ts b/spec/async.spec.ts index 2748e6014d..e563407f7a 100644 --- a/spec/async.spec.ts +++ b/spec/async.spec.ts @@ -1,5 +1,5 @@ import _Ajv from "./ajv" -import type {SchemaObject, ValidateFunction} from "../dist/types" +import type {SchemaObject, AnyValidateFunction} from "../dist/types" const should = require("./chai").should() @@ -332,7 +332,7 @@ describe("compileAsync method", () => { } }) - function shouldReject(p: Promise, rx: RegExp) { + function shouldReject(p: Promise, rx: RegExp) { return p.then( (validate) => { should.not.exist(validate) diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index 8c93b8e63b..c523197dc9 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -1,6 +1,6 @@ import getAjvInstances from "./ajv_instances" import Ajv from "./ajv" -import {ValidateFunction} from "../dist/types" +import {AnyValidateFunction} from "../dist/types" const should = require("./chai").should() @@ -320,7 +320,7 @@ describe("resolve", () => { validate([{a: 5}]).should.equal(false) } - function testInlined(validate: ValidateFunction, expectedInlined) { + function testInlined(validate: AnyValidateFunction, expectedInlined) { const inlined: any = !validate.source?.code.includes("scope.validate") inlined.should.equal(expectedInlined) } diff --git a/spec/types/async-validate.spec.ts b/spec/types/async-validate.spec.ts index 1cdf5067a4..82fa02f4c8 100644 --- a/spec/types/async-validate.spec.ts +++ b/spec/types/async-validate.spec.ts @@ -1,19 +1,45 @@ -import type {SchemaObject, SyncSchemaObject, AsyncSchemaObject} from "../../dist/types" +import type {AnySchemaObject, SchemaObject, AsyncSchema} from "../../dist/types" import _Ajv from "../ajv" const should = require("../chai").should() -describe("validate function result type depends on $async property type", () => { +interface Foo { + foo: number +} + +describe("$async validation and type guards", () => { const ajv = new _Ajv() describe("$async: undefined", () => { - const validate = ajv.compile({}) - it("should have result type boolean | promise", async () => { - const result = validate({}) - if (typeof result === "boolean") { - should.exist(result) - } else { - await result.then((data) => data.should.exist) + it("should have result type boolean 1", () => { + const validate = ajv.compile({properties: {foo: {type: "number"}}}) + const data: unknown = {foo: 1} + let result: boolean + if ((result = validate(data))) { + data.foo.should.equal(1) + } + result.should.equal(true) + }) + + it("should have result type boolean 2", () => { + const schema: SchemaObject = {properties: {foo: {type: "number"}}} + const validate = ajv.compile(schema) + const data: unknown = {foo: 1} + let result: boolean + if ((result = validate(data))) { + data.foo.should.equal(1) + } + result.should.equal(true) + }) + + it("should have result type boolean 3", () => { + const schema: AnySchemaObject = {properties: {foo: {type: "number"}}} + const validate = ajv.compile(schema) + const data: unknown = {foo: 1} + let result: boolean + if ((result = validate(data))) { + data.foo.should.equal(1) } + result.should.equal(true) }) }) @@ -25,7 +51,14 @@ describe("validate function result type depends on $async property type", () => }) it("should have result type boolean 2", () => { - const schema: SyncSchemaObject = {$async: false} + const schema: SchemaObject = {$async: false} + const validate = ajv.compile(schema) + const result: boolean = validate({}) + should.exist(result) + }) + + it("should have result type boolean 3", () => { + const schema: AnySchemaObject = {$async: false} const validate = ajv.compile(schema) const result: boolean = validate({}) should.exist(result) @@ -34,47 +67,39 @@ describe("validate function result type depends on $async property type", () => describe("$async: true", () => { it("should have result type promise 1", async () => { - const validate = ajv.compile({$async: true}) - const result: Promise = validate({}) + const validate = ajv.compile({$async: true, properties: {foo: {type: "number"}}}) + const result: Promise = validate({foo: 1}) await result.then((data) => data.should.exist) }) it("should have result type promise 2", async () => { - const schema: AsyncSchemaObject = {$async: true} - const validate = ajv.compile(schema) - const result: Promise = validate({}) - await result.then((data) => data.should.exist) + const schema: AsyncSchema = {$async: true, properties: {foo: {type: "number"}}} + const validate = ajv.compile(schema) + const result: Promise = validate({foo: 1}) + await result.then((data) => data.foo.should.equal(1)) }) }) describe("$async: boolean", () => { - it("should have result type boolean | promise", async () => { - const schema = {$async: true} - const validate = ajv.compile(schema) - const result = validate({}) - if (typeof result === "boolean") { - result.should.equal(true) + it("should have result type boolean | promise 1", async () => { + const schema = {$async: true, properties: {foo: {type: "number"}}} + const validate = ajv.compile(schema) + const data: unknown = {foo: 1} + let result: boolean | Promise + if ((result = validate(data))) { + if (typeof result == "boolean") { + data.foo.should.equal(1) + } else { + await result.then((_data) => _data.foo.should.equal(1)) + } } else { - await result.then((data) => data.should.exist) + should.fail() } }) - it("should have result type boolean | promise", async () => { + it("should have result type boolean | promise 2", async () => { const schema = {$async: false} const validate = ajv.compile(schema) - const result = validate({}) - if (typeof result === "boolean") { - result.should.equal(true) - } else { - await result.then((data) => data.should.exist) - } - }) - }) - - describe("= boolean", () => { - const schema: SchemaObject = {} - const validate = ajv.compile(schema) - it("should have result type boolean | promise", async () => { const result = validate({}) if (typeof result === "boolean") { should.exist(result) @@ -85,28 +110,24 @@ describe("validate function result type depends on $async property type", () => }) describe("$async: unknown", () => { - const schema: Record = {} - const validate = ajv.compile(schema) - it("should have result type boolean | promise", async () => { - const result = validate({}) - if (typeof result === "boolean") { - should.exist(result) - } else { - await result.then((data) => data.should.exist) + const schema: Record = {properties: {foo: {type: "number"}}} + const validate = ajv.compile(schema) + it("should have result type boolean", () => { + const data = {foo: 1} + let result: boolean + if ((result = validate(data))) { + data.foo.should.equal(1) } + result.should.equal(true) }) }) - describe("$async: any", () => { + describe("schema: any", () => { const schema: any = {} const validate = ajv.compile(schema) - it("should have result type boolean | promise", async () => { - const result = validate({}) - if (typeof result === "boolean") { - should.exist(result) - } else { - await result.then((data) => data.should.exist) - } + it("should have result type boolean | promise", () => { + const result: boolean = validate({}) + result.should.equal(true) }) }) }) diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts index 59229a363c..730890d2d2 100644 --- a/spec/types/json-schema.spec.ts +++ b/spec/types/json-schema.spec.ts @@ -1,6 +1,6 @@ import _Ajv from "../ajv" import type {JSONSchemaType} from "../../dist/types/json-schema" -import type {SyncSchemaObject} from "../../dist/types" +import type {SchemaObject} from "../../dist/types" import chai from "../chai" const should = chai.should() @@ -132,9 +132,9 @@ describe("JSONSchemaType type and validation as a type guard", () => { }) }) - describe("schema has type SyncSchemaObject", () => { + describe("schema has type SchemaObject", () => { it("should prove the type of validated data", () => { - const schema = mySchema as SyncSchemaObject + const schema = mySchema as SchemaObject const validate = ajv.compile(schema) if (validate(validData)) { validData.foo.should.equal("foo") From 4bb210de5cd6f1ba9190b80a26e52ec9eb56bff8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 14:36:21 +0100 Subject: [PATCH 224/322] 7.0.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0aace6442..543cade93c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv", - "version": "7.0.0-alpha.0", + "version": "7.0.0-alpha.1", "description": "Another JSON Schema Validator", "main": "dist/ajv.js", "types": "dist/ajv.d.ts", From c207bbc8a4fa1be5c7510b25b8f470ae5a2c7854 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 15:22:17 +0100 Subject: [PATCH 225/322] update ajv-formats and fix tests --- package.json | 2 +- spec/json-schema.spec.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 543cade93c..2ae1e0dcbd 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@types/node": "^14.0.27", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", - "ajv-formats": "^0.3.0", + "ajv-formats": "^0.3.2", "browserify": "^16.2.0", "chai": "^4.0.1", "coveralls": "^3.0.1", diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index 100134cb98..d7b84439f8 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -1,4 +1,3 @@ -import type Ajv from "../dist/ajv" import getAjvInstances from "./ajv_instances" import jsonSchemaTest from "json-schema-test" import options from "./ajv_options" @@ -35,7 +34,7 @@ runTest( require("./_json/draft7") ) -function runTest(instances: Ajv[], draft: number, tests) { +function runTest(instances, draft: number, tests) { for (const ajv of instances) { if (draft === 6) { ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) From c5c1b84152960e5f63630d3444e5d7d67e7c2a77 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 16:15:23 +0100 Subject: [PATCH 226/322] export additional types --- lib/ajv.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/ajv.ts b/lib/ajv.ts index 9f3c9e00bd..5df36f4149 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -3,7 +3,17 @@ export { FormatDefinition, AsyncFormatDefinition, KeywordDefinition, + CodeKeywordDefinition, + MacroKeywordDefinition, + FuncKeywordDefinition, Vocabulary, + Schema, + AsyncSchema, + Options, + ValidateFunction, + AsyncValidateFunction, + CacheInterface, + Logger, } from "./types" export interface Plugin { (ajv: Ajv, options?: Opts): Ajv From 61e7520d4bf76ac4f6a81bf9585d9142378d3081 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 17:37:55 +0100 Subject: [PATCH 227/322] refactor: import keyword definiions (rather than require) --- lib/ajv.ts | 1 + lib/types/index.ts | 8 ++-- .../applicator/additionalItems.ts | 14 ++++--- .../applicator/additionalProperties.ts | 15 ++++--- lib/vocabularies/applicator/allOf.ts | 2 +- lib/vocabularies/applicator/anyOf.ts | 2 +- lib/vocabularies/applicator/contains.ts | 2 +- lib/vocabularies/applicator/dependencies.ts | 28 +++++++------ lib/vocabularies/applicator/if.ts | 16 +++---- lib/vocabularies/applicator/index.ts | 42 ++++++++++++------- lib/vocabularies/applicator/items.ts | 2 +- lib/vocabularies/applicator/not.ts | 2 +- lib/vocabularies/applicator/oneOf.ts | 14 ++++--- .../applicator/patternProperties.ts | 2 +- lib/vocabularies/applicator/properties.ts | 2 +- lib/vocabularies/applicator/propertyNames.ts | 14 ++++--- lib/vocabularies/applicator/thenElse.ts | 2 +- lib/vocabularies/core/index.ts | 3 +- lib/vocabularies/core/ref.ts | 15 +++---- lib/vocabularies/format/format.ts | 13 +++--- lib/vocabularies/format/index.ts | 3 +- lib/vocabularies/validation/const.ts | 14 ++++--- lib/vocabularies/validation/enum.ts | 14 ++++--- lib/vocabularies/validation/index.ts | 30 ++++++++----- lib/vocabularies/validation/limit.ts | 14 ++++--- lib/vocabularies/validation/limitItems.ts | 20 +++++---- lib/vocabularies/validation/limitLength.ts | 20 +++++---- .../validation/limitProperties.ts | 20 +++++---- lib/vocabularies/validation/multipleOf.ts | 14 ++++--- lib/vocabularies/validation/pattern.ts | 14 ++++--- lib/vocabularies/validation/required.ts | 15 +++---- lib/vocabularies/validation/uniqueItems.ts | 16 +++---- 32 files changed, 226 insertions(+), 167 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 5df36f4149..4b001b783e 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -3,6 +3,7 @@ export { FormatDefinition, AsyncFormatDefinition, KeywordDefinition, + KeywordErrorDefinition, CodeKeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, diff --git a/lib/types/index.ts b/lib/types/index.ts index 14adce9148..581f82dae9 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -141,14 +141,14 @@ export interface AsyncValidateFunction extends ValidateFunction { export type AnyValidateFunction = ValidateFunction | AsyncValidateFunction -export interface ErrorObject { - keyword: string +export interface ErrorObject { + keyword: T dataPath: string schemaPath: string params: Record // TODO add interface - // Added to validation errors of propertyNames keyword schema + // Added to validation errors of "propertyNames" keyword schema propertyName?: string - // Excluded if messages set to false. + // Excluded if option `messages` set to false. message?: string // These are added with the `verbose` option. schema?: unknown diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index fc79b6b9bd..92c7c423cf 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -1,14 +1,20 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: ({params: {len}}) => str`should NOT have more than ${len} items`, + params: ({params: {len}}) => _`{limit: ${len}}`, +} + const def: CodeKeywordDefinition = { keyword: "additionalItems", type: "array", schemaType: ["boolean", "object"], before: "uniqueItems", + error, code(cxt: KeywordCxt) { const {gen, schema, parentSchema, data, it} = cxt const len = gen.const("len", _`${data}.length`) @@ -33,10 +39,6 @@ const def: CodeKeywordDefinition = { }) } }, - error: { - message: ({params: {len}}) => str`should NOT have more than ${len} items`, - params: ({params: {len}}) => _`{limit: ${len}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 109fe658a1..7e4f55a917 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,14 +1,20 @@ -import type {CodeKeywordDefinition, KeywordErrorCxt} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" +const error: KeywordErrorDefinition = { + message: "should NOT have additional properties", + params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`, +} + const def: CodeKeywordDefinition = { keyword: "additionalProperties", type: "object", schemaType: ["boolean", "object", "undefined"], // "undefined" is needed to support option removeAdditional: "all" trackErrors: true, + error, code(cxt) { const {gen, schema, parentSchema, data, errsCount, it} = cxt if (!errsCount) throw new Error("ajv implementation error") @@ -91,13 +97,6 @@ const def: CodeKeywordDefinition = { applySubschema(it, subschema, valid) } }, - error: { - message: "should NOT have additional properties", - params: ({params}: KeywordErrorCxt): Code => - _`{additionalProperty: ${params.additionalProperty}}`, - }, } -module.exports = def - export default def diff --git a/lib/vocabularies/applicator/allOf.ts b/lib/vocabularies/applicator/allOf.ts index 33aa3cc7c0..540a012b25 100644 --- a/lib/vocabularies/applicator/allOf.ts +++ b/lib/vocabularies/applicator/allOf.ts @@ -18,4 +18,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index a7f3a6ce06..d9beac3005 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -44,4 +44,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/contains.ts b/lib/vocabularies/applicator/contains.ts index e4b0aa28bd..28b29c4058 100644 --- a/lib/vocabularies/applicator/contains.ts +++ b/lib/vocabularies/applicator/contains.ts @@ -40,4 +40,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index 5efb26782c..da279861b8 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, SchemaMap, AnySchema} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition, SchemaMap, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" @@ -11,10 +11,23 @@ interface PropertyDependencies { type SchemaDependencies = SchemaMap +const error: KeywordErrorDefinition = { + message: ({params: {property, depsCount, deps}}) => { + const property_ies = depsCount === 1 ? "property" : "properties" + return str`should have ${property_ies} ${deps} when property ${property} is present` + }, + params: ({params: {property, depsCount, deps, missingProperty}}) => + _`{property: ${property}, + missingProperty: ${missingProperty}, + depsCount: ${depsCount}, + deps: ${deps}}`, // TODO change to reference? +} + const def: CodeKeywordDefinition = { keyword: "dependencies", type: "object", schemaType: "object", + error, code(cxt: KeywordCxt) { const {gen, schema, data, it} = cxt const [propDeps, schDeps] = splitDependencies() @@ -71,17 +84,6 @@ const def: CodeKeywordDefinition = { } } }, - error: { - message: ({params: {property, depsCount, deps}}) => { - const property_ies = depsCount === 1 ? "property" : "properties" - return str`should have ${property_ies} ${deps} when property ${property} is present` - }, - params: ({params: {property, depsCount, deps, missingProperty}}) => - _`{property: ${property}, - missingProperty: ${missingProperty}, - depsCount: ${depsCount}, - deps: ${deps}}`, // TODO change to reference? - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index af8997c208..4bcd3654c4 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,13 +1,19 @@ -import type {CodeKeywordDefinition, SchemaObjCxt} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition, SchemaObjCxt} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: ({params}) => str`should match "${params.ifClause}" schema`, + params: ({params}) => _`{failingKeyword: ${params.ifClause}}`, +} + const def: CodeKeywordDefinition = { keyword: "if", schemaType: ["object", "boolean"], trackErrors: true, + error, code(cxt: KeywordCxt) { const {gen, parentSchema, it} = cxt if (parentSchema.then === undefined && parentSchema.else === undefined) { @@ -56,15 +62,11 @@ const def: CodeKeywordDefinition = { } } }, - error: { - message: ({params}) => str`should match "${params.ifClause}" schema`, - params: ({params}) => _`{failingKeyword: ${params.ifClause}}`, - }, } -module.exports = def - function hasSchema(it: SchemaObjCxt, keyword: string): boolean { const schema = it.schema[keyword] return schema !== undefined && !alwaysValidSchema(it, schema) } + +export default def diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 086c1ad13f..15abf1fdfe 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -1,23 +1,37 @@ import type {Vocabulary} from "../../types" +import additionalItems from "./additionalItems" +import items from "./items" +import contains from "./contains" +import dependencies from "./dependencies" +import propertyNames from "./propertyNames" +import additionalProperties from "./additionalProperties" +import properties from "./properties" +import patternProperties from "./patternProperties" +import notKeyword from "./not" +import anyOf from "./anyOf" +import oneOf from "./oneOf" +import allOf from "./allOf" +import ifKeyword from "./if" +import thenElse from "./thenElse" const applicator: Vocabulary = [ // array - require("./additionalItems"), - require("./items"), - require("./contains"), + additionalItems, + items, + contains, // object - require("./dependencies"), - require("./propertyNames"), - require("./additionalProperties"), - require("./properties"), - require("./patternProperties"), + dependencies, + propertyNames, + additionalProperties, + properties, + patternProperties, // any - require("./not"), - require("./anyOf"), - require("./oneOf"), - require("./allOf"), - require("./if"), - require("./thenElse"), + notKeyword, + anyOf, + oneOf, + allOf, + ifKeyword, + thenElse, ] export default applicator diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 15a89db1e6..68cba36d1c 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -48,4 +48,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/not.ts b/lib/vocabularies/applicator/not.ts index bb183fa776..4c2deabfcd 100644 --- a/lib/vocabularies/applicator/not.ts +++ b/lib/vocabularies/applicator/not.ts @@ -37,4 +37,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 8c17f6bfc5..cc4c74d548 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,13 +1,19 @@ -import type {CodeKeywordDefinition, AnySchema} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: "should match exactly one schema in oneOf", + params: ({params}) => _`{passingSchemas: ${params.passing}}`, +} + const def: CodeKeywordDefinition = { keyword: "oneOf", schemaType: "array", trackErrors: true, + error, code(cxt: KeywordCxt) { const {gen, schema, it} = cxt if (!Array.isArray(schema)) throw new Error("ajv implementation error") @@ -54,10 +60,6 @@ const def: CodeKeywordDefinition = { }) } }, - error: { - message: "should match exactly one schema in oneOf", - params: ({params}) => _`{passingSchemas: ${params.passing}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 7d11ea4cc3..c8ffa2e3c7 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -61,4 +61,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 493af3b1c9..76926a343e 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -47,4 +47,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 42e8638df5..72d67c4f0d 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,13 +1,19 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? + params: ({params}) => _`{propertyName: ${params.propertyName}}`, +} + const def: CodeKeywordDefinition = { keyword: "propertyNames", type: "object", schemaType: ["object", "boolean"], + error, code(cxt: KeywordCxt) { const {gen, schema, data, it} = cxt if (alwaysValidSchema(it, schema)) return @@ -28,10 +34,6 @@ const def: CodeKeywordDefinition = { cxt.ok(valid) }, - error: { - message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? - params: ({params}) => _`{propertyName: ${params.propertyName}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/applicator/thenElse.ts b/lib/vocabularies/applicator/thenElse.ts index ba0541a197..e3002a015f 100644 --- a/lib/vocabularies/applicator/thenElse.ts +++ b/lib/vocabularies/applicator/thenElse.ts @@ -10,4 +10,4 @@ const def: CodeKeywordDefinition = { }, } -module.exports = def +export default def diff --git a/lib/vocabularies/core/index.ts b/lib/vocabularies/core/index.ts index 5845d0384b..5b8fb6f364 100644 --- a/lib/vocabularies/core/index.ts +++ b/lib/vocabularies/core/index.ts @@ -1,5 +1,6 @@ import type {Vocabulary} from "../../types" +import refKeyword from "./ref" -const core: Vocabulary = ["$schema", "$id", "$defs", "definitions", require("./ref")] +const core: Vocabulary = ["$schema", "$id", "$defs", "definitions", refKeyword] export default core diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index d76b5ad890..ea316a0d05 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition, AnySchema} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" @@ -7,9 +7,15 @@ import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" import {SchemaEnv, resolveRef} from "../../compile" +const error: KeywordErrorDefinition = { + message: ({schema}) => str`can't resolve reference ${schema}`, + params: ({schema}) => _`{ref: ${schema}}`, +} + const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", + error, code(cxt: KeywordCxt) { const {gen, schema, it} = cxt const {allErrors, baseId, schemaEnv: env, opts, validateName, self} = it @@ -101,11 +107,6 @@ const def: CodeKeywordDefinition = { gen.assign(N.errors, _`${N.vErrors}.length`) } }, - // TODO incorrect error message - error: { - message: ({schema}) => str`can't resolve reference ${schema}`, - params: ({schema}) => _`{ref: ${schema}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 22b6e4ca29..437af8477a 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -3,6 +3,7 @@ import type { FormatValidator, AsyncFormatValidator, CodeKeywordDefinition, + KeywordErrorDefinition, } from "../../types" import type KeywordCxt from "../../compile/context" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" @@ -16,11 +17,17 @@ type FormatValidate = | RegExp | string +const error: KeywordErrorDefinition = { + message: ({schemaCode}) => str`should match format "${schemaCode}"`, + params: ({schemaCode}) => _`{format: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: "format", type: ["number", "string"], schemaType: "string", $data: true, + error, code(cxt: KeywordCxt, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt const {opts, errSchemaPath, schemaEnv, self} = it @@ -107,10 +114,6 @@ const def: CodeKeywordDefinition = { } } }, - error: { - message: ({schemaCode}) => str`should match format "${schemaCode}"`, - params: ({schemaCode}) => _`{format: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/format/index.ts b/lib/vocabularies/format/index.ts index 9763393d67..bca2f5b3d8 100644 --- a/lib/vocabularies/format/index.ts +++ b/lib/vocabularies/format/index.ts @@ -1,5 +1,6 @@ import type {Vocabulary} from "../../types" +import formatKeyword from "./format" -const format: Vocabulary = [require("./format")] +const format: Vocabulary = [formatKeyword] export default format diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index 648ae97a6e..f44d7b9b16 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,11 +1,17 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_} from "../../compile/codegen" import equal from "fast-deep-equal" +const error: KeywordErrorDefinition = { + message: "should be equal to constant", + params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: "const", $data: true, + error, code(cxt: KeywordCxt) { const eql = cxt.gen.scopeValue("func", { ref: equal, @@ -13,10 +19,6 @@ const def: CodeKeywordDefinition = { }) cxt.fail$data(_`!${eql}(${cxt.data}, ${cxt.schemaCode})`) }, - error: { - message: "should be equal to constant", - params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index 84e973e85a..f2bfb302ee 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,12 +1,18 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" import equal from "fast-deep-equal" +const error: KeywordErrorDefinition = { + message: "should be equal to one of the allowed values", + params: ({schemaCode}) => _`{allowedValues: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: "enum", schemaType: "array", $data: true, + error, code(cxt: KeywordCxt) { const {gen, data, $data, schema, schemaCode, it} = cxt if (!$data && schema.length === 0) throw new Error("enum must have non-empty array") @@ -41,10 +47,6 @@ const def: CodeKeywordDefinition = { return _`${data} === ${sch}` } }, - error: { - message: "should be equal to one of the allowed values", - params: ({schemaCode}) => _`{allowedValues: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 116ce17590..1c555ec41c 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -1,22 +1,32 @@ import type {Vocabulary} from "../../types" +import limit from "./limit" +import multipleOf from "./multipleOf" +import limitLength from "./limitLength" +import pattern from "./pattern" +import limitProperties from "./limitProperties" +import required from "./required" +import limitItems from "./limitItems" +import uniqueItems from "./uniqueItems" +import constKeyword from "./const" +import enumKeyword from "./enum" const validation: Vocabulary = [ // number - require("./limit"), - require("./multipleOf"), + limit, + multipleOf, // string - require("./limitLength"), - require("./pattern"), + limitLength, + pattern, // object - require("./limitProperties"), - require("./required"), + limitProperties, + required, // array - require("./limitItems"), - require("./uniqueItems"), + limitItems, + uniqueItems, // any {keyword: "nullable", schemaType: "boolean"}, - require("./const"), - require("./enum"), + constKeyword, + enumKeyword, ] export default validation diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index a5bf82de29..22086f1ebb 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -1,4 +1,4 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str, operators, Code} from "../../compile/codegen" @@ -11,20 +11,22 @@ const OPS: {[index: string]: {fail: Code; ok: Code; okStr: string}} = { exclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE}, } +const error: KeywordErrorDefinition = { + message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, + params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].okStr}, limit: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], type: "number", schemaType: "number", $data: true, + error, code(cxt: KeywordCxt) { const {keyword, data, schemaCode} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) cxt.fail$data(_`(${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data}))`) }, - error: { - message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, - params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].okStr}, limit: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/limitItems.ts b/lib/vocabularies/validation/limitItems.ts index e7615fb3a5..9bf865eb6c 100644 --- a/lib/vocabularies/validation/limitItems.ts +++ b/lib/vocabularies/validation/limitItems.ts @@ -1,24 +1,26 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message({keyword, schemaCode}) { + const comp = keyword === "maxItems" ? "more" : "fewer" + return str`should NOT have ${comp} than ${schemaCode} items` + }, + params: ({schemaCode}) => _`{limit: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: ["maxItems", "minItems"], type: "array", schemaType: "number", $data: true, + error, code(cxt: KeywordCxt) { const {keyword, data, schemaCode} = cxt const op = keyword === "maxItems" ? operators.GT : operators.LT cxt.fail$data(_`${data}.length ${op} ${schemaCode}`) }, - error: { - message({keyword, schemaCode}) { - const comp = keyword === "maxItems" ? "more" : "fewer" - return str`should NOT have ${comp} than ${schemaCode} items` - }, - params: ({schemaCode}) => _`{limit: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/limitLength.ts b/lib/vocabularies/validation/limitLength.ts index 448b63dc15..04bf9f734b 100644 --- a/lib/vocabularies/validation/limitLength.ts +++ b/lib/vocabularies/validation/limitLength.ts @@ -1,13 +1,22 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" import ucs2length from "../../compile/ucs2length" +const error: KeywordErrorDefinition = { + message({keyword, schemaCode}) { + const comp = keyword === "maxLength" ? "more" : "fewer" + return str`should NOT have ${comp} than ${schemaCode} items` + }, + params: ({schemaCode}) => _`{limit: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: ["maxLength", "minLength"], type: "string", schemaType: "number", $data: true, + error, code(cxt: KeywordCxt) { const {keyword, data, schemaCode, it} = cxt const op = keyword === "maxLength" ? operators.GT : operators.LT @@ -23,13 +32,6 @@ const def: CodeKeywordDefinition = { } cxt.fail$data(_`${len} ${op} ${schemaCode}`) }, - error: { - message({keyword, schemaCode}) { - const comp = keyword === "maxLength" ? "more" : "fewer" - return str`should NOT have ${comp} than ${schemaCode} items` - }, - params: ({schemaCode}) => _`{limit: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/limitProperties.ts b/lib/vocabularies/validation/limitProperties.ts index 04e7e8ba8c..ec5943deb5 100644 --- a/lib/vocabularies/validation/limitProperties.ts +++ b/lib/vocabularies/validation/limitProperties.ts @@ -1,24 +1,26 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str, operators} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message({keyword, schemaCode}) { + const comp = keyword === "maxProperties" ? "more" : "fewer" + return str`should NOT have ${comp} than ${schemaCode} items` + }, + params: ({schemaCode}) => _`{limit: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: ["maxProperties", "minProperties"], type: "object", schemaType: "number", $data: true, + error, code(cxt: KeywordCxt) { const {keyword, data, schemaCode} = cxt const op = keyword === "maxProperties" ? operators.GT : operators.LT cxt.fail$data(_`Object.keys(${data}).length ${op} ${schemaCode}`) }, - error: { - message({keyword, schemaCode}) { - const comp = keyword === "maxProperties" ? "more" : "fewer" - return str`should NOT have ${comp} than ${schemaCode} items` - }, - params: ({schemaCode}) => _`{limit: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index c4298dfc72..d1a763e7a8 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,12 +1,18 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, + params: ({schemaCode}) => _`{multipleOf: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: "multipleOf", type: "number", schemaType: "number", $data: true, + error, code(cxt: KeywordCxt) { const {gen, data, schemaCode, it} = cxt // const bdt = bad$DataType(schemaCode, def.schemaType, $data) @@ -17,10 +23,6 @@ const def: CodeKeywordDefinition = { : _`${res} !== parseInt(${res})` cxt.fail$data(_`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid}))`) }, - error: { - message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, - params: ({schemaCode}) => _`{multipleOf: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index e4ceefce9c..0d41aab587 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,22 +1,24 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {usePattern} from "../util" import {_, str} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, + params: ({schemaCode}) => _`{pattern: ${schemaCode}}`, +} + const def: CodeKeywordDefinition = { keyword: "pattern", type: "string", schemaType: "string", $data: true, + error, code(cxt: KeywordCxt) { const {gen, data, $data, schema, schemaCode} = cxt const regExp = $data ? _`(new RegExp(${schemaCode}, "u"))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch cxt.fail$data(_`!${regExp}.test(${data})`) }, - error: { - message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, - params: ({schemaCode}) => _`{pattern: ${schemaCode}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 85a3b85286..dc66ff15be 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,14 +1,20 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {_, str, nil, Name} from "../../compile/codegen" +const error: KeywordErrorDefinition = { + message: ({params: {missingProperty}}) => str`should have required property '${missingProperty}'`, + params: ({params: {missingProperty}}) => _`{missingProperty: ${missingProperty}}`, +} + const def: CodeKeywordDefinition = { keyword: "required", type: "object", schemaType: "array", $data: true, + error, code(cxt: KeywordCxt) { const {gen, schema, schemaCode, data, $data, it} = cxt if (!$data && schema.length === 0) return @@ -62,11 +68,6 @@ const def: CodeKeywordDefinition = { ) } }, - error: { - message: ({params: {missingProperty}}) => - str`should have required property '${missingProperty}'`, - params: ({params: {missingProperty}}) => _`{missingProperty: ${missingProperty}}`, - }, } -module.exports = def +export default def diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 02da608607..53f065efef 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,15 +1,22 @@ -import type {CodeKeywordDefinition} from "../../types" +import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {getSchemaTypes} from "../../compile/validate/dataType" import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" import equal from "fast-deep-equal" +const error: KeywordErrorDefinition = { + message: ({params: {i, j}}) => + str`should NOT have duplicate items (items ## ${j} and ${i} are identical)`, + params: ({params: {i, j}}) => _`{i: ${i}, j: ${j}}`, +} + const def: CodeKeywordDefinition = { keyword: "uniqueItems", type: "array", schemaType: "boolean", $data: true, + error, code(cxt: KeywordCxt) { const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt if (!$data && !schema) return @@ -64,11 +71,6 @@ const def: CodeKeywordDefinition = { ) } }, - error: { - message: ({params: {i, j}}) => - str`should NOT have duplicate items (items ## ${j} and ${i} are identical)`, - params: ({params: {i, j}}) => _`{i: ${i}, j: ${j}}`, - }, } -module.exports = def +export default def From defca2ce21dcd98fbedffde2dd693c6672ee3b13 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Sep 2020 20:18:22 +0100 Subject: [PATCH 228/322] feat: DefinedError as discriminated union with different parameter types, closes #843, closes #1090 --- lib/compile/validate/dataType.ts | 4 ++ lib/types/index.ts | 8 ++- .../applicator/additionalItems.ts | 4 ++ .../applicator/additionalProperties.ts | 4 ++ lib/vocabularies/applicator/dependencies.ts | 9 ++- lib/vocabularies/applicator/if.ts | 4 ++ lib/vocabularies/applicator/oneOf.ts | 4 ++ lib/vocabularies/applicator/propertyNames.ts | 4 ++ lib/vocabularies/core/ref.ts | 4 ++ lib/vocabularies/errors.ts | 69 +++++++++++++++++++ lib/vocabularies/format/format.ts | 4 ++ lib/vocabularies/validation/const.ts | 4 ++ lib/vocabularies/validation/enum.ts | 4 ++ lib/vocabularies/validation/limit.ts | 9 +++ lib/vocabularies/validation/multipleOf.ts | 4 ++ lib/vocabularies/validation/pattern.ts | 4 ++ lib/vocabularies/validation/required.ts | 4 ++ lib/vocabularies/validation/uniqueItems.ts | 5 ++ spec/types/async-validate.spec.ts | 3 +- spec/types/error-parameters.spec.ts | 31 +++++++++ 20 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 lib/vocabularies/errors.ts create mode 100644 spec/types/error-parameters.spec.ts diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index f45206fd08..b3354e22b9 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -134,6 +134,10 @@ function assignParentData({gen, parentData, parentDataProperty}: SchemaObjCxt, e ) } +export interface TypeErrorParams { + type: string +} + const typeError: KeywordErrorDefinition = { message: ({schema}) => str`should be ${schema}`, params: ({schema, schemaValue}) => diff --git a/lib/types/index.ts b/lib/types/index.ts index 581f82dae9..c07f058ba0 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -3,6 +3,8 @@ import type {SchemaEnv} from "../compile" import type KeywordCxt from "../compile/context" import type Ajv from "../ajv" +export {DefinedError} from "../vocabularies/errors" + interface _SchemaObject { $id?: string $schema?: string @@ -141,11 +143,11 @@ export interface AsyncValidateFunction extends ValidateFunction { export type AnyValidateFunction = ValidateFunction | AsyncValidateFunction -export interface ErrorObject { - keyword: T +export interface ErrorObject> { + keyword: K dataPath: string schemaPath: string - params: Record // TODO add interface + params: P // Added to validation errors of "propertyNames" keyword schema propertyName?: string // Excluded if option `messages` set to false. diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index 92c7c423cf..df9d5643cc 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" +export interface AdditionalItemsErrorParams { + limit: number +} + const error: KeywordErrorDefinition = { message: ({params: {len}}) => str`should NOT have more than ${len} items`, params: ({params: {len}}) => _`{limit: ${len}}`, diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 7e4f55a917..e54712eebf 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -4,6 +4,10 @@ import {applySubschema, SubschemaApplication, Type} from "../../compile/subschem import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" +export interface AdditionalPropsErrorParams { + additionalProperty: string +} + const error: KeywordErrorDefinition = { message: "should NOT have additional properties", params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`, diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index da279861b8..c5ccbe5476 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -11,6 +11,13 @@ interface PropertyDependencies { type SchemaDependencies = SchemaMap +export interface DependenciesErrorParams { + property: string + missingProperty: string + depsCount: number + deps: string // TODO change to string[] +} + const error: KeywordErrorDefinition = { message: ({params: {property, depsCount, deps}}) => { const property_ies = depsCount === 1 ? "property" : "properties" @@ -20,7 +27,7 @@ const error: KeywordErrorDefinition = { _`{property: ${property}, missingProperty: ${missingProperty}, depsCount: ${depsCount}, - deps: ${deps}}`, // TODO change to reference? + deps: ${deps}}`, // TODO change to reference } const def: CodeKeywordDefinition = { diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 4bcd3654c4..714e38de14 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" +export interface IfErrorParams { + failingKeyword: string +} + const error: KeywordErrorDefinition = { message: ({params}) => str`should match "${params.ifClause}" schema`, params: ({params}) => _`{failingKeyword: ${params.ifClause}}`, diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index cc4c74d548..37f3bfd1ed 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" +export interface OneOfErrorParams { + passingSchemas: [number, number] +} + const error: KeywordErrorDefinition = { message: "should match exactly one schema in oneOf", params: ({params}) => _`{passingSchemas: ${params.passing}}`, diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 72d67c4f0d..5e775291d7 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -4,6 +4,10 @@ import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" +export interface PropertyNamesErrorParams { + propertyName: string +} + const error: KeywordErrorDefinition = { message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? params: ({params}) => _`{propertyName: ${params.propertyName}}`, diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index ea316a0d05..af536da5bf 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -7,6 +7,10 @@ import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" import {SchemaEnv, resolveRef} from "../../compile" +export interface RefErrorParams { + ref: string +} + const error: KeywordErrorDefinition = { message: ({schema}) => str`can't resolve reference ${schema}`, params: ({schema}) => _`{ref: ${schema}}`, diff --git a/lib/vocabularies/errors.ts b/lib/vocabularies/errors.ts new file mode 100644 index 0000000000..ea36eabee6 --- /dev/null +++ b/lib/vocabularies/errors.ts @@ -0,0 +1,69 @@ +import type {ErrorObject} from "../types" +import type {RefErrorParams} from "./core/ref" +import type {TypeErrorParams} from "../compile/validate/dataType" +import type {AdditionalItemsErrorParams} from "./applicator/additionalItems" +import type {AdditionalPropsErrorParams} from "./applicator/additionalProperties" +import type {DependenciesErrorParams} from "./applicator/dependencies" +import type {IfErrorParams} from "./applicator/if" +import type {OneOfErrorParams} from "./applicator/oneOf" +import type {PropertyNamesErrorParams} from "./applicator/propertyNames" +import type {LimitErrorParams, LimitNumberErrorParams} from "./validation/limit" +import type {MultipleOfErrorParams} from "./validation/multipleOf" +import type {PatternErrorParams} from "./validation/pattern" +import type {RequiredErrorParams} from "./validation/required" +import type {UniqueItemsErrorParams} from "./validation/uniqueItems" +import type {ConstErrorParams} from "./validation/const" +import type {EnumErrorParams} from "./validation/enum" +import type {FormatErrorParams} from "./format/format" + +type LimitKeyword = + | "maxItems" + | "minItems" + | "minProperties" + | "maxProperties" + | "minLength" + | "maxLength" + +type LimitNumberKeyword = "maximum" | "minimum" | "exclusiveMaximum" | "exclusiveMinimum" + +type RefError = ErrorObject<"ref", RefErrorParams> +type TypeError = ErrorObject<"type", TypeErrorParams> +type ErrorWithoutParams = ErrorObject< + "anyOf" | "contains" | "not" | "false schema", + Record +> +type AdditionalItemsError = ErrorObject<"additionalItems", AdditionalItemsErrorParams> +type AdditionalPropsError = ErrorObject<"additionalProperties", AdditionalPropsErrorParams> +type DependenciesError = ErrorObject<"dependencies", DependenciesErrorParams> +type IfKeywordError = ErrorObject<"if", IfErrorParams> +type OneOfError = ErrorObject<"oneOf", OneOfErrorParams> +type PropertyNamesError = ErrorObject<"propertyNames", PropertyNamesErrorParams> +type LimitError = ErrorObject +type LimitNumberError = ErrorObject +type MultipleOfError = ErrorObject<"multipleOf", MultipleOfErrorParams> +type PatternError = ErrorObject<"pattern", PatternErrorParams> +type RequiredError = ErrorObject<"required", RequiredErrorParams> +type UniqueItemsError = ErrorObject<"uniqueItems", UniqueItemsErrorParams> +type ConstError = ErrorObject<"const", ConstErrorParams> +type EnumError = ErrorObject<"enum", EnumErrorParams> +type FormatError = ErrorObject<"format", FormatErrorParams> + +export type DefinedError = + | RefError + | TypeError + | ErrorWithoutParams + | AdditionalItemsError + | AdditionalPropsError + | DependenciesError + | IfKeywordError + | OneOfError + | PropertyNamesError + | LimitNumberError + | MultipleOfError + | LimitError + | PatternError + | RequiredError + | UniqueItemsError + | ConstError + | EnumError + | FormatError diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 437af8477a..281bc47f71 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -17,6 +17,10 @@ type FormatValidate = | RegExp | string +export interface FormatErrorParams { + format: string +} + const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should match format "${schemaCode}"`, params: ({schemaCode}) => _`{format: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index f44d7b9b16..ad9352c907 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -3,6 +3,10 @@ import type KeywordCxt from "../../compile/context" import {_} from "../../compile/codegen" import equal from "fast-deep-equal" +export interface ConstErrorParams { + allowedValue: any +} + const error: KeywordErrorDefinition = { message: "should be equal to constant", params: ({schemaCode}) => _`{allowedValue: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index f2bfb302ee..de041b9caf 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -3,6 +3,10 @@ import type KeywordCxt from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" import equal from "fast-deep-equal" +export interface EnumErrorParams { + allowedValues: any[] +} + const error: KeywordErrorDefinition = { message: "should be equal to one of the allowed values", params: ({schemaCode}) => _`{allowedValues: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts index 22086f1ebb..790894746d 100644 --- a/lib/vocabularies/validation/limit.ts +++ b/lib/vocabularies/validation/limit.ts @@ -11,6 +11,15 @@ const OPS: {[index: string]: {fail: Code; ok: Code; okStr: string}} = { exclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE}, } +export interface LimitErrorParams { + limit: number +} + +export interface LimitNumberErrorParams { + limit: number + comparison: "<=" | ">=" | "<" | ">" +} + const error: KeywordErrorDefinition = { message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].okStr}, limit: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index d1a763e7a8..11d4de480b 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -2,6 +2,10 @@ import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str} from "../../compile/codegen" +export interface MultipleOfErrorParams { + multipleOf: number +} + const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, params: ({schemaCode}) => _`{multipleOf: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 0d41aab587..d51a46268e 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -3,6 +3,10 @@ import type KeywordCxt from "../../compile/context" import {usePattern} from "../util" import {_, str} from "../../compile/codegen" +export interface PatternErrorParams { + pattern: string +} + const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, params: ({schemaCode}) => _`{pattern: ${schemaCode}}`, diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index dc66ff15be..a14de9e775 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -4,6 +4,10 @@ import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {_, str, nil, Name} from "../../compile/codegen" +export interface RequiredErrorParams { + missingProperty: string +} + const error: KeywordErrorDefinition = { message: ({params: {missingProperty}}) => str`should have required property '${missingProperty}'`, params: ({params: {missingProperty}}) => _`{missingProperty: ${missingProperty}}`, diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 53f065efef..f89f0371cf 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -5,6 +5,11 @@ import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" import equal from "fast-deep-equal" +export interface UniqueItemsErrorParams { + i: number + j: number +} + const error: KeywordErrorDefinition = { message: ({params: {i, j}}) => str`should NOT have duplicate items (items ## ${j} and ${i} are identical)`, diff --git a/spec/types/async-validate.spec.ts b/spec/types/async-validate.spec.ts index 82fa02f4c8..f8eba8d8be 100644 --- a/spec/types/async-validate.spec.ts +++ b/spec/types/async-validate.spec.ts @@ -1,6 +1,7 @@ import type {AnySchemaObject, SchemaObject, AsyncSchema} from "../../dist/types" import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() interface Foo { foo: number diff --git a/spec/types/error-parameters.spec.ts b/spec/types/error-parameters.spec.ts new file mode 100644 index 0000000000..638b5fadc4 --- /dev/null +++ b/spec/types/error-parameters.spec.ts @@ -0,0 +1,31 @@ +import {DefinedError} from "../../dist/types" +import _Ajv from "../ajv" +import chai from "../chai" +const should = chai.should() + +describe("error object parameters type", () => { + const ajv = new _Ajv({allErrors: true}) + + it("should be determined by the keyword", () => { + const validate = ajv.compile({minimum: 0, multipleOf: 2}) + const valid = validate(-1) + valid.should.equal(false) + const errs = validate.errors + if (errs) { + errs.length.should.equal(2) + for (const err of errs as DefinedError[]) { + switch (err.keyword) { + case "minimum": + err.params.limit.should.equal(0) + err.params.comparison.should.equal(">=") + break + case "multipleOf": + err.params.multipleOf.should.equal(2) + break + default: + should.fail() + } + } + } + }) +}) From f7a5f9dc967932637d221ab2fd264aa48d434bd8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 08:26:30 +0100 Subject: [PATCH 229/322] refactor: keyword validation errors --- lib/ajv.ts | 2 + lib/compile/validate/dataType.ts | 5 +- lib/types/index.ts | 2 - .../applicator/additionalItems.ts | 8 +-- .../applicator/additionalProperties.ts | 9 +-- lib/vocabularies/applicator/dependencies.ts | 23 ++++-- lib/vocabularies/applicator/if.ts | 11 +-- lib/vocabularies/applicator/index.ts | 45 ++++++++++-- lib/vocabularies/applicator/oneOf.ts | 11 +-- lib/vocabularies/applicator/propertyNames.ts | 6 +- lib/vocabularies/core/ref.ts | 11 +-- lib/vocabularies/errors.ts | 71 ++----------------- lib/vocabularies/format/format.ts | 5 +- lib/vocabularies/validation/const.ts | 6 +- lib/vocabularies/validation/enum.ts | 6 +- lib/vocabularies/validation/index.ts | 33 ++++++--- lib/vocabularies/validation/limit.ts | 41 ----------- lib/vocabularies/validation/limitNumber.ts | 39 ++++++++++ lib/vocabularies/validation/multipleOf.ts | 6 +- lib/vocabularies/validation/pattern.ts | 6 +- lib/vocabularies/validation/required.ts | 6 +- lib/vocabularies/validation/uniqueItems.ts | 7 +- spec/types/error-parameters.spec.ts | 2 +- 23 files changed, 174 insertions(+), 187 deletions(-) delete mode 100644 lib/vocabularies/validation/limit.ts create mode 100644 lib/vocabularies/validation/limitNumber.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 4b001b783e..58755a825c 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -13,6 +13,7 @@ export { Options, ValidateFunction, AsyncValidateFunction, + ErrorObject, CacheInterface, Logger, } from "./types" @@ -23,6 +24,7 @@ export interface Plugin { import KeywordCxt from "./compile/context" export {KeywordCxt} +export {DefinedError} from "./vocabularies/errors" import type { Schema, diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index b3354e22b9..d9642069fc 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -2,6 +2,7 @@ import type { SchemaObjCxt, KeywordErrorDefinition, KeywordErrorCxt, + ErrorObject, AnySchemaObject, } from "../../types" import type {ValidationRules} from "../rules" @@ -134,9 +135,7 @@ function assignParentData({gen, parentData, parentDataProperty}: SchemaObjCxt, e ) } -export interface TypeErrorParams { - type: string -} +export type TypeError = ErrorObject<"type", {type: string}> const typeError: KeywordErrorDefinition = { message: ({schema}) => str`should be ${schema}`, diff --git a/lib/types/index.ts b/lib/types/index.ts index c07f058ba0..c6c6592404 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -3,8 +3,6 @@ import type {SchemaEnv} from "../compile" import type KeywordCxt from "../compile/context" import type Ajv from "../ajv" -export {DefinedError} from "../vocabularies/errors" - interface _SchemaObject { $id?: string $schema?: string diff --git a/lib/vocabularies/applicator/additionalItems.ts b/lib/vocabularies/applicator/additionalItems.ts index df9d5643cc..88d1ae8c6c 100644 --- a/lib/vocabularies/applicator/additionalItems.ts +++ b/lib/vocabularies/applicator/additionalItems.ts @@ -1,12 +1,10 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_, Name, str} from "../../compile/codegen" -export interface AdditionalItemsErrorParams { - limit: number -} +export type AdditionalItemsError = ErrorObject<"additionalItems", {limit: number}> const error: KeywordErrorDefinition = { message: ({params: {len}}) => str`should NOT have more than ${len} items`, @@ -14,7 +12,7 @@ const error: KeywordErrorDefinition = { } const def: CodeKeywordDefinition = { - keyword: "additionalItems", + keyword: "additionalItems" as const, type: "array", schemaType: ["boolean", "object"], before: "uniqueItems", diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index e54712eebf..c2ab3d2559 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,12 +1,13 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" -export interface AdditionalPropsErrorParams { - additionalProperty: string -} +export type AdditionalPropertiesError = ErrorObject< + "additionalProperties", + {additionalProperty: string} +> const error: KeywordErrorDefinition = { message: "should NOT have additional properties", diff --git a/lib/vocabularies/applicator/dependencies.ts b/lib/vocabularies/applicator/dependencies.ts index c5ccbe5476..f6437eb735 100644 --- a/lib/vocabularies/applicator/dependencies.ts +++ b/lib/vocabularies/applicator/dependencies.ts @@ -1,4 +1,10 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition, SchemaMap, AnySchema} from "../../types" +import type { + CodeKeywordDefinition, + ErrorObject, + KeywordErrorDefinition, + SchemaMap, + AnySchema, +} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, propertyInData} from "../util" import {applySubschema} from "../../compile/subschema" @@ -11,12 +17,15 @@ interface PropertyDependencies { type SchemaDependencies = SchemaMap -export interface DependenciesErrorParams { - property: string - missingProperty: string - depsCount: number - deps: string // TODO change to string[] -} +export type DependenciesError = ErrorObject< + "dependencies", + { + property: string + missingProperty: string + depsCount: number + deps: string // TODO change to string[] + } +> const error: KeywordErrorDefinition = { message: ({params: {property, depsCount, deps}}) => { diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 714e38de14..8ff19f2576 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,12 +1,15 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition, SchemaObjCxt} from "../../types" +import type { + CodeKeywordDefinition, + ErrorObject, + KeywordErrorDefinition, + SchemaObjCxt, +} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str, Name} from "../../compile/codegen" -export interface IfErrorParams { - failingKeyword: string -} +export type IfKeywordError = ErrorObject<"if", {failingKeyword: string}> const error: KeywordErrorDefinition = { message: ({params}) => str`should match "${params.ifClause}" schema`, diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index 15abf1fdfe..ca3c16958a 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -1,19 +1,36 @@ -import type {Vocabulary} from "../../types" -import additionalItems from "./additionalItems" +import type {ErrorObject, Vocabulary} from "../../types" +import additionalItems, {AdditionalItemsError} from "./additionalItems" import items from "./items" import contains from "./contains" -import dependencies from "./dependencies" -import propertyNames from "./propertyNames" -import additionalProperties from "./additionalProperties" +import dependencies, {DependenciesError} from "./dependencies" +import propertyNames, {PropertyNamesError} from "./propertyNames" +import additionalProperties, {AdditionalPropertiesError} from "./additionalProperties" import properties from "./properties" import patternProperties from "./patternProperties" import notKeyword from "./not" import anyOf from "./anyOf" -import oneOf from "./oneOf" +import oneOf, {OneOfError} from "./oneOf" import allOf from "./allOf" -import ifKeyword from "./if" +import ifKeyword, {IfKeywordError} from "./if" import thenElse from "./thenElse" +export type ApplicatorKeyword = + | "additionalItems" + | "items" + | "contains" + | "dependencies" + | "propertyNames" + | "additionalProperties" + | "properties" + | "patternProperties" + | "not" + | "anyOf" + | "oneOf" + | "allOf" + | "if" + | "then" + | "else" + const applicator: Vocabulary = [ // array additionalItems, @@ -35,3 +52,17 @@ const applicator: Vocabulary = [ ] export default applicator + +export type ApplicatorKeywordError = + | ErrorWithoutParams + | AdditionalItemsError + | AdditionalPropertiesError + | DependenciesError + | IfKeywordError + | OneOfError + | PropertyNamesError + +export type ErrorWithoutParams = ErrorObject< + "anyOf" | "contains" | "not" | "false schema", + Record +> diff --git a/lib/vocabularies/applicator/oneOf.ts b/lib/vocabularies/applicator/oneOf.ts index 37f3bfd1ed..bcd554085c 100644 --- a/lib/vocabularies/applicator/oneOf.ts +++ b/lib/vocabularies/applicator/oneOf.ts @@ -1,12 +1,15 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition, AnySchema} from "../../types" +import type { + CodeKeywordDefinition, + ErrorObject, + KeywordErrorDefinition, + AnySchema, +} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_} from "../../compile/codegen" -export interface OneOfErrorParams { - passingSchemas: [number, number] -} +export type OneOfError = ErrorObject<"oneOf", {passingSchemas: [number, number]}> const error: KeywordErrorDefinition = { message: "should match exactly one schema in oneOf", diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 5e775291d7..5c65a1d928 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -1,12 +1,10 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../util" import {applySubschema} from "../../compile/subschema" import {_, str} from "../../compile/codegen" -export interface PropertyNamesErrorParams { - propertyName: string -} +export type PropertyNamesError = ErrorObject<"propertyNames", {propertyName: string}> const error: KeywordErrorDefinition = { message: ({params}) => str`property name '${params.propertyName}' is invalid`, // TODO double quotes? diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index af536da5bf..aeb3e0af36 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,4 +1,9 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition, AnySchema} from "../../types" +import type { + CodeKeywordDefinition, + ErrorObject, + KeywordErrorDefinition, + AnySchema, +} from "../../types" import type KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" @@ -7,9 +12,7 @@ import {_, str, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" import {SchemaEnv, resolveRef} from "../../compile" -export interface RefErrorParams { - ref: string -} +export type RefError = ErrorObject<"ref", {ref: string}> const error: KeywordErrorDefinition = { message: ({schema}) => str`can't resolve reference ${schema}`, diff --git a/lib/vocabularies/errors.ts b/lib/vocabularies/errors.ts index ea36eabee6..f501fd2294 100644 --- a/lib/vocabularies/errors.ts +++ b/lib/vocabularies/errors.ts @@ -1,69 +1,12 @@ -import type {ErrorObject} from "../types" -import type {RefErrorParams} from "./core/ref" -import type {TypeErrorParams} from "../compile/validate/dataType" -import type {AdditionalItemsErrorParams} from "./applicator/additionalItems" -import type {AdditionalPropsErrorParams} from "./applicator/additionalProperties" -import type {DependenciesErrorParams} from "./applicator/dependencies" -import type {IfErrorParams} from "./applicator/if" -import type {OneOfErrorParams} from "./applicator/oneOf" -import type {PropertyNamesErrorParams} from "./applicator/propertyNames" -import type {LimitErrorParams, LimitNumberErrorParams} from "./validation/limit" -import type {MultipleOfErrorParams} from "./validation/multipleOf" -import type {PatternErrorParams} from "./validation/pattern" -import type {RequiredErrorParams} from "./validation/required" -import type {UniqueItemsErrorParams} from "./validation/uniqueItems" -import type {ConstErrorParams} from "./validation/const" -import type {EnumErrorParams} from "./validation/enum" -import type {FormatErrorParams} from "./format/format" - -type LimitKeyword = - | "maxItems" - | "minItems" - | "minProperties" - | "maxProperties" - | "minLength" - | "maxLength" - -type LimitNumberKeyword = "maximum" | "minimum" | "exclusiveMaximum" | "exclusiveMinimum" - -type RefError = ErrorObject<"ref", RefErrorParams> -type TypeError = ErrorObject<"type", TypeErrorParams> -type ErrorWithoutParams = ErrorObject< - "anyOf" | "contains" | "not" | "false schema", - Record -> -type AdditionalItemsError = ErrorObject<"additionalItems", AdditionalItemsErrorParams> -type AdditionalPropsError = ErrorObject<"additionalProperties", AdditionalPropsErrorParams> -type DependenciesError = ErrorObject<"dependencies", DependenciesErrorParams> -type IfKeywordError = ErrorObject<"if", IfErrorParams> -type OneOfError = ErrorObject<"oneOf", OneOfErrorParams> -type PropertyNamesError = ErrorObject<"propertyNames", PropertyNamesErrorParams> -type LimitError = ErrorObject -type LimitNumberError = ErrorObject -type MultipleOfError = ErrorObject<"multipleOf", MultipleOfErrorParams> -type PatternError = ErrorObject<"pattern", PatternErrorParams> -type RequiredError = ErrorObject<"required", RequiredErrorParams> -type UniqueItemsError = ErrorObject<"uniqueItems", UniqueItemsErrorParams> -type ConstError = ErrorObject<"const", ConstErrorParams> -type EnumError = ErrorObject<"enum", EnumErrorParams> -type FormatError = ErrorObject<"format", FormatErrorParams> +import type {RefError} from "./core/ref" +import type {TypeError} from "../compile/validate/dataType" +import type {ApplicatorKeywordError} from "./applicator" +import type {ValidationKeywordError} from "./validation" +import type {FormatError} from "./format/format" export type DefinedError = | RefError | TypeError - | ErrorWithoutParams - | AdditionalItemsError - | AdditionalPropsError - | DependenciesError - | IfKeywordError - | OneOfError - | PropertyNamesError - | LimitNumberError - | MultipleOfError - | LimitError - | PatternError - | RequiredError - | UniqueItemsError - | ConstError - | EnumError + | ApplicatorKeywordError + | ValidationKeywordError | FormatError diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 281bc47f71..74ce00d72a 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -4,6 +4,7 @@ import type { AsyncFormatValidator, CodeKeywordDefinition, KeywordErrorDefinition, + ErrorObject, } from "../../types" import type KeywordCxt from "../../compile/context" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" @@ -17,9 +18,7 @@ type FormatValidate = | RegExp | string -export interface FormatErrorParams { - format: string -} +export type FormatError = ErrorObject<"format", {format: string}> const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should match format "${schemaCode}"`, diff --git a/lib/vocabularies/validation/const.ts b/lib/vocabularies/validation/const.ts index ad9352c907..fc122737fb 100644 --- a/lib/vocabularies/validation/const.ts +++ b/lib/vocabularies/validation/const.ts @@ -1,11 +1,9 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_} from "../../compile/codegen" import equal from "fast-deep-equal" -export interface ConstErrorParams { - allowedValue: any -} +export type ConstError = ErrorObject<"const", {allowedValue: any}> const error: KeywordErrorDefinition = { message: "should be equal to constant", diff --git a/lib/vocabularies/validation/enum.ts b/lib/vocabularies/validation/enum.ts index de041b9caf..d6fbdb108b 100644 --- a/lib/vocabularies/validation/enum.ts +++ b/lib/vocabularies/validation/enum.ts @@ -1,11 +1,9 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, or, Name, Code} from "../../compile/codegen" import equal from "fast-deep-equal" -export interface EnumErrorParams { - allowedValues: any[] -} +export type EnumError = ErrorObject<"enum", {allowedValues: any[]}> const error: KeywordErrorDefinition = { message: "should be equal to one of the allowed values", diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 1c555ec41c..ab27a5ad11 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -1,18 +1,18 @@ -import type {Vocabulary} from "../../types" -import limit from "./limit" -import multipleOf from "./multipleOf" +import type {ErrorObject, Vocabulary} from "../../types" +import limitNumber, {LimitNumberError} from "./limitNumber" +import multipleOf, {MultipleOfError} from "./multipleOf" import limitLength from "./limitLength" -import pattern from "./pattern" +import pattern, {PatternError} from "./pattern" import limitProperties from "./limitProperties" -import required from "./required" +import required, {RequiredError} from "./required" import limitItems from "./limitItems" -import uniqueItems from "./uniqueItems" -import constKeyword from "./const" -import enumKeyword from "./enum" +import uniqueItems, {UniqueItemsError} from "./uniqueItems" +import constKeyword, {ConstError} from "./const" +import enumKeyword, {EnumError} from "./enum" const validation: Vocabulary = [ // number - limit, + limitNumber, multipleOf, // string limitLength, @@ -30,3 +30,18 @@ const validation: Vocabulary = [ ] export default validation + +type LimitError = ErrorObject< + "maxItems" | "minItems" | "minProperties" | "maxProperties" | "minLength" | "maxLength", + {limit: number} +> + +export type ValidationKeywordError = + | LimitError + | LimitNumberError + | MultipleOfError + | PatternError + | RequiredError + | UniqueItemsError + | ConstError + | EnumError diff --git a/lib/vocabularies/validation/limit.ts b/lib/vocabularies/validation/limit.ts deleted file mode 100644 index 790894746d..0000000000 --- a/lib/vocabularies/validation/limit.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" -import type KeywordCxt from "../../compile/context" -import {_, str, operators, Code} from "../../compile/codegen" - -const ops = operators - -const OPS: {[index: string]: {fail: Code; ok: Code; okStr: string}} = { - maximum: {okStr: "<=", ok: ops.LTE, fail: ops.GT}, - minimum: {okStr: ">=", ok: ops.GTE, fail: ops.LT}, - exclusiveMaximum: {okStr: "<", ok: ops.LT, fail: ops.GTE}, - exclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE}, -} - -export interface LimitErrorParams { - limit: number -} - -export interface LimitNumberErrorParams { - limit: number - comparison: "<=" | ">=" | "<" | ">" -} - -const error: KeywordErrorDefinition = { - message: ({keyword, schemaCode}) => str`should be ${OPS[keyword].okStr} ${schemaCode}`, - params: ({keyword, schemaCode}) => _`{comparison: ${OPS[keyword].okStr}, limit: ${schemaCode}}`, -} - -const def: CodeKeywordDefinition = { - keyword: ["maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"], - type: "number", - schemaType: "number", - $data: true, - error, - code(cxt: KeywordCxt) { - const {keyword, data, schemaCode} = cxt - // const bdt = bad$DataType(schemaCode, def.schemaType, $data) - cxt.fail$data(_`(${data} ${OPS[keyword].fail} ${schemaCode} || isNaN(${data}))`) - }, -} - -export default def diff --git a/lib/vocabularies/validation/limitNumber.ts b/lib/vocabularies/validation/limitNumber.ts new file mode 100644 index 0000000000..66e30f7ddb --- /dev/null +++ b/lib/vocabularies/validation/limitNumber.ts @@ -0,0 +1,39 @@ +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" +import {_, str, operators, Code} from "../../compile/codegen" + +const ops = operators + +type Kwd = "maximum" | "minimum" | "exclusiveMaximum" | "exclusiveMinimum" + +type Comparison = "<=" | ">=" | "<" | ">" + +const KWDs: {[K in Kwd]: {okStr: Comparison; ok: Code; fail: Code}} = { + maximum: {okStr: "<=", ok: ops.LTE, fail: ops.GT}, + minimum: {okStr: ">=", ok: ops.GTE, fail: ops.LT}, + exclusiveMaximum: {okStr: "<", ok: ops.LT, fail: ops.GTE}, + exclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE}, +} + +export type LimitNumberError = ErrorObject + +const error: KeywordErrorDefinition = { + message: ({keyword, schemaCode}) => str`should be ${KWDs[keyword as Kwd].okStr} ${schemaCode}`, + params: ({keyword, schemaCode}) => + _`{comparison: ${KWDs[keyword as Kwd].okStr}, limit: ${schemaCode}}`, +} + +const def: CodeKeywordDefinition = { + keyword: Object.keys(KWDs), + type: "number", + schemaType: "number", + $data: true, + error, + code(cxt: KeywordCxt) { + const {keyword, data, schemaCode} = cxt + // const bdt = bad$DataType(schemaCode, def.schemaType, $data) + cxt.fail$data(_`(${data} ${KWDs[keyword as Kwd].fail} ${schemaCode} || isNaN(${data}))`) + }, +} + +export default def diff --git a/lib/vocabularies/validation/multipleOf.ts b/lib/vocabularies/validation/multipleOf.ts index 11d4de480b..ef703769c2 100644 --- a/lib/vocabularies/validation/multipleOf.ts +++ b/lib/vocabularies/validation/multipleOf.ts @@ -1,10 +1,8 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, str} from "../../compile/codegen" -export interface MultipleOfErrorParams { - multipleOf: number -} +export type MultipleOfError = ErrorObject<"multipleOf", {multipleOf: number}> const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should be multiple of ${schemaCode}`, diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index d51a46268e..b23aa1c2c1 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -1,11 +1,9 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {usePattern} from "../util" import {_, str} from "../../compile/codegen" -export interface PatternErrorParams { - pattern: string -} +export type PatternError = ErrorObject<"pattern", {pattern: string}> const error: KeywordErrorDefinition = { message: ({schemaCode}) => str`should match pattern "${schemaCode}"`, diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index a14de9e775..702b04fefb 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -1,12 +1,10 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {propertyInData, noPropertyInData} from "../util" import {checkReportMissingProp, checkMissingProp, reportMissingProp} from "../missing" import {_, str, nil, Name} from "../../compile/codegen" -export interface RequiredErrorParams { - missingProperty: string -} +export type RequiredError = ErrorObject<"required", {missingProperty: string}> const error: KeywordErrorDefinition = { message: ({params: {missingProperty}}) => str`should have required property '${missingProperty}'`, diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index f89f0371cf..9c0533dc6b 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -1,14 +1,11 @@ -import type {CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {getSchemaTypes} from "../../compile/validate/dataType" import {checkDataTypes, DataType} from "../../compile/util" import {_, str, Name} from "../../compile/codegen" import equal from "fast-deep-equal" -export interface UniqueItemsErrorParams { - i: number - j: number -} +export type UniqueItemsError = ErrorObject<"uniqueItems", {i: number; j: number}> const error: KeywordErrorDefinition = { message: ({params: {i, j}}) => diff --git a/spec/types/error-parameters.spec.ts b/spec/types/error-parameters.spec.ts index 638b5fadc4..16358e69fe 100644 --- a/spec/types/error-parameters.spec.ts +++ b/spec/types/error-parameters.spec.ts @@ -1,4 +1,4 @@ -import {DefinedError} from "../../dist/types" +import {DefinedError} from "../../dist/ajv" import _Ajv from "../ajv" import chai from "../chai" const should = chai.should() From 4fb63c7e8123b340336cce3a21f9c8b650fdb60f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 12:52:38 +0100 Subject: [PATCH 230/322] remove coveralls from dependencies --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 2ae1e0dcbd..64655ef1da 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "ajv-formats": "^0.3.2", "browserify": "^16.2.0", "chai": "^4.0.1", - "coveralls": "^3.0.1", "cross-env": "^7.0.2", "eslint": "^7.8.1", "eslint-config-prettier": "^6.11.0", From 9180f4c626818a596bdcc7a508fc0b97fc45a4fc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 13:01:57 +0100 Subject: [PATCH 231/322] style: update files to match prettier config --- .github/ISSUE_TEMPLATE.md | 4 ++-- .github/ISSUE_TEMPLATE/bug-or-error-report.md | 5 ++--- .github/ISSUE_TEMPLATE/typescript.md | 2 +- .prettierignore | 2 ++ lib/refs/json-schema-draft-06.json | 10 +--------- lib/refs/json-schema-draft-07.json | 10 +--------- package.json | 2 +- spec/tests/issues/226_json_with_control_chars.json | 9 +-------- spec/tests/rules/items.json | 11 ++--------- spec/tests/schemas/advanced.json | 6 +----- spec/tests/schemas/basic.json | 4 +--- spec/tests/schemas/complex2.json | 5 +---- spec/tests/schemas/cosmicrealms.json | 7 +------ 13 files changed, 17 insertions(+), 60 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 46d6f970fe..2d5317e14f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -17,6 +17,7 @@ This template is for bug or error reports. For other issues please use: ```javascript + ``` **JSON Schema** @@ -25,7 +26,6 @@ This template is for bug or error reports. For other issues please use: ```json - ``` **Sample data** @@ -34,7 +34,6 @@ This template is for bug or error reports. For other issues please use: ```json - ``` **Your code** @@ -51,6 +50,7 @@ Thank you! --> ```javascript + ``` **Validation result, data AFTER validation, error messages** diff --git a/.github/ISSUE_TEMPLATE/bug-or-error-report.md b/.github/ISSUE_TEMPLATE/bug-or-error-report.md index 9f4aeed90d..897166f412 100644 --- a/.github/ISSUE_TEMPLATE/bug-or-error-report.md +++ b/.github/ISSUE_TEMPLATE/bug-or-error-report.md @@ -21,6 +21,7 @@ For other issues please see https://github.com/ajv-validator/ajv/blob/master/CON ```javascript + ``` **JSON Schema** @@ -29,7 +30,6 @@ For other issues please see https://github.com/ajv-validator/ajv/blob/master/CON ```json - ``` **Sample data** @@ -38,7 +38,6 @@ For other issues please see https://github.com/ajv-validator/ajv/blob/master/CON ```json - ``` **Your code** @@ -55,13 +54,13 @@ Thank you! --> ```javascript + ``` **Validation result, data AFTER validation, error messages** ``` - ``` **What results did you expect?** diff --git a/.github/ISSUE_TEMPLATE/typescript.md b/.github/ISSUE_TEMPLATE/typescript.md index 26bf33ed3e..05ad1a7a2b 100644 --- a/.github/ISSUE_TEMPLATE/typescript.md +++ b/.github/ISSUE_TEMPLATE/typescript.md @@ -22,13 +22,13 @@ Please make it as small as posssible to reproduce the issue --> ```typescript + ``` **Typescript compiler error messages** ``` - ``` **Describe the change that should be made to address the issue?** diff --git a/.prettierignore b/.prettierignore index 8d0387cf3e..c61c4f88f2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,6 @@ spec/JSON-Schema-Test-Suite .browser coverage dist +bundle .nyc_output +spec/_json diff --git a/lib/refs/json-schema-draft-06.json b/lib/refs/json-schema-draft-06.json index cbaabcedd0..5410064ba8 100644 --- a/lib/refs/json-schema-draft-06.json +++ b/lib/refs/json-schema-draft-06.json @@ -16,15 +16,7 @@ "allOf": [{"$ref": "#/definitions/nonNegativeInteger"}, {"default": 0}] }, "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] + "enum": ["array", "boolean", "integer", "null", "number", "object", "string"] }, "stringArray": { "type": "array", diff --git a/lib/refs/json-schema-draft-07.json b/lib/refs/json-schema-draft-07.json index 69174b8435..6a74851043 100644 --- a/lib/refs/json-schema-draft-07.json +++ b/lib/refs/json-schema-draft-07.json @@ -16,15 +16,7 @@ "allOf": [{"$ref": "#/definitions/nonNegativeInteger"}, {"default": 0}] }, "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] + "enum": ["array", "boolean", "integer", "null", "number", "object", "string"] }, "stringArray": { "type": "array", diff --git a/package.json b/package.json index 64655ef1da..5748eb114d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && karma start", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", "test": "npm run build && npm run json-tests && npm run eslint && npm run test-cov", - "test-ci": "AJV_FULL_TEST=true npm test", + "test-ci": "AJV_FULL_TEST=true npm test && npm run prettier:check", "prepublish": "npm run build", "watch": "watch \"npm run build\" ./lib" }, diff --git a/spec/tests/issues/226_json_with_control_chars.json b/spec/tests/issues/226_json_with_control_chars.json index 3e56dd8b2b..de00a012f0 100644 --- a/spec/tests/issues/226_json_with_control_chars.json +++ b/spec/tests/issues/226_json_with_control_chars.json @@ -41,14 +41,7 @@ { "description": "JSON with control characters - 'required' (#226)", "schema": { - "required": [ - "foo\nbar", - "foo\"bar", - "foo\\bar", - "foo\rbar", - "foo\tbar", - "foo\fbar" - ] + "required": ["foo\nbar", "foo\"bar", "foo\\bar", "foo\rbar", "foo\tbar", "foo\fbar"] }, "tests": [ { diff --git a/spec/tests/rules/items.json b/spec/tests/rules/items.json index b1726fa6f5..ca21717679 100644 --- a/spec/tests/rules/items.json +++ b/spec/tests/rules/items.json @@ -25,10 +25,7 @@ "child": { "type": "array", "additionalItems": false, - "items": [ - {"$ref": "#/definitions/sub-child"}, - {"$ref": "#/definitions/sub-child"} - ] + "items": [{"$ref": "#/definitions/sub-child"}, {"$ref": "#/definitions/sub-child"}] }, "sub-child": { "type": "object", @@ -76,11 +73,7 @@ }, { "description": "wrong child", - "data": [ - {"foo": null}, - [{"foo": null}, {"foo": null}], - [{"foo": null}, {"foo": null}] - ], + "data": [{"foo": null}, [{"foo": null}, {"foo": null}], [{"foo": null}, {"foo": null}]], "valid": false }, { diff --git a/spec/tests/schemas/advanced.json b/spec/tests/schemas/advanced.json index 7a8b115366..cd5162778d 100644 --- a/spec/tests/schemas/advanced.json +++ b/spec/tests/schemas/advanced.json @@ -71,11 +71,7 @@ }, "server": { "type": "string", - "anyOf": [ - {"format": "hostname"}, - {"format": "ipv4"}, - {"format": "ipv6"} - ] + "anyOf": [{"format": "hostname"}, {"format": "ipv4"}, {"format": "ipv6"}] } }, "required": ["type", "server", "remotePath"], diff --git a/spec/tests/schemas/basic.json b/spec/tests/schemas/basic.json index 45800a228b..553855cf76 100644 --- a/spec/tests/schemas/basic.json +++ b/spec/tests/schemas/basic.json @@ -117,9 +117,7 @@ }, { "description": "valid product with tag", - "data": [ - {"tags": ["product"], "id": 1, "name": "product", "price": 100} - ], + "data": [{"tags": ["product"], "id": 1, "name": "product", "price": 100}], "valid": true }, { diff --git a/spec/tests/schemas/complex2.json b/spec/tests/schemas/complex2.json index 0dcf32ff36..759af9afdf 100644 --- a/spec/tests/schemas/complex2.json +++ b/spec/tests/schemas/complex2.json @@ -149,10 +149,7 @@ "minProperties": 1, "maxProperties": 3, "additionalProperties": { - "anyOf": [ - {"$ref": "#/definitions/base58"}, - {"$ref": "#/definitions/hex"} - ] + "anyOf": [{"$ref": "#/definitions/base58"}, {"$ref": "#/definitions/hex"}] } } } diff --git a/spec/tests/schemas/cosmicrealms.json b/spec/tests/schemas/cosmicrealms.json index 1b506608c7..79ea7efe0f 100644 --- a/spec/tests/schemas/cosmicrealms.json +++ b/spec/tests/schemas/cosmicrealms.json @@ -84,12 +84,7 @@ "zip": 65794, "topThreeFavoriteColors": ["blue", "black", "yellow"], "favoriteSingleDigitWholeNumbers": [2, 1, 3, 9], - "ipAddresses": [ - "225.234.40.3", - "96.216.243.54", - "18.126.145.83", - "196.17.191.239" - ] + "ipAddresses": ["225.234.40.3", "96.216.243.54", "18.126.145.83", "196.17.191.239"] }, "valid": true }, From 52cfe8be1046dec664de59f711c5dd6dcfef958f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 18:20:00 +0100 Subject: [PATCH 232/322] docs: formats in ajv-formats --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index af77c176dd..03b13bbf27 100644 --- a/README.md +++ b/README.md @@ -343,13 +343,16 @@ JSON Schema specification defines several annotation keywords that describe sche ## Formats -From version 7 Ajv does not include formats defined by JSON Schema specification - these and several others formats are provided by [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin. +From version 7 Ajv does not include formats defined by JSON Schema specification - these and several other formats are provided by [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin. To add all formats from this plugin: ```javascript +import Ajv from "ajv" +import addFormats from "ajv-formats" + const ajv = new Ajv() -require("ajv-formats")(ajv) +addFormats(ajv) ``` See ajv-formats documentation for further details. @@ -362,7 +365,7 @@ The following formats are defined in [ajv-formats](https://github.com/ajv-valida - _date_: full-date according to [RFC3339](http://tools.ietf.org/html/rfc3339#section-5.6). - _time_: time with optional time-zone. -- _date-time_: date-time from the same source (time-zone is mandatory). `date`, `time` and `date-time` validate ranges in `full` mode and only regexp in `fast` mode (see [options](#options)). +- _date-time_: date-time from the same source (time-zone is mandatory). - _uri_: full URI. - _uri-reference_: URI reference, including full and relative URIs. - _uri-template_: URI template according to [RFC6570](https://tools.ietf.org/html/rfc6570) @@ -378,12 +381,10 @@ The following formats are defined in [ajv-formats](https://github.com/ajv-valida **Please note**: JSON Schema draft-07 also defines formats `iri`, `iri-reference`, `idn-hostname` and `idn-email` for URLs, hostnames and emails with international characters. These formats are available in [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) plugin. -You can add (and replace) any formats using [addFormat](#api-addformat) method. +You can add and replace any formats using [addFormat](#api-addformat) method. The option `unknownFormats` allows changing the default behaviour when an unknown format is encountered. In this case Ajv can either fail schema compilation (default) or ignore it (default in versions before 5.0.0). You also can allow specific format(s) that will be ignored. See [Options](#options) for details. -You can find regular expressions used for format validation and the sources that were used in [formats.js](https://github.com/ajv-validator/ajv/blob/master/lib/compile/formats.js). - ## Combining schemas with \$ref You can structure your validation logic across multiple schema files and have schemas reference each other using `$ref` keyword. From 5fbf421d271ddee696a4c7bc782352c85f389891 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 18:25:04 +0100 Subject: [PATCH 233/322] remove prettier check from CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5748eb114d..64655ef1da 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && karma start", "test-all": "npm run test-cov && if-node-version 12 npm run test-browser", "test": "npm run build && npm run json-tests && npm run eslint && npm run test-cov", - "test-ci": "AJV_FULL_TEST=true npm test && npm run prettier:check", + "test-ci": "AJV_FULL_TEST=true npm test", "prepublish": "npm run build", "watch": "watch \"npm run build\" ./lib" }, From bb342fd02b90f3199c885790e4a33c09970fff50 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 18:28:50 +0100 Subject: [PATCH 234/322] remove unused keyword name union type --- lib/vocabularies/applicator/index.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/vocabularies/applicator/index.ts b/lib/vocabularies/applicator/index.ts index ca3c16958a..e46a17e69b 100644 --- a/lib/vocabularies/applicator/index.ts +++ b/lib/vocabularies/applicator/index.ts @@ -14,23 +14,6 @@ import allOf from "./allOf" import ifKeyword, {IfKeywordError} from "./if" import thenElse from "./thenElse" -export type ApplicatorKeyword = - | "additionalItems" - | "items" - | "contains" - | "dependencies" - | "propertyNames" - | "additionalProperties" - | "properties" - | "patternProperties" - | "not" - | "anyOf" - | "oneOf" - | "allOf" - | "if" - | "then" - | "else" - const applicator: Vocabulary = [ // array additionalItems, From e7228eb1bbb629dc90e7dab5e227575a8c469836 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 19:48:09 +0100 Subject: [PATCH 235/322] feat: replace unknownFormats option with `strict: false` and formats with true as format definition --- README.md | 38 ++++++++++++++++------------- lib/ajv.ts | 3 ++- lib/types/index.ts | 6 ++--- lib/vocabularies/format/format.ts | 18 ++++++-------- spec/extras.spec.ts | 2 +- spec/json-schema.spec.ts | 7 +++++- spec/options/unknownFormats.spec.ts | 12 ++++----- spec/schema-tests.spec.ts | 2 +- 8 files changed, 47 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 03b13bbf27..3d7266ab21 100644 --- a/README.md +++ b/README.md @@ -296,9 +296,15 @@ To reiterate, neither this nor other strict mode restrictions change the validat #### Prohibit unknown formats -TODO +By default unknown formats throw exception during schema compilation (and fail validation in case format keyword value is \$data reference). It is possible to opt out of format validation completely with `format: false` option. You can define all known formats with `addFormat` method or `formats` option - to have some format ignored pass `true` as its definition: -This will supercede unknownFormats option. +``` +const ajv = new Ajv({formats: { + reserved: true +}) +``` + +Standard JSON Schema formats are provided in [ajv-formats](https://github.com/ajv-validator/ajv-formats) package - see [Formats](#formats) section. #### Prohibit ignored defaults @@ -383,8 +389,6 @@ The following formats are defined in [ajv-formats](https://github.com/ajv-valida You can add and replace any formats using [addFormat](#api-addformat) method. -The option `unknownFormats` allows changing the default behaviour when an unknown format is encountered. In this case Ajv can either fail schema compilation (default) or ignore it (default in versions before 5.0.0). You also can allow specific format(s) that will be ignored. See [Options](#options) for details. - ## Combining schemas with \$ref You can structure your validation logic across multiple schema files and have schemas reference each other using `$ref` keyword. @@ -1062,13 +1066,18 @@ Schema can be removed using: If no parameter is passed all schemas but meta-schemas will be removed and the cache will be cleared. -##### .addFormat(String name, String|RegExp|Function|Object format) -> Ajv +##### addFormat(name: string, format: Format) => Ajv -Add format to validate strings or numbers. - -Strings are converted to RegExp. +```typescript +type Format = + | true // to ignore this format (and pass validation) + | string // will be converted to RegExp + | RegExp + | (data: string) => boolean + | Object // format definition (see below and in types) +``` -Function should return validation result as `true` or `false`. +Add format to validate strings or numbers. If object is passed it should have properties `validate`, `compare` and `async`: @@ -1155,7 +1164,6 @@ Defaults: $comment: false, format: true, formats: {}, - unknownFormats: true, schemas: {}, logger: undefined, // referenced schema options: @@ -1202,15 +1210,11 @@ Defaults: - `false` (default): ignore \$comment keyword. - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function -- _format_: formats validation mode. Option values: - - `true` (default) - validate added formats (see [Formats](#formats)). +- _format_: format validation. Option values: + - `true` (default) - validate added formats (see [Formats](#formats)). In strict mode unknown formats will throw exception during schema compilation (and fail validation in case format keyword value is \$data reference). - `false` - ignore all format keywords. -- _formats_: an object with format definitions. Keys and values will be passed to `addFormat` method. +- _formats_: an object with format definitions. Keys and values will be passed to `addFormat` method. Pass `true` as format definition to ignore some formats. - _keywords_: an array of keyword definitions or strings. Values will be passed to `addKeyword` method. -- _unknownFormats_: handling of unknown formats. Option values: - - `true` (default) - if an unknown format is encountered the exception is thrown during schema compilation. If `format` keyword value is [\$data reference](#data-reference) and it is unknown the validation will fail. - - `[String]` - an array of unknown format names that will be ignored. This option can be used to allow usage of third party schemas with format(s) for which you don't have definitions, but still fail if another unknown format is used. If `format` keyword value is [\$data reference](#data-reference) and it is not in this array the validation will fail. - - `"ignore"` - to log warning during schema compilation and always pass validation (the default behaviour in versions before 5.0.0). This option is not recommended, as it allows to mistype format name and it won't be validated without any error message. This behaviour is required by JSON Schema specification. - _schemas_: an array or object of schemas that will be added to the instance. In case you pass the array the schemas must have IDs in them. When the object is passed the method `addSchema(value, key)` will be called for each schema in this object. - _logger_: sets the logging method. Default is the global `console` object that should have methods `log`, `warn` and `error`. See [Error logging](#error-logging). Option values: - logger instance - it should have methods `log`, `warn` and `error`. If any of these methods is missing an exception will be thrown. diff --git a/lib/ajv.ts b/lib/ajv.ts index 58755a825c..4d0d2e2424 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -65,7 +65,7 @@ import draft7MetaSchema from "./refs/json-schema-draft-07.json" const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" -const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] +const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] as const const META_SUPPORT_DATA = ["/properties"] const EXT_SCOPE_NAMES = new Set([ "validate", @@ -514,6 +514,7 @@ function checkDeprecatedOptions(this: Ajv, opts: Options): void { if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath") if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId") if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") + if (opts.unknownFormats !== undefined) this.logger.error("NOT SUPPORTED: option unknownFormats") if (opts.jsPropertySyntax !== undefined) this.logger.warn("DEPRECATED: option jsPropertySyntax") if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") } diff --git a/lib/types/index.ts b/lib/types/index.ts index c6c6592404..c1e1df746d 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -40,10 +40,9 @@ export interface CurrentOptions { $data?: boolean allErrors?: boolean verbose?: boolean - format?: false + format?: boolean formats?: {[name: string]: Format} keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated - unknownFormats?: true | string[] | "ignore" schemas?: AnySchema[] | {[key: string]: AnySchema} missingRefs?: true | "ignore" | "fail" extendRefs?: true | "ignore" | "fail" @@ -85,13 +84,13 @@ export interface Options extends CurrentOptions { nullable?: boolean // "nullable" keyword is supported by default schemaId?: string uniqueItems?: boolean + unknownFormats?: true | string[] | "ignore" // deprecated: jsPropertySyntax?: boolean // added instead of jsonPointers unicode?: boolean } export interface InstanceOptions extends Options { - [opt: string]: unknown strict: boolean | "log" code: CodeOptions loopRequired: number @@ -302,6 +301,7 @@ export interface AsyncFormatDefinition { } export type AddedFormat = + | true | RegExp | FormatValidator | FormatDefinition diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 74ce00d72a..579f1198e0 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -8,7 +8,6 @@ import type { } from "../../types" import type KeywordCxt from "../../compile/context" import {_, str, nil, or, Code, getProperty} from "../../compile/codegen" -import N from "../../compile/names" type FormatValidate = | FormatValidator @@ -17,6 +16,7 @@ type FormatValidate = | AsyncFormatValidator | RegExp | string + | true export type FormatError = ErrorObject<"format", {format: string}> @@ -56,12 +56,8 @@ const def: CodeKeywordDefinition = { cxt.fail$data(or(unknownFmt(), invalidFmt())) // TODO this is not tested. Possibly require ajv-formats to test formats in ajv as well function unknownFmt(): Code { - if (opts.unknownFormats === "ignore") return nil - let unknown = _`${schemaCode} && !${format}` - if (Array.isArray(opts.unknownFormats)) { - unknown = _`${unknown} && !${N.self}.opts.unknownFormats.includes(${schemaCode})` - } - return _`(${unknown})` + if (opts.strict === false) return nil + return _`(${schemaCode} && !${format})` } function invalidFmt(): Code { @@ -69,7 +65,9 @@ const def: CodeKeywordDefinition = { ? _`${fDef}.async ? await ${format}(${data}) : ${format}(${data})` : _`${format}(${data})` const validData = _`typeof ${format} == "function" ? ${callFormat} : ${format}.test(${data})` - return _`(${format} && ${fType} === ${ruleType as string} && !(${validData}))` + return _`(${format} && ${format} !== true && ${fType} === ${ + ruleType as string + } && !(${validData}))` } } @@ -79,15 +77,15 @@ const def: CodeKeywordDefinition = { unknownFormat() return } + if (formatDef === true) return const [fmtType, format, fmtRef] = getFormat(formatDef) if (fmtType === ruleType) cxt.pass(validCondition()) function unknownFormat(): void { - if (opts.unknownFormats === "ignore") { + if (opts.strict === false) { self.logger.warn(unknownMsg()) return } - if (Array.isArray(opts.unknownFormats) && opts.unknownFormats.includes(schema)) return throw new Error(unknownMsg()) function unknownMsg(): string { diff --git a/spec/extras.spec.ts b/spec/extras.spec.ts index b4aec3f577..a020b819c1 100644 --- a/spec/extras.spec.ts +++ b/spec/extras.spec.ts @@ -5,7 +5,7 @@ import {afterError, afterEach} from "./after_test" const instances = getAjvInstances(options, { $data: true, - unknownFormats: ["allowedUnknown"], + formats: {allowedUnknown: true}, }) jsonSchemaTest(instances, { diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index d7b84439f8..c5af831461 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -28,7 +28,12 @@ runTest(getAjvInstances(options, {meta: false, strict: false}), 6, require("./_j runTest( getAjvInstances(options, { strict: false, - unknownFormats: ["idn-email", "idn-hostname", "iri", "iri-reference"], + formats: { + "idn-email": true, + "idn-hostname": true, + iri: true, + "iri-reference": true, + }, }), 7, require("./_json/draft7") diff --git a/spec/options/unknownFormats.spec.ts b/spec/options/unknownFormats.spec.ts index 26035c6f72..b3945362ba 100644 --- a/spec/options/unknownFormats.spec.ts +++ b/spec/options/unknownFormats.spec.ts @@ -2,11 +2,10 @@ import _Ajv from "../ajv" const should = require("../chai").should() const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ -describe("unknownFormats option", () => { +describe("specifying allowed unknown formats with `formats` option", () => { describe("= true (default)", () => { it("should fail schema compilation if unknown format is used", () => { test(new _Ajv()) - test(new _Ajv({unknownFormats: true})) function test(ajv) { should.throw(() => { @@ -17,7 +16,6 @@ describe("unknownFormats option", () => { it("should fail validation if unknown format is used via $data", () => { test(new _Ajv({$data: true})) - test(new _Ajv({$data: true, unknownFormats: true})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) @@ -40,7 +38,7 @@ describe("unknownFormats option", () => { describe('= "ignore (default before 5.0.0)"', () => { it("should pass schema compilation and be valid if unknown format is used", () => { - test(new _Ajv({unknownFormats: "ignore", logger: false})) + test(new _Ajv({strict: false, logger: false})) function test(ajv) { const validate = ajv.compile({format: "unknown"}) @@ -49,7 +47,7 @@ describe("unknownFormats option", () => { }) it("should be valid if unknown format is used via $data", () => { - test(new _Ajv({$data: true, unknownFormats: "ignore"})) + test(new _Ajv({$data: true, strict: false})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) @@ -71,7 +69,7 @@ describe("unknownFormats option", () => { describe("= [String]", () => { it("should pass schema compilation and be valid if allowed unknown format is used", () => { - test(new _Ajv({unknownFormats: ["allowed"]})) + test(new _Ajv({formats: {allowed: true}})) function test(ajv) { const validate = ajv.compile({format: "allowed"}) @@ -84,7 +82,7 @@ describe("unknownFormats option", () => { }) it("should be valid if allowed unknown format is used via $data", () => { - test(new _Ajv({$data: true, unknownFormats: ["allowed"]})) + test(new _Ajv({$data: true, formats: {allowed: true}})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) diff --git a/spec/schema-tests.spec.ts b/spec/schema-tests.spec.ts index 3d70c60714..456f6180d9 100644 --- a/spec/schema-tests.spec.ts +++ b/spec/schema-tests.spec.ts @@ -4,7 +4,7 @@ import options from "./ajv_options" import {afterError, afterEach} from "./after_test" import addFormats from "ajv-formats" -const instances = getAjvInstances(options, {strict: false, unknownFormats: ["allowedUnknown"]}) +const instances = getAjvInstances(options, {strict: false, formats: {allowedUnknown: true}}) const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), From 15c21a9bab17f21d3b299b6819608611bfffd041 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Sep 2020 20:00:22 +0100 Subject: [PATCH 236/322] docs: update method signatures to be closer to typescript --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3d7266ab21..fc08d0c537 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ To reiterate, neither this nor other strict mode restrictions change the validat #### Prohibit unknown formats -By default unknown formats throw exception during schema compilation (and fail validation in case format keyword value is \$data reference). It is possible to opt out of format validation completely with `format: false` option. You can define all known formats with `addFormat` method or `formats` option - to have some format ignored pass `true` as its definition: +By default unknown formats throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). It is possible to opt out of format validation completely with `format: false` option. You can define all known formats with `addFormat` method or `formats` option - to have some format ignored pass `true` as its definition: ``` const ajv = new Ajv({formats: { @@ -972,19 +972,23 @@ See [Coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.m ## API -##### new Ajv(Object options) -> Object +#### new Ajv(opts: object) -Create Ajv instance. +Create Ajv instance: -##### .compile(Object schema) -> Function<Object data> +```javascript +const ajv = new Ajv() +``` + +#### ajv.compile(schema: object): (data: any) =\> boolean | Promise\ Generate validating function and cache the compiled schema for future use. -Validating function returns a boolean value. This function has properties `errors` and `schema`. Errors encountered during the last validation are assigned to `errors` property (it is assigned `null` if there was no errors). `schema` property contains the reference to the original schema. +Validating function returns a boolean value (or promise for async schemas). This function has properties `errors` and `schema`. Errors encountered during the last validation are assigned to `errors` property (it is assigned `null` if there was no errors). `schema` property contains the reference to the original schema. The schema passed to this method will be validated against meta-schema unless `validateSchema` option is false. If schema is invalid, an error will be thrown. See [options](#options). -##### .compileAsync(Object schema [, Boolean meta][, function callback]) -> Promise +#### ajv.compileAsync(schema: object, meta?: boolean): Promise\ Asynchronous version of `compile` method that loads missing remote schemas using asynchronous function in `options.loadSchema`. This function returns a Promise that resolves to a validation function. An optional callback passed to `compileAsync` will be called with 2 parameters: error (or null) and validating function. The returned promise will reject (and the callback will be called with an error) when: @@ -998,7 +1002,7 @@ You can asynchronously compile meta-schema by passing `true` as the second param See example in [Asynchronous compilation](#asynchronous-schema-compilation). -##### .validate(Object schema|String key|String ref, data) -> Boolean +#### ajv.validate(schemaOrRef: object | string, data: any): boolean Validate data using passed schema (it will be compiled and cached). @@ -1010,7 +1014,7 @@ Validation errors will be available in the `errors` property of Ajv instance (`n If the schema is asynchronous (has `$async` keyword on the top level) this method returns a Promise. See [Asynchronous validation](#asynchronous-validation). -##### .addSchema(Array<Object>|Object schema [, String key]) -> Ajv +#### ajv.addSchema(schema: object | object[], key?: string): Ajv Add schema(s) to validator instance. This method does not compile schemas (but it still validates them). Because of that dependencies can be added in any order and circular dependencies are supported. It also prevents unnecessary compilation of schemas that are containers for other schemas but not used as a whole. @@ -1028,16 +1032,16 @@ By default the schema is validated against meta-schema before it is added, and i This allows you to do nice things like the following. ```javascript -var validate = new Ajv().addSchema(schema).addFormat(name, regex).getSchema(uri) +const validate = new Ajv().addSchema(schema).addFormat(name, regex).getSchema(uri) ``` -##### .addMetaSchema(Array<Object>|Object schema [, String key]) -> Ajv +#### ajv.addMetaSchema(schema: object | object[], key?: string): Ajv Adds meta schema(s) that can be used to validate other schemas. That function should be used instead of `addSchema` because there may be instance options that would compile a meta schema incorrectly (at the moment it is `removeAdditional` option). There is no need to explicitly add draft-07 meta schema (http://json-schema.org/draft-07/schema) - it is added by default, unless option `meta` is set to `false`. You only need to use it if you have a changed meta-schema that you want to use to validate your schemas. See `validateSchema`. -##### .validateSchema(Object schema) -> Boolean +#### ajv.validateSchema(schema: object): boolean Validates schema. This method should be used to validate schemas rather than `validate` due to the inconsistency of `uri` format in JSON Schema standard. @@ -1049,11 +1053,11 @@ If schema has `$schema` property, then the schema with this id (that should be p Errors will be available at `ajv.errors`. -##### .getSchema(String key) -> Function<Object data> +#### ajv.getSchema(key: string): undefined | ((data: any) =\> boolean | Promise\) Retrieve compiled schema previously added with `addSchema` by the key passed to `addSchema` or by its full reference (id). The returned validating function has `schema` property with the reference to the original schema. -##### .removeSchema([Object schema|String key|String ref|RegExp pattern]) -> Ajv +#### ajv.removeSchema(schemaOrRef: object | string | RegExp): Ajv Remove added/cached schema. Even if schema is referenced by other schemas it can be safely removed as dependent schemas have local references. @@ -1066,7 +1070,7 @@ Schema can be removed using: If no parameter is passed all schemas but meta-schemas will be removed and the cache will be cleared. -##### addFormat(name: string, format: Format) => Ajv +#### ajv.addFormat(name: string, format: Format): Ajv ```typescript type Format = @@ -1088,7 +1092,7 @@ If object is passed it should have properties `validate`, `compare` and `async`: Formats can be also added via `formats` option. -##### .addKeyword(Object definition) -> Ajv +#### ajv.addKeyword(definition: object):s Ajv Add validation keyword to Ajv instance. @@ -1130,11 +1134,11 @@ _compile_, _macro_ and _code_ are mutually exclusive, only one should be used at See [User defined keywords](#user-defined-keywords) for more details. -##### .getKeyword(String keyword) -> Object|Boolean +#### ajv.getKeyword(keyword: string): object | boolean Returns keyword definition, `false` if the keyword is unknown. -##### .removeKeyword(String keyword) -> Ajv +#### ajv.removeKeyword(keyword: string): Ajv Removes added or pre-defined keyword so you can redefine them. @@ -1142,7 +1146,7 @@ While this method can be used to extend pre-defined keywords, it can also be use **Please note**: schemas compiled before the keyword is removed will continue to work without changes. To recompile schemas use `removeSchema` method and compile them again. -##### .errorsText([Array<Object> errors [, Object options]]) -> String +#### ajv.errorsText(errors?: object[], options?: object): string Returns the text with all errors in a String. @@ -1211,7 +1215,7 @@ Defaults: - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function - _format_: format validation. Option values: - - `true` (default) - validate added formats (see [Formats](#formats)). In strict mode unknown formats will throw exception during schema compilation (and fail validation in case format keyword value is \$data reference). + - `true` (default) - validate added formats (see [Formats](#formats)). In strict mode unknown formats will throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). - `false` - ignore all format keywords. - _formats_: an object with format definitions. Keys and values will be passed to `addFormat` method. Pass `true` as format definition to ignore some formats. - _keywords_: an array of keyword definitions or strings. Values will be passed to `addKeyword` method. From bf63813f29d6d8cbc06bc2597c88327e666ed3fe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 18 Sep 2020 13:06:49 +0100 Subject: [PATCH 237/322] update docs with error params, typescript examples, update type signatures and test --- README.md | 478 ++++++++++++++++++++---------- lib/ajv.ts | 37 ++- lib/types/index.ts | 4 +- spec/types/async-validate.spec.ts | 2 +- spec/types/json-schema.spec.ts | 4 +- 5 files changed, 346 insertions(+), 179 deletions(-) diff --git a/README.md b/README.md index fc08d0c537..2443b63816 100644 --- a/README.md +++ b/README.md @@ -155,39 +155,67 @@ npm install ajv Try it in the Node.js REPL: https://runkit.com/npm/ajv -The fastest validation call: +In JavaScript: ```javascript // Node.js require: -var Ajv = require("ajv") +const Ajv = require("ajv") // or ESM/TypeScript import import Ajv from "ajv" -var ajv = new Ajv() // options can be passed, e.g. {allErrors: true} -var validate = ajv.compile(schema) -var valid = validate(data) +const ajv = new Ajv() // options can be passed, e.g. {allErrors: true} +const validate = ajv.compile(schema) +const valid = validate(data) if (!valid) console.log(validate.errors) ``` -or with less code +In TypeScript: -```javascript -// ... -var valid = ajv.validate(schema, data) -if (!valid) console.log(ajv.errors) -// ... -``` +```typescript +import Ajv, {JSONSchemaType, DefinedError} from "ajv" -or +const ajv = new Ajv() -```javascript -// ... -var valid = ajv.addSchema(schema, "mySchema").validate("mySchema", data) -if (!valid) console.log(ajv.errorsText()) -// ... +type MyData = {foo: number} + +// optional schema type annotation for schema to match MyData type +const schema: JSONSchemaType = { + type: "object", + properties: { + foo: {type: "number", minimum: 0} + } + required: ["foo"], + additionalProperties: false +} + +// validate is a type guard for MyData because schema was typed +const validate = ajv.compile(schema) + +// or, if you did not use type annotation for the schema, +// type parameter can be used to make it type guard: +// const validate = ajv.compile(schema) + +const data: any = {foo: 1} + +if (validate(data)) { + // data is MyData here + console.log(data.foo) +} else { + // The type cast is needed to allow user-defined keywords and errors + // You can extend this type to include your error types as needed. + for (const err of validate.errors as DefinedError[]) { + switch (err.keyword) { + case "minimum": + // err type is narrowed here to have "minimum" error params properties + console.log(err.params.limit) + break + // ... + } + } +} ``` -See [API](#api) and [Options](#options) for more details. +See [this test](./spec/types/json-schema.spec.ts) for an advanced example, [API](#api) and [Options](#options) for more details. Ajv compiles schemas to functions and caches them in all cases (using schema serialized with [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) or another function passed via options), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. @@ -195,13 +223,9 @@ The best performance is achieved when using compiled functions returned by `comp **Please note**: every time a validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback). See [Validation errors](#validation-errors) -**Note for TypeScript users**: `ajv` provides its own TypeScript declarations -out of the box, so you don't need to install the deprecated `@types/ajv` -module. - ## Using in browser -You can require Ajv directly from the code you browserify - in this case Ajv will be a part of your bundle. +It is recommended that you bundle Ajv together with your code. If you need to use Ajv in several bundles you can create a separate UMD bundle using `npm run bundle` script (thanks to [siddo420](https://github.com/siddo420)). @@ -215,10 +239,6 @@ This bundle can be used with different module systems; it creates global `Ajv` i The browser bundle is available on [cdnjs](https://cdnjs.com/libraries/ajv). -Ajv is tested with these browsers: - -[![Sauce Test Status](https://saucelabs.com/browser-matrix/epoberezkin.svg)](https://saucelabs.com/u/epoberezkin) - **Please note**: some frameworks, e.g. Dojo, may redefine global require in such way that is not compatible with CommonJS module format. In such case Ajv bundle has to be loaded before the framework and then you can use global Ajv (see issue [#234](https://github.com/ajv-validator/ajv/issues/234)). ### Ajv and Content Security Policies (CSP) @@ -298,7 +318,7 @@ To reiterate, neither this nor other strict mode restrictions change the validat By default unknown formats throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). It is possible to opt out of format validation completely with `format: false` option. You can define all known formats with `addFormat` method or `formats` option - to have some format ignored pass `true` as its definition: -``` +```javascript const ajv = new Ajv({formats: { reserved: true }) @@ -396,7 +416,7 @@ You can structure your validation logic across multiple schema files and have sc Example: ```javascript -var schema = { +const schema = { $id: "http://example.com/schemas/schema.json", type: "object", properties: { @@ -405,7 +425,7 @@ var schema = { }, } -var defsSchema = { +const defsSchema = { $id: "http://example.com/schemas/defs.json", definitions: { int: {type: "integer"}, @@ -417,15 +437,15 @@ var defsSchema = { Now to compile your schema you can either pass all schemas to Ajv instance: ```javascript -var ajv = new Ajv({schemas: [schema, defsSchema]}) -var validate = ajv.getSchema("http://example.com/schemas/schema.json") +const ajv = new Ajv({schemas: [schema, defsSchema]}) +const validate = ajv.getSchema("http://example.com/schemas/schema.json") ``` or use `addSchema` method: ```javascript -var ajv = new Ajv() -var validate = ajv.addSchema(defsSchema).compile(schema) +const ajv = new Ajv() +const validate = ajv.addSchema(defsSchema).compile(schema) ``` See [Options](#options) and [addSchema](#api) method. @@ -453,9 +473,9 @@ Examples. This schema requires that the value in property `smaller` is less or equal than the value in the property larger: ```javascript -var ajv = new Ajv({$data: true}) +const ajv = new Ajv({$data: true}) -var schema = { +const schema = { properties: { smaller: { type: "number", @@ -465,7 +485,7 @@ var schema = { }, } -var validData = { +const validData = { smaller: 5, larger: 7, } @@ -476,14 +496,14 @@ ajv.validate(schema, validData) // true This schema requires that the properties have the same format as their field names: ```javascript -var schema = { +const schema = { additionalProperties: { type: "string", format: {$data: "0#"}, }, } -var validData = { +const validData = { "date-time": "1963-06-19T08:30:06.283185Z", email: "joe.bloggs@example.com", } @@ -608,10 +628,10 @@ During asynchronous compilation remote references are loaded using supplied func Example: ```javascript -var ajv = new Ajv({loadSchema: loadSchema}) +const ajv = new Ajv({loadSchema: loadSchema}) ajv.compileAsync(schema).then(function (validate) { - var valid = validate(data) + const valid = validate(data) // ... }) @@ -664,7 +684,7 @@ function checkIdExists(schema, data) { }) } -var schema = { +const schema = { $async: true, properties: { userId: { @@ -678,7 +698,7 @@ var schema = { }, } -var validate = ajv.compile(schema) +const validate = ajv.compile(schema) validate({userId: 1, postId: 19}) .then(function (data) { @@ -694,8 +714,8 @@ validate({userId: 1, postId: 19}) #### Using transpilers ```javascript -var ajv = new Ajv({processCode: transpileFunc}) -var validate = ajv.compile(schema) // transpiled es7 async function +const ajv = new Ajv({processCode: transpileFunc}) +const validate = ajv.compile(schema) // transpiled es7 async function validate(data).then(successFunc).catch(errorFunc) ``` @@ -783,8 +803,8 @@ This option modifies original data. Example: ```javascript -var ajv = new Ajv({removeAdditional: true}) -var schema = { +const ajv = new Ajv({removeAdditional: true}) +const schema = { additionalProperties: false, properties: { foo: {type: "number"}, @@ -797,7 +817,7 @@ var schema = { }, } -var data = { +const data = { foo: 0, additional1: 1, // will be removed; `additionalProperties` == false bar: { @@ -806,7 +826,7 @@ var data = { }, } -var validate = ajv.compile(schema) +const validate = ajv.compile(schema) console.log(validate(data)) // true console.log(data) // { "foo": 0, "bar": { "baz": "abc", "additional2": 2 } @@ -873,8 +893,8 @@ This option modifies original data. Example 1 (`default` in `properties`): ```javascript -var ajv = new Ajv({useDefaults: true}) -var schema = { +const ajv = new Ajv({useDefaults: true}) +const schema = { type: "object", properties: { foo: {type: "number"}, @@ -883,9 +903,9 @@ var schema = { required: ["foo", "bar"], } -var data = {foo: 1} +const data = {foo: 1} -var validate = ajv.compile(schema) +const validate = ajv.compile(schema) console.log(validate(data)) // true console.log(data) // { "foo": 1, "bar": "baz" } @@ -894,14 +914,14 @@ console.log(data) // { "foo": 1, "bar": "baz" } Example 2 (`default` in `items`): ```javascript -var schema = { +const schema = { type: "array", items: [{type: "number"}, {type: "string", default: "foo"}], } -var data = [1] +const data = [1] -var validate = ajv.compile(schema) +const validate = ajv.compile(schema) console.log(validate(data)) // true console.log(data) // [ 1, "foo" ] @@ -929,8 +949,8 @@ This option modifies original data. Example 1: ```javascript -var ajv = new Ajv({coerceTypes: true}) -var schema = { +const ajv = new Ajv({coerceTypes: true}) +const schema = { type: "object", properties: { foo: {type: "number"}, @@ -939,9 +959,9 @@ var schema = { required: ["foo", "bar"], } -var data = {foo: "1", bar: "false"} +const data = {foo: "1", bar: "false"} -var validate = ajv.compile(schema) +const validate = ajv.compile(schema) console.log(validate(data)) // true console.log(data) // { "foo": 1, "bar": false } @@ -950,17 +970,17 @@ console.log(data) // { "foo": 1, "bar": false } Example 2 (array coercions): ```javascript -var ajv = new Ajv({coerceTypes: "array"}) -var schema = { +const ajv = new Ajv({coerceTypes: "array"}) +const schema = { properties: { foo: {type: "array", items: {type: "number"}}, bar: {type: "boolean"}, }, } -var data = {foo: "1", bar: ["false"]} +const data = {foo: "1", bar: ["false"]} -var validate = ajv.compile(schema) +const validate = ajv.compile(schema) console.log(validate(data)) // true console.log(data) // { "foo": [1], "bar": false } @@ -984,10 +1004,36 @@ const ajv = new Ajv() Generate validating function and cache the compiled schema for future use. -Validating function returns a boolean value (or promise for async schemas). This function has properties `errors` and `schema`. Errors encountered during the last validation are assigned to `errors` property (it is assigned `null` if there was no errors). `schema` property contains the reference to the original schema. +Validating function returns a boolean value (or promise for async schemas that must have `$async: true` property - see [Asynchronous validation](#asynchronous-validation)). This function has properties `errors` and `schema`. Errors encountered during the last validation are assigned to `errors` property (it is assigned `null` if there was no errors). `schema` property contains the reference to the original schema. The schema passed to this method will be validated against meta-schema unless `validateSchema` option is false. If schema is invalid, an error will be thrown. See [options](#options). +In typescript returned validation function can be a type guard if you pass type parameter: + +```typescript +interface Foo { + foo: number +} + +const FooSchema: JSONSchemaType = { + type: "object", + properties: {foo: {type: "number"}}, + required: ["foo"], + additionalProperties: false, +} + +const validate = ajv.compile(FooSchema) // type of validate extends `(data: any) => data is Foo` +const data: any = {foo: 1} +if (validate(data)) { + // data is Foo here + console.log(data.foo) +} else { + console.log(validate.errors) +} +``` + +See more advanced example in [the test](./spec/types/json-schema.spec.ts). + #### ajv.compileAsync(schema: object, meta?: boolean): Promise\ Asynchronous version of `compile` method that loads missing remote schemas using asynchronous function in `options.loadSchema`. This function returns a Promise that resolves to a validation function. An optional callback passed to `compileAsync` will be called with 2 parameters: error (or null) and validating function. The returned promise will reject (and the callback will be called with an error) when: @@ -1000,6 +1046,8 @@ The function compiles schema and loads the first missing schema (or meta-schema) You can asynchronously compile meta-schema by passing `true` as the second parameter. +Similarly to `compile`, it can return type guard in typescript. + See example in [Asynchronous compilation](#asynchronous-schema-compilation). #### ajv.validate(schemaOrRef: object | string, data: any): boolean @@ -1010,6 +1058,8 @@ Instead of the schema you can use the key that was previously passed to `addSche Validation errors will be available in the `errors` property of Ajv instance (`null` if there were no errors). +In typescript this method can act as a type guard (similarly to function retured by `compile` method - see example there). + **Please note**: every time this method is called the errors are overwritten so you need to copy them to another variable if you want to use them later. If the schema is asynchronous (has `$async` keyword on the top level) this method returns a Promise. See [Asynchronous validation](#asynchronous-validation). @@ -1085,10 +1135,19 @@ Add format to validate strings or numbers. If object is passed it should have properties `validate`, `compare` and `async`: -- _validate_: a string, RegExp or a function as described above. -- _compare_: an optional comparison function that accepts two strings and compares them according to the format meaning. This function is used with keywords `formatMaximum`/`formatMinimum` (defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package). It should return `1` if the first value is bigger than the second value, `-1` if it is smaller and `0` if it is equal. -- _async_: an optional `true` value if `validate` is an asynchronous function; in this case it should return a promise that resolves with a value `true` or `false`. -- _type_: an optional type of data that the format applies to. It can be `"string"` (default) or `"number"` (see https://github.com/ajv-validator/ajv/issues/291#issuecomment-259923858). If the type of data is different, the validation will pass. +```typescript +interface FormatDefinition { // actual type definition is more precise - see types.ts + validate: string | RegExp | (data: number | string) => boolean | Promise + compare: (data1: string, data2: string): number // an optional function that accepts two strings + // and compares them according to the format meaning. + // This function is used with keywords `formatMaximum`/`formatMinimum` + // (defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package). + // It should return `1` if the first value is bigger than the second value, + // `-1` if it is smaller and `0` if it is equal. + async?: true // if `validate` is an asynchronous function + type?: "string" | "number" // "string" is default. If data type is different, the validation will pass. +} +``` Formats can be also added via `formats` option. @@ -1104,31 +1163,47 @@ It is recommended to use an application-specific prefix for keywords to avoid cu Example Keywords: - `"xyz-example"`: valid, and uses prefix for the xyz project to avoid name collisions. -- `"example"`: valid, but not recommended as it could collide with future versions of JSON Schema etc. +- `"example"`: valid, but not recommended as it may collide with future versions of JSON Schema etc. - `"3-example"`: invalid as numbers are not allowed to be the first character in a keyword Keyword definition is an object with the following properties: -- _keyword_: keyword name string -- _type_: optional string or array of strings with data type(s) that the keyword applies to. If not present, the keyword will apply to all types. -- _schemaType_: optional string or array of strings with the required schema type -- _code_: function to generate code, used for all pre-defined keywords -- _validate_: validating function -- _compile_: compiling function -- _macro_: macro function -- _error_: optional error definition object -- _schema_: an optional `false` value used with "validate" keyword to not pass schema -- _metaSchema_: an optional meta-schema for keyword schema -- _dependencies_: an optional list of properties that must be present in the parent schema - it will be checked during schema compilation -- _implements_: an optional list of keyword names to reserve that this keyword implements -- _modifying_: `true` MUST be passed if keyword modifies data -- _valid_: pass `true`/`false` to pre-define validation result, the result returned from validation function will be ignored. This option cannot be used with macro keywords. -- _\$data_: an optional `true` value to support [\$data reference](#data-reference) as the value of keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has _code_ or _validate_ function (the latter can be used in addition to _compile_ or _macro_). -- _\$dataError_: optional error definition for invalid \$data schema -- _async_: an optional `true` value if the validation function is asynchronous (whether it is compiled or passed in _validate_ property); in this case it should return a promise that resolves with a value `true` or `false`. This option is ignored in case of "macro" and "inline" keywords. -- _errors_: an optional boolean or string `"full"` indicating whether keyword returns errors. If this property is not set Ajv will determine if the errors were set in case of failed validation. - -_compile_, _macro_ and _code_ are mutually exclusive, only one should be used at a time. _validate_ can be used separately or in addition to _compile_ or _macro_ to support \$data reference. +```typescript +interface KeywordDefinition { + // actual type definition is more precise - see types.ts + keyword: string // keyword name + type?: string | string[] // JSON data type(s) the keyword applies to. Default - all types. + schemaType?: string | string[] // the required schema JSON type + code?: Function // function to generate code, used for all pre-defined keywords + validate?: Function // validating function + compile?: Function // compiling function + macro?: Function // macro function + error?: object // error definition object - see types.ts + schema?: false // used with "validate" keyword to not pass schema to function + metaSchema?: object // meta-schema for keyword schema + dependencies?: string[] // properties that must be present in the parent schema - + // it will be checked during schema compilation + implements?: string[] // keyword names to reserve that this keyword implements + modifying?: true // MUST be passed if keyword modifies data + valid?: boolean // to pre-define validation result, validation function result will be ignored - + // this option MUST NOT be used with `macro` keywords. + $data?: true // to support [\$data reference](#data-reference) as the value of keyword. + // The reference will be resolved at validation time. If the keyword has meta-schema, + // it would be extended to allow $data and it will be used to validate the resolved value. + // Supporting $data reference requires that keyword has `code` or `validate` function + // (the latter can be used in addition to `compile` or `macro`). + $dataError?: object // error definition object for invalid \$data schema - see types.ts + async?: true // if the validation function is asynchronous + // (whether it is compiled or passed in `validate` property). + // It should return a promise that resolves with a value `true` or `false`. + // This option is ignored in case of "macro" and "code" keywords. + errors?: boolean | "full" // whether keyword returns errors. + // If this property is not passed Ajv will determine + // if the errors were set in case of failed validation. +} +``` + +`compile`, `macro` and `code` are mutually exclusive, only one should be used at a time. `validate` can be used separately or in addition to `compile` or `macro` to support [\$data reference](#data-reference). **Please note**: If the keyword is validating data type that is different from the type(s) in its definition, the validation function will not be called (and expanded macro will not be used), so there is no need to check for data type inside validation function or inside schema returned by macro function (unless you want to enforce a specific type and for some reason do not want to use a separate `type` keyword for that). In the same way as standard keywords work, if the keyword does not apply to the data type being validated, the validation of this keyword will succeed. @@ -1154,63 +1229,65 @@ Options can have properties `separator` (string used to separate errors, ", " by ## Options -Defaults: +Option defaults: ```javascript -{ +// see types/index.ts for actual types +const defaultOptions = { // strict mode options - strict: true, + strict: true, allowMatchingProperties: false, // validation and reporting options: - $data: false, - allErrors: false, - verbose: false, - $comment: false, - format: true, - formats: {}, - schemas: {}, - logger: undefined, + allErrors: false, + verbose: false, + $comment: false, + format: true, + formats: {}, + keywords: {}, + schemas: {}, + $data: false // | true, + logger: undefined, // referenced schema options: - missingRefs: true, - extendRefs: "ignore", // recommended 'fail' - loadSchema: undefined, // function(uri: string): Promise {} + missingRefs: true, + extendRefs: "ignore", // recommended 'fail' + loadSchema: undefined, // function(uri: string): Promise {} // options to modify validated data: removeAdditional: false, - useDefaults: false, - coerceTypes: false, + useDefaults: false, + coerceTypes: false, // advanced options: - meta: true, - validateSchema: true, - addUsedSchema: true, - inlineRefs: true, - passContext: false, - loopRequired: Infinity, - loopEnum: Infinity, - ownProperties: false, + meta: true, + validateSchema: true, + addUsedSchema: true, + inlineRefs: true, + passContext: false, + loopRequired: Infinity, + loopEnum: Infinity, + ownProperties: false, multipleOfPrecision: false, - messages: true, - sourceCode: false, - processCode: undefined, // function (str: string, schema: object): string {} - cache: new Cache, - serialize: undefined + messages: true, + sourceCode: false, + processCode: undefined, // (code: string, schemaEnv: object) => string + cache: new Cache, + serialize: undefined // (schema: object | boolean) => any jsPropertySyntax: false, // deprecated } ``` ##### Strict mode options -- _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: - - `true` (default) - use strict mode and throw an exception when any strict mode restrictions is violated. +- _strict_ (NEW in v7): By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: + - `true` (default) - use strict mode and throw an exception when any strict mode restriction is violated. - `"log"` - log warning when any strict mode restriction is violated. - - `false` - ignore any strict mode restriction. -- _allowMatchingProperties_: pass true to allow overlap between "properties" and "patternProperties". See [Strict Mode](#strict-mode). + - `false` - ignore all strict mode restrictions. +- _allowMatchingProperties_ (NEW in v7): pass true to allow overlap between "properties" and "patternProperties". Does not affect other strict mode restrictions. See [Strict Mode](#strict-mode). ##### Validation and reporting options - _\$data_: support [\$data references](#data-reference). Draft 6 meta-schema that is added by default will be extended to allow them. If you want to use another meta-schema you need to use $dataMetaSchema method to add support for $data reference. See [API](#api). - _allErrors_: check all rules collecting all errors. Default is to return after the first error. - _verbose_: include the reference to the part of the schema (`schema` and `parentSchema`) and validated data in errors (false by default). -- _\$comment_ (NEW in Ajv version 6.0): log or pass the value of `$comment` keyword to a function. Option values: +- _\$comment_: log or pass the value of `$comment` keyword to a function. Option values: - `false` (default): ignore \$comment keyword. - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function @@ -1266,7 +1343,7 @@ Defaults: - integer number - to limit the maximum number of keywords of the schema that will be inlined. - _passContext_: pass validation context to _compile_ and _validate_ keyword functions. If this option is `true` and you pass some context to the compiled validation function with `validate.call(context, data)`, the `context` will be available as `this` in your keywords. By default `this` is Ajv instance. - _loopRequired_: by default `required` keyword is compiled into a single expression (or a sequence of statements in `allErrors` mode). In case of a very large number of properties in this keyword it may result in a very big validation function. Pass integer to set the number of properties above which `required` keyword will be validated in a loop - smaller validation function size but also worse performance. -- _loopEnum_: by default `enum` keyword is compiled into a single expression. In case of a very large number of allowed values it may result in a large validation function. Pass integer to set the number of values above which `enum` keyword will be validated in a loop. +- _loopEnum_ (NEW in v7): by default `enum` keyword is compiled into a single expression. In case of a very large number of allowed values it may result in a large validation function. Pass integer to set the number of values above which `enum` keyword will be validated in a loop. - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). - _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when messages are generated outside of Ajv code (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). @@ -1284,45 +1361,126 @@ In case of validation failure, Ajv assigns the array of errors to `errors` prope Each error is an object with the following properties: -- _keyword_: validation keyword. -- _dataPath_: JSON pointer to the part of the data that was validated (e.g., `"/prop/1/subProp"`). -- _schemaPath_: the path (JSON-pointer as a URI fragment) to the schema of the keyword that failed validation. -- _params_: the object with the additional information about error that can be used to generate error messages (e.g., using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package). See below for parameters set by all keywords. -- _message_: the standard error message (can be excluded with option `messages` set to false). -- _schema_: the schema of the keyword (added with `verbose` option). -- _parentSchema_: the schema containing the keyword (added with `verbose` option) -- _data_: the data validated by the keyword (added with `verbose` option). - -**Please note**: `propertyNames` keyword schema validation errors have an additional property `propertyName`, `dataPath` points to the object. After schema validation for each property name, if it is invalid an additional error is added with the property `keyword` equal to `"propertyNames"`. +```typescript +interface ErrorObject { + keyword: string // validation keyword. + dataPath: string // JSON pointer to the part of the data that was validated (e.g., `"/prop/1/subProp"`). + schemaPath: string // the path (JSON-pointer as a URI fragment) to the schema of the failing keyword. + // the object with the additional information about error that can be used to generate error messages + // (e.g., using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n) package). + // See below for parameters set by all keywords. + params: object // type is defined by keyword value, see below + propertyName?: string // set for errors in `propertyNames` keyword schema. + // `dataPath` still points to the object in this case. + message?: string // the standard error message (can be excluded with option `messages` set to false). + schema?: any // the schema of the keyword (added with `verbose` option). + parentSchema?: object // the schema containing the keyword (added with `verbose` option) + data?: any // the data validated by the keyword (added with `verbose` option). +} +``` ### Error parameters Properties of `params` object in errors depend on the keyword that failed validation. -- `maxItems`, `minItems`, `maxLength`, `minLength`, `maxProperties`, `minProperties` - property `limit` (number, the schema of the keyword). -- `additionalItems` - property `limit` (the maximum number of allowed items in case when `items` keyword is an array of schemas and `additionalItems` is false). -- `additionalProperties` - property `additionalProperty` (the property not used in `properties` and `patternProperties` keywords). -- `dependencies` - properties: - - `property` (dependent property), - - `missingProperty` (required missing dependency - only the first one is reported currently) - - `deps` (required dependencies, comma separated list as a string), - - `depsCount` (the number of required dependencies). -- `format` - property `format` (the schema of the keyword). -- `maximum`, `minimum` - properties: - - `limit` (number, the schema of the keyword), - - `exclusive` (boolean, the schema of `exclusiveMaximum` or `exclusiveMinimum`), - - `comparison` (string, comparison operation to compare the data to the limit, with the data on the left and the limit on the right; can be "<", "<=", ">", ">=") -- `multipleOf` - property `multipleOf` (the schema of the keyword) -- `pattern` - property `pattern` (the schema of the keyword) -- `required` - property `missingProperty` (required property that is missing). -- `propertyNames` - property `propertyName` (an invalid property name). -- `patternRequired` (in ajv-keywords) - property `missingPattern` (required pattern that did not match any property). -- `type` - property `type` (required type(s), a string, can be a comma-separated list) -- `uniqueItems` - properties `i` and `j` (indices of duplicate items). -- `const` - property `allowedValue` pointing to the value (the schema of the keyword). -- `enum` - property `allowedValues` pointing to the array of values (the schema of the keyword). -- `$ref` - property `ref` with the referenced schema URI. -- `oneOf` - property `passingSchemas` (array of indices of passing schemas, null if no schema passes). +In typescript, the ErrorObject is a discriminated union that allows to determine the type of error parameters based on the value of keyword: + +```typescript +const ajv = new Ajv() +const validate = ajv.compile(schema) +if (validate(data)) { + // data is MyData here + // ... +} else { + // DefinedError is a type for all pre-defined keywords errors, + // validate.errors has type ErrorObject[] - to allow user-defined keywords with any error parameters. + // Users can extend DefinedError to include the keywords errors they defined. + for (const err of validate.errors as DefinedError[]) { + switch (err.keyword) { + case "maximum": + console.log(err.limit) + break + case "pattern": + console.log(err.pattern) + break + // ... + } + } +} +``` + +Also see an example in [this test](./spec/types/error-parameters.spec.ts) + +- `maxItems`, `minItems`, `maxLength`, `minLength`, `maxProperties`, `minProperties`: + +```typescript +type ErrorParams = {limit: number} // keyword value +``` + +- `additionalItems`: + +```typescript +// when `items` is an array of schemas and `additionalItems` is false: +type ErrorParams = {limit: number} // the maximum number of allowed items +``` + +- `additionalProperties`: + +```typescript +type ErrorParams = {additionalProperty: string} +// the property not defined in `properties` and `patternProperties` keywords +``` + +- `dependencies`: + +```typescript +type ErrorParams = { + property: string // dependent property, + missingProperty: string // required missing dependency - only the first one is reported + deps: string // required dependencies, comma separated list as a string (TODO change to string[]) + depsCount: number // the number of required dependencies +} +``` + +- `format`: + +```typescript +type ErrorParams = {format: string} // keyword value +``` + +- `maximum`, `minimum`, `exclusiveMaximum`, `exclusiveMinimum`: + +```typescript +type ErrorParams = { + limit: number // keyword value + comparison: "<=" | ">=" | "<" | ">" // operation to compare the data to the limit, + // with data on the left and the limit on the right +} +``` + +- `multipleOf`: + +```typescript +type ErrorParams = {multipleOf: number} // keyword value +``` + +- `pattern`: + +```typescript +type ErrorParams = {pattern: string} // keyword value +``` + +- `required`: + +```typescript +type ErrorParams = {missingProperty: string} // required property that is missing +``` + +- `propertyNames`: + +```typescript +type ErrorParams = {propertyName: string} // invalid property name +``` User-defined keywords can define other keyword parameters. diff --git a/lib/ajv.ts b/lib/ajv.ts index 4d0d2e2424..06442032c2 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -25,6 +25,7 @@ export interface Plugin { import KeywordCxt from "./compile/context" export {KeywordCxt} export {DefinedError} from "./vocabularies/errors" +export {JSONSchemaType} from "./types/json-schema" import type { Schema, @@ -141,12 +142,14 @@ export default class Ajv { // Validate data using schema // AnySchema will be compiled and cached using as a key JSON serialized with // [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) - validate(schema: Schema | JSONSchemaType | string, data: unknown): data is T - validate(schema: AsyncSchema, data: unknown): Promise - validate(schemaKeyRef: AnySchema | string, data: unknown): data is T | Promise - validate( + validate(schema: Schema | string, data: unknown): boolean + validate(schemaKeyRef: AnySchema | string, data: unknown): boolean | Promise + validate(schema: Schema | JSONSchemaType | string, data: unknown): data is T + validate(schema: AsyncSchema, data: unknown | T): Promise + validate(schemaKeyRef: AnySchema | string, data: unknown): data is T | Promise + validate( schemaKeyRef: AnySchema | string, // key, ref or schema object - data: unknown // to be validated + data: unknown | T // to be validated ): boolean | Promise { let v: AnyValidateFunction | undefined if (typeof schemaKeyRef == "string") { @@ -164,10 +167,10 @@ export default class Ajv { // Create validation function for passed schema // _meta: true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. - compile(schema: Schema | JSONSchemaType, _meta?: boolean): ValidateFunction - compile(schema: AsyncSchema, _meta?: boolean): AsyncValidateFunction - compile(schema: AnySchema, _meta?: boolean): AnyValidateFunction - compile(schema: AnySchema, _meta?: boolean): AnyValidateFunction { + compile(schema: Schema | JSONSchemaType, _meta?: boolean): ValidateFunction + compile(schema: AsyncSchema, _meta?: boolean): AsyncValidateFunction + compile(schema: AnySchema, _meta?: boolean): AnyValidateFunction + compile(schema: AnySchema, _meta?: boolean): AnyValidateFunction { const sch = this._addSchema(schema, _meta) return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction } @@ -176,14 +179,20 @@ export default class Ajv { // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. // TODO allow passing schema URI // meta - optional true to compile meta-schema - compileAsync( + compileAsync( schema: SchemaObject | JSONSchemaType, _meta?: boolean ): Promise> - compileAsync(schema: AsyncSchema, meta?: boolean): Promise> + compileAsync(schema: AsyncSchema, meta?: boolean): Promise> // eslint-disable-next-line @typescript-eslint/unified-signatures - compileAsync(schema: AnySchemaObject, meta?: boolean): Promise> - compileAsync(schema: AnySchemaObject, meta?: boolean): Promise> { + compileAsync( + schema: AnySchemaObject, + meta?: boolean + ): Promise> + compileAsync( + schema: AnySchemaObject, + meta?: boolean + ): Promise> { if (typeof this.opts.loadSchema != "function") { throw new Error("options.loadSchema should be a function") } @@ -298,7 +307,7 @@ export default class Ajv { // Get compiled schema by `key` or `ref`. // (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id) - getSchema(keyRef: string): AnyValidateFunction | undefined { + getSchema(keyRef: string): AnyValidateFunction | undefined { let sch while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch if (sch === undefined) { diff --git a/lib/types/index.ts b/lib/types/index.ts index c1e1df746d..cf2e65deb4 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -118,7 +118,7 @@ interface SourceCode { scope: Scope } -export interface ValidateFunction { +export interface ValidateFunction { ( this: Ajv | any, data: any, @@ -133,7 +133,7 @@ export interface ValidateFunction { source?: SourceCode } -export interface AsyncValidateFunction extends ValidateFunction { +export interface AsyncValidateFunction extends ValidateFunction { (...args: Parameters>): Promise $async: true } diff --git a/spec/types/async-validate.spec.ts b/spec/types/async-validate.spec.ts index f8eba8d8be..9acfbc396e 100644 --- a/spec/types/async-validate.spec.ts +++ b/spec/types/async-validate.spec.ts @@ -100,7 +100,7 @@ describe("$async validation and type guards", () => { it("should have result type boolean | promise 2", async () => { const schema = {$async: false} - const validate = ajv.compile(schema) + const validate = ajv.compile(schema) const result = validate({}) if (typeof result === "boolean") { should.exist(result) diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts index 730890d2d2..0370f6e255 100644 --- a/spec/types/json-schema.spec.ts +++ b/spec/types/json-schema.spec.ts @@ -119,13 +119,13 @@ describe("JSONSchemaType type and validation as a type guard", () => { describe("schema has type JSONSchemaType", () => { it("should prove the type of validated data", () => { - const validate = ajv.compile(mySchema) + const validate = ajv.compile(mySchema) if (validate(validData)) { validData.foo.should.equal("foo") } should.not.exist(validate.errors) - if (ajv.validate(mySchema, validData)) { + if (ajv.validate(mySchema, validData)) { validData.foo.should.equal("foo") } should.not.exist(ajv.errors) From 5f5265fea56b4037d20600ec843ccc98bb73cae3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 18 Sep 2020 13:23:23 +0100 Subject: [PATCH 238/322] exclude removed options from public interface, add RemovedOptions interface to check options for JS users --- lib/ajv.ts | 3 ++- lib/types/index.ts | 10 +++++----- spec/options/schemaId.spec.ts | 2 -- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 06442032c2..f00cd3b0c5 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -37,6 +37,7 @@ import type { KeywordDefinition, Options, InstanceOptions, + RemovedOptions, AnyValidateFunction, ValidateFunction, AsyncValidateFunction, @@ -519,7 +520,7 @@ export interface ErrorsTextOptions { dataVar?: string } -function checkDeprecatedOptions(this: Ajv, opts: Options): void { +function checkDeprecatedOptions(this: Ajv, opts: Options & RemovedOptions): void { if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath") if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId") if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") diff --git a/lib/types/index.ts b/lib/types/index.ts index cf2e65deb4..c278d56665 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -35,7 +35,7 @@ export type LoadSchemaFunction = ( cb?: (err: Error | null, schema?: AnySchemaObject) => void ) => Promise -export interface CurrentOptions { +export interface Options { strict?: boolean | "log" $data?: boolean allErrors?: boolean @@ -72,22 +72,22 @@ export interface CurrentOptions { | true | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) allowMatchingProperties?: boolean // disables a strict mode restriction + // deprecated: + jsPropertySyntax?: boolean // added instead of jsonPointers + unicode?: boolean } export interface CodeOptions { formats?: Code // code to require (or construct) map of available formats - for standalone code } -export interface Options extends CurrentOptions { +export interface RemovedOptions { // removed: errorDataPath?: "object" | "property" nullable?: boolean // "nullable" keyword is supported by default schemaId?: string uniqueItems?: boolean unknownFormats?: true | string[] | "ignore" - // deprecated: - jsPropertySyntax?: boolean // added instead of jsonPointers - unicode?: boolean } export interface InstanceOptions extends Options { diff --git a/spec/options/schemaId.spec.ts b/spec/options/schemaId.spec.ts index a4831d8f50..3e8e509879 100644 --- a/spec/options/schemaId.spec.ts +++ b/spec/options/schemaId.spec.ts @@ -4,7 +4,6 @@ const should = require("../chai").should() describe("removed schemaId option", () => { it("should use $id and throw exception when id is used", () => { test(new _Ajv({logger: false})) - test(new _Ajv({schemaId: "$id", logger: false})) function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) @@ -18,7 +17,6 @@ describe("removed schemaId option", () => { it("should use $id and ignore id when strict: false", () => { test(new _Ajv({logger: false, strict: false})) - test(new _Ajv({schemaId: "$id", logger: false, strict: false})) function test(ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) From 656a3155572ee150009848fafa6261ff37f89a51 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 18 Sep 2020 14:28:58 +0100 Subject: [PATCH 239/322] replace option format with validateFormats, refactor checkDeprecatedProperties --- README.md | 32 ++++++++------- lib/ajv.ts | 53 +++++++++++++++++++------ lib/types/index.ts | 21 +++++++--- lib/vocabularies/format/format.ts | 2 +- spec/options/options_validation.spec.ts | 2 +- 5 files changed, 75 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 2443b63816..00921e19aa 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ const schema: JSONSchemaType = { additionalProperties: false } -// validate is a type guard for MyData because schema was typed +// validate is a type guard for MyData - type is inferred from schema type const validate = ajv.compile(schema) // or, if you did not use type annotation for the schema, @@ -316,7 +316,7 @@ To reiterate, neither this nor other strict mode restrictions change the validat #### Prohibit unknown formats -By default unknown formats throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). It is possible to opt out of format validation completely with `format: false` option. You can define all known formats with `addFormat` method or `formats` option - to have some format ignored pass `true` as its definition: +By default unknown formats throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). It is possible to opt out of format validation completely with options `validateFormats: false`. You can define all known formats with `addFormat` method or `formats` option - to have some format ignored pass `true` as its definition: ```javascript const ajv = new Ajv({formats: { @@ -992,7 +992,7 @@ See [Coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.m ## API -#### new Ajv(opts: object) +#### new Ajv(options: object) Create Ajv instance: @@ -1000,6 +1000,8 @@ Create Ajv instance: const ajv = new Ajv() ``` +See [Options](#options) + #### ajv.compile(schema: object): (data: any) =\> boolean | Promise\ Generate validating function and cache the compiled schema for future use. @@ -1194,7 +1196,7 @@ interface KeywordDefinition { // (the latter can be used in addition to `compile` or `macro`). $dataError?: object // error definition object for invalid \$data schema - see types.ts async?: true // if the validation function is asynchronous - // (whether it is compiled or passed in `validate` property). + // (whether it is returned from `compile` or passed in `validate` property). // It should return a promise that resolves with a value `true` or `false`. // This option is ignored in case of "macro" and "code" keywords. errors?: boolean | "full" // whether keyword returns errors. @@ -1237,15 +1239,15 @@ const defaultOptions = { // strict mode options strict: true, allowMatchingProperties: false, + validateFormats: true, // validation and reporting options: + $data: false, allErrors: false, verbose: false, $comment: false, - format: true, formats: {}, keywords: {}, schemas: {}, - $data: false // | true, logger: undefined, // referenced schema options: missingRefs: true, @@ -1274,13 +1276,16 @@ const defaultOptions = { } ``` -##### Strict mode options +##### Strict mode options (NEW in v7) -- _strict_ (NEW in v7): By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: +- _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: - `true` (default) - use strict mode and throw an exception when any strict mode restriction is violated. - `"log"` - log warning when any strict mode restriction is violated. - `false` - ignore all strict mode restrictions. -- _allowMatchingProperties_ (NEW in v7): pass true to allow overlap between "properties" and "patternProperties". Does not affect other strict mode restrictions. See [Strict Mode](#strict-mode). +- _allowMatchingProperties_: pass true to allow overlap between "properties" and "patternProperties". Does not affect other strict mode restrictions. See [Strict Mode](#strict-mode). +- _validateFormats_: format validation. Option values: + - `true` (default) - validate formats (see [Formats](#formats)). In [strict mode](#strict-mode) unknown formats will throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). + - `false` - do not validate any format keywords (TODO they will still collect annotations once supported). ##### Validation and reporting options @@ -1291,9 +1296,6 @@ const defaultOptions = { - `false` (default): ignore \$comment keyword. - `true`: log the keyword value to console. - function: pass the keyword value, its schema path and root schema to the specified function -- _format_: format validation. Option values: - - `true` (default) - validate added formats (see [Formats](#formats)). In strict mode unknown formats will throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). - - `false` - ignore all format keywords. - _formats_: an object with format definitions. Keys and values will be passed to `addFormat` method. Pass `true` as format definition to ignore some formats. - _keywords_: an array of keyword definitions or strings. Values will be passed to `addKeyword` method. - _schemas_: an array or object of schemas that will be added to the instance. In case you pass the array the schemas must have IDs in them. When the object is passed the method `addSchema(value, key)` will be called for each schema in this object. @@ -1338,9 +1340,9 @@ const defaultOptions = { - `false` - skip schema validation. - _addUsedSchema_: by default methods `compile` and `validate` add schemas to the instance if they have `$id` (or `id`) property that doesn't start with "#". If `$id` is present and it is not unique the exception will be thrown. Set this option to `false` to skip adding schemas to the instance and the `$id` uniqueness check when these methods are used. This option does not affect `addSchema` method. - _inlineRefs_: Affects compilation of referenced schemas. Option values: - - `true` (default) - the referenced schemas that don't have refs in them are inlined, regardless of their size - that substantially improves performance at the cost of the bigger size of compiled schema functions. - - `false` - to not inline referenced schemas (they will be compiled as separate functions). - - integer number - to limit the maximum number of keywords of the schema that will be inlined. + - `true` (default) - the referenced schemas that don't have refs in them are inlined, regardless of their size - it improves performance. + - `false` - to not inline referenced schemas (they will always be compiled as separate functions). + - integer number - to limit the maximum number of keywords of the schema that will be inlined (to balance the total size of compiled functions and performance). - _passContext_: pass validation context to _compile_ and _validate_ keyword functions. If this option is `true` and you pass some context to the compiled validation function with `validate.call(context, data)`, the `context` will be available as `this` in your keywords. By default `this` is Ajv instance. - _loopRequired_: by default `required` keyword is compiled into a single expression (or a sequence of statements in `allErrors` mode). In case of a very large number of properties in this keyword it may result in a very big validation function. Pass integer to set the number of properties above which `required` keyword will be validated in a loop - smaller validation function size but also worse performance. - _loopEnum_ (NEW in v7): by default `enum` keyword is compiled into a single expression. In case of a very large number of allowed values it may result in a large validation function. Pass integer to set the number of values above which `enum` keyword will be validated in a loop. diff --git a/lib/ajv.ts b/lib/ajv.ts index f00cd3b0c5..a5617c48ad 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -38,6 +38,7 @@ import type { Options, InstanceOptions, RemovedOptions, + DeprecatedOptions, AnyValidateFunction, ValidateFunction, AsyncValidateFunction, @@ -67,7 +68,7 @@ import draft7MetaSchema from "./refs/json-schema-draft-07.json" const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" -const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"] as const +const META_IGNORE_OPTIONS: (keyof Options)[] = ["removeAdditional", "useDefaults", "coerceTypes"] const META_SUPPORT_DATA = ["/properties"] const EXT_SCOPE_NAMES = new Set([ "validate", @@ -90,6 +91,28 @@ const optsDefaults = { addUsedSchema: true, } +type OptionsInfo = { + [key in keyof T]-?: string | undefined +} + +const removedOptions: OptionsInfo = { + errorDataPath: "", + format: "`validateFormats: false` can be used instead.", + nullable: '"nullable" keyword is supported by default.', + jsonPointers: "Deprecated jsPropertySyntax can be used instead.", + schemaId: "JSON Schema draft-04 is not supported in Ajv v7.", + strictDefaults: "It is default now, see option `strict`.", + strictKeywords: "It is default now, see option `strict`.", + strictNumbers: "It is default now, see option `strict`.", + uniqueItems: '"uniqueItems" keyword is always validated.', + unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).", +} + +const deprecatedOptions: OptionsInfo = { + jsPropertySyntax: "", + unicode: '"minLength"/"maxLength" account for unicode characters by default.', +} + export default class Ajv { opts: InstanceOptions errors?: ErrorObject[] | null // errors from the last validation @@ -115,14 +138,16 @@ export default class Ajv { serialize: opts.serialize === false ? (x) => x : opts.serialize ?? stableStringify, addUsedSchema: opts.addUsedSchema ?? true, validateSchema: opts.validateSchema ?? true, + validateFormats: opts.validateFormats ?? true, } this.logger = getLogger(opts.logger) - const formatOpt = opts.format - opts.format = false + const formatOpt = opts.validateFormats + opts.validateFormats = false this._cache = opts.cache || new Cache() this.RULES = getRules() - checkDeprecatedOptions.call(this, opts) + checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED") + checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn") this._metaOpts = getMetaSchemaOptions.call(this) if (opts.formats) addInitialFormats.call(this) @@ -137,7 +162,7 @@ export default class Ajv { addDefaultMetaSchema.call(this) if (typeof opts.meta == "object") this.addMetaSchema(opts.meta) addInitialSchemas.call(this) - opts.format = formatOpt + opts.validateFormats = formatOpt } // Validate data using schema @@ -520,13 +545,17 @@ export interface ErrorsTextOptions { dataVar?: string } -function checkDeprecatedOptions(this: Ajv, opts: Options & RemovedOptions): void { - if (opts.errorDataPath !== undefined) this.logger.error("NOT SUPPORTED: option errorDataPath") - if (opts.schemaId !== undefined) this.logger.error("NOT SUPPORTED: option schemaId") - if (opts.uniqueItems !== undefined) this.logger.error("NOT SUPPORTED: option uniqueItems") - if (opts.unknownFormats !== undefined) this.logger.error("NOT SUPPORTED: option unknownFormats") - if (opts.jsPropertySyntax !== undefined) this.logger.warn("DEPRECATED: option jsPropertySyntax") - if (opts.unicode !== undefined) this.logger.warn("DEPRECATED: option unicode") +function checkOptions( + this: Ajv, + checkOpts: OptionsInfo, + options: Options & RemovedOptions, + msg: string, + log: "warn" | "error" = "error" +): void { + for (const key in checkOpts) { + const opt = key as keyof typeof checkOpts + if (opt in options) this.logger[log](`${msg}: option ${key}. ${checkOpts[opt]}`) + } } function defaultMeta(this: Ajv): string | AnySchemaObject | undefined { diff --git a/lib/types/index.ts b/lib/types/index.ts index c278d56665..7afa4917b7 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -35,12 +35,13 @@ export type LoadSchemaFunction = ( cb?: (err: Error | null, schema?: AnySchemaObject) => void ) => Promise -export interface Options { +export type Options = CurrentOptions & DeprecatedOptions + +export interface CurrentOptions { strict?: boolean | "log" $data?: boolean allErrors?: boolean verbose?: boolean - format?: boolean formats?: {[name: string]: Format} keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated schemas?: AnySchema[] | {[key: string]: AnySchema} @@ -72,20 +73,27 @@ export interface Options { | true | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) allowMatchingProperties?: boolean // disables a strict mode restriction - // deprecated: - jsPropertySyntax?: boolean // added instead of jsonPointers - unicode?: boolean + validateFormats?: boolean } export interface CodeOptions { formats?: Code // code to require (or construct) map of available formats - for standalone code } +export interface DeprecatedOptions { + jsPropertySyntax?: boolean // added instead of jsonPointers + unicode?: boolean +} + export interface RemovedOptions { - // removed: + format?: boolean errorDataPath?: "object" | "property" nullable?: boolean // "nullable" keyword is supported by default + jsonPointers?: boolean schemaId?: string + strictDefaults?: boolean + strictKeywords?: boolean + strictNumbers?: boolean uniqueItems?: boolean unknownFormats?: true | string[] | "ignore" } @@ -98,6 +106,7 @@ export interface InstanceOptions extends Options { serialize: (schema: AnySchema) => unknown addUsedSchema: boolean validateSchema: boolean | "log" + validateFormats: boolean } export interface Logger { diff --git a/lib/vocabularies/format/format.ts b/lib/vocabularies/format/format.ts index 579f1198e0..c0c5f0455a 100644 --- a/lib/vocabularies/format/format.ts +++ b/lib/vocabularies/format/format.ts @@ -34,7 +34,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCxt, ruleType?: string) { const {gen, data, $data, schema, schemaCode, it} = cxt const {opts, errSchemaPath, schemaEnv, self} = it - if (opts.format === false) return + if (!opts.validateFormats) return if ($data) validate$DataFormat() else validateFormat() diff --git a/spec/options/options_validation.spec.ts b/spec/options/options_validation.spec.ts index f8c8aeac01..2b72769db8 100644 --- a/spec/options/options_validation.spec.ts +++ b/spec/options/options_validation.spec.ts @@ -6,7 +6,7 @@ describe("validation options", () => { describe("format", () => { it("should not validate formats if option format == false", () => { const ajv = new _Ajv({formats: {date: DATE_FORMAT}}), - ajvFF = new _Ajv({formats: {date: DATE_FORMAT}, format: false}) + ajvFF = new _Ajv({formats: {date: DATE_FORMAT}, validateFormats: false}) const schema = {format: "date"} const invalideDateTime = "06/19/1963" // expects hyphens From 5a3dc3ea8a696f86533b84af189f27b81e33a97e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 18 Sep 2020 18:16:32 +0100 Subject: [PATCH 240/322] refactor option defaults - now all "true" by default options are explicitly assigned in InstanceOptions --- README.md | 40 +++++++++++++++++++++++++++++++--------- lib/ajv.ts | 34 +++++++++++++++++++--------------- lib/types/index.ts | 5 ++++- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 00921e19aa..187daf9ff8 100644 --- a/README.md +++ b/README.md @@ -1257,6 +1257,10 @@ const defaultOptions = { removeAdditional: false, useDefaults: false, coerceTypes: false, + // code generation options: + codegen: {es5: false, lines: false} + sourceCode: false, + processCode: undefined, // (code: string, schemaEnv: object) => string // advanced options: meta: true, validateSchema: true, @@ -1268,15 +1272,13 @@ const defaultOptions = { ownProperties: false, multipleOfPrecision: false, messages: true, - sourceCode: false, - processCode: undefined, // (code: string, schemaEnv: object) => string cache: new Cache, serialize: undefined // (schema: object | boolean) => any jsPropertySyntax: false, // deprecated } ``` -##### Strict mode options (NEW in v7) +#### Strict mode options (NEW in v7) - _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: - `true` (default) - use strict mode and throw an exception when any strict mode restriction is violated. @@ -1287,7 +1289,7 @@ const defaultOptions = { - `true` (default) - validate formats (see [Formats](#formats)). In [strict mode](#strict-mode) unknown formats will throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). - `false` - do not validate any format keywords (TODO they will still collect annotations once supported). -##### Validation and reporting options +#### Validation and reporting options - _\$data_: support [\$data references](#data-reference). Draft 6 meta-schema that is added by default will be extended to allow them. If you want to use another meta-schema you need to use $dataMetaSchema method to add support for $data reference. See [API](#api). - _allErrors_: check all rules collecting all errors. Default is to return after the first error. @@ -1303,7 +1305,7 @@ const defaultOptions = { - logger instance - it should have methods `log`, `warn` and `error`. If any of these methods is missing an exception will be thrown. - `false` - logging is disabled. -##### Referenced schema options +#### Referenced schema options - _missingRefs_: handling of missing referenced schemas. Option values: - `true` (default) - if the reference cannot be resolved during compilation the exception is thrown. The thrown error has properties `missingRef` (with hash fragment) and `missingSchema` (without it). Both properties are resolved relative to the current base id (usually schema id, unless it was substituted). @@ -1315,7 +1317,7 @@ const defaultOptions = { - `true` - validate all keywords in the schemas with `$ref` (the default behaviour in versions before 5.0.0). - _loadSchema_: asynchronous function that will be used to load remote schemas when `compileAsync` [method](#api-compileAsync) is used and some reference is missing (option `missingRefs` should NOT be 'fail' or 'ignore'). This function should accept remote schema uri as a parameter and return a Promise that resolves to a schema. See example in [Asynchronous compilation](#asynchronous-schema-compilation). -##### Options to modify validated data +#### Options to modify validated data - _removeAdditional_: remove additional properties - see example in [Filtering data](#filtering-data). This option is not used if schema is added with `addMetaSchema` method. Option values: - `false` (default) - not to remove additional properties @@ -1331,7 +1333,29 @@ const defaultOptions = { - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). -##### Advanced options +#### Code generation options + +- _codegen_ (new in v7): code generation options, passed to `CodeGen` constructor (see Code generation TODO). This object contains properties: + +```typescript +type CodeGenOptions = { + es5?: boolean // to generate es5 code - by default code is es6, with "for-of" loops, "let" and "const" + lines?: boolean // break code to lines - to simplify debugging of generated functions +} +``` + +- _sourceCode_: add `source` property (with properties `code` and `scope`) to validating function. + +```typescript +type Source = { + code: string // this code can be different from the result of toString call + scope: Scope // see Code generation +} +``` + +- _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. + +#### Advanced options - _meta_: add [meta-schema](http://json-schema.org/documentation.html) so it can be used by other schemas (true by default). If an object is passed, it will be used as the default meta-schema for schemas that have no `$schema` keyword. This default meta-schema MUST have `$schema` keyword. - _validateSchema_: validate added/compiled schemas against meta-schema (true by default). `$schema` property in the schema can be http://json-schema.org/draft-07/schema or absent (draft-07 meta-schema will be used) or can be a reference to the schema previously added with `addMetaSchema` method. Option values: @@ -1349,8 +1373,6 @@ const defaultOptions = { - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). - _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when messages are generated outside of Ajv code (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). -- _sourceCode_: add `sourceCode` property to validating function (for debugging; this code can be different from the result of toString call). -- _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)`, `del(key)` and `clear()`. - _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used. - _jsPropertySyntax_ (deprecated) - set to `true` to report `dataPath` in errors as in v6, using JavaScript property syntax (e.g., `".prop[1].subProp"`). By default `dataPath` in errors is reported as JSON pointer. This option is added for backward compatibility and is not recommended - this format is difficult to parse even in JS code. diff --git a/lib/ajv.ts b/lib/ajv.ts index a5617c48ad..a2ff70cc51 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -83,16 +83,8 @@ const EXT_SCOPE_NAMES = new Set([ "Error", ]) -const optsDefaults = { - strict: true, - code: {}, - loopRequired: Infinity, - loopEnum: Infinity, - addUsedSchema: true, -} - type OptionsInfo = { - [key in keyof T]-?: string | undefined + [K in keyof T]-?: string | undefined } const removedOptions: OptionsInfo = { @@ -113,6 +105,22 @@ const deprecatedOptions: OptionsInfo = { unicode: '"minLength"/"maxLength" account for unicode characters by default.', } +function optDefaults(o: Options): InstanceOptions { + return { + strict: o.strict ?? true, + code: o.code ?? {}, + loopRequired: o.loopRequired ?? Infinity, + loopEnum: o.loopEnum ?? Infinity, + meta: o.meta ?? true, + messages: o.messages ?? true, + inlineRefs: o.inlineRefs ?? true, + addUsedSchema: o.addUsedSchema ?? true, + validateSchema: o.validateSchema ?? true, + validateFormats: o.validateFormats ?? true, + serialize: o.serialize === false ? (x) => x : o.serialize ?? stableStringify, + } +} + export default class Ajv { opts: InstanceOptions errors?: ErrorObject[] | null // errors from the last validation @@ -133,12 +141,8 @@ export default class Ajv { constructor(opts: Options = {}) { opts = this.opts = { - ...optsDefaults, ...opts, - serialize: opts.serialize === false ? (x) => x : opts.serialize ?? stableStringify, - addUsedSchema: opts.addUsedSchema ?? true, - validateSchema: opts.validateSchema ?? true, - validateFormats: opts.validateFormats ?? true, + ...optDefaults(opts), } this.logger = getLogger(opts.logger) const formatOpt = opts.validateFormats @@ -577,7 +581,7 @@ function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | string | undefined { function addDefaultMetaSchema(this: Ajv): void { const {$data, meta} = this.opts if ($data) this.addMetaSchema($dataRefSchema, $dataRefSchema.$id, false) - if (meta === false) return + if (!meta) return const metaSchema = $data ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) : draft7MetaSchema diff --git a/lib/types/index.ts b/lib/types/index.ts index 7afa4917b7..eb5ee09f8f 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -51,7 +51,7 @@ export interface CurrentOptions { removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" - meta?: AnySchemaObject | boolean + meta?: SchemaObject | boolean defaultMeta?: string | AnySchemaObject validateSchema?: boolean | "log" addUsedSchema?: boolean @@ -103,6 +103,9 @@ export interface InstanceOptions extends Options { code: CodeOptions loopRequired: number loopEnum: number + meta: SchemaObject | boolean + messages: boolean + inlineRefs: boolean | number serialize: (schema: AnySchema) => unknown addUsedSchema: boolean validateSchema: boolean | "log" From 013ce3e787decb2e3a6f7834fb5e64d6fa48eabd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 07:58:52 +0100 Subject: [PATCH 241/322] chore: fix eslint script for windows --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64655ef1da..02fe10424a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ".tonic_example.js" ], "scripts": { - "eslint": "eslint 'lib/**/*.ts' 'spec/**/*.*s' scripts --ignore-pattern spec/JSON-Schema-Test-Suite", + "eslint": "eslint \"lib/**/*.ts\" \"spec/**/*.*s\" scripts --ignore-pattern spec/JSON-Schema-Test-Suite", "prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'", "prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'", "test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register 'spec/**/*.spec.ts' -R dot", From 5ec8ee25218afc562cb4fc150ab3312e3d4ccceb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 08:19:35 +0100 Subject: [PATCH 242/322] docs: fix example --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 187daf9ff8..c3522f3744 100644 --- a/README.md +++ b/README.md @@ -182,10 +182,10 @@ type MyData = {foo: number} const schema: JSONSchemaType = { type: "object", properties: { - foo: {type: "number", minimum: 0} - } + foo: {type: "number", minimum: 0}, + }, required: ["foo"], - additionalProperties: false + additionalProperties: false, } // validate is a type guard for MyData - type is inferred from schema type From 89fb180fc8ff4eb647f07701ef1568e9e85645f6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 09:52:24 +0100 Subject: [PATCH 243/322] refactor: move Options, Logger and Cache to ajv.ts --- lib/ajv.ts | 138 +++++++++++++++++++++++++--- lib/cache.ts | 26 ------ lib/compile/validate/index.ts | 5 +- lib/types/index.ts | 98 +------------------- spec/ajv.ts | 5 +- spec/ajv_async_instances.ts | 4 +- spec/ajv_instances.ts | 4 +- spec/ajv_options.ts | 2 +- spec/boolean.spec.ts | 2 +- spec/types/error-parameters.spec.ts | 2 +- spec/types/json-schema.spec.ts | 4 +- 11 files changed, 140 insertions(+), 150 deletions(-) delete mode 100644 lib/cache.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index a2ff70cc51..77fa971a3f 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -9,13 +9,11 @@ export { FuncKeywordDefinition, Vocabulary, Schema, + SchemaObject, AsyncSchema, - Options, ValidateFunction, AsyncValidateFunction, ErrorObject, - CacheInterface, - Logger, } from "./types" export interface Plugin { (ajv: Ajv, options?: Opts): Ajv @@ -35,26 +33,19 @@ import type { AsyncSchema, Vocabulary, KeywordDefinition, - Options, - InstanceOptions, - RemovedOptions, - DeprecatedOptions, AnyValidateFunction, ValidateFunction, AsyncValidateFunction, - CacheInterface, - Logger, ErrorObject, Format, AddedFormat, } from "./types" import type {JSONSchemaType} from "./types/json-schema" -import Cache from "./cache" import {ValidationError, MissingRefError} from "./compile/error_classes" import {getRules, ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" import {SchemaEnv, compileSchema, resolveSchema} from "./compile" -import {ValueScope} from "./compile/codegen" +import {Code, CodeGenOptions, ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" @@ -83,6 +74,69 @@ const EXT_SCOPE_NAMES = new Set([ "Error", ]) +export type Options = CurrentOptions & DeprecatedOptions + +interface CurrentOptions { + strict?: boolean | "log" + $data?: boolean + allErrors?: boolean + verbose?: boolean + formats?: {[name: string]: Format} + keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated + schemas?: AnySchema[] | {[key: string]: AnySchema} + missingRefs?: true | "ignore" | "fail" + extendRefs?: true | "ignore" | "fail" + loadSchema?: (uri: string) => Promise + removeAdditional?: boolean | "all" | "failing" + useDefaults?: boolean | "empty" + coerceTypes?: boolean | "array" + meta?: SchemaObject | boolean + defaultMeta?: string | AnySchemaObject + validateSchema?: boolean | "log" + addUsedSchema?: boolean + inlineRefs?: boolean | number + passContext?: boolean + loopRequired?: number + loopEnum?: number + ownProperties?: boolean + multipleOfPrecision?: boolean | number + messages?: boolean + code?: CodeOptions + sourceCode?: boolean + processCode?: (code: string, schema?: SchemaEnv) => string + codegen?: CodeGenOptions + cache?: CacheInterface + logger?: Logger | false + serialize?: false | ((schema: AnySchema) => unknown) + $comment?: + | true + | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) + allowMatchingProperties?: boolean // disables a strict mode restriction + validateFormats?: boolean +} + +interface CodeOptions { + formats?: Code // code to require (or construct) map of available formats - for standalone code +} + +interface DeprecatedOptions { + jsPropertySyntax?: boolean // added instead of jsonPointers + unicode?: boolean +} + +interface RemovedOptions { + format?: boolean + errorDataPath?: "object" | "property" + nullable?: boolean // "nullable" keyword is supported by default + jsonPointers?: boolean + schemaId?: string + strictDefaults?: boolean + strictKeywords?: boolean + strictNumbers?: boolean + uniqueItems?: boolean + unknownFormats?: true | string[] | "ignore" +} + type OptionsInfo = { [K in keyof T]-?: string | undefined } @@ -105,7 +159,26 @@ const deprecatedOptions: OptionsInfo = { unicode: '"minLength"/"maxLength" account for unicode characters by default.', } -function optDefaults(o: Options): InstanceOptions { +type RequiredInstOpt = + | "strict" + | "code" + | "loopRequired" + | "loopEnum" + | "meta" + | "messages" + | "inlineRefs" + | "addUsedSchema" + | "validateSchema" + | "validateFormats" + +type RequiredInstanceOptions = { + [K in RequiredInstOpt]: NonNullable +} + +export type InstanceOptions = Omit & + RequiredInstanceOptions & {serialize: (schema: AnySchema) => unknown} + +function requiredOptions(o: Options): RequiredInstanceOptions { return { strict: o.strict ?? true, code: o.code ?? {}, @@ -117,7 +190,43 @@ function optDefaults(o: Options): InstanceOptions { addUsedSchema: o.addUsedSchema ?? true, validateSchema: o.validateSchema ?? true, validateFormats: o.validateFormats ?? true, - serialize: o.serialize === false ? (x) => x : o.serialize ?? stableStringify, + } +} + +export interface Logger { + log(...args: unknown[]): unknown + warn(...args: unknown[]): unknown + error(...args: unknown[]): unknown +} + +export interface CacheInterface { + put(key: unknown, value: SchemaEnv): void + get(key: unknown): SchemaEnv | undefined + del(key: unknown): void + clear(): void +} + +class Cache implements CacheInterface { + private _cache: {[key: string]: SchemaEnv | undefined} + + constructor() { + this._cache = {} + } + + put(key: string, value: SchemaEnv): void { + this._cache[key] = value + } + + get(key: string): SchemaEnv | undefined { + return this._cache[key] + } + + del(key: string): void { + delete this._cache[key] + } + + clear(): void { + this._cache = {} } } @@ -142,7 +251,8 @@ export default class Ajv { constructor(opts: Options = {}) { opts = this.opts = { ...opts, - ...optDefaults(opts), + ...requiredOptions(opts), + serialize: opts.serialize === false ? (x) => x : opts.serialize ?? stableStringify, } this.logger = getLogger(opts.logger) const formatOpt = opts.validateFormats diff --git a/lib/cache.ts b/lib/cache.ts deleted file mode 100644 index 8783c9b6fd..0000000000 --- a/lib/cache.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {SchemaEnv} from "./compile" -import {CacheInterface} from "./types" - -export default class Cache implements CacheInterface { - private _cache: {[key: string]: SchemaEnv | undefined} - - constructor() { - this._cache = {} - } - - put(key: string, value: SchemaEnv): void { - this._cache[key] = value - } - - get(key: string): SchemaEnv | undefined { - return this._cache[key] - } - - del(key: string): void { - delete this._cache[key] - } - - clear(): void { - this._cache = {} - } -} diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 3e74c9b1cd..85a55e06c4 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,4 +1,5 @@ -import type {AnySchema, SchemaCxt, SchemaObjCxt, Options} from "../../types" +import type {AnySchema, SchemaCxt, SchemaObjCxt} from "../../types" +import type {InstanceOptions} from "../../ajv" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" @@ -46,7 +47,7 @@ function topSchemaObjCode(it: SchemaObjCxt): void { return } -function funcSourceUrl(schema: AnySchema, opts: Options): Code { +function funcSourceUrl(schema: AnySchema, opts: InstanceOptions): Code { return typeof schema == "object" && schema.$id && (opts.sourceCode || opts.processCode) ? _`/*# sourceURL=${schema.$id} */` : nil diff --git a/lib/types/index.ts b/lib/types/index.ts index eb5ee09f8f..fb01132fb2 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1,7 +1,8 @@ -import type {CodeGen, Code, Name, CodeGenOptions, Scope} from "../compile/codegen" +import type {CodeGen, Code, Name, Scope} from "../compile/codegen" import type {SchemaEnv} from "../compile" import type KeywordCxt from "../compile/context" import type Ajv from "../ajv" +import type {InstanceOptions} from "../ajv" interface _SchemaObject { $id?: string @@ -30,101 +31,6 @@ export interface SchemaMap { [key: string]: AnySchema | undefined } -export type LoadSchemaFunction = ( - uri: string, - cb?: (err: Error | null, schema?: AnySchemaObject) => void -) => Promise - -export type Options = CurrentOptions & DeprecatedOptions - -export interface CurrentOptions { - strict?: boolean | "log" - $data?: boolean - allErrors?: boolean - verbose?: boolean - formats?: {[name: string]: Format} - keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated - schemas?: AnySchema[] | {[key: string]: AnySchema} - missingRefs?: true | "ignore" | "fail" - extendRefs?: true | "ignore" | "fail" - loadSchema?: LoadSchemaFunction - removeAdditional?: boolean | "all" | "failing" - useDefaults?: boolean | "empty" - coerceTypes?: boolean | "array" - meta?: SchemaObject | boolean - defaultMeta?: string | AnySchemaObject - validateSchema?: boolean | "log" - addUsedSchema?: boolean - inlineRefs?: boolean | number - passContext?: boolean - loopRequired?: number - loopEnum?: number - ownProperties?: boolean - multipleOfPrecision?: boolean | number - messages?: boolean - code?: CodeOptions - sourceCode?: boolean - processCode?: (code: string, schema?: SchemaEnv) => string - codegen?: CodeGenOptions - cache?: CacheInterface - logger?: Logger | false - serialize?: false | ((schema: AnySchema) => unknown) - $comment?: - | true - | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) - allowMatchingProperties?: boolean // disables a strict mode restriction - validateFormats?: boolean -} - -export interface CodeOptions { - formats?: Code // code to require (or construct) map of available formats - for standalone code -} - -export interface DeprecatedOptions { - jsPropertySyntax?: boolean // added instead of jsonPointers - unicode?: boolean -} - -export interface RemovedOptions { - format?: boolean - errorDataPath?: "object" | "property" - nullable?: boolean // "nullable" keyword is supported by default - jsonPointers?: boolean - schemaId?: string - strictDefaults?: boolean - strictKeywords?: boolean - strictNumbers?: boolean - uniqueItems?: boolean - unknownFormats?: true | string[] | "ignore" -} - -export interface InstanceOptions extends Options { - strict: boolean | "log" - code: CodeOptions - loopRequired: number - loopEnum: number - meta: SchemaObject | boolean - messages: boolean - inlineRefs: boolean | number - serialize: (schema: AnySchema) => unknown - addUsedSchema: boolean - validateSchema: boolean | "log" - validateFormats: boolean -} - -export interface Logger { - log(...args: unknown[]): unknown - warn(...args: unknown[]): unknown - error(...args: unknown[]): unknown -} - -export interface CacheInterface { - put(key: unknown, value: SchemaEnv): void - get(key: unknown): SchemaEnv | undefined - del(key: unknown): void - clear(): void -} - interface SourceCode { code: string scope: Scope diff --git a/spec/ajv.ts b/spec/ajv.ts index 33f9a4ffd6..9f88ded7a5 100644 --- a/spec/ajv.ts +++ b/spec/ajv.ts @@ -1,7 +1,6 @@ -import type Ajv from "../dist/ajv" +import type Ajv from ".." -const AjvClass: typeof Ajv = - typeof window == "object" ? (window as any).Ajv : require("" + "../dist/ajv") +const AjvClass: typeof Ajv = typeof window == "object" ? (window as any).Ajv : require("" + "..") export default AjvClass diff --git a/spec/ajv_async_instances.ts b/spec/ajv_async_instances.ts index 974bc9951e..0ebf2940df 100644 --- a/spec/ajv_async_instances.ts +++ b/spec/ajv_async_instances.ts @@ -1,6 +1,6 @@ import getAjvInstances from "./ajv_instances" -import type Ajv from "../dist/ajv" -import type {Options} from "../dist/types" +import type Ajv from ".." +import type {Options} from ".." export default function getAjvSyncInstances(extraOpts?: Options): Ajv[] { return getAjvInstances( diff --git a/spec/ajv_instances.ts b/spec/ajv_instances.ts index 26e6f2883c..f7299959bb 100644 --- a/spec/ajv_instances.ts +++ b/spec/ajv_instances.ts @@ -1,6 +1,6 @@ import _Ajv from "./ajv" -import type Ajv from "../dist/ajv" -import type {Options} from "../dist/types" +import type Ajv from ".." +import type {Options} from ".." export default function getAjvInstances(options: Options, extraOpts: Options = {}): Ajv[] { return _getAjvInstances(options, {...extraOpts, logger: false, codegen: {lines: true}}) diff --git a/spec/ajv_options.ts b/spec/ajv_options.ts index 8118488e01..e42bd0af0b 100644 --- a/spec/ajv_options.ts +++ b/spec/ajv_options.ts @@ -1,4 +1,4 @@ -import type {Options} from "../dist/types" +import type {Options} from ".." const isBrowser = typeof window == "object" const fullTest = !isBrowser && process.env.AJV_FULL_TEST diff --git a/spec/boolean.spec.ts b/spec/boolean.spec.ts index 358e8cd5db..c06108ab8c 100644 --- a/spec/boolean.spec.ts +++ b/spec/boolean.spec.ts @@ -1,5 +1,5 @@ import _Ajv from "./ajv" -import type Ajv from "../dist/ajv" +import type Ajv from ".." require("./chai").should() diff --git a/spec/types/error-parameters.spec.ts b/spec/types/error-parameters.spec.ts index 16358e69fe..e985c2c641 100644 --- a/spec/types/error-parameters.spec.ts +++ b/spec/types/error-parameters.spec.ts @@ -1,4 +1,4 @@ -import {DefinedError} from "../../dist/ajv" +import {DefinedError} from "../.." import _Ajv from "../ajv" import chai from "../chai" const should = chai.should() diff --git a/spec/types/json-schema.spec.ts b/spec/types/json-schema.spec.ts index 0370f6e255..ddc9a32fee 100644 --- a/spec/types/json-schema.spec.ts +++ b/spec/types/json-schema.spec.ts @@ -1,6 +1,6 @@ import _Ajv from "../ajv" -import type {JSONSchemaType} from "../../dist/types/json-schema" -import type {SchemaObject} from "../../dist/types" +import type {JSONSchemaType} from "../.." +import type {SchemaObject} from "../.." import chai from "../chai" const should = chai.should() From a1505a94888759da10145c74661dd65a0c069e83 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 11:24:59 +0100 Subject: [PATCH 244/322] feat: change cache interface to use Map, remove Cache class, use actual schema reference as the key, remove fast-json-stable-stringify from dependencies --- README.md | 22 +++++++--- lib/ajv.ts | 74 +++++++++++---------------------- package.json | 1 - spec/ajv.spec.ts | 81 ++++++++++++++++--------------------- spec/async_validate.spec.ts | 17 ++++---- 5 files changed, 80 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index c3522f3744..3e004fd46d 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ if (validate(data)) { See [this test](./spec/types/json-schema.spec.ts) for an advanced example, [API](#api) and [Options](#options) for more details. -Ajv compiles schemas to functions and caches them in all cases (using schema serialized with [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) or another function passed via options), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. +Ajv compiles schemas to functions and caches them in all cases (using schema itself as a key for Map) or another function passed via options), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. The best performance is achieved when using compiled functions returned by `compile` or `getSchema` methods (there is no additional function call). @@ -1118,7 +1118,7 @@ Schema can be removed using: - key passed to `addSchema` - it's full reference (id) - RegExp that should match schema id or key (meta-schemas won't be removed) -- actual schema object that will be stable-stringified to remove schema from cache +- actual schema object (that will be optionally serialized) to remove schema from cache If no parameter is passed all schemas but meta-schemas will be removed and the cache will be cleared. @@ -1272,8 +1272,8 @@ const defaultOptions = { ownProperties: false, multipleOfPrecision: false, messages: true, - cache: new Cache, - serialize: undefined // (schema: object | boolean) => any + cache: new Map(), + serialize: (x) => x // (schema: object | boolean) => any jsPropertySyntax: false, // deprecated } ``` @@ -1373,8 +1373,18 @@ type Source = { - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). - _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when messages are generated outside of Ajv code (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). -- _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)`, `del(key)` and `clear()`. -- _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used. +- _cache_: an optional instance of cache to store compiled schemas using schema (optionally serialized) as a key. If not passed, then a Map instance is used. Required interface for cache has the same methods as Map: + +```typescript +interface Cache { + set(key: unknown, value: object): void + get(key: unknown): object | undefined + delete(key: unknown): void + clear(): void +} +``` + +- _serialize_: an optional function to serialize schema to cache key. By default schema reference itself is used as a key. - _jsPropertySyntax_ (deprecated) - set to `true` to report `dataPath` in errors as in v6, using JavaScript property syntax (e.g., `".prop[1].subProp"`). By default `dataPath` in errors is reported as JSON pointer. This option is added for backward compatibility and is not recommended - this format is difficult to parse even in JS code. ## Validation errors diff --git a/lib/ajv.ts b/lib/ajv.ts index 77fa971a3f..f087e1295f 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -52,7 +52,6 @@ import validationVocabulary from "./vocabularies/validation" import applicatorVocabulary from "./vocabularies/applicator" import formatVocabulary from "./vocabularies/format" import {metadataVocabulary, contentVocabulary} from "./vocabularies/metadata" -import stableStringify from "fast-json-stable-stringify" import {eachItem} from "./compile/util" import $dataRefSchema from "./refs/data.json" import draft7MetaSchema from "./refs/json-schema-draft-07.json" @@ -107,7 +106,7 @@ interface CurrentOptions { codegen?: CodeGenOptions cache?: CacheInterface logger?: Logger | false - serialize?: false | ((schema: AnySchema) => unknown) + serialize?: (schema: AnySchema) => unknown $comment?: | true | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) @@ -159,24 +158,22 @@ const deprecatedOptions: OptionsInfo = { unicode: '"minLength"/"maxLength" account for unicode characters by default.', } -type RequiredInstOpt = - | "strict" - | "code" - | "loopRequired" - | "loopEnum" - | "meta" - | "messages" - | "inlineRefs" - | "addUsedSchema" - | "validateSchema" - | "validateFormats" - type RequiredInstanceOptions = { - [K in RequiredInstOpt]: NonNullable + [K in + | "strict" + | "code" + | "inlineRefs" + | "loopRequired" + | "loopEnum" + | "meta" + | "messages" + | "serialize" + | "addUsedSchema" + | "validateSchema" + | "validateFormats"]: NonNullable } -export type InstanceOptions = Omit & - RequiredInstanceOptions & {serialize: (schema: AnySchema) => unknown} +export type InstanceOptions = Options & RequiredInstanceOptions function requiredOptions(o: Options): RequiredInstanceOptions { return { @@ -187,6 +184,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions { meta: o.meta ?? true, messages: o.messages ?? true, inlineRefs: o.inlineRefs ?? true, + serialize: o.serialize || ((x) => x), // "||" is to account for removed "false" option value addUsedSchema: o.addUsedSchema ?? true, validateSchema: o.validateSchema ?? true, validateFormats: o.validateFormats ?? true, @@ -200,36 +198,12 @@ export interface Logger { } export interface CacheInterface { - put(key: unknown, value: SchemaEnv): void + set(key: unknown, value: SchemaEnv): void get(key: unknown): SchemaEnv | undefined - del(key: unknown): void + delete(key: unknown): void clear(): void } -class Cache implements CacheInterface { - private _cache: {[key: string]: SchemaEnv | undefined} - - constructor() { - this._cache = {} - } - - put(key: string, value: SchemaEnv): void { - this._cache[key] = value - } - - get(key: string): SchemaEnv | undefined { - return this._cache[key] - } - - del(key: string): void { - delete this._cache[key] - } - - clear(): void { - this._cache = {} - } -} - export default class Ajv { opts: InstanceOptions errors?: ErrorObject[] | null // errors from the last validation @@ -252,13 +226,12 @@ export default class Ajv { opts = this.opts = { ...opts, ...requiredOptions(opts), - serialize: opts.serialize === false ? (x) => x : opts.serialize ?? stableStringify, } this.logger = getLogger(opts.logger) const formatOpt = opts.validateFormats opts.validateFormats = false - this._cache = opts.cache || new Cache() + this._cache = opts.cache || new Map() this.RULES = getRules() checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED") checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn") @@ -280,8 +253,7 @@ export default class Ajv { } // Validate data using schema - // AnySchema will be compiled and cached using as a key JSON serialized with - // [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) + // AnySchema will be compiled and cached using schema itsekf as a key for Map validate(schema: Schema | string, data: unknown): boolean validate(schemaKeyRef: AnySchema | string, data: unknown): boolean | Promise validate(schema: Schema | JSONSchemaType | string, data: unknown): data is T @@ -477,14 +449,14 @@ export default class Ajv { return this case "string": { const sch = getSchEnv.call(this, schemaKeyRef) - if (typeof sch == "object") this._cache.del(sch.cacheKey) + if (typeof sch == "object") this._cache.delete(sch.cacheKey) delete this.schemas[schemaKeyRef] delete this.refs[schemaKeyRef] return this } case "object": { const cacheKey = this.opts.serialize(schemaKeyRef) - this._cache.del(cacheKey) + this._cache.delete(cacheKey) let id = schemaKeyRef.$id if (id) { id = normalizeId(id) @@ -597,7 +569,7 @@ export default class Ajv { if (typeof sch == "string") { delete schemas[keyRef] } else if (sch && !sch.meta) { - this._cache.del(sch.cacheKey) + this._cache.delete(sch.cacheKey) delete schemas[keyRef] } } @@ -619,7 +591,7 @@ export default class Ajv { const localRefs = getSchemaRefs.call(this, schema) sch = new SchemaEnv({schema, cacheKey, meta, localRefs}) - this._cache.put(sch.cacheKey, sch) + this._cache.set(sch.cacheKey, sch) const id = sch.baseId if (addSchema && !id.startsWith("#")) { // TODO atm it is allowed to overwrite schemas without id (instead of not adding them) diff --git a/package.json b/package.json index 02fe10424a..a5228270d1 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "tonicExampleFilename": ".tonic_example.js", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.5.0", "uri-js": "^4.2.2" }, diff --git a/spec/ajv.spec.ts b/spec/ajv.spec.ts index 29cdfcc4d3..30caee882e 100644 --- a/spec/ajv.spec.ts +++ b/spec/ajv.spec.ts @@ -1,5 +1,4 @@ import _Ajv from "./ajv" -import stableStringify from "fast-json-stable-stringify" import {_} from "../dist/compile/codegen" const should = require("./chai").should() @@ -24,16 +23,13 @@ describe("Ajv", () => { }) it("should cache compiled functions for the same schema", () => { - const v1 = ajv.compile({ + const schema = { $id: "//e.com/int.json", type: "integer", minimum: 1, - }) - const v2 = ajv.compile({ - $id: "//e.com/int.json", - minimum: 1, - type: "integer", - }) + } + const v1 = ajv.compile(schema) + const v2 = ajv.compile(schema) v1.should.equal(v2) }) @@ -302,50 +298,46 @@ describe("Ajv", () => { describe("removeSchema method", () => { it("should remove schema by key", () => { - const schema = {type: "integer"}, - str = stableStringify(schema) + const schema = {type: "integer"} ajv.addSchema(schema, "int") const v = ajv.getSchema("int") v.should.be.a("function") - ajv._cache.get(str).validate.should.equal(v) + ajv._cache.get(schema).validate.should.equal(v) ajv.removeSchema("int") should.not.exist(ajv.getSchema("int")) - should.not.exist(ajv._cache.get(str)) + should.not.exist(ajv._cache.get(schema)) }) it("should remove schema by id", () => { - const schema = {$id: "//e.com/int.json", type: "integer"}, - str = stableStringify(schema) + const schema = {$id: "//e.com/int.json", type: "integer"} ajv.addSchema(schema) const v = ajv.getSchema("//e.com/int.json") v.should.be.a("function") - ajv._cache.get(str).validate.should.equal(v) + ajv._cache.get(schema).validate.should.equal(v) ajv.removeSchema("//e.com/int.json") should.not.exist(ajv.getSchema("//e.com/int.json")) - should.not.exist(ajv._cache.get(str)) + should.not.exist(ajv._cache.get(schema)) }) it("should remove schema by schema object", () => { - const schema = {type: "integer"}, - str = stableStringify(schema) + const schema = {type: "integer"} ajv.addSchema(schema) - ajv._cache.get(str).should.be.an("object") - ajv.removeSchema({type: "integer"}) - should.not.exist(ajv._cache.get(str)) + ajv._cache.get(schema).should.be.an("object") + ajv.removeSchema(schema) + should.not.exist(ajv._cache.get(schema)) }) it("should remove schema with id by schema object", () => { - const schema = {$id: "//e.com/int.json", type: "integer"}, - str = stableStringify(schema) + const schema = {$id: "//e.com/int.json", type: "integer"} ajv.addSchema(schema) - ajv._cache.get(str).should.be.an("object") - ajv.removeSchema({$id: "//e.com/int.json", type: "integer"}) - // should.not.exist(ajv.getSchema('//e.com/int.json')); - should.not.exist(ajv._cache.get(str)) + ajv._cache.get(schema).should.be.an("object") + ajv.removeSchema(schema) + should.not.exist(ajv.getSchema("//e.com/int.json")) + should.not.exist(ajv._cache.get(schema)) }) it("should not throw if there is no schema with passed id", () => { @@ -356,41 +348,36 @@ describe("Ajv", () => { }) it("should remove all schemas but meta-schemas if called without an arguments", () => { - const schema1 = {$id: "//e.com/int.json", type: "integer"}, - str1 = stableStringify(schema1) + const schema1 = {$id: "//e.com/int.json", type: "integer"} ajv.addSchema(schema1) - ajv._cache.get(str1).should.be.an("object") + ajv._cache.get(schema1).should.be.an("object") - const schema2 = {type: "integer"}, - str2 = stableStringify(schema2) + const schema2 = {type: "integer"} ajv.addSchema(schema2) - ajv._cache.get(str2).should.be.an("object") + ajv._cache.get(schema2).should.be.an("object") ajv.removeSchema() - should.not.exist(ajv._cache.get(str1)) - should.not.exist(ajv._cache.get(str2)) + should.not.exist(ajv._cache.get(schema1)) + should.not.exist(ajv._cache.get(schema2)) }) it("should remove all schemas but meta-schemas with key/id matching pattern", () => { - const schema1 = {$id: "//e.com/int.json", type: "integer"}, - str1 = stableStringify(schema1) + const schema1 = {$id: "//e.com/int.json", type: "integer"} ajv.addSchema(schema1) - ajv._cache.get(str1).should.be.an("object") + ajv._cache.get(schema1).should.be.an("object") - const schema2 = {$id: "str.json", type: "string"}, - str2 = stableStringify(schema2) + const schema2 = {$id: "str.json", type: "string"} ajv.addSchema(schema2, "//e.com/str.json") - ajv._cache.get(str2).should.be.an("object") + ajv._cache.get(schema2).should.be.an("object") - const schema3 = {type: "integer"}, - str3 = stableStringify(schema3) + const schema3 = {type: "integer"} ajv.addSchema(schema3) - ajv._cache.get(str3).should.be.an("object") + ajv._cache.get(schema3).should.be.an("object") ajv.removeSchema(/e\.com/) - should.not.exist(ajv._cache.get(str1)) - should.not.exist(ajv._cache.get(str2)) - ajv._cache.get(str3).should.be.an("object") + should.not.exist(ajv._cache.get(schema1)) + should.not.exist(ajv._cache.get(schema2)) + ajv._cache.get(schema3).should.be.an("object") }) it("should return instance of itself", () => { diff --git a/spec/async_validate.spec.ts b/spec/async_validate.spec.ts index 8762f35ef3..d0f8c0bfa9 100644 --- a/spec/async_validate.spec.ts +++ b/spec/async_validate.spec.ts @@ -50,9 +50,7 @@ describe("async schemas, formats and keywords", function () { ajv.compile(schema) }) - schema.$async = true - - ajv.compile(schema) + ajv.compile({...schema, $async: true}) }) }) @@ -61,7 +59,7 @@ describe("async schemas, formats and keywords", function () { it("should fail compilation if async format is inside sync schema", () => { instances.forEach((_ajv) => { - const schema: any = { + let schema: any = { type: "string", format: "english_word", } @@ -69,7 +67,7 @@ describe("async schemas, formats and keywords", function () { shouldThrowFunc("async format in sync schema", () => { _ajv.compile(schema) }) - schema.$async = true + schema = {...schema, $async: true} _ajv.compile(schema) }) }) @@ -98,7 +96,7 @@ describe("async schemas, formats and keywords", function () { it("should fail compilation if async keyword is inside sync schema", () => { instances.forEach((_ajv) => { - const schema: any = { + let schema: any = { type: "object", properties: { userId: { @@ -112,7 +110,7 @@ describe("async schemas, formats and keywords", function () { _ajv.compile(schema) }) - schema.$async = true + schema = {...schema, $async: true} _ajv.compile(schema) }) }) @@ -317,7 +315,7 @@ describe("async schemas, formats and keywords", function () { }) it("should fail compilation if sync schema references async schema", () => { - const schema: any = { + let schema: any = { $id: "http://e.com/obj.json#", type: "object", properties: { @@ -347,8 +345,7 @@ describe("async schemas, formats and keywords", function () { ajv.compile(schema) }) - schema.$id = "http://e.com/obj2.json#" - schema.$async = true + schema = {...schema, $id: "http://e.com/obj2.json#", $async: true} ajv.compile(schema) }) From cfe3e9206541b60a126b1673a032b5ce4fe63c84 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 12:17:29 +0100 Subject: [PATCH 245/322] feat: remove options extendRefs (it is default now) and missingRef (always fail compilation), add deprecated ignoreKeywordsWithRef to pass draft 6/7 tests --- README.md | 16 ---- lib/ajv.ts | 8 +- lib/compile/error_classes.ts | 8 +- lib/compile/validate/index.ts | 8 +- lib/compile/validate/iterate.ts | 2 +- lib/vocabularies/core/ref.ts | 34 +------- lib/vocabularies/errors.ts | 8 +- spec/ajv_options.ts | 1 - spec/async_validate.spec.ts | 2 +- spec/errors.spec.ts | 12 --- .../533_missing_ref_error_when_ignore.spec.ts | 26 ------ spec/json-schema.spec.ts | 12 ++- spec/options/options_refs.spec.ts | 85 +++---------------- 13 files changed, 41 insertions(+), 181 deletions(-) delete mode 100644 spec/issues/533_missing_ref_error_when_ignore.spec.ts diff --git a/README.md b/README.md index 3e004fd46d..a04d957fad 100644 --- a/README.md +++ b/README.md @@ -1249,9 +1249,6 @@ const defaultOptions = { keywords: {}, schemas: {}, logger: undefined, - // referenced schema options: - missingRefs: true, - extendRefs: "ignore", // recommended 'fail' loadSchema: undefined, // function(uri: string): Promise {} // options to modify validated data: removeAdditional: false, @@ -1274,7 +1271,6 @@ const defaultOptions = { messages: true, cache: new Map(), serialize: (x) => x // (schema: object | boolean) => any - jsPropertySyntax: false, // deprecated } ``` @@ -1304,17 +1300,6 @@ const defaultOptions = { - _logger_: sets the logging method. Default is the global `console` object that should have methods `log`, `warn` and `error`. See [Error logging](#error-logging). Option values: - logger instance - it should have methods `log`, `warn` and `error`. If any of these methods is missing an exception will be thrown. - `false` - logging is disabled. - -#### Referenced schema options - -- _missingRefs_: handling of missing referenced schemas. Option values: - - `true` (default) - if the reference cannot be resolved during compilation the exception is thrown. The thrown error has properties `missingRef` (with hash fragment) and `missingSchema` (without it). Both properties are resolved relative to the current base id (usually schema id, unless it was substituted). - - `"ignore"` - to log error during compilation and always pass validation. - - `"fail"` - to log error and successfully compile schema but fail validation if this rule is checked. -- _extendRefs_: validation of other keywords when `$ref` is present in the schema. Option values: - - `"ignore"` (default) - when `$ref` is used other keywords are ignored (as per [JSON Reference](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03#section-3) standard). A warning will be logged during the schema compilation. - - `"fail"` (recommended) - if other validation keywords are used together with `$ref` the exception will be thrown when the schema is compiled. This option is recommended to make sure schema has no keywords that are ignored, which can be confusing. - - `true` - validate all keywords in the schemas with `$ref` (the default behaviour in versions before 5.0.0). - _loadSchema_: asynchronous function that will be used to load remote schemas when `compileAsync` [method](#api-compileAsync) is used and some reference is missing (option `missingRefs` should NOT be 'fail' or 'ignore'). This function should accept remote schema uri as a parameter and return a Promise that resolves to a schema. See example in [Asynchronous compilation](#asynchronous-schema-compilation). #### Options to modify validated data @@ -1385,7 +1370,6 @@ interface Cache { ``` - _serialize_: an optional function to serialize schema to cache key. By default schema reference itself is used as a key. -- _jsPropertySyntax_ (deprecated) - set to `true` to report `dataPath` in errors as in v6, using JavaScript property syntax (e.g., `".prop[1].subProp"`). By default `dataPath` in errors is reported as JSON pointer. This option is added for backward compatibility and is not recommended - this format is difficult to parse even in JS code. ## Validation errors diff --git a/lib/ajv.ts b/lib/ajv.ts index f087e1295f..78ddbe322e 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -83,8 +83,6 @@ interface CurrentOptions { formats?: {[name: string]: Format} keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated schemas?: AnySchema[] | {[key: string]: AnySchema} - missingRefs?: true | "ignore" | "fail" - extendRefs?: true | "ignore" | "fail" loadSchema?: (uri: string) => Promise removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" @@ -119,6 +117,7 @@ interface CodeOptions { } interface DeprecatedOptions { + ignoreKeywordsWithRef?: boolean jsPropertySyntax?: boolean // added instead of jsonPointers unicode?: boolean } @@ -128,6 +127,8 @@ interface RemovedOptions { errorDataPath?: "object" | "property" nullable?: boolean // "nullable" keyword is supported by default jsonPointers?: boolean + extendRefs?: true | "ignore" | "fail" + missingRefs?: true | "ignore" | "fail" schemaId?: string strictDefaults?: boolean strictKeywords?: boolean @@ -145,6 +146,8 @@ const removedOptions: OptionsInfo = { format: "`validateFormats: false` can be used instead.", nullable: '"nullable" keyword is supported by default.', jsonPointers: "Deprecated jsPropertySyntax can be used instead.", + extendRefs: "Deprecated ignoreKeywordsWithRef can be used instead.", + missingRefs: "Pass empty schema with $id that should be ignored to ajv.addSchema.", schemaId: "JSON Schema draft-04 is not supported in Ajv v7.", strictDefaults: "It is default now, see option `strict`.", strictKeywords: "It is default now, see option `strict`.", @@ -154,6 +157,7 @@ const removedOptions: OptionsInfo = { } const deprecatedOptions: OptionsInfo = { + ignoreKeywordsWithRef: "", jsPropertySyntax: "", unicode: '"minLength"/"maxLength" account for unicode characters by default.', } diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts index 5b842ef389..d24d36b7d8 100644 --- a/lib/compile/error_classes.ts +++ b/lib/compile/error_classes.ts @@ -17,12 +17,8 @@ export class MissingRefError extends Error { readonly missingRef: string readonly missingSchema: string - static message(baseId: string, ref: string): string { - return `can't resolve reference ${ref} from id ${baseId}` - } - - constructor(baseId: string, ref: string, message?: string) { - super(message || MissingRefError.message(baseId, ref)) + constructor(baseId: string, ref: string) { + super(`can't resolve reference ${ref} from id ${baseId}`) this.missingRef = resolveUrl(baseId, ref) this.missingSchema = normalizeId(getFullPath(this.missingRef)) } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 85a55e06c4..cb4c146e84 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -94,12 +94,8 @@ function typeAndKeywords(it: SchemaObjCxt, errsCount?: Name): void { function checkRefsAndKeywords(it: SchemaObjCxt): void { const {schema, errSchemaPath, opts, self} = it - if (schema.$ref && schemaHasRulesButRef(schema, self.RULES)) { - if (opts.extendRefs === "fail") { - throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`) - } else if (opts.extendRefs !== true) { - self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) - } + if (schema.$ref && opts.ignoreKeywordsWithRef && schemaHasRulesButRef(schema, self.RULES)) { + self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`) } } diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 2283eb02e1..cca7e182ff 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -16,7 +16,7 @@ export function schemaKeywords( ): void { const {gen, schema, data, allErrors, opts, self} = it const {RULES} = self - if (schema.$ref && !(opts.extendRefs === true && schemaHasRulesButRef(schema, RULES))) { + if (schema.$ref && (opts.ignoreKeywordsWithRef || !schemaHasRulesButRef(schema, RULES))) { gen.block(() => keywordCode(it, "$ref", (RULES.all.$ref as Rule).definition)) // TODO typecast return } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index aeb3e0af36..0565bf4d5b 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -1,35 +1,22 @@ -import type { - CodeKeywordDefinition, - ErrorObject, - KeywordErrorDefinition, - AnySchema, -} from "../../types" +import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" import {MissingRefError} from "../../compile/error_classes" import {applySubschema} from "../../compile/subschema" import {callValidateCode} from "../util" -import {_, str, nil, Code, Name} from "../../compile/codegen" +import {_, nil, Code, Name} from "../../compile/codegen" import N from "../../compile/names" import {SchemaEnv, resolveRef} from "../../compile" -export type RefError = ErrorObject<"ref", {ref: string}> - -const error: KeywordErrorDefinition = { - message: ({schema}) => str`can't resolve reference ${schema}`, - params: ({schema}) => _`{ref: ${schema}}`, -} - const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", - error, code(cxt: KeywordCxt) { const {gen, schema, it} = cxt const {allErrors, baseId, schemaEnv: env, opts, validateName, self} = it const passCxt = opts.passContext ? N.this : nil if (schema === "#" || schema === "#/") return callRootRef() const schOrFunc = resolveRef.call(self, env.root, baseId, schema) - if (schOrFunc === undefined) return missingRef() + if (schOrFunc === undefined) throw new MissingRefError(baseId, schema) if (schOrFunc instanceof SchemaEnv) return callValidate(schOrFunc) return inlineRefSchema(schOrFunc) @@ -72,21 +59,6 @@ const def: CodeKeywordDefinition = { cxt.ok(valid) } - function missingRef(): void { - const msg = MissingRefError.message(baseId, schema) - switch (opts.missingRefs) { - case "fail": - self.logger.error(msg) - cxt.fail() - return - case "ignore": - self.logger.warn(msg) - return - default: - throw new MissingRefError(baseId, schema, msg) - } - } - function callAsyncRef(v: Code): void { if (!env.$async) throw new Error("async schema referenced by sync schema") const valid = gen.let("valid") diff --git a/lib/vocabularies/errors.ts b/lib/vocabularies/errors.ts index f501fd2294..20cdee72c2 100644 --- a/lib/vocabularies/errors.ts +++ b/lib/vocabularies/errors.ts @@ -1,12 +1,6 @@ -import type {RefError} from "./core/ref" import type {TypeError} from "../compile/validate/dataType" import type {ApplicatorKeywordError} from "./applicator" import type {ValidationKeywordError} from "./validation" import type {FormatError} from "./format/format" -export type DefinedError = - | RefError - | TypeError - | ApplicatorKeywordError - | ValidationKeywordError - | FormatError +export type DefinedError = TypeError | ApplicatorKeywordError | ValidationKeywordError | FormatError diff --git a/spec/ajv_options.ts b/spec/ajv_options.ts index e42bd0af0b..1d3024f925 100644 --- a/spec/ajv_options.ts +++ b/spec/ajv_options.ts @@ -7,7 +7,6 @@ const options: Options = fullTest ? { allErrors: true, verbose: true, - extendRefs: "ignore", inlineRefs: false, codegen: {es5: true, lines: true}, } diff --git a/spec/async_validate.spec.ts b/spec/async_validate.spec.ts index d0f8c0bfa9..beb4a75eaf 100644 --- a/spec/async_validate.spec.ts +++ b/spec/async_validate.spec.ts @@ -182,7 +182,7 @@ describe("async schemas, formats and keywords", function () { describe("async referenced schemas", () => { beforeEach(() => { - instances = getAjvAsyncInstances({inlineRefs: false, extendRefs: "ignore"}) + instances = getAjvAsyncInstances({inlineRefs: false, ignoreKeywordsWithRef: true}) addFormatEnglishWord() }) diff --git a/spec/errors.spec.ts b/spec/errors.spec.ts index 77a57f7918..fdf3ec951c 100644 --- a/spec/errors.spec.ts +++ b/spec/errors.spec.ts @@ -933,18 +933,6 @@ describe("Validation errors", () => { }) }) - describe("$ref errors", () => { - it("should have correct message and params", () => { - const _ajv = new Ajv({missingRefs: "fail", logger: false}) - const schema = {$ref: "#/unknown"} - const validate: any = _ajv.compile(schema) - shouldBeInvalid(validate, {}) - shouldBeError(validate.errors[0], "$ref", "#/$ref", "", "can't resolve reference #/unknown", { - ref: "#/unknown", - }) - }) - }) - function testSchema1(schema, schemaPathPrefix = "#/properties/foo") { _testSchema1(ajv, schema, schemaPathPrefix) _testSchema1(ajvJP, schema, schemaPathPrefix) diff --git a/spec/issues/533_missing_ref_error_when_ignore.spec.ts b/spec/issues/533_missing_ref_error_when_ignore.spec.ts deleted file mode 100644 index bc4943c094..0000000000 --- a/spec/issues/533_missing_ref_error_when_ignore.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import _Ajv from "../ajv" -const should = require("../chai").should() - -describe('issue #533, throwing missing ref exception with option missingRefs: "ignore"', () => { - const schema = { - type: "object", - properties: { - foo: {$ref: "#/definitions/missing"}, - bar: {$ref: "#/definitions/missing"}, - }, - } - - it("should pass validation without throwing exception", () => { - const ajv = new _Ajv({missingRefs: "ignore", logger: false}) - const validate = ajv.compile(schema) - validate({foo: "anything"}).should.equal(true) - validate({foo: "anything", bar: "whatever"}).should.equal(true) - }) - - it("should throw exception during schema compilation with option missingRefs: true", () => { - const ajv = new _Ajv() - should.throw(() => { - ajv.compile(schema) - }) - }) -}) diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index c5af831461..5589ddefc2 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -24,10 +24,20 @@ const SKIP = { ], } -runTest(getAjvInstances(options, {meta: false, strict: false}), 6, require("./_json/draft6")) runTest( getAjvInstances(options, { + meta: false, strict: false, + ignoreKeywordsWithRef: true, + }), + 6, + require("./_json/draft6") +) + +runTest( + getAjvInstances(options, { + strict: false, + ignoreKeywordsWithRef: true, formats: { "idn-email": true, "idn-hostname": true, diff --git a/spec/options/options_refs.spec.ts b/spec/options/options_refs.spec.ts index fb39026a1f..05e4fefc6b 100644 --- a/spec/options/options_refs.spec.ts +++ b/spec/options/options_refs.spec.ts @@ -1,60 +1,31 @@ import _Ajv from "../ajv" +import type {Options} from "../.." const should = require("../chai").should() describe("referenced schema options", () => { - describe("extendRefs", () => { - describe("= true", () => { + describe("ignoreKeywordsWithRef", () => { + describe("= undefined", () => { it("should allow extending $ref with other keywords", () => { - test(new _Ajv({extendRefs: true}), true) + test({}, true) }) - it("should NOT log warning if extendRefs is true", () => { - testWarning(new _Ajv({extendRefs: true})) + it("should NOT log warning", () => { + testWarning() }) }) - describe('= "ignore" and default', () => { + describe("= true", () => { it("should ignore other keywords when $ref is used", () => { - test(new _Ajv({logger: false})) - test(new _Ajv({extendRefs: "ignore", logger: false}), false) + test({ignoreKeywordsWithRef: true, logger: false}, false) }) it("should log warning when other keywords are used with $ref", () => { - testWarning(new _Ajv(), /keywords\signored/) - testWarning(new _Ajv({extendRefs: "ignore"}), /keywords\signored/) + testWarning({ignoreKeywordsWithRef: true}, /keywords\signored/) }) }) - describe('= "fail"', () => { - it("should fail schema compilation if other keywords are used with $ref", () => { - testFail(new _Ajv({extendRefs: "fail"})) - - function testFail(ajv) { - should.throw(() => { - const schema = { - definitions: { - int: {type: "integer"}, - }, - $ref: "#/definitions/int", - minimum: 10, - } - ajv.compile(schema) - }) - - should.not.throw(() => { - const schema = { - definitions: { - int: {type: "integer"}, - }, - allOf: [{$ref: "#/definitions/int"}, {minimum: 10}], - } - ajv.compile(schema) - }) - } - }) - }) - - function test(ajv, shouldExtendRef?: boolean) { + function test(opts: Options, shouldExtendRef: boolean) { + const ajv = new _Ajv(opts) const schema = { definitions: { int: {type: "integer"}, @@ -89,7 +60,7 @@ describe("referenced schema options", () => { validate({foo: 10, bar: 1}).should.equal(false) } - function testWarning(ajv, msgPattern?: RegExp) { + function testWarning(opts: Options = {}, msgPattern?: RegExp) { let oldConsole try { oldConsole = console.warn @@ -98,6 +69,8 @@ describe("referenced schema options", () => { consoleMsg = Array.prototype.join.call(args, " ") } + const ajv = new _Ajv(opts) + const schema = { definitions: { int: {type: "integer"}, @@ -122,35 +95,5 @@ describe("referenced schema options", () => { ajv.compile({$ref: "missing_reference"}) }) }) - - it('should not throw and pass validation with missingRef == "ignore"', () => { - testMissingRefsIgnore(new _Ajv({missingRefs: "ignore", logger: false})) - testMissingRefsIgnore(new _Ajv({missingRefs: "ignore", allErrors: true, logger: false})) - - function testMissingRefsIgnore(ajv) { - const validate = ajv.compile({$ref: "missing_reference"}) - validate({}).should.equal(true) - } - }) - - it('should not throw and fail validation with missingRef == "fail" if the ref is used', () => { - testMissingRefsFail(new _Ajv({missingRefs: "fail", logger: false})) - testMissingRefsFail(new _Ajv({missingRefs: "fail", verbose: true, logger: false})) - testMissingRefsFail(new _Ajv({missingRefs: "fail", allErrors: true, logger: false})) - testMissingRefsFail( - new _Ajv({missingRefs: "fail", allErrors: true, verbose: true, logger: false}) - ) - - function testMissingRefsFail(ajv) { - let validate = ajv.compile({ - anyOf: [{type: "number"}, {$ref: "missing_reference"}], - }) - validate(123).should.equal(true) - validate("foo").should.equal(false) - - validate = ajv.compile({$ref: "missing_reference"}) - validate({}).should.equal(false) - } - }) }) }) From c7cf624185cbc88481d0baf364c7d33c9e0a3a73 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 15:02:36 +0100 Subject: [PATCH 246/322] feat: combine code generation options in "code" option --- README.md | 44 +++++++++++++------------------ lib/ajv.ts | 41 +++++++++++++++++----------- lib/compile/index.ts | 7 ++--- lib/compile/validate/index.ts | 2 +- spec/ajv_async_instances.ts | 2 +- spec/ajv_instances.ts | 2 +- spec/ajv_options.ts | 4 +-- spec/options/options_code.spec.ts | 6 ++--- spec/resolve.spec.ts | 8 +++--- 9 files changed, 60 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index a04d957fad..ce8c8f25d9 100644 --- a/README.md +++ b/README.md @@ -1254,10 +1254,6 @@ const defaultOptions = { removeAdditional: false, useDefaults: false, coerceTypes: false, - // code generation options: - codegen: {es5: false, lines: false} - sourceCode: false, - processCode: undefined, // (code: string, schemaEnv: object) => string // advanced options: meta: true, validateSchema: true, @@ -1271,6 +1267,7 @@ const defaultOptions = { messages: true, cache: new Map(), serialize: (x) => x // (schema: object | boolean) => any + code: {es5: false, lines: false} } ``` @@ -1318,28 +1315,6 @@ const defaultOptions = { - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). -#### Code generation options - -- _codegen_ (new in v7): code generation options, passed to `CodeGen` constructor (see Code generation TODO). This object contains properties: - -```typescript -type CodeGenOptions = { - es5?: boolean // to generate es5 code - by default code is es6, with "for-of" loops, "let" and "const" - lines?: boolean // break code to lines - to simplify debugging of generated functions -} -``` - -- _sourceCode_: add `source` property (with properties `code` and `scope`) to validating function. - -```typescript -type Source = { - code: string // this code can be different from the result of toString call - scope: Scope // see Code generation -} -``` - -- _processCode_: an optional function to process generated code before it is passed to Function constructor. It can be used to either beautify (the validating function is generated without line-breaks) or to transpile code. - #### Advanced options - _meta_: add [meta-schema](http://json-schema.org/documentation.html) so it can be used by other schemas (true by default). If an object is passed, it will be used as the default meta-schema for schemas that have no `$schema` keyword. This default meta-schema MUST have `$schema` keyword. @@ -1370,6 +1345,23 @@ interface Cache { ``` - _serialize_: an optional function to serialize schema to cache key. By default schema reference itself is used as a key. +- _code_ (new in v7): code generation options: + +```typescript +type CodeOptions = { + es5?: boolean // to generate es5 code - by default code is es6, with "for-of" loops, "let" and "const" + lines?: boolean // add line-breaks to code - to simplify debugging of generated functions + source?: boolean // add `source` property (see Source below) to validating function. + process?: (code: string, schema?: SchemaEnv) => string // an optional function to process generated code + // before it is passed to Function constructor. + // It can be used to either beautify or to transpile code. +} + +type Source = { + code: string // unlike func.toString() it includes assignments exernal to function scope + scope: Scope // see Code generation (TODO) +} +``` ## Validation errors diff --git a/lib/ajv.ts b/lib/ajv.ts index 78ddbe322e..9d8147f9f0 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -24,6 +24,7 @@ import KeywordCxt from "./compile/context" export {KeywordCxt} export {DefinedError} from "./vocabularies/errors" export {JSONSchemaType} from "./types/json-schema" +export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" import type { Schema, @@ -45,7 +46,7 @@ import {ValidationError, MissingRefError} from "./compile/error_classes" import {getRules, ValidationRules, Rule, RuleGroup} from "./compile/rules" import {checkType} from "./compile/validate/dataType" import {SchemaEnv, compileSchema, resolveSchema} from "./compile" -import {Code, CodeGenOptions, ValueScope} from "./compile/codegen" +import {Code, ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" @@ -76,17 +77,27 @@ const EXT_SCOPE_NAMES = new Set([ export type Options = CurrentOptions & DeprecatedOptions interface CurrentOptions { + // strict mode options strict?: boolean | "log" + allowMatchingProperties?: boolean // disables a strict mode restriction + validateFormats?: boolean + // validation and reporting options: $data?: boolean allErrors?: boolean verbose?: boolean + $comment?: + | true + | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) formats?: {[name: string]: Format} keywords?: Vocabulary | {[x: string]: KeywordDefinition} // map is deprecated schemas?: AnySchema[] | {[key: string]: AnySchema} + logger?: Logger | false loadSchema?: (uri: string) => Promise + // options to modify validated data: removeAdditional?: boolean | "all" | "failing" useDefaults?: boolean | "empty" coerceTypes?: boolean | "array" + // advanced options: meta?: SchemaObject | boolean defaultMeta?: string | AnySchemaObject validateSchema?: boolean | "log" @@ -98,27 +109,23 @@ interface CurrentOptions { ownProperties?: boolean multipleOfPrecision?: boolean | number messages?: boolean - code?: CodeOptions - sourceCode?: boolean - processCode?: (code: string, schema?: SchemaEnv) => string - codegen?: CodeGenOptions cache?: CacheInterface - logger?: Logger | false serialize?: (schema: AnySchema) => unknown - $comment?: - | true - | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) - allowMatchingProperties?: boolean // disables a strict mode restriction - validateFormats?: boolean -} - -interface CodeOptions { - formats?: Code // code to require (or construct) map of available formats - for standalone code + code?: { + es5?: boolean + lines?: boolean + formats?: Code // code to require (or construct) map of available formats - for standalone code + source?: boolean + process?: (code: string, schema?: SchemaEnv) => string + } } interface DeprecatedOptions { + /** @deprecated */ ignoreKeywordsWithRef?: boolean + /** @deprecated */ jsPropertySyntax?: boolean // added instead of jsonPointers + /** @deprecated */ unicode?: boolean } @@ -129,6 +136,8 @@ interface RemovedOptions { jsonPointers?: boolean extendRefs?: true | "ignore" | "fail" missingRefs?: true | "ignore" | "fail" + processCode?: (code: string, schema?: SchemaEnv) => string + sourceCode?: boolean schemaId?: string strictDefaults?: boolean strictKeywords?: boolean @@ -148,6 +157,8 @@ const removedOptions: OptionsInfo = { jsonPointers: "Deprecated jsPropertySyntax can be used instead.", extendRefs: "Deprecated ignoreKeywordsWithRef can be used instead.", missingRefs: "Pass empty schema with $id that should be ignored to ajv.addSchema.", + processCode: "Use option `code: {process: (code, schemaEnv: object) => string}`", + sourceCode: "Use option `code: {source: true}`", schemaId: "JSON Schema draft-04 is not supported in Ajv v7.", strictDefaults: "It is default now, see option `strict`.", strictKeywords: "It is default now, see option `strict`.", diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 18f09db02a..191539969d 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -59,7 +59,8 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { const _sch = getCompilingSchema.call(this, sch) if (_sch) return _sch const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails - const gen = new CodeGen(this.scope, {...this.opts.codegen, forInOwn: this.opts.ownProperties}) + const {es5, lines} = this.opts.code + const gen = new CodeGen(this.scope, {es5, lines, forInOwn: this.opts.ownProperties}) let _ValidationError if (sch.$async) { _ValidationError = gen.scopeValue("Error", { @@ -99,7 +100,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { this._compilations.add(sch) validateFunctionCode(schemaCxt) sourceCode = `${gen.scopeRefs(N.scope)}${gen}` - if (this.opts.processCode) sourceCode = this.opts.processCode(sourceCode, sch) + if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch) // console.log("\n\n\n *** \n", sourceCode) const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode) const validate: AnyValidateFunction = makeValidate(this, this.scope.get()) @@ -109,7 +110,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { validate.schema = sch.schema validate.schemaEnv = sch if (sch.$async) (validate as AsyncValidateFunction).$async = true - if (this.opts.sourceCode === true) { + if (this.opts.code.source === true) { validate.source = { code: sourceCode, scope: this.scope, diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index cb4c146e84..5312b94e21 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -48,7 +48,7 @@ function topSchemaObjCode(it: SchemaObjCxt): void { } function funcSourceUrl(schema: AnySchema, opts: InstanceOptions): Code { - return typeof schema == "object" && schema.$id && (opts.sourceCode || opts.processCode) + return typeof schema == "object" && schema.$id && (opts.code.source || opts.code.process) ? _`/*# sourceURL=${schema.$id} */` : nil } diff --git a/spec/ajv_async_instances.ts b/spec/ajv_async_instances.ts index 0ebf2940df..6214b293f5 100644 --- a/spec/ajv_async_instances.ts +++ b/spec/ajv_async_instances.ts @@ -7,7 +7,7 @@ export default function getAjvSyncInstances(extraOpts?: Options): Ajv[] { { strict: false, allErrors: true, - codegen: {lines: true}, + code: {lines: true}, }, extraOpts ) diff --git a/spec/ajv_instances.ts b/spec/ajv_instances.ts index f7299959bb..cea6847c8e 100644 --- a/spec/ajv_instances.ts +++ b/spec/ajv_instances.ts @@ -3,7 +3,7 @@ import type Ajv from ".." import type {Options} from ".." export default function getAjvInstances(options: Options, extraOpts: Options = {}): Ajv[] { - return _getAjvInstances(options, {...extraOpts, logger: false, codegen: {lines: true}}) + return _getAjvInstances(options, {...extraOpts, logger: false, code: {lines: true}}) } function _getAjvInstances(opts: Options, useOpts: Options): Ajv[] { diff --git a/spec/ajv_options.ts b/spec/ajv_options.ts index 1d3024f925..d512ae8f52 100644 --- a/spec/ajv_options.ts +++ b/spec/ajv_options.ts @@ -8,9 +8,9 @@ const options: Options = fullTest allErrors: true, verbose: true, inlineRefs: false, - codegen: {es5: true, lines: true}, + code: {es5: true, lines: true}, } - : {allErrors: true, codegen: {es5: true, lines: true}} + : {allErrors: true, code: {es5: true, lines: true}} export default options diff --git a/spec/options/options_code.spec.ts b/spec/options/options_code.spec.ts index 11562e5708..861851c680 100644 --- a/spec/options/options_code.spec.ts +++ b/spec/options/options_code.spec.ts @@ -5,7 +5,7 @@ describe("code generation options", () => { describe("sourceCode", () => { describe("= true", () => { it("should add source.code property", () => { - test(new _Ajv({sourceCode: true})) + test(new _Ajv({code: {source: true}})) function test(ajv) { const validate = ajv.compile({type: "number"}) @@ -17,7 +17,7 @@ describe("code generation options", () => { describe("= false and default", () => { it("should not add source and sourceCode properties", () => { test(new _Ajv()) - test(new _Ajv({sourceCode: false})) + test(new _Ajv({code: {source: false}})) function test(ajv) { const validate = ajv.compile({type: "number"}) @@ -37,7 +37,7 @@ describe("code generation options", () => { const unprocessedLines = validate.toString().split("\n").length const beautify = require("js-beautify").js_beautify - const ajvPC = new _Ajv({processCode: beautify}) + const ajvPC = new _Ajv({code: {process: beautify}}) validate = ajvPC.compile({type: "string"}) validate.toString().split("\n").length.should.be.above(unprocessedLines) validate("foo").should.equal(true) diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index c523197dc9..72ffffcb63 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -215,22 +215,22 @@ describe("resolve", () => { ] it("by default should inline schema if it doesn't contain refs", () => { - const ajv = new Ajv({schemas, sourceCode: true}) + const ajv = new Ajv({schemas, code: {source: true}}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs == false", () => { - const ajv = new Ajv({schemas, inlineRefs: false, sourceCode: true}) + const ajv = new Ajv({schemas, inlineRefs: false, code: {source: true}}) testSchemas(ajv, false) }) it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { - const ajv = new Ajv({schemas, inlineRefs: 4, sourceCode: true}) + const ajv = new Ajv({schemas, inlineRefs: 4, code: {source: true}}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { - const ajv = new Ajv({schemas: schemas, inlineRefs: 2, sourceCode: true}) + const ajv = new Ajv({schemas: schemas, inlineRefs: 2, code: {source: true}}) testSchemas(ajv, false) }) From 9e2e6b99380d4d37d6cb5a22a8059aa38f9c1957 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 17:45:59 +0100 Subject: [PATCH 247/322] refactor: group all validation function params after data into dataCxt, refactor code generation for object literals --- lib/compile/codegen/code.ts | 16 +++++++++++ lib/compile/codegen/index.ts | 9 ++++++- lib/compile/errors.ts | 43 ++++++++++++++++++++---------- lib/compile/index.ts | 4 +-- lib/compile/names.ts | 1 + lib/compile/validate/index.ts | 50 ++++++++++++++++++++++++----------- lib/types/index.ts | 28 ++++++++------------ lib/vocabularies/util.ts | 17 +++++++----- spec/keyword.spec.ts | 2 +- 9 files changed, 112 insertions(+), 58 deletions(-) diff --git a/lib/compile/codegen/code.ts b/lib/compile/codegen/code.ts index 746e874b08..8ddee43aa8 100644 --- a/lib/compile/codegen/code.ts +++ b/lib/compile/codegen/code.ts @@ -14,6 +14,10 @@ export class _Code { return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"' } + emptyStr(): boolean { + return this._str === "" || this._str === '""' + } + add(c: _Code): void { this._str += c._str } @@ -31,6 +35,10 @@ export class Name extends _Code { return false } + emptyStr(): boolean { + return false + } + add(_c: _Code): void { throw new Error("CodeGen: can't add to Name") } @@ -66,6 +74,10 @@ export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[] ) } +export function strConcat(c1: Code, c2: Code): Code { + return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}` +} + function interpolate(x: TemplateArg): TemplateArg { return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null ? x @@ -90,3 +102,7 @@ function safeStringify(x: unknown): string { export function getProperty(key: Code | string | number): Code { return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]` } + +export function keyValue(key: Name, value: SafeExpr, es5?: boolean): Code { + return key === value && !es5 ? key : _`${key}: ${value}` +} diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index 117795bf2d..485fc1ecd9 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -2,7 +2,7 @@ import type {ScopeValueSets, NameValue, ValueScope, ValueScopeName} from "./scop import {_, nil, _Code, Code, Name} from "./code" import {Scope} from "./scope" -export {_, str, nil, getProperty, stringify, Name, Code} from "./code" +export {_, str, strConcat, nil, getProperty, stringify, Name, Code} from "./code" export {Scope, ScopeStore, ValueScope} from "./scope" enum BlockKind { @@ -116,6 +116,13 @@ export class CodeGen { return this } + object(...keyValues: [Name, SafeExpr][]): _Code { + const values = keyValues + .map(([key, value]) => (key === value && !this.opts.es5 ? key : `${key}: ${value}`)) + .reduce((c1, c2) => `${c1},${c2}`) + return new _Code(`{${values}}`) + } + if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen { this._blocks.push(BlockKind.If) this._out += `if(${condition}){` + this._n diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 0ccd54845e..2a6ca4e6fa 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,5 +1,6 @@ import type {KeywordErrorCxt, KeywordErrorDefinition, SchemaCxt} from "../types" -import {CodeGen, _, str, Code, Name} from "./codegen" +import {CodeGen, _, str, strConcat, Code, Name} from "./codegen" +import {SafeExpr} from "./codegen/code" import N from "./names" export const keywordError: KeywordErrorDefinition = { @@ -59,7 +60,7 @@ export function extendErrors({ gen.const(err, _`${N.vErrors}[${i}]`) gen.if( _`${err}.dataPath === undefined`, - _`${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` + _`${err}.dataPath = ${strConcat(N.dataPath, it.errorPath)}` ) gen.code(_`${err}.schemaPath = ${str`${it.errSchemaPath}/${keyword}`}`) if (it.opts.verbose) { @@ -84,28 +85,42 @@ function returnErrors(it: SchemaCxt, errs: Code): void { } } +const E = { + keyword: new Name("keyword"), + schemaPath: new Name("schemaPath"), + params: new Name("params"), + propertyName: new Name("propertyName"), + message: new Name("message"), + schema: new Name("schema"), + parentSchema: new Name("parentSchema"), +} + function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { const { keyword, data, schemaValue, - it: {createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, + it: {gen, createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, } = cxt if (createErrors === false) return _`{}` const {params, message} = error - const msg = typeof message == "string" ? message : message(cxt) - const par = params ? params(cxt) : _`{}` - const out = _`{keyword: ${keyword}, dataPath: (${N.dataPath} || "") + ${errorPath}` - out.add(_`, schemaPath: ${str`${errSchemaPath}/${keyword}`}, params: ${par}`) - if (propertyName) { - out.add(_`, propertyName: ${propertyName}`) - } + const keyValues: [Name, SafeExpr][] = [ + [E.keyword, _`${keyword}`], + [N.dataPath, strConcat(N.dataPath, errorPath)], + [E.schemaPath, str`${errSchemaPath}/${keyword}`], + [E.params, params ? params(cxt) : _`{}`], + ] + if (propertyName) keyValues.push([E.propertyName, propertyName]) if (opts.messages !== false) { - out.add(_`, message: ${msg}`) + const msg = typeof message == "string" ? _`${message}` : message(cxt) + keyValues.push([E.message, msg]) } if (opts.verbose) { - out.add(_`, schema: ${schemaValue}, parentSchema: ${topSchemaRef}${schemaPath}, data: ${data}`) + keyValues.push( + [E.schema, schemaValue], + [E.parentSchema, _`${topSchemaRef}${schemaPath}`], + [N.data, data] + ) } - out.add(_`}`) - return out + return gen.object(...keyValues) } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 191539969d..5e1bd4592b 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -6,7 +6,7 @@ import type { SchemaCxt, } from "../types" import type Ajv from "../ajv" -import {CodeGen, _, nil, str, Name} from "./codegen" +import {CodeGen, _, nil, Name} from "./codegen" import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" @@ -90,7 +90,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { baseId: sch.baseId || rootId, schemaPath: nil, errSchemaPath: "#", - errorPath: str``, + errorPath: _`""`, opts: this.opts, self: this, } diff --git a/lib/compile/names.ts b/lib/compile/names.ts index b2942974b8..11387cb046 100644 --- a/lib/compile/names.ts +++ b/lib/compile/names.ts @@ -4,6 +4,7 @@ const names = { // validation function arguments data: new Name("data"), // data passed to validation function // args passed from referencing schema + dataCxt: new Name("dataCxt"), dataPath: new Name("dataPath"), parentData: new Name("parentData"), parentDataProperty: new Name("parentDataProperty"), diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 5312b94e21..54390b3bc4 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -3,7 +3,7 @@ import type {InstanceOptions} from "../../ajv" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" -import {CodeGen, _, nil, str, Block, Code, Name} from "../codegen" +import {_, nil, str, Block, Code, Name, CodeGen} from "../codegen" import N from "../names" import {resolveUrl} from "../resolve" import {schemaCxtHasRules, schemaHasRulesButRef} from "../util" @@ -26,21 +26,46 @@ function validateFunction( body: Block ): void { gen.return(() => - gen.func( - validateName, - _`${N.data}, ${N.dataPath}, ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}`, - schemaEnv.$async, - () => gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`).code(body) - ) + opts.code.es5 + ? gen.func(validateName, _`${N.data}, ${N.dataCxt}`, schemaEnv.$async, () => { + gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`) + destructureDataCxtES5(gen) + gen.code(body) + }) + : gen.func( + validateName, + _`${N.data}, {${N.dataPath}="", ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}=${N.data}}={}`, + schemaEnv.$async, + () => gen.code(_`${funcSourceUrl(schema, opts)}`).code(body) + ) + ) +} + +function destructureDataCxtES5(gen: CodeGen): void { + gen.if( + N.dataCxt, + () => { + gen.var(N.dataPath, _`${N.dataCxt}.${N.dataPath}`) + gen.var(N.parentData, _`${N.dataCxt}.${N.parentData}`) + gen.var(N.parentDataProperty, _`${N.dataCxt}.${N.parentDataProperty}`) + gen.var(N.rootData, _`${N.dataCxt}.${N.rootData}`) + }, + () => { + gen.var(N.dataPath, _`""`) + gen.var(N.parentData, _`undefined`) + gen.var(N.parentDataProperty, _`undefined`) + gen.var(N.rootData, N.data) + } ) } function topSchemaObjCode(it: SchemaObjCxt): void { - const {schema, opts} = it + const {schema, opts, gen} = it validateFunction(it, () => { if (opts.$comment && schema.$comment) commentKeyword(it) checkNoDefault(it) - initializeTop(it.gen) + gen.let(N.vErrors, null) + gen.let(N.errors, 0) typeAndKeywords(it) returnResults(it) }) @@ -106,13 +131,6 @@ function checkNoDefault(it: SchemaObjCxt): void { } } -function initializeTop(gen: CodeGen): void { - gen.let(N.vErrors, null) - gen.let(N.errors, 0) - gen.if(_`${N.rootData} === undefined`, () => gen.assign(N.rootData, N.data)) - // gen.if(_`${N.dataPath} === undefined`, () => gen.assign(N.dataPath, _`""`)) // TODO maybe add it -} - function updateContext(it: SchemaObjCxt): void { if (it.schema.$id) it.baseId = resolveUrl(it.baseId, it.schema.$id) } diff --git a/lib/types/index.ts b/lib/types/index.ts index fb01132fb2..e35bfada1c 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -36,15 +36,15 @@ interface SourceCode { scope: Scope } +interface DataValidationCxt { + dataPath: string + parentData: Record | any[] + parentDataProperty: string | number + rootData: Record | any[] +} + export interface ValidateFunction { - ( - this: Ajv | any, - data: any, - dataPath?: string, - parentData?: Record | any[], - parentDataProperty?: string | number, - rootData?: Record | any[] - ): data is T + (this: Ajv | any, data: any, dataCxt?: DataValidationCxt): data is T errors?: null | ErrorObject[] schema?: AnySchema schemaEnv?: SchemaEnv @@ -140,15 +140,9 @@ export interface DataValidateFunction { } export interface SchemaValidateFunction { - ( - schema: any, - data: any, - parentSchema?: AnySchemaObject, - dataPath?: string, - parentData?: Record | any[], - parentDataProperty?: string | number, - rootData?: Record | any[] - ): boolean | Promise + (schema: any, data: any, parentSchema?: AnySchemaObject, dataCxt?: DataValidationCxt): + | boolean + | Promise errors?: Partial[] } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index fe7759c8be..5179e4fb64 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,7 +1,7 @@ import type {AnySchema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" import type KeywordCxt from "../compile/context" import {schemaHasRules} from "../compile/util" -import {CodeGen, _, nil, Code, Name, getProperty} from "../compile/codegen" +import {CodeGen, _, strConcat, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" export function schemaRefOrVal( @@ -63,16 +63,19 @@ export function noPropertyInData( } export function callValidateCode( - {schemaCode, data, it}: KeywordCxt, + {schemaCode, data, it: {gen, topSchemaRef, schemaPath, errorPath}, it}: KeywordCxt, func: Code, context: Code, passSchema?: boolean ): Code { - const dataAndSchema = passSchema - ? _`${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` - : data - const dataPath = _`(${N.dataPath} || '') + ${it.errorPath}` // TODO refactor other places - const args = _`${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` + const dataAndSchema = passSchema ? _`${schemaCode}, ${data}, ${topSchemaRef}${schemaPath}` : data + const dataCxt = gen.object( + [N.dataPath, strConcat(N.dataPath, errorPath)], + [N.parentData, it.parentData], + [N.parentDataProperty, it.parentDataProperty], + [N.rootData, N.rootData] + ) + const args = _`${dataAndSchema}, ${dataCxt}` return context !== nil ? _`${func}.call(${context}, ${args})` : _`${func}(${args})` } diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index c4d3257893..ea4bd63b86 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -1200,7 +1200,7 @@ describe("User-defined keywords", () => { function testModifying(withOption) { const collectionFormat = { - csv: function (data, _dataPath, parentData, parentDataProperty) { + csv: function (data, {parentData, parentDataProperty}) { parentData[parentDataProperty] = data.split(",") return true }, From 6be22e42d95f7e9f4882d8b90ea4fafc7a6f6cbe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 20 Sep 2020 09:46:03 +0100 Subject: [PATCH 248/322] feat: strictTypes option --- lib/ajv.ts | 1 + lib/compile/index.ts | 1 + lib/compile/subschema.ts | 22 ++++---- lib/compile/validate/iterate.ts | 53 +++++++++++++++++++ lib/types/index.ts | 1 + .../applicator/additionalProperties.ts | 5 +- lib/vocabularies/applicator/items.ts | 12 ++++- .../applicator/patternProperties.ts | 5 +- lib/vocabularies/applicator/properties.ts | 1 + lib/vocabularies/applicator/propertyNames.ts | 8 ++- lib/vocabularies/core/ref.ts | 1 + lib/vocabularies/util.ts | 11 ++-- lib/vocabularies/validation/required.ts | 7 +-- spec/json-schema.spec.ts | 2 + spec/options/strictDefaults.spec.ts | 8 +-- spec/options/strictKeywords.spec.ts | 6 ++- spec/options/unknownFormats.spec.ts | 7 +-- spec/options/useDefaults.spec.ts | 6 +++ spec/resolve.spec.ts | 9 ++-- spec/security.spec.ts | 1 + spec/types/async-validate.spec.ts | 2 +- spec/types/error-parameters.spec.ts | 2 +- 22 files changed, 133 insertions(+), 38 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 9d8147f9f0..057a253cf6 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -79,6 +79,7 @@ export type Options = CurrentOptions & DeprecatedOptions interface CurrentOptions { // strict mode options strict?: boolean | "log" + strictTypes?: boolean | "log" allowMatchingProperties?: boolean // disables a strict mode restriction validateFormats?: boolean // validation and reporting options: diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 5e1bd4592b..e894c00346 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -86,6 +86,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { ValidationError: _ValidationError, schema: sch.schema, schemaEnv: sch, + strictSchema: true, rootId, baseId: sch.baseId || rootId, schemaPath: nil, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 67b36e1d8a..8b599a0831 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -3,9 +3,10 @@ import {subschemaCode} from "./validate" import {escapeFragment, escapeJsonPointer} from "./util" import {_, str, Code, Name, getProperty} from "./codegen" -export interface SubschemaContext { - // TODO use Optional? +interface SubschemaContext { + // TODO use Optional? align with SchemCxt property types schema: AnySchema + strictSchema?: boolean schemaPath: Code errSchemaPath: string topSchemaRef?: Code @@ -27,12 +28,11 @@ export enum Type { Str, } -export type SubschemaApplication = Partial - -interface SubschemaApplicationParams { +export type SubschemaCxt = Partial<{ keyword: string schemaProp: string | number schema: AnySchema + strictSchema: boolean schemaPath: Code errSchemaPath: string topSchemaRef: Code @@ -43,9 +43,9 @@ interface SubschemaApplicationParams { compositeRule: true createErrors: boolean allErrors: boolean -} +}> -export function applySubschema(it: SchemaObjCxt, appl: SubschemaApplication, valid: Name): void { +export function applySubschema(it: SchemaObjCxt, appl: SubschemaCxt, valid: Name): void { const subschema = getSubschema(it, appl) extendSubschemaData(subschema, it, appl) extendSubschemaMode(subschema, appl) @@ -55,7 +55,7 @@ export function applySubschema(it: SchemaObjCxt, appl: SubschemaApplication, val function getSubschema( it: SchemaObjCxt, - {keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaApplication + {keyword, schemaProp, schema, strictSchema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaCxt ): SubschemaContext { if (keyword !== undefined && schema !== undefined) { throw new Error('both "keyword" and "schema" passed, only one allowed') @@ -82,6 +82,7 @@ function getSubschema( } return { schema, + strictSchema, schemaPath, topSchemaRef, errSchemaPath, @@ -94,7 +95,7 @@ function getSubschema( function extendSubschemaData( subschema: SubschemaContext, it: SchemaObjCxt, - {dataProp, dataPropType: dpType, data, propertyName}: SubschemaApplication + {dataProp, dataPropType: dpType, data, propertyName}: SubschemaCxt ): void { if (data !== undefined && dataProp !== undefined) { throw new Error('both "data" and "dataProp" passed, only one allowed') @@ -128,11 +129,12 @@ function extendSubschemaData( function extendSubschemaMode( subschema: SubschemaContext, - {compositeRule, createErrors, allErrors}: SubschemaApplication + {compositeRule, createErrors, allErrors, strictSchema}: SubschemaCxt ): void { if (compositeRule !== undefined) subschema.compositeRule = compositeRule if (createErrors !== undefined) subschema.createErrors = createErrors if (allErrors !== undefined) subschema.allErrors = allErrors + subschema.strictSchema = strictSchema // not inherited } function getErrorPath( diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index cca7e182ff..bbb2a29bc8 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -2,6 +2,7 @@ import type {SchemaObjCxt} from "../../types" import type {Rule, RuleGroup} from "../rules" import {shouldUseGroup, shouldUseRule} from "./applicability" import {checkDataType, schemaHasRulesButRef} from "../util" +import {checkStrictMode} from "../../vocabularies/util" import {keywordCode} from "./keyword" import {assignDefaults} from "./defaults" import {reportTypeError} from "./dataType" @@ -20,6 +21,7 @@ export function schemaKeywords( gen.block(() => keywordCode(it, "$ref", (RULES.all.$ref as Rule).definition)) // TODO typecast return } + checkStrictTypes(it, types) gen.block(() => { for (const group of RULES.rules) { if (shouldUseGroup(schema, group)) { @@ -60,3 +62,54 @@ function iterateKeywords(it: SchemaObjCxt, group: RuleGroup): void { } }) } + +function checkStrictTypes(it: SchemaObjCxt, types: string[]): void { + if (!it.strictSchema || it.schemaEnv.meta || !it.opts.strictTypes) return + checkMultipleTypes(it, types, it.opts.strictTypes) + checkApplicableTypes(it, types, it.opts.strictTypes) +} + +function checkMultipleTypes(it: SchemaObjCxt, ts: string[], mode: boolean | "log"): void { + if ( + ts.length > 1 && + !(ts.length === 2 && ts.includes("null")) && + (ts.includes("object") || ts.includes("array")) + ) { + strictTypesError(it, "multiple non-primitive types", mode) + } +} + +function checkApplicableTypes(it: SchemaObjCxt, ts: string[], mode: boolean | "log"): void { + const rules = it.self.RULES.all + for (const keyword in rules) { + const rule = rules[keyword] + if (typeof rule == "object" && shouldUseRule(it.schema, rule)) { + const {type} = rule.definition + if (Array.isArray(type)) { + if (!type.some((t) => hasApplicableType(ts, t))) { + strictTypesError(it, "missing appllicable type", mode) + return + } + } else if (type) { + if (!hasApplicableType(ts, type)) { + strictTypesError(it, "missing appllicable type", mode) + return + } + } + } + } +} + +function hasApplicableType(schemaTypes: string[], keywordType: string): boolean { + return ( + schemaTypes.includes(keywordType) || + (keywordType === "number" && schemaTypes.includes("integer")) + ) +} + +function strictTypesError(it: SchemaObjCxt, msg: string, mode: boolean | "log"): void { + const schemaPath = it.schemaEnv.baseId + it.errSchemaPath + msg += ` at "${schemaPath}/type" (strictTypes)` + throw new Error(msg) + checkStrictMode(it, msg, mode) +} diff --git a/lib/types/index.ts b/lib/types/index.ts index e35bfada1c..f5228b882a 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -87,6 +87,7 @@ export interface SchemaCxt { ValidationError?: Name schema: AnySchema schemaEnv: SchemaEnv + strictSchema?: boolean rootId: string // TODO ? baseId: string schemaPath: Code diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index c2ab3d2559..fdb5658da8 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,6 +1,6 @@ import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" -import {applySubschema, SubschemaApplication, Type} from "../../compile/subschema" +import {applySubschema, SubschemaCxt, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" @@ -87,10 +87,11 @@ const def: CodeKeywordDefinition = { } function applyAdditionalSchema(key: Name, valid: Name, errors?: false): void { - const subschema: SubschemaApplication = { + const subschema: SubschemaCxt = { keyword: "additionalProperties", dataProp: key, dataPropType: Type.Str, + strictSchema: it.strictSchema, } if (errors === false) { Object.assign(subschema, { diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 68cba36d1c..0941730a29 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -29,6 +29,7 @@ const def: CodeKeywordDefinition = { keyword: "items", schemaProp: i, dataProp: i, + strictSchema: it.strictSchema, }, valid ) @@ -40,7 +41,16 @@ const def: CodeKeywordDefinition = { function validateItems(): void { const valid = gen.name("valid") gen.forRange("i", 0, len, (i) => { - applySubschema(it, {keyword: "items", dataProp: i, dataPropType: Type.Num}, valid) + applySubschema( + it, + { + keyword: "items", + dataProp: i, + dataPropType: Type.Num, + strictSchema: it.strictSchema, + }, + valid + ) if (!it.allErrors) gen.ifNot(valid, _`break`) }) cxt.ok(valid) diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index c8ffa2e3c7..01c0db3146 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -10,10 +10,10 @@ const def: CodeKeywordDefinition = { schemaType: "object", code(cxt: KeywordCxt) { const {gen, schema, data, parentSchema, it} = cxt + const {opts} = it const patterns = schemaProperties(it, schema) if (patterns.length === 0) return - const checkProperties = - it.opts.strict && !it.opts.allowMatchingProperties && parentSchema.properties + const checkProperties = opts.strict && !opts.allowMatchingProperties && parentSchema.properties const valid = gen.name("valid") validatePatternProperties() @@ -51,6 +51,7 @@ const def: CodeKeywordDefinition = { schemaProp: pat, dataProp: key, dataPropType: Type.Str, + strictSchema: it.strictSchema, }, valid ) diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 76926a343e..728b943dd5 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -40,6 +40,7 @@ const def: CodeKeywordDefinition = { keyword: "properties", schemaProp: prop, dataProp: prop, + strictSchema: it.strictSchema, }, valid ) diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 5c65a1d928..275e64f9d8 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -25,7 +25,13 @@ const def: CodeKeywordDefinition = { cxt.setParams({propertyName: key}) applySubschema( it, - {keyword: "propertyNames", data: key, propertyName: key, compositeRule: true}, + { + keyword: "propertyNames", + data: key, + propertyName: key, + compositeRule: true, + strictSchema: it.strictSchema, + }, valid ) gen.ifNot(valid, () => { diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 0565bf4d5b..26176373c3 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -50,6 +50,7 @@ const def: CodeKeywordDefinition = { it, { schema: sch, + strictSchema: true, schemaPath: nil, topSchemaRef: schName, errSchemaPath: schema, diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 5179e4fb64..10dfc3effe 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -87,10 +87,9 @@ export function usePattern(gen: CodeGen, pattern: string): Name { }) } -export function checkStrictMode(it: SchemaCxt, msg: string): void { - const {opts, self} = it - if (opts.strict) { - if (opts.strict === "log") self.logger.warn(msg) - else throw new Error(msg) - } +export function checkStrictMode(it: SchemaCxt, msg: string, mode = it.opts.strict): void { + if (!mode) return + msg = `strict mode: ${msg}` + if (mode === true) throw new Error(msg) + it.self.logger.warn(msg) } diff --git a/lib/vocabularies/validation/required.ts b/lib/vocabularies/validation/required.ts index 702b04fefb..20ada72324 100644 --- a/lib/vocabularies/validation/required.ts +++ b/lib/vocabularies/validation/required.ts @@ -19,8 +19,9 @@ const def: CodeKeywordDefinition = { error, code(cxt: KeywordCxt) { const {gen, schema, schemaCode, data, $data, it} = cxt + const {opts} = it if (!$data && schema.length === 0) return - const useLoop = schema.length >= it.opts.loopRequired + const useLoop = schema.length >= opts.loopRequired if (it.allErrors) allErrorsMode() else exitOnErrorMode() @@ -50,7 +51,7 @@ const def: CodeKeywordDefinition = { function loopAllRequired(): void { gen.forOf("prop", schemaCode, (prop) => { cxt.setParams({missingProperty: prop}) - gen.if(noPropertyInData(data, prop, it.opts.ownProperties), () => cxt.error()) + gen.if(noPropertyInData(data, prop, opts.ownProperties), () => cxt.error()) }) } @@ -60,7 +61,7 @@ const def: CodeKeywordDefinition = { missing, schemaCode, () => { - gen.assign(valid, propertyInData(data, missing, it.opts.ownProperties)) + gen.assign(valid, propertyInData(data, missing, opts.ownProperties)) gen.ifNot(valid, () => { cxt.error() gen.break() diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index 5589ddefc2..61a2c2574d 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -28,6 +28,7 @@ runTest( getAjvInstances(options, { meta: false, strict: false, + strictTypes: false, ignoreKeywordsWithRef: true, }), 6, @@ -37,6 +38,7 @@ runTest( runTest( getAjvInstances(options, { strict: false, + strictTypes: false, ignoreKeywordsWithRef: true, formats: { "idn-email": true, diff --git a/spec/options/strictDefaults.spec.ts b/spec/options/strictDefaults.spec.ts index b0e67c0e98..8e498cdf29 100644 --- a/spec/options/strictDefaults.spec.ts +++ b/spec/options/strictDefaults.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "chai" +const should = chai.should() describe("strict option with defaults (replaced strictDefaults)", () => { describe("useDefaults = true", () => { @@ -98,7 +99,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { properties: {}, } ajv.compile(schema) - should.equal(output.warning, "default is ignored in the schema root") + output.warning.should.match(/default is ignored in the schema root/) }) it('should log a warning given an ignored default in oneOf when strict is "log"', () => { @@ -121,7 +122,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { ], } ajv.compile(schema) - should.equal(output.warning, "default is ignored for: data.foo") + output.warning.should.match(/default is ignored for: data.foo/) }) }) }) @@ -135,6 +136,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { function test(ajv) { const schema = { + type: "object", default: 5, properties: {}, } diff --git a/spec/options/strictKeywords.spec.ts b/spec/options/strictKeywords.spec.ts index 1042a13da5..422927b9c9 100644 --- a/spec/options/strictKeywords.spec.ts +++ b/spec/options/strictKeywords.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "chai" +const should = chai.should() describe("strict option with keywords (replaced strictKeywords)", () => { describe("strict = false", () => { @@ -44,11 +45,12 @@ describe("strict option with keywords (replaced strictKeywords)", () => { logger: getLogger(output), }) const schema = { + type: "object", properties: {}, unknownKeyword: 1, } ajv.compile(schema) - should.equal(output.warning, 'unknown keyword: "unknownKeyword"') + output.warning.should.match(/unknown keyword: "unknownKeyword"/) }) }) diff --git a/spec/options/unknownFormats.spec.ts b/spec/options/unknownFormats.spec.ts index b3945362ba..6a6fc8e8b9 100644 --- a/spec/options/unknownFormats.spec.ts +++ b/spec/options/unknownFormats.spec.ts @@ -15,7 +15,7 @@ describe("specifying allowed unknown formats with `formats` option", () => { }) it("should fail validation if unknown format is used via $data", () => { - test(new _Ajv({$data: true})) + test(new _Ajv({$data: true, strictTypes: false})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) @@ -69,7 +69,7 @@ describe("specifying allowed unknown formats with `formats` option", () => { describe("= [String]", () => { it("should pass schema compilation and be valid if allowed unknown format is used", () => { - test(new _Ajv({formats: {allowed: true}})) + test(new _Ajv({formats: {allowed: true}, strictTypes: false})) function test(ajv) { const validate = ajv.compile({format: "allowed"}) @@ -87,8 +87,9 @@ describe("specifying allowed unknown formats with `formats` option", () => { function test(ajv) { ajv.addFormat("date", DATE_FORMAT) const validate = ajv.compile({ + type: "object", properties: { - foo: {format: {$data: "1/bar"}}, + foo: {type: ["string", "number"], format: {$data: "1/bar"}}, bar: {type: "string"}, }, }) diff --git a/spec/options/useDefaults.spec.ts b/spec/options/useDefaults.spec.ts index e82bd3baee..8a824d7ae0 100644 --- a/spec/options/useDefaults.spec.ts +++ b/spec/options/useDefaults.spec.ts @@ -16,6 +16,7 @@ describe("useDefaults option", () => { function test(ajv) { const schema = { + type: "object", properties: { foo: {type: "string", default: "abc"}, bar: {type: "number", default: 1}, @@ -60,6 +61,7 @@ describe("useDefaults option", () => { function test(ajv) { const schema = { + type: "array", items: [ {type: "string", default: "abc"}, {type: "number", default: 1}, @@ -124,6 +126,7 @@ describe("useDefaults option", () => { function test(ajv) { const schema = { + type: "object", properties: { items: { type: "array", @@ -153,8 +156,10 @@ describe("useDefaults option", () => { beforeEach(() => { schema = { + type: "object", properties: { obj: { + type: "object", properties: { str: {default: "foo"}, n1: {default: 1}, @@ -163,6 +168,7 @@ describe("useDefaults option", () => { }, }, arr: { + type: "array", items: [{default: "foo"}, {default: 1}, {default: 2}, {default: 3}], }, }, diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index 72ffffcb63..8cb12a1b46 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -48,6 +48,7 @@ describe("resolve", () => { type: "null", }, }, + type: "object", properties: { foo: {$ref: "#foo"}, bar: {$ref: "otherschema.json#bar"}, @@ -192,6 +193,7 @@ describe("resolve", () => { try { ajv.compile({ $id: opts.baseId, + type: "object", properties: {a: {$ref: opts.ref}}, }) } catch (e) { @@ -204,14 +206,15 @@ describe("resolve", () => { describe("inline referenced schemas without refs in them", () => { const schemas = [ - {$id: "http://e.com/obj.json#", properties: {a: {$ref: "int.json#"}}}, + {$id: "http://e.com/obj.json#", type: "object", properties: {a: {$ref: "int.json#"}}}, {$id: "http://e.com/int.json#", type: "integer", minimum: 2, maximum: 4}, { $id: "http://e.com/obj1.json#", + type: "object", definitions: {int: {type: "integer", minimum: 2, maximum: 4}}, properties: {a: {$ref: "#/definitions/int"}}, }, - {$id: "http://e.com/list.json#", items: {$ref: "obj.json#"}}, + {$id: "http://e.com/list.json#", type: "array", items: {$ref: "obj.json#"}}, ] it("by default should inline schema if it doesn't contain refs", () => { @@ -230,7 +233,7 @@ describe("resolve", () => { }) it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { - const ajv = new Ajv({schemas: schemas, inlineRefs: 2, code: {source: true}}) + const ajv = new Ajv({schemas, inlineRefs: 2, code: {source: true}}) testSchemas(ajv, false) }) diff --git a/spec/security.spec.ts b/spec/security.spec.ts index dfadb10fbd..f4fd070888 100644 --- a/spec/security.spec.ts +++ b/spec/security.spec.ts @@ -5,6 +5,7 @@ import {afterError, afterEach} from "./after_test" const instances = getAjvInstances(options, { schemas: [require("../dist/refs/json-schema-secure.json")], + strictTypes: false, }) jsonSchemaTest(instances, { diff --git a/spec/types/async-validate.spec.ts b/spec/types/async-validate.spec.ts index 9acfbc396e..c092bc35aa 100644 --- a/spec/types/async-validate.spec.ts +++ b/spec/types/async-validate.spec.ts @@ -8,7 +8,7 @@ interface Foo { } describe("$async validation and type guards", () => { - const ajv = new _Ajv() + const ajv = new _Ajv({strictTypes: false}) describe("$async: undefined", () => { it("should have result type boolean 1", () => { diff --git a/spec/types/error-parameters.spec.ts b/spec/types/error-parameters.spec.ts index e985c2c641..b8c3603a6c 100644 --- a/spec/types/error-parameters.spec.ts +++ b/spec/types/error-parameters.spec.ts @@ -7,7 +7,7 @@ describe("error object parameters type", () => { const ajv = new _Ajv({allErrors: true}) it("should be determined by the keyword", () => { - const validate = ajv.compile({minimum: 0, multipleOf: 2}) + const validate = ajv.compile({type: "number", minimum: 0, multipleOf: 2}) const valid = validate(-1) valid.should.equal(false) const errs = validate.errors From 88a1d47d2fd09526672e9ebc7133ffea1c8af46a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 20 Sep 2020 14:35:06 +0100 Subject: [PATCH 249/322] refactor: AddedKeywordDefinition with type and schemaType always arrays --- lib/ajv.ts | 48 +++++++++++++------ lib/compile/context.ts | 37 +++++++------- lib/compile/index.ts | 7 +-- lib/compile/resolve.ts | 7 +-- lib/compile/rules.ts | 29 ++++++----- lib/compile/util.ts | 12 ++--- lib/compile/validate/applicability.ts | 6 +-- lib/compile/validate/dataType.ts | 29 ++++++----- lib/compile/validate/index.ts | 2 +- lib/compile/validate/iterate.ts | 25 ++++------ lib/compile/validate/keyword.ts | 7 +-- lib/types/index.ts | 17 +++++-- .../applicator/additionalProperties.ts | 14 ++++-- lib/vocabularies/validation/uniqueItems.ts | 2 +- spec/json-schema.spec.ts | 3 +- spec/keyword.spec.ts | 30 +++++++----- 16 files changed, 159 insertions(+), 116 deletions(-) diff --git a/lib/ajv.ts b/lib/ajv.ts index 057a253cf6..915171b60e 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -34,6 +34,7 @@ import type { AsyncSchema, Vocabulary, KeywordDefinition, + AddedKeywordDefinition, AnyValidateFunction, ValidateFunction, AsyncValidateFunction, @@ -43,11 +44,11 @@ import type { } from "./types" import type {JSONSchemaType} from "./types/json-schema" import {ValidationError, MissingRefError} from "./compile/error_classes" -import {getRules, ValidationRules, Rule, RuleGroup} from "./compile/rules" -import {checkType} from "./compile/validate/dataType" +import {getRules, ValidationRules, Rule, RuleGroup, JSONType} from "./compile/rules" import {SchemaEnv, compileSchema, resolveSchema} from "./compile" import {Code, ValueScope} from "./compile/codegen" import {normalizeId, getSchemaRefs} from "./compile/resolve" +import {getJSONTypes} from "./compile/validate/dataType" import coreVocabulary from "./vocabularies/core" import validationVocabulary from "./vocabularies/validation" import applicatorVocabulary from "./vocabularies/applicator" @@ -492,7 +493,6 @@ export default class Ajv { return this } - addKeyword(kwdOrDef: string | KeywordDefinition): Ajv addKeyword( kwdOrDef: string | KeywordDefinition, def?: KeywordDefinition // deprecated @@ -507,20 +507,34 @@ export default class Ajv { } else if (typeof kwdOrDef == "object" && def === undefined) { def = kwdOrDef keyword = def.keyword + if (Array.isArray(keyword) && !keyword.length) { + throw new Error("addKeywords: keyword must be non-empty array") + } } else { throw new Error("invalid addKeywords parameters") } checkKeyword.call(this, keyword, def) - if (def) keywordMetaschema.call(this, def) - - eachItem(keyword, (kwd) => { - eachItem(def?.type, (t) => addRule.call(this, kwd, t, def)) - }) + if (!def) { + eachItem(keyword, (kwd) => addRule.call(this, kwd)) + return this + } + keywordMetaschema.call(this, def) + const definition: AddedKeywordDefinition = { + ...def, + type: getJSONTypes(def.type), + schemaType: getJSONTypes(def.schemaType), + } + eachItem( + keyword, + definition.type.length === 0 + ? (k) => addRule.call(this, k, definition) + : (k) => definition.type.forEach((t) => addRule.call(this, k, definition, t)) + ) return this } - getKeyword(keyword: string): KeywordDefinition | boolean { + getKeyword(keyword: string): AddedKeywordDefinition | boolean { const rule = this.RULES.all[keyword] return typeof rule == "object" ? rule.definition : !!rule } @@ -557,8 +571,8 @@ export default class Ajv { $dataMetaSchema(metaSchema: AnySchemaObject, keywordsJsonPointers: string[]): AnySchemaObject { const rules = this.RULES.all + metaSchema = JSON.parse(JSON.stringify(metaSchema)) for (const jsonPointer of keywordsJsonPointers) { - metaSchema = JSON.parse(JSON.stringify(metaSchema)) const segments = jsonPointer.split("/").slice(1) // first segment is an empty string let keywords = metaSchema for (const seg of segments) keywords = keywords[seg] as AnySchemaObject @@ -738,7 +752,6 @@ function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefini if (!KEYWORD_NAME.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`) }) if (!def) return - if (def.type) eachItem(def.type, (t) => checkType(t, RULES)) if (def.$data && !("code" in def || "validate" in def)) { throw new Error('$data keyword must have "code" or "validate" function') } @@ -747,8 +760,8 @@ function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefini function addRule( this: Ajv, keyword: string, - dataType?: string, - definition?: KeywordDefinition + definition?: AddedKeywordDefinition, + dataType?: JSONType ): void { const {RULES} = this let ruleGroup = RULES.rules.find(({type: t}) => t === dataType) @@ -759,7 +772,14 @@ function addRule( RULES.keywords[keyword] = true if (!definition) return - const rule: Rule = {keyword, definition} + const rule: Rule = { + keyword, + definition: { + ...definition, + type: getJSONTypes(definition.type), + schemaType: getJSONTypes(definition.schemaType), + }, + } if (definition.before) addBeforeRule.call(this, ruleGroup, rule, definition.before) else ruleGroup.rules.push(rule) RULES.all[keyword] = rule diff --git a/lib/compile/context.ts b/lib/compile/context.ts index bdc0ab2a6b..7a4d170a18 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -1,10 +1,11 @@ import type { - KeywordDefinition, + AddedKeywordDefinition, KeywordErrorCxt, KeywordCxtParams, SchemaObjCxt, AnySchemaObject, } from "../types" +import {JSONType} from "./rules" import {schemaRefOrVal} from "../vocabularies/util" import {getData, checkDataTypes, DataType} from "./util" import { @@ -26,14 +27,14 @@ export default class KeywordCxt implements KeywordErrorCxt { readonly schema: any readonly schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value readonly schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) - readonly schemaType?: string | string[] + readonly schemaType: JSONType[] readonly parentSchema: AnySchemaObject readonly errsCount?: Name params: KeywordCxtParams readonly it: SchemaObjCxt - readonly def: KeywordDefinition + readonly def: AddedKeywordDefinition - constructor(it: SchemaObjCxt, def: KeywordDefinition, keyword: string) { + constructor(it: SchemaObjCxt, def: AddedKeywordDefinition, keyword: string) { validateKeywordUsage(it, def, keyword) this.gen = it.gen this.allErrors = it.allErrors @@ -52,7 +53,7 @@ export default class KeywordCxt implements KeywordErrorCxt { this.schemaCode = it.gen.const("vSchema", getData(this.$data, it)) } else { this.schemaCode = this.schemaValue - if (def.schemaType && !validSchemaType(this.schema, def.schemaType)) { + if (!validSchemaType(this.schema, def.schemaType, def.allowUndefined)) { throw new Error(`${keyword} value must be ${JSON.stringify(def.schemaType)}`) } } @@ -132,7 +133,7 @@ export default class KeywordCxt implements KeywordErrorCxt { const {gen, schemaCode, schemaType, def} = this gen.if(or(_`${schemaCode} === undefined`, $dataValid)) if (valid !== nil) gen.assign(valid, true) - if (schemaType || def.validateSchema) { + if (schemaType.length || def.validateSchema) { gen.elseIf(this.invalid$data()) this.$dataError() if (valid !== nil) gen.assign(valid, false) @@ -145,7 +146,7 @@ export default class KeywordCxt implements KeywordErrorCxt { return or(wrong$DataType(), invalid$DataSchema()) function wrong$DataType(): Code { - if (schemaType) { + if (schemaType.length) { if (!(schemaCode instanceof Name)) throw new Error("ajv implementation error") const st = Array.isArray(schemaType) ? schemaType : [schemaType] return _`(${checkDataTypes(st, schemaCode, it.opts.strict, DataType.Wrong)})` @@ -163,21 +164,23 @@ export default class KeywordCxt implements KeywordErrorCxt { } } -function validSchemaType(schema: unknown, schemaType: string | string[]): boolean { +function validSchemaType(schema: unknown, schemaType: JSONType[], allowUndefined = false): boolean { // TODO add tests - if (Array.isArray(schemaType)) { - return schemaType.some((st) => validSchemaType(schema, st)) - } - return schemaType === "array" - ? Array.isArray(schema) - : schemaType === "object" - ? schema && typeof schema == "object" && !Array.isArray(schema) - : typeof schema == schemaType + return ( + !schemaType.length || + schemaType.some((st) => + st === "array" + ? Array.isArray(schema) + : st === "object" + ? schema && typeof schema == "object" && !Array.isArray(schema) + : typeof schema == st || (allowUndefined && typeof schema == "undefined") + ) + ) } function validateKeywordUsage( {schema, opts, self}: SchemaObjCxt, - def: KeywordDefinition, + def: AddedKeywordDefinition, keyword: string ): void { if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { diff --git a/lib/compile/index.ts b/lib/compile/index.ts index e894c00346..d0839a6ebe 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -10,7 +10,7 @@ import {CodeGen, _, nil, Name} from "./codegen" import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" -import {toHash, schemaHasRulesButRef, unescapeFragment} from "./util" +import {schemaHasRulesButRef, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" import URI = require("uri-js") @@ -123,6 +123,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { delete sch.validate delete sch.validateName if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode) + // console.log("\n\n\n *** \n", sourceCode) throw e } finally { this._compilations.delete(sch) @@ -207,7 +208,7 @@ export function resolveSchema( return getJsonPointer.call(this, p, schOrRef) } -const PREVENT_SCOPE_CHANGE = toHash([ +const PREVENT_SCOPE_CHANGE = new Set([ "properties", "patternProperties", "enum", @@ -226,7 +227,7 @@ function getJsonPointer( schema = schema[unescapeFragment(part)] if (schema === undefined) return // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def? - if (!PREVENT_SCOPE_CHANGE[part] && typeof schema == "object" && schema.$id) { + if (!PREVENT_SCOPE_CHANGE.has(part) && typeof schema == "object" && schema.$id) { baseId = resolveUrl(baseId, schema.$id) } } diff --git a/lib/compile/resolve.ts b/lib/compile/resolve.ts index 72ecf3e25a..484a0ad3f4 100644 --- a/lib/compile/resolve.ts +++ b/lib/compile/resolve.ts @@ -1,6 +1,6 @@ import type {AnySchema, AnySchemaObject} from "../types" import type Ajv from "../ajv" -import {eachItem, toHash} from "./util" +import {eachItem} from "./util" import equal from "fast-deep-equal" import traverse from "json-schema-traverse" import URI = require("uri-js") @@ -11,7 +11,7 @@ export interface LocalRefs { } // TODO refactor to use keyword definitions -const SIMPLE_INLINED = toHash([ +const SIMPLE_INLINED = new Set([ "type", "format", "pattern", @@ -29,6 +29,7 @@ const SIMPLE_INLINED = toHash([ "enum", "const", ]) + export function inlineRef(schema: AnySchema, limit: boolean | number = true): boolean { if (typeof schema == "boolean") return true if (limit === true) return !hasRef(schema) @@ -51,7 +52,7 @@ function countKeys(schema: AnySchemaObject): number { for (const key in schema) { if (key === "$ref") return Infinity count++ - if (SIMPLE_INLINED[key]) continue + if (SIMPLE_INLINED.has(key)) continue if (typeof schema[key] == "object") { eachItem(schema[key], (sch) => (count += countKeys(sch))) } diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 53ba254ab9..e5283d656e 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -1,8 +1,17 @@ -import type {KeywordDefinition} from "../types" -import {toHash} from "./util" +import type {AddedKeywordDefinition} from "../types" -interface ValidationTypes { - [key: string]: boolean | RuleGroup | undefined +const _jsonTypes = ["string", "number", "integer", "boolean", "null", "object", "array"] as const + +export type JSONType = typeof _jsonTypes[number] + +const jsonTypes: Set = new Set(_jsonTypes) + +export function isJSONType(x: unknown): x is JSONType { + return typeof x == "string" && jsonTypes.has(x) +} + +type ValidationTypes = { + [K in JSONType]: boolean | RuleGroup | undefined } export interface ValidationRules { @@ -13,20 +22,18 @@ export interface ValidationRules { } export interface RuleGroup { - type?: string + type?: JSONType rules: Rule[] } // This interface wraps KeywordDefinition because definition can have multiple keywords export interface Rule { keyword: string - definition: KeywordDefinition + definition: AddedKeywordDefinition } -const ALL = ["type", "$comment"] - export function getRules(): ValidationRules { - const groups = { + const groups: Record<"number" | "string" | "array" | "object", RuleGroup> = { number: {type: "number", rules: []}, string: {type: "string", rules: []}, array: {type: "array", rules: []}, @@ -35,7 +42,7 @@ export function getRules(): ValidationRules { return { types: {...groups, integer: true, boolean: true, null: true}, rules: [groups.number, groups.string, groups.array, groups.object, {rules: []}], - all: toHash(ALL), - keywords: toHash(ALL), + all: {type: true, $comment: true}, + keywords: {type: true, $comment: true}, } } diff --git a/lib/compile/util.ts b/lib/compile/util.ts index 606a51bd60..a3999dff62 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,6 +1,6 @@ import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" import type {SchemaCxt, AnySchema} from "../types" -import type {Rule, ValidationRules} from "./rules" +import type {JSONType, Rule, ValidationRules} from "./rules" import N from "./names" export enum DataType { @@ -9,7 +9,7 @@ export enum DataType { } export function checkDataType( - dataType: string, + dataType: JSONType, data: Name, strictNums?: boolean | "log", correct = DataType.Correct @@ -42,7 +42,7 @@ export function checkDataType( } export function checkDataTypes( - dataTypes: string[], + dataTypes: JSONType[], data: Name, strictNums?: boolean | "log", correct?: DataType @@ -62,13 +62,13 @@ export function checkDataTypes( cond = nil } if (types.number) delete types.integer - for (const t in types) cond = and(cond, checkDataType(t, data, strictNums, correct)) + for (const t in types) cond = and(cond, checkDataType(t as JSONType, data, strictNums, correct)) return cond } // TODO refactor to use Set -export function toHash(arr: string[]): {[key: string]: true | undefined} { - const hash: {[key: string]: true} = {} +export function toHash(arr: T[]): {[K in T]?: true} { + const hash: {[K in T]?: true} = {} for (const item of arr) hash[item] = true return hash } diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index 839b74e077..e12f1988bd 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,11 +1,11 @@ import type {SchemaObjCxt, AnySchemaObject} from "../../types" -import type {RuleGroup, Rule} from "../rules" +import type {JSONType, RuleGroup, Rule} from "../rules" export function schemaHasRulesForType( {schema, self}: SchemaObjCxt, - ty: string + type: JSONType ): boolean | undefined { - const group = self.RULES.types[ty] + const group = self.RULES.types[type] return group && group !== true && shouldUseGroup(schema, group) } diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index d9642069fc..5a3bdb5791 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -5,17 +5,15 @@ import type { ErrorObject, AnySchemaObject, } from "../../types" -import type {ValidationRules} from "../rules" +import {isJSONType, JSONType} from "../rules" import {schemaHasRulesForType} from "./applicability" -import {toHash, checkDataTypes, DataType} from "../util" +import {checkDataTypes, DataType} from "../util" import {schemaRefOrVal} from "../../vocabularies/util" import {reportError} from "../errors" import {_, str, Name} from "../codegen" -export function getSchemaTypes({self}: SchemaObjCxt, schema: AnySchemaObject): string[] { - const st: undefined | string | string[] = schema.type - const types: string[] = Array.isArray(st) ? st : st ? [st] : [] - types.forEach((t) => checkType(t, self.RULES)) +export function getSchemaTypes(schema: AnySchemaObject): JSONType[] { + const types = getJSONTypes(schema.type) const hasNull = types.includes("null") if (hasNull && schema.nullable === false) { throw new Error('{"type": "null"} contradicts {"nullable": "false"}') @@ -25,12 +23,13 @@ export function getSchemaTypes({self}: SchemaObjCxt, schema: AnySchemaObject): s return types } -export function checkType(t: string, RULES: ValidationRules): void { - if (typeof t == "string" && t in RULES.types) return - throw new Error('"type" keyword must be allowed string or string[]: ' + t) +export function getJSONTypes(ts: unknown | unknown[]): JSONType[] { + const types: unknown[] = Array.isArray(ts) ? ts : ts ? [ts] : [] + if (types.every(isJSONType)) return types + throw new Error("type must be JSONType or JSONType[]: " + types.join(",")) } -export function coerceAndCheckDataType(it: SchemaObjCxt, types: string[]): boolean { +export function coerceAndCheckDataType(it: SchemaObjCxt, types: JSONType[]): boolean { const {gen, data, opts} = it const coerceTo = coerceToTypes(types, opts.coerceTypes) const checkTypes = @@ -46,14 +45,14 @@ export function coerceAndCheckDataType(it: SchemaObjCxt, types: string[]): boole return checkTypes } -const COERCIBLE = toHash(["string", "number", "integer", "boolean", "null"]) -function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string[] { +const COERCIBLE: Set = new Set(["string", "number", "integer", "boolean", "null"]) +function coerceToTypes(types: JSONType[], coerceTypes?: boolean | "array"): JSONType[] { return coerceTypes - ? types.filter((t) => COERCIBLE[t] || (coerceTypes === "array" && t === "array")) + ? types.filter((t) => COERCIBLE.has(t) || (coerceTypes === "array" && t === "array")) : [] } -function coerceData(it: SchemaObjCxt, types: string[], coerceTo: string[]): void { +function coerceData(it: SchemaObjCxt, types: JSONType[], coerceTo: JSONType[]): void { const {gen, data, opts} = it const dataType = gen.let("dataType", _`typeof ${data}`) const coerced = gen.let("coerced", _`undefined`) @@ -67,7 +66,7 @@ function coerceData(it: SchemaObjCxt, types: string[], coerceTo: string[]): void } gen.if(_`${coerced} !== undefined`) for (const t of coerceTo) { - if (t in COERCIBLE || (t === "array" && opts.coerceTypes === "array")) { + if (COERCIBLE.has(t) || (t === "array" && opts.coerceTypes === "array")) { coerceSpecificType(t) } } diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 54390b3bc4..f15b08d7f1 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -112,7 +112,7 @@ function checkKeywords(it: SchemaObjCxt): void { } function typeAndKeywords(it: SchemaObjCxt, errsCount?: Name): void { - const types = getSchemaTypes(it, it.schema) + const types = getSchemaTypes(it.schema) const checkedTypes = coerceAndCheckDataType(it, types) schemaKeywords(it, types, !checkedTypes, errsCount) } diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index bbb2a29bc8..8e981d1212 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -1,5 +1,5 @@ import type {SchemaObjCxt} from "../../types" -import type {Rule, RuleGroup} from "../rules" +import type {JSONType, Rule, RuleGroup} from "../rules" import {shouldUseGroup, shouldUseRule} from "./applicability" import {checkDataType, schemaHasRulesButRef} from "../util" import {checkStrictMode} from "../../vocabularies/util" @@ -11,7 +11,7 @@ import N from "../names" export function schemaKeywords( it: SchemaObjCxt, - types: string[], + types: JSONType[], typeErrors: boolean, errsCount?: Name ): void { @@ -63,13 +63,13 @@ function iterateKeywords(it: SchemaObjCxt, group: RuleGroup): void { }) } -function checkStrictTypes(it: SchemaObjCxt, types: string[]): void { +function checkStrictTypes(it: SchemaObjCxt, types: JSONType[]): void { if (!it.strictSchema || it.schemaEnv.meta || !it.opts.strictTypes) return checkMultipleTypes(it, types, it.opts.strictTypes) checkApplicableTypes(it, types, it.opts.strictTypes) } -function checkMultipleTypes(it: SchemaObjCxt, ts: string[], mode: boolean | "log"): void { +function checkMultipleTypes(it: SchemaObjCxt, ts: JSONType[], mode: boolean | "log"): void { if ( ts.length > 1 && !(ts.length === 2 && ts.includes("null")) && @@ -79,28 +79,21 @@ function checkMultipleTypes(it: SchemaObjCxt, ts: string[], mode: boolean | "log } } -function checkApplicableTypes(it: SchemaObjCxt, ts: string[], mode: boolean | "log"): void { +function checkApplicableTypes(it: SchemaObjCxt, ts: JSONType[], mode: boolean | "log"): void { const rules = it.self.RULES.all for (const keyword in rules) { const rule = rules[keyword] if (typeof rule == "object" && shouldUseRule(it.schema, rule)) { const {type} = rule.definition - if (Array.isArray(type)) { - if (!type.some((t) => hasApplicableType(ts, t))) { - strictTypesError(it, "missing appllicable type", mode) - return - } - } else if (type) { - if (!hasApplicableType(ts, type)) { - strictTypesError(it, "missing appllicable type", mode) - return - } + if (type.length && !type.some((t) => hasApplicableType(ts, t))) { + strictTypesError(it, "missing appllicable type", mode) + return } } } } -function hasApplicableType(schemaTypes: string[], keywordType: string): boolean { +function hasApplicableType(schemaTypes: JSONType[], keywordType: JSONType): boolean { return ( schemaTypes.includes(keywordType) || (keywordType === "number" && schemaTypes.includes("integer")) diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index c4c916fb34..c4889cd7f8 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -1,5 +1,5 @@ import type { - KeywordDefinition, + AddedKeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, AnySchema, @@ -8,6 +8,7 @@ import type { AnyValidateFunction, } from "../../types" import KeywordCxt from "../context" +import {JSONType} from "../rules" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" import {callValidateCode} from "../../vocabularies/util" @@ -19,8 +20,8 @@ type KeywordCompilationResult = AnySchema | SchemaValidateFunction | AnyValidate export function keywordCode( it: SchemaObjCxt, keyword: string, - def: KeywordDefinition, - ruleType?: string + def: AddedKeywordDefinition, + ruleType?: JSONType ): void { const cxt = new KeywordCxt(it, def, keyword) if ("code" in def) { diff --git a/lib/types/index.ts b/lib/types/index.ts index f5228b882a..20570a19bd 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1,5 +1,6 @@ import type {CodeGen, Code, Name, Scope} from "../compile/codegen" import type {SchemaEnv} from "../compile" +import type {JSONType} from "../compile/rules" import type KeywordCxt from "../compile/context" import type Ajv from "../ajv" import type {InstanceOptions} from "../ajv" @@ -46,8 +47,8 @@ interface DataValidationCxt { export interface ValidateFunction { (this: Ajv | any, data: any, dataCxt?: DataValidationCxt): data is T errors?: null | ErrorObject[] - schema?: AnySchema - schemaEnv?: SchemaEnv + schema: AnySchema + schemaEnv: SchemaEnv source?: SourceCode } @@ -106,8 +107,9 @@ export interface SchemaObjCxt extends SchemaCxt { interface _KeywordDef { keyword: string | string[] - type?: string | string[] - schemaType?: string | string[] + type?: JSONType | JSONType[] + schemaType?: JSONType | JSONType[] + allowUndefined?: boolean $data?: boolean implements?: string[] before?: string @@ -167,6 +169,11 @@ export type KeywordDefinition = | FuncKeywordDefinition | MacroKeywordDefinition +export type AddedKeywordDefinition = KeywordDefinition & { + type: JSONType[] + schemaType: JSONType[] +} + export interface KeywordErrorDefinition { message: string | ((cxt: KeywordErrorCxt) => Code) params?: (cxt: KeywordErrorCxt) => Code @@ -183,7 +190,7 @@ export interface KeywordErrorCxt { parentSchema?: AnySchemaObject schemaCode: Code | number | boolean schemaValue: Code | number | boolean - schemaType?: string | string[] + schemaType?: JSONType | JSONType[] errsCount?: Name params: KeywordCxtParams it: SchemaCxt diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index fdb5658da8..cc42343a94 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -1,4 +1,9 @@ -import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" +import type { + CodeKeywordDefinition, + AddedKeywordDefinition, + ErrorObject, + KeywordErrorDefinition, +} from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" import {applySubschema, SubschemaCxt, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" @@ -14,10 +19,11 @@ const error: KeywordErrorDefinition = { params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`, } -const def: CodeKeywordDefinition = { +const def: CodeKeywordDefinition & AddedKeywordDefinition = { keyword: "additionalProperties", - type: "object", - schemaType: ["boolean", "object", "undefined"], // "undefined" is needed to support option removeAdditional: "all" + type: ["object"], + schemaType: ["boolean", "object"], + allowUndefined: true, trackErrors: true, error, code(cxt) { diff --git a/lib/vocabularies/validation/uniqueItems.ts b/lib/vocabularies/validation/uniqueItems.ts index 9c0533dc6b..94967bcb48 100644 --- a/lib/vocabularies/validation/uniqueItems.ts +++ b/lib/vocabularies/validation/uniqueItems.ts @@ -23,7 +23,7 @@ const def: CodeKeywordDefinition = { const {gen, data, $data, schema, parentSchema, schemaCode, it} = cxt if (!$data && !schema) return const valid = gen.let("valid") - const itemTypes = parentSchema.items ? getSchemaTypes(it, parentSchema.items) : [] + const itemTypes = parentSchema.items ? getSchemaTypes(parentSchema.items) : [] cxt.block$data(valid, validateUniqueItems, _`${schemaCode} === false`) cxt.ok(valid) diff --git a/spec/json-schema.spec.ts b/spec/json-schema.spec.ts index 61a2c2574d..f3f02945c0 100644 --- a/spec/json-schema.spec.ts +++ b/spec/json-schema.spec.ts @@ -3,6 +3,7 @@ import jsonSchemaTest from "json-schema-test" import options from "./ajv_options" import {afterError, afterEach} from "./after_test" import addFormats from "ajv-formats" +import draft6MetaSchema from "../dist/refs/json-schema-draft-06.json" const remoteRefs = { "http://localhost:1234/integer.json": require("./JSON-Schema-Test-Suite/remotes/integer.json"), @@ -54,7 +55,7 @@ runTest( function runTest(instances, draft: number, tests) { for (const ajv of instances) { if (draft === 6) { - ajv.addMetaSchema(require("../dist/refs/json-schema-draft-06.json")) + ajv.addMetaSchema(draft6MetaSchema) ajv.opts.defaultMeta = "http://json-schema.org/draft-06/schema#" } for (const id in remoteRefs) ajv.addSchema(remoteRefs[id], id) diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index ea4bd63b86..14dc4250f1 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -2,13 +2,14 @@ import type {ErrorObject, SchemaObject, SchemaValidateFunction} from "../lib/typ // currently most tests include compiled code, if any code re-compiled locally, instanceof would fail import {_, nil} from "../dist/compile/codegen" import getAjvInstances from "./ajv_instances" -import Ajv from "./ajv" +import _Ajv from "./ajv" +import type Ajv from ".." const should = require("./chai").should(), equal = require("../dist/compile/equal") describe("User-defined keywords", () => { - let ajv, instances + let ajv: Ajv, instances: Ajv[] beforeEach(() => { instances = getAjvInstances({ @@ -257,12 +258,12 @@ describe("User-defined keywords", () => { instances.forEach((_ajv) => { _ajv.addKeyword({ keyword: "macroRef", - macro: function (schema, _parentSchema, it) { + macro(schema, _parentSchema, it) { it.baseId.should.equal("#") const ref = schema.$ref const validate = _ajv.getSchema(ref) - if (validate) return validate.schema - throw new ajv.constructor.MissingRefError(it.baseId, ref) + if (!validate) throw new _Ajv.MissingRefError(it.baseId, ref) + return validate.schema }, metaSchema: { type: "object", @@ -895,11 +896,11 @@ describe("User-defined keywords", () => { shouldBeInvalid(validate, 1.99, numErrors) if (createsErrors) { - shouldBeRangeError(validate.errors[0], "", "#/x-range", ">=", 2) + shouldBeRangeError(validate.errors?.[0], "", "#/x-range", ">=", 2) } shouldBeInvalid(validate, 4.01, numErrors) if (createsErrors) { - shouldBeRangeError(validate.errors[0], "", "#/x-range", "<=", 4) + shouldBeRangeError(validate.errors?.[0], "", "#/x-range", "<=", 4) } schema = { @@ -918,11 +919,11 @@ describe("User-defined keywords", () => { shouldBeInvalid(validate, {foo: 2}, numErrors) if (createsErrors) { - shouldBeRangeError(validate.errors[0], "/foo", "#/properties/foo/x-range", ">", 2, true) + shouldBeRangeError(validate.errors?.[0], "/foo", "#/properties/foo/x-range", ">", 2, true) } shouldBeInvalid(validate, {foo: 4}, numErrors) if (createsErrors) { - shouldBeRangeError(validate.errors[0], "/foo", "#/properties/foo/x-range", "<", 4, true) + shouldBeRangeError(validate.errors?.[0], "/foo", "#/properties/foo/x-range", "<", 4, true) } }) } @@ -1078,7 +1079,7 @@ describe("User-defined keywords", () => { ajv.addKeyword({ keyword, type: dataType, - validate: () => {}, + validate: () => true, }) } }) @@ -1100,13 +1101,16 @@ describe("User-defined keywords", () => { } ajv.addKeyword(definition) - ajv.getKeyword("mykeyword").should.equal(definition) + const def = ajv.getKeyword("mykeyword") + def.should.be.an("object") + // TODO type cast + ;(def as {keyword: string[]}).keyword.should.equal("mykeyword") }) }) describe("removeKeyword", () => { it("should remove and allow redefining keyword", () => { - ajv = new Ajv({strict: false}) + ajv = new _Ajv({strict: false}) ajv.addKeyword({ keyword: "positive", @@ -1151,7 +1155,7 @@ describe("User-defined keywords", () => { }) it("should remove and allow redefining standard keyword", () => { - ajv = new Ajv({strict: false}) + ajv = new _Ajv({strict: false}) const schema = {minimum: 1} let validate = ajv.compile(schema) From 419a79689d281f547b43e8626e6d63b77bc2c08c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 20 Sep 2020 20:14:13 +0100 Subject: [PATCH 250/322] test: fixing tests failing because of strictTypes [WIP] --- lib/ajv.ts | 3 + lib/compile/context.ts | 2 +- lib/compile/errors.ts | 3 +- lib/compile/index.ts | 44 +++++++++-- lib/compile/subschema.ts | 27 +++++-- lib/compile/util.ts | 5 +- lib/compile/validate/applicability.ts | 3 +- lib/compile/validate/boolSchema.ts | 3 +- lib/compile/validate/dataType.ts | 3 +- lib/compile/validate/defaults.ts | 2 +- lib/compile/validate/index.ts | 3 +- lib/compile/validate/iterate.ts | 50 ++++++++----- lib/compile/validate/keyword.ts | 4 +- lib/types/index.ts | 36 +-------- .../applicator/additionalProperties.ts | 4 +- lib/vocabularies/applicator/if.ts | 8 +- lib/vocabularies/applicator/propertyNames.ts | 1 + lib/vocabularies/core/ref.ts | 1 + lib/vocabularies/util.ts | 3 +- spec/after_test.ts | 3 +- spec/ajv.spec.ts | 3 +- spec/async.spec.ts | 17 ++++- spec/async_validate.spec.ts | 4 +- spec/errors.spec.ts | 3 +- .../240_mutual_recur_frags_common_ref.spec.ts | 16 ++++ .../342_uniqueItems_non-json_objects.spec.ts | 2 +- .../485_type_validation_priority.spec.ts | 2 +- spec/issues/50_refs_with_definitions.spec.ts | 3 +- ...3_removeAdditional_to_remove_proto.spec.ts | 2 + .../768_passContext_recursive_ref.spec.ts | 1 + spec/keyword.spec.ts | 73 +++++++++++++------ spec/options/comment.spec.ts | 2 + spec/options/meta_validateSchema.spec.ts | 3 +- spec/options/nullable.spec.ts | 3 +- spec/options/options_add_schemas.spec.ts | 16 +++- spec/options/options_code.spec.ts | 3 +- spec/options/options_refs.spec.ts | 7 +- spec/options/options_reporting.spec.ts | 13 +++- spec/options/options_validation.spec.ts | 27 ++++--- spec/options/ownProperties.spec.ts | 22 ++++-- spec/options/removeAdditional.spec.ts | 6 ++ spec/options/schemaId.spec.ts | 3 +- spec/options/strict.spec.ts | 5 +- spec/options/strictDefaults.spec.ts | 5 +- spec/options/strictKeywords.spec.ts | 2 +- spec/options/strictTypes.spec.ts | 28 +++++++ spec/options/unknownFormats.spec.ts | 4 +- spec/options/useDefaults.spec.ts | 1 + spec/resolve.spec.ts | 5 +- spec/security/array.json | 20 ++--- spec/security/object.json | 4 +- spec/security/string.json | 8 +- spec/types/async-validate.spec.ts | 39 ++++++++-- 53 files changed, 388 insertions(+), 172 deletions(-) create mode 100644 spec/options/strictTypes.spec.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 915171b60e..264d8ba432 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -23,6 +23,7 @@ export interface Plugin { import KeywordCxt from "./compile/context" export {KeywordCxt} export {DefinedError} from "./vocabularies/errors" +export {JSONType} from "./compile/rules" export {JSONSchemaType} from "./types/json-schema" export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" @@ -178,6 +179,7 @@ const deprecatedOptions: OptionsInfo = { type RequiredInstanceOptions = { [K in | "strict" + | "strictTypes" | "code" | "inlineRefs" | "loopRequired" @@ -195,6 +197,7 @@ export type InstanceOptions = Options & RequiredInstanceOptions function requiredOptions(o: Options): RequiredInstanceOptions { return { strict: o.strict ?? true, + strictTypes: o.strictTypes ?? (o.strict ?? true ? "log" : false), code: o.code ?? {}, loopRequired: o.loopRequired ?? Infinity, loopEnum: o.loopEnum ?? Infinity, diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 7a4d170a18..7d9a0ea4b7 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -2,9 +2,9 @@ import type { AddedKeywordDefinition, KeywordErrorCxt, KeywordCxtParams, - SchemaObjCxt, AnySchemaObject, } from "../types" +import {SchemaObjCxt} from "./index" import {JSONType} from "./rules" import {schemaRefOrVal} from "../vocabularies/util" import {getData, checkDataTypes, DataType} from "./util" diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 2a6ca4e6fa..fb6c317eb3 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,4 +1,5 @@ -import type {KeywordErrorCxt, KeywordErrorDefinition, SchemaCxt} from "../types" +import type {KeywordErrorCxt, KeywordErrorDefinition} from "../types" +import type {SchemaCxt} from "./index" import {CodeGen, _, str, strConcat, Code, Name} from "./codegen" import {SafeExpr} from "./codegen/code" import N from "./names" diff --git a/lib/compile/index.ts b/lib/compile/index.ts index d0839a6ebe..00a88dd5cf 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -1,23 +1,50 @@ -import type { - AnySchema, - AnySchemaObject, - AnyValidateFunction, - AsyncValidateFunction, - SchemaCxt, -} from "../types" +import type {AnySchema, AnySchemaObject, AnyValidateFunction, AsyncValidateFunction} from "../types" import type Ajv from "../ajv" -import {CodeGen, _, nil, Name} from "./codegen" +import type {InstanceOptions} from "../ajv" +import {CodeGen, _, nil, Name, Code} from "./codegen" import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" import {schemaHasRulesButRef, unescapeFragment} from "./util" import {validateFunctionCode} from "./validate" import URI = require("uri-js") +import {JSONType} from "./rules" export interface SchemaRefs { [ref: string]: SchemaEnv | AnySchema | undefined } +export interface SchemaCxt { + gen: CodeGen + allErrors?: boolean + data: Name + parentData: Name + parentDataProperty: Code | number + dataNames: Name[] + dataPathArr: (Code | number)[] + dataLevel: number + dataTypes: JSONType[] + topSchemaRef: Code + validateName: Name + ValidationError?: Name + schema: AnySchema + schemaEnv: SchemaEnv + strictSchema?: boolean + rootId: string // TODO ? + baseId: string + schemaPath: Code + errSchemaPath: string // this is actual string, should not be changed to Code + errorPath: Code + propertyName?: Name + compositeRule?: boolean + createErrors?: boolean + opts: InstanceOptions + self: Ajv +} + +export interface SchemaObjCxt extends SchemaCxt { + schema: AnySchemaObject +} interface SchemaEnvArgs { schema: AnySchema root?: SchemaEnv @@ -81,6 +108,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { dataNames: [N.data], dataPathArr: [nil], // TODO can its lenght be used as dataLevel if nil is removed? dataLevel: 0, + dataTypes: [], topSchemaRef: gen.scopeValue("schema", {ref: sch.schema}), validateName, ValidationError: _ValidationError, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 8b599a0831..5c25ca708f 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -1,7 +1,9 @@ -import type {AnySchema, SchemaObjCxt} from "../types" +import type {AnySchema} from "../types" +import type {SchemaObjCxt} from "./index" import {subschemaCode} from "./validate" import {escapeFragment, escapeJsonPointer} from "./util" import {_, str, Code, Name, getProperty} from "./codegen" +import {JSONType} from "./rules" interface SubschemaContext { // TODO use Optional? align with SchemCxt property types @@ -12,6 +14,7 @@ interface SubschemaContext { topSchemaRef?: Code errorPath?: Code dataLevel?: number + dataTypes?: JSONType[] data?: Name parentData?: Name parentDataProperty?: Code | number @@ -28,7 +31,7 @@ export enum Type { Str, } -export type SubschemaCxt = Partial<{ +export type SubschemaArgs = Partial<{ keyword: string schemaProp: string | number schema: AnySchema @@ -38,6 +41,7 @@ export type SubschemaCxt = Partial<{ topSchemaRef: Code data: Name | Code dataProp: Code | string | number + dataTypes: JSONType[] propertyName: Name dataPropType: Type compositeRule: true @@ -45,7 +49,7 @@ export type SubschemaCxt = Partial<{ allErrors: boolean }> -export function applySubschema(it: SchemaObjCxt, appl: SubschemaCxt, valid: Name): void { +export function applySubschema(it: SchemaObjCxt, appl: SubschemaArgs, valid: Name): void { const subschema = getSubschema(it, appl) extendSubschemaData(subschema, it, appl) extendSubschemaMode(subschema, appl) @@ -55,7 +59,15 @@ export function applySubschema(it: SchemaObjCxt, appl: SubschemaCxt, valid: Name function getSubschema( it: SchemaObjCxt, - {keyword, schemaProp, schema, strictSchema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaCxt + { + keyword, + schemaProp, + schema, + strictSchema, + schemaPath, + errSchemaPath, + topSchemaRef, + }: SubschemaArgs ): SubschemaContext { if (keyword !== undefined && schema !== undefined) { throw new Error('both "keyword" and "schema" passed, only one allowed') @@ -95,7 +107,7 @@ function getSubschema( function extendSubschemaData( subschema: SubschemaContext, it: SchemaObjCxt, - {dataProp, dataPropType: dpType, data, propertyName}: SubschemaCxt + {dataProp, dataPropType: dpType, data, dataTypes, propertyName}: SubschemaArgs ): void { if (data !== undefined && dataProp !== undefined) { throw new Error('both "data" and "dataProp" passed, only one allowed') @@ -119,9 +131,12 @@ function extendSubschemaData( // TODO something is possibly wrong here with not changing parentDataProperty and not appending dataPathArr } + if (dataTypes) subschema.dataTypes = dataTypes + function dataContextProps(_nextData: Name): void { subschema.data = _nextData subschema.dataLevel = it.dataLevel + 1 + subschema.dataTypes = [] subschema.parentData = it.data subschema.dataNames = [...it.dataNames, _nextData] } @@ -129,7 +144,7 @@ function extendSubschemaData( function extendSubschemaMode( subschema: SubschemaContext, - {compositeRule, createErrors, allErrors, strictSchema}: SubschemaCxt + {compositeRule, createErrors, allErrors, strictSchema}: SubschemaArgs ): void { if (compositeRule !== undefined) subschema.compositeRule = compositeRule if (createErrors !== undefined) subschema.createErrors = createErrors diff --git a/lib/compile/util.ts b/lib/compile/util.ts index a3999dff62..e9bf11a8ad 100644 --- a/lib/compile/util.ts +++ b/lib/compile/util.ts @@ -1,6 +1,7 @@ -import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" -import type {SchemaCxt, AnySchema} from "../types" +import type {AnySchema} from "../types" +import type {SchemaCxt} from "./index" import type {JSONType, Rule, ValidationRules} from "./rules" +import {_, nil, and, operators, Code, Name, getProperty} from "./codegen" import N from "./names" export enum DataType { diff --git a/lib/compile/validate/applicability.ts b/lib/compile/validate/applicability.ts index e12f1988bd..478b704ac5 100644 --- a/lib/compile/validate/applicability.ts +++ b/lib/compile/validate/applicability.ts @@ -1,4 +1,5 @@ -import type {SchemaObjCxt, AnySchemaObject} from "../../types" +import type {AnySchemaObject} from "../../types" +import type {SchemaObjCxt} from ".." import type {JSONType, RuleGroup, Rule} from "../rules" export function schemaHasRulesForType( diff --git a/lib/compile/validate/boolSchema.ts b/lib/compile/validate/boolSchema.ts index 879fcba4e0..0e1b252ad5 100644 --- a/lib/compile/validate/boolSchema.ts +++ b/lib/compile/validate/boolSchema.ts @@ -1,4 +1,5 @@ -import type {KeywordErrorDefinition, SchemaCxt, KeywordErrorCxt} from "../../types" +import type {KeywordErrorDefinition, KeywordErrorCxt} from "../../types" +import type {SchemaCxt} from ".." import {reportError} from "../errors" import {_, Name} from "../codegen" import N from "../names" diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 5a3bdb5791..1f51000c32 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -1,10 +1,10 @@ import type { - SchemaObjCxt, KeywordErrorDefinition, KeywordErrorCxt, ErrorObject, AnySchemaObject, } from "../../types" +import type {SchemaObjCxt} from ".." import {isJSONType, JSONType} from "../rules" import {schemaHasRulesForType} from "./applicability" import {checkDataTypes, DataType} from "../util" @@ -18,6 +18,7 @@ export function getSchemaTypes(schema: AnySchemaObject): JSONType[] { if (hasNull && schema.nullable === false) { throw new Error('{"type": "null"} contradicts {"nullable": "false"}') } else if (!hasNull && schema.nullable === true) { + if (!types.length) throw new Error('"nullable" cannot be used without "type"') types.push("null") } return types diff --git a/lib/compile/validate/defaults.ts b/lib/compile/validate/defaults.ts index 2a30cab817..124363b7e1 100644 --- a/lib/compile/validate/defaults.ts +++ b/lib/compile/validate/defaults.ts @@ -1,4 +1,4 @@ -import type {SchemaObjCxt} from "../../types" +import type {SchemaObjCxt} from ".." import {_, getProperty, stringify} from "../codegen" import {checkStrictMode} from "../../vocabularies/util" diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index f15b08d7f1..c7d9769e69 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -1,4 +1,5 @@ -import type {AnySchema, SchemaCxt, SchemaObjCxt} from "../../types" +import type {AnySchema} from "../../types" +import type {SchemaCxt, SchemaObjCxt} from ".." import type {InstanceOptions} from "../../ajv" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 8e981d1212..3a28a09d7e 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -1,4 +1,4 @@ -import type {SchemaObjCxt} from "../../types" +import type {SchemaObjCxt} from ".." import type {JSONType, Rule, RuleGroup} from "../rules" import {shouldUseGroup, shouldUseRule} from "./applicability" import {checkDataType, schemaHasRulesButRef} from "../util" @@ -64,45 +64,61 @@ function iterateKeywords(it: SchemaObjCxt, group: RuleGroup): void { } function checkStrictTypes(it: SchemaObjCxt, types: JSONType[]): void { - if (!it.strictSchema || it.schemaEnv.meta || !it.opts.strictTypes) return - checkMultipleTypes(it, types, it.opts.strictTypes) - checkApplicableTypes(it, types, it.opts.strictTypes) + if (it.schemaEnv.meta || !it.opts.strictTypes) return + checkContextTypes(it, types) + checkMultipleTypes(it, types) + checkKeywordTypes(it, it.dataTypes) } -function checkMultipleTypes(it: SchemaObjCxt, ts: JSONType[], mode: boolean | "log"): void { +function checkContextTypes(it: SchemaObjCxt, types: JSONType[]): void { + if (!types.length) return + if (!it.dataTypes.length) { + it.dataTypes = types + return + } + types.forEach((t) => { + if (!includesType(it.dataTypes, t)) { + strictTypesError(it, `type "${t}" not allowed by context "${it.dataTypes.join(",")}"`) + } + }) + it.dataTypes = it.dataTypes.filter((t) => includesType(types, t)) +} + +function checkMultipleTypes(it: SchemaObjCxt, ts: JSONType[]): void { if ( ts.length > 1 && !(ts.length === 2 && ts.includes("null")) && (ts.includes("object") || ts.includes("array")) ) { - strictTypesError(it, "multiple non-primitive types", mode) + strictTypesError(it, "multiple non-primitive types") } } -function checkApplicableTypes(it: SchemaObjCxt, ts: JSONType[], mode: boolean | "log"): void { +function checkKeywordTypes(it: SchemaObjCxt, ts: JSONType[]): void { const rules = it.self.RULES.all for (const keyword in rules) { const rule = rules[keyword] if (typeof rule == "object" && shouldUseRule(it.schema, rule)) { const {type} = rule.definition if (type.length && !type.some((t) => hasApplicableType(ts, t))) { - strictTypesError(it, "missing appllicable type", mode) + strictTypesError(it, `missing type "${type.join(",")}" for keyword "${keyword}"`) return } } } } -function hasApplicableType(schemaTypes: JSONType[], keywordType: JSONType): boolean { - return ( - schemaTypes.includes(keywordType) || - (keywordType === "number" && schemaTypes.includes("integer")) - ) +function hasApplicableType(schTs: JSONType[], kwdT: JSONType): boolean { + return schTs.includes(kwdT) || (kwdT === "number" && schTs.includes("integer")) +} + +function includesType(ts: JSONType[], t: JSONType): boolean { + return ts.includes(t) || (t === "integer" && ts.includes("number")) } -function strictTypesError(it: SchemaObjCxt, msg: string, mode: boolean | "log"): void { +function strictTypesError(it: SchemaObjCxt, msg: string): void { const schemaPath = it.schemaEnv.baseId + it.errSchemaPath - msg += ` at "${schemaPath}/type" (strictTypes)` - throw new Error(msg) - checkStrictMode(it, msg, mode) + msg += ` at "${schemaPath}" (strictTypes)` + // throw new Error(msg) + checkStrictMode(it, msg, it.opts.strictTypes) } diff --git a/lib/compile/validate/keyword.ts b/lib/compile/validate/keyword.ts index c4889cd7f8..7c1ba84d6d 100644 --- a/lib/compile/validate/keyword.ts +++ b/lib/compile/validate/keyword.ts @@ -3,12 +3,12 @@ import type { MacroKeywordDefinition, FuncKeywordDefinition, AnySchema, - SchemaObjCxt, SchemaValidateFunction, AnyValidateFunction, } from "../../types" +import type {SchemaObjCxt} from ".." +import type {JSONType} from "../rules" import KeywordCxt from "../context" -import {JSONType} from "../rules" import {applySubschema} from "../subschema" import {extendErrors} from "../errors" import {callValidateCode} from "../../vocabularies/util" diff --git a/lib/types/index.ts b/lib/types/index.ts index 20570a19bd..8370b59f6b 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1,9 +1,8 @@ import type {CodeGen, Code, Name, Scope} from "../compile/codegen" -import type {SchemaEnv} from "../compile" +import type {SchemaEnv, SchemaCxt, SchemaObjCxt} from "../compile" import type {JSONType} from "../compile/rules" import type KeywordCxt from "../compile/context" import type Ajv from "../ajv" -import type {InstanceOptions} from "../ajv" interface _SchemaObject { $id?: string @@ -74,37 +73,6 @@ export interface ErrorObject> { data?: unknown } -export interface SchemaCxt { - gen: CodeGen - allErrors?: boolean - data: Name - parentData: Name - parentDataProperty: Code | number - dataNames: Name[] - dataPathArr: (Code | number)[] - dataLevel: number - topSchemaRef: Code - validateName: Name - ValidationError?: Name - schema: AnySchema - schemaEnv: SchemaEnv - strictSchema?: boolean - rootId: string // TODO ? - baseId: string - schemaPath: Code - errSchemaPath: string // this is actual string, should not be changed to Code - errorPath: Code - propertyName?: Name - compositeRule?: boolean - createErrors?: boolean - opts: InstanceOptions - self: Ajv -} - -export interface SchemaObjCxt extends SchemaCxt { - schema: AnySchemaObject -} - interface _KeywordDef { keyword: string | string[] type?: JSONType | JSONType[] @@ -190,7 +158,7 @@ export interface KeywordErrorCxt { parentSchema?: AnySchemaObject schemaCode: Code | number | boolean schemaValue: Code | number | boolean - schemaType?: JSONType | JSONType[] + schemaType?: JSONType[] errsCount?: Name params: KeywordCxtParams it: SchemaCxt diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index cc42343a94..593569bcc9 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -5,7 +5,7 @@ import type { KeywordErrorDefinition, } from "../../types" import {allSchemaProperties, schemaRefOrVal, alwaysValidSchema, usePattern} from "../util" -import {applySubschema, SubschemaCxt, Type} from "../../compile/subschema" +import {applySubschema, SubschemaArgs, Type} from "../../compile/subschema" import {_, nil, or, Code, Name} from "../../compile/codegen" import N from "../../compile/names" @@ -93,7 +93,7 @@ const def: CodeKeywordDefinition & AddedKeywordDefinition = { } function applyAdditionalSchema(key: Name, valid: Name, errors?: false): void { - const subschema: SubschemaCxt = { + const subschema: SubschemaArgs = { keyword: "additionalProperties", dataProp: key, dataPropType: Type.Str, diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 8ff19f2576..f81c14d9c2 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -1,9 +1,5 @@ -import type { - CodeKeywordDefinition, - ErrorObject, - KeywordErrorDefinition, - SchemaObjCxt, -} from "../../types" +import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" +import type {SchemaObjCxt} from "../../compile" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema} from "../../compile/subschema" diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 275e64f9d8..2f81090186 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -28,6 +28,7 @@ const def: CodeKeywordDefinition = { { keyword: "propertyNames", data: key, + dataTypes: ["string"], propertyName: key, compositeRule: true, strictSchema: it.strictSchema, diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 26176373c3..59eae90689 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -51,6 +51,7 @@ const def: CodeKeywordDefinition = { { schema: sch, strictSchema: true, + dataTypes: [], schemaPath: nil, topSchemaRef: schName, errSchemaPath: schema, diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index 10dfc3effe..774a642ee8 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,4 +1,5 @@ -import type {AnySchema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" +import type {AnySchema, SchemaMap} from "../types" +import type {SchemaCxt, SchemaObjCxt} from "../compile" import type KeywordCxt from "../compile/context" import {schemaHasRules} from "../compile/util" import {CodeGen, _, strConcat, nil, Code, Name, getProperty} from "../compile/codegen" diff --git a/spec/after_test.ts b/spec/after_test.ts index c1104c508d..64515d8618 100644 --- a/spec/after_test.ts +++ b/spec/after_test.ts @@ -1,4 +1,5 @@ -const should = require("./chai").should() +import chai from "./chai" +const should = chai.should() module.exports = { error: afterError, diff --git a/spec/ajv.spec.ts b/spec/ajv.spec.ts index 30caee882e..6e01137d06 100644 --- a/spec/ajv.spec.ts +++ b/spec/ajv.spec.ts @@ -1,6 +1,7 @@ import _Ajv from "./ajv" import {_} from "../dist/compile/codegen" -const should = require("./chai").should() +import chai from "./chai" +const should = chai.should() describe("Ajv", () => { let ajv diff --git a/spec/async.spec.ts b/spec/async.spec.ts index e563407f7a..3f0198222c 100644 --- a/spec/async.spec.ts +++ b/spec/async.spec.ts @@ -1,7 +1,7 @@ import _Ajv from "./ajv" import type {SchemaObject, AnyValidateFunction} from "../dist/types" - -const should = require("./chai").should() +import chai from "./chai" +const should = chai.should() describe("compileAsync method", () => { let ajv, loadCallCount @@ -9,6 +9,7 @@ describe("compileAsync method", () => { const SCHEMAS = { "http://example.com/object.json": { $id: "http://example.com/object.json", + type: "object", properties: { a: {type: "string"}, b: {$ref: "int2plus.json"}, @@ -26,6 +27,7 @@ describe("compileAsync method", () => { }, "http://example.com/leaf.json": { $id: "http://example.com/leaf.json", + type: "object", properties: { name: {type: "string"}, subtree: {$ref: "tree.json"}, @@ -33,6 +35,7 @@ describe("compileAsync method", () => { }, "http://example.com/recursive.json": { $id: "http://example.com/recursive.json", + type: "object", properties: { b: {$ref: "parent.json"}, }, @@ -40,6 +43,7 @@ describe("compileAsync method", () => { }, "http://example.com/invalid.json": { $id: "http://example.com/recursive.json", + type: "object", properties: { invalid: {type: "number"}, }, @@ -87,6 +91,7 @@ describe("compileAsync method", () => { it("should compile schemas loading missing schemas with options.loadSchema function", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "object.json"}, }, @@ -102,6 +107,7 @@ describe("compileAsync method", () => { it("should compile schemas loading missing schemas and return promise with function", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "object.json"}, }, @@ -117,6 +123,7 @@ describe("compileAsync method", () => { it("should correctly load schemas when missing reference has JSON path", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "object.json#/properties/b"}, }, @@ -150,6 +157,7 @@ describe("compileAsync method", () => { it("should correctly compile with remote schemas that reference the compiled schema", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "recursive.json"}, }, @@ -167,6 +175,7 @@ describe("compileAsync method", () => { it('should resolve reference containing "properties" segment with the same property (issue #220)', () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: { $ref: "object.json#/properties/a", @@ -242,6 +251,7 @@ describe("compileAsync method", () => { it("should queue calls so only one compileAsync executes at a time (#52)", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "object.json"}, }, @@ -290,6 +300,7 @@ describe("compileAsync method", () => { it("if loaded schema is invalid", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "invalid.json"}, }, @@ -300,6 +311,7 @@ describe("compileAsync method", () => { it("if required schema is loaded but the reference cannot be resolved", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "object.json#/definitions/not_found"}, }, @@ -310,6 +322,7 @@ describe("compileAsync method", () => { it("if loadSchema returned error", () => { const schema = { $id: "http://example.com/parent.json", + type: "object", properties: { a: {$ref: "object.json"}, }, diff --git a/spec/async_validate.spec.ts b/spec/async_validate.spec.ts index beb4a75eaf..56a2922db9 100644 --- a/spec/async_validate.spec.ts +++ b/spec/async_validate.spec.ts @@ -1,7 +1,7 @@ import getAjvAsyncInstances from "./ajv_async_instances" import _Ajv from "./ajv" - -const should = require("./chai").should() +import chai from "./chai" +const should = chai.should() describe("async schemas, formats and keywords", function () { this.timeout(30000) diff --git a/spec/errors.spec.ts b/spec/errors.spec.ts index fdf3ec951c..34e2706ef3 100644 --- a/spec/errors.spec.ts +++ b/spec/errors.spec.ts @@ -1,6 +1,7 @@ import Ajv from "./ajv" -const should = require("./chai").should() +import chai from "./chai" +const should = chai.should() describe("Validation errors", () => { let ajv, ajvJP, fullAjv diff --git a/spec/issues/240_mutual_recur_frags_common_ref.spec.ts b/spec/issues/240_mutual_recur_frags_common_ref.spec.ts index 8117d68f17..24ac1052b4 100644 --- a/spec/issues/240_mutual_recur_frags_common_ref.spec.ts +++ b/spec/issues/240_mutual_recur_frags_common_ref.spec.ts @@ -8,12 +8,14 @@ describe("issue #240, mutually recursive fragment refs reference a common schema $defs: { resource: { $id: "#resource", + type: "object", properties: { id: {type: "string"}, }, }, resourceIdentifier: { $id: "#resource_identifier", + type: "object", properties: { id: {type: "string"}, type: {type: "string"}, @@ -25,6 +27,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema const domainSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://domain.schema#", + type: "object", properties: { data: { oneOf: [ @@ -41,9 +44,11 @@ describe("issue #240, mutually recursive fragment refs reference a common schema const librarySchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://library.schema#", + type: "object", properties: { name: {type: "string"}, links: { + type: "object", properties: { catalogItems: { type: "array", @@ -59,6 +64,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema $id: "#resource_identifier", allOf: [ { + type: "object", properties: { type: { type: "string", @@ -75,9 +81,11 @@ describe("issue #240, mutually recursive fragment refs reference a common schema const catalogItemSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://catalog_item.schema#", + type: "object", properties: { name: {type: "string"}, links: { + type: "object", properties: { library: {$ref: "schema://library.schema#resource_identifier"}, }, @@ -88,6 +96,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema $id: "#resource_identifier", allOf: [ { + type: "object", properties: { type: { type: "string", @@ -106,6 +115,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema $id: "schema://catalog_item_resource_identifier.schema#", allOf: [ { + type: "object", properties: { type: { type: "string", @@ -134,9 +144,11 @@ describe("issue #240, mutually recursive fragment refs reference a common schema const librarySchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://library.schema#", + type: "object", properties: { name: {type: "string"}, links: { + type: "object", properties: { catalogItems: { type: "array", @@ -150,6 +162,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema $id: "#resource_identifier", allOf: [ { + type: "object", properties: { type: { type: "string", @@ -166,9 +179,11 @@ describe("issue #240, mutually recursive fragment refs reference a common schema const catalogItemSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "schema://catalog_item.schema#", + type: "object", properties: { name: {type: "string"}, links: { + type: "object", properties: { library: {$ref: "schema://library.schema#resource_identifier"}, }, @@ -179,6 +194,7 @@ describe("issue #240, mutually recursive fragment refs reference a common schema $id: "#resource_identifier", allOf: [ { + type: "object", properties: { type: { type: "string", diff --git a/spec/issues/342_uniqueItems_non-json_objects.spec.ts b/spec/issues/342_uniqueItems_non-json_objects.spec.ts index 8907f287cf..dd5e046797 100644 --- a/spec/issues/342_uniqueItems_non-json_objects.spec.ts +++ b/spec/issues/342_uniqueItems_non-json_objects.spec.ts @@ -6,7 +6,7 @@ describe("issue #342, support uniqueItems with some non-JSON objects", () => { before(() => { const ajv = new _Ajv() - validate = ajv.compile({uniqueItems: true}) + validate = ajv.compile({type: "array", uniqueItems: true}) }) it("should allow different RegExps", () => { diff --git a/spec/issues/485_type_validation_priority.spec.ts b/spec/issues/485_type_validation_priority.spec.ts index 4fa5d040e2..bd7be19529 100644 --- a/spec/issues/485_type_validation_priority.spec.ts +++ b/spec/issues/485_type_validation_priority.spec.ts @@ -3,7 +3,7 @@ require("../chai").should() describe("issue #485, order of type validation", () => { it("should validate types before keywords", () => { - const ajv = new _Ajv({allErrors: true}) + const ajv = new _Ajv({allErrors: true, strictTypes: false}) const validate: any = ajv.compile({ type: ["integer", "string"], required: ["foo"], diff --git a/spec/issues/50_refs_with_definitions.spec.ts b/spec/issues/50_refs_with_definitions.spec.ts index 58d8967643..24f3cf4d36 100644 --- a/spec/issues/50_refs_with_definitions.spec.ts +++ b/spec/issues/50_refs_with_definitions.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe('issue #50: references with "definitions"', () => { it("should be supported by addSchema", spec("addSchema")) diff --git a/spec/issues/743_removeAdditional_to_remove_proto.spec.ts b/spec/issues/743_removeAdditional_to_remove_proto.spec.ts index fa62f2c9d3..4a621677bd 100644 --- a/spec/issues/743_removeAdditional_to_remove_proto.spec.ts +++ b/spec/issues/743_removeAdditional_to_remove_proto.spec.ts @@ -6,8 +6,10 @@ describe("issue #743, property __proto__ should be removed with removeAdditional const ajv = new _Ajv({removeAdditional: true}) const schema = { + type: "object", properties: { obj: { + type: "object", additionalProperties: false, properties: { a: {type: "string"}, diff --git a/spec/issues/768_passContext_recursive_ref.spec.ts b/spec/issues/768_passContext_recursive_ref.spec.ts index 4e1db66399..a58c4f1070 100644 --- a/spec/issues/768_passContext_recursive_ref.spec.ts +++ b/spec/issues/768_passContext_recursive_ref.spec.ts @@ -73,6 +73,7 @@ describe("issue #768, fix passContext in recursive $ref", () => { $id: "foo", definitions: { bar: { + type: "object", properties: { baz: { $ref: "boo", diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index 14dc4250f1..ddcfcec12f 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -4,19 +4,24 @@ import {_, nil} from "../dist/compile/codegen" import getAjvInstances from "./ajv_instances" import _Ajv from "./ajv" import type Ajv from ".." +import assert from "assert" -const should = require("./chai").should(), +import chai from "./chai" +const should = chai.should(), equal = require("../dist/compile/equal") describe("User-defined keywords", () => { let ajv: Ajv, instances: Ajv[] beforeEach(() => { - instances = getAjvInstances({ - allErrors: true, - verbose: true, - inlineRefs: false, - }) + instances = getAjvInstances( + { + allErrors: true, + verbose: true, + inlineRefs: false, + }, + {strictTypes: false} + ) ajv = instances[0] }) @@ -302,6 +307,7 @@ describe("User-defined keywords", () => { _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) const schema = { + type: "object", deepProperties: { "a.b.c": {type: "number", range: [2, 4]}, "d.e.f.g": {type: "string"}, @@ -402,9 +408,9 @@ describe("User-defined keywords", () => { } else { const deepProperties = {} deepProperties[path.slice(1).join(".")] = _schema[prop] - properties[path[0]] = {deepProperties: deepProperties} + properties[path[0]] = {type: "object", deepProperties} } - expanded.push({properties: properties}) + expanded.push({type: "object", properties}) } return expanded.length === 1 ? expanded[0] : {allOf: expanded} @@ -418,6 +424,7 @@ describe("User-defined keywords", () => { _ajv.addKeyword({keyword: "even", type: "number", macro: macroEven}) const schema = { + type: "number", range: [4, 6], even: true, } @@ -440,6 +447,7 @@ describe("User-defined keywords", () => { _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) const schema = { + type: "number", range: [1, 4], minimum: 2.5, } @@ -456,6 +464,7 @@ describe("User-defined keywords", () => { _ajv.addKeyword({keyword: "range", type: "number", macro: macroRange}) const schema = { + type: "number", allOf: [{range: [4, 8]}, {range: [2, 6]}], } @@ -478,6 +487,7 @@ describe("User-defined keywords", () => { _ajv.addKeyword({keyword: "myContains", type: "array", macro: macroContains}) const schema = { + type: "array", myContains: { type: "number", range: [4, 7], @@ -794,7 +804,10 @@ describe("User-defined keywords", () => { function testEvenKeyword(evenDefinition, numErrors = 1) { instances.forEach((_ajv) => { _ajv.addKeyword(evenDefinition) - const schema = {"x-even": true} + const schema = { + type: ["number", "string"], + "x-even": true, + } const validate = _ajv.compile(schema) shouldBeValid(validate, 2) @@ -808,7 +821,10 @@ describe("User-defined keywords", () => { instances.forEach((_ajv) => { _ajv.addKeyword(definition) - let schema: any = {"x-even-$data": true} + let schema: any = { + type: ["number", "string"], + "x-even-$data": true, + } let validate = _ajv.compile(schema) shouldBeValid(validate, 2) @@ -817,8 +833,12 @@ describe("User-defined keywords", () => { shouldBeInvalid(validate, 3, numErrors) schema = { + type: "object", properties: { - data: {"x-even-$data": {$data: "1/evenValue"}}, + data: { + type: ["number", "string"], + "x-even-$data": {$data: "1/evenValue"}, + }, evenValue: {}, }, } @@ -886,7 +906,10 @@ describe("User-defined keywords", () => { _ajv.addKeyword(definition) _ajv.addKeyword({keyword: "exclusiveRange", schemaType: "boolean"}) - let schema: SchemaObject = {"x-range": [2, 4]} + let schema: SchemaObject = { + type: ["number", "string"], + "x-range": [2, 4], + } let validate = _ajv.compile(schema) shouldBeValid(validate, 2) @@ -904,8 +927,10 @@ describe("User-defined keywords", () => { } schema = { + type: "object", properties: { foo: { + type: ["number"], "x-range": [2, 4], exclusiveRange: true, }, @@ -930,16 +955,17 @@ describe("User-defined keywords", () => { function testMultipleRangeKeyword(definition, numErrors?: number) { instances.forEach((_ajv) => { + _ajv.opts.strictTypes = false _ajv.addKeyword(definition) _ajv.addKeyword({keyword: "exclusiveRange", schemaType: "boolean"}) const schema = { properties: { - a: {"x-range": [2, 4], exclusiveRange: true}, - b: {"x-range": [2, 4], exclusiveRange: false}, + a: {type: "number", "x-range": [2, 4], exclusiveRange: true}, + b: {type: "number", "x-range": [2, 4], exclusiveRange: false}, }, - additionalProperties: {"x-range": [5, 7]}, - items: {"x-range": [5, 7]}, + additionalProperties: {type: "number", "x-range": [5, 7]}, + items: {type: "number", "x-range": [5, 7]}, } const validate = _ajv.compile(schema) @@ -1102,9 +1128,8 @@ describe("User-defined keywords", () => { ajv.addKeyword(definition) const def = ajv.getKeyword("mykeyword") - def.should.be.an("object") - // TODO type cast - ;(def as {keyword: string[]}).keyword.should.equal("mykeyword") + assert(typeof def == "object") + def.keyword.should.equal("mykeyword") }) }) @@ -1227,7 +1252,10 @@ describe("User-defined keywords", () => { properties: { foo: { allOf: [ - {collectionFormat: "csv"}, + { + type: "string", + collectionFormat: "csv", + }, { type: "array", items: {type: "string"}, @@ -1268,6 +1296,7 @@ describe("User-defined keywords", () => { it("should require properties in the parent schema", () => { ajv.addKeyword({ keyword: "allRequired", + type: "object", macro: (schema, parentSchema) => schema ? {required: Object.keys(parentSchema.properties)} : true, schemaType: "boolean", @@ -1275,14 +1304,16 @@ describe("User-defined keywords", () => { }) const invalidSchema = { + type: "object", allRequired: true, } should.throw(() => { ajv.compile(invalidSchema) - }) + }, /parent schema must have dependencies of allRequired: properties/) const schema = { + type: "object", properties: { foo: true, }, diff --git a/spec/options/comment.spec.ts b/spec/options/comment.spec.ts index c8f5eef5c7..0dcbb9fe06 100644 --- a/spec/options/comment.spec.ts +++ b/spec/options/comment.spec.ts @@ -21,6 +21,7 @@ describe("$comment option", () => { it("should log the text from $comment keyword", () => { const schema = { $comment: "object root", + type: "object", properties: { foo: {$comment: "property foo"}, bar: {$comment: "property bar", type: "integer"}, @@ -59,6 +60,7 @@ describe("$comment option", () => { it("should pass the text from $comment keyword to the hook", () => { const schema = { $comment: "object root", + type: "object", properties: { foo: {$comment: "property foo"}, bar: {$comment: "property bar", type: "integer"}, diff --git a/spec/options/meta_validateSchema.spec.ts b/spec/options/meta_validateSchema.spec.ts index 3b9e3c95a1..820d9f4ba9 100644 --- a/spec/options/meta_validateSchema.spec.ts +++ b/spec/options/meta_validateSchema.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("meta and validateSchema options", () => { it("should add draft-7 meta schema by default", () => { diff --git a/spec/options/nullable.spec.ts b/spec/options/nullable.spec.ts index 708a3c2653..d7cb86cd8e 100644 --- a/spec/options/nullable.spec.ts +++ b/spec/options/nullable.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("nullable keyword", () => { let ajv diff --git a/spec/options/options_add_schemas.spec.ts b/spec/options/options_add_schemas.spec.ts index b46f76f73e..ddbe05ee25 100644 --- a/spec/options/options_add_schemas.spec.ts +++ b/spec/options/options_add_schemas.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("options to add schemas", () => { describe("schemas", () => { @@ -22,7 +23,14 @@ describe("options to add schemas", () => { schemas: [ {$id: "int", type: "integer"}, {$id: "str", type: "string"}, - {$id: "obj", properties: {int: {$ref: "int"}, str: {$ref: "str"}}}, + { + $id: "obj", + type: "object", + properties: { + int: {$ref: "int"}, + str: {$ref: "str"}, + }, + }, ], }) @@ -96,11 +104,11 @@ describe("options to add schemas", () => { it("should NOT throw with duplicate ID", () => { ajv.compile({$id: "str", type: "string"}) should.not.throw(() => { - ajv.compile({$id: "str", minLength: 2}) + ajv.compile({$id: "str", type: "string", minLength: 2}) }) const schema = {$id: "int", type: "integer"} - const schema2 = {$id: "int", minimum: 0} + const schema2 = {$id: "int", type: "integer", minimum: 0} ajv.validate(schema, 1).should.equal(true) should.not.throw(() => { ajv.validate(schema2, 1).should.equal(true) diff --git a/spec/options/options_code.spec.ts b/spec/options/options_code.spec.ts index 861851c680..0d89f44d3c 100644 --- a/spec/options/options_code.spec.ts +++ b/spec/options/options_code.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("code generation options", () => { describe("sourceCode", () => { diff --git a/spec/options/options_refs.spec.ts b/spec/options/options_refs.spec.ts index 05e4fefc6b..0e70c152bf 100644 --- a/spec/options/options_refs.spec.ts +++ b/spec/options/options_refs.spec.ts @@ -1,6 +1,7 @@ import _Ajv from "../ajv" import type {Options} from "../.." -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("referenced schema options", () => { describe("ignoreKeywordsWithRef", () => { @@ -30,6 +31,7 @@ describe("referenced schema options", () => { definitions: { int: {type: "integer"}, }, + type: "number", $ref: "#/definitions/int", minimum: 10, } @@ -46,9 +48,11 @@ describe("referenced schema options", () => { properties: { foo: { $ref: "#/definitions/int", + type: "number", minimum: 10, }, bar: { + type: "number", allOf: [{$ref: "#/definitions/int"}, {minimum: 10}], }, }, @@ -75,6 +79,7 @@ describe("referenced schema options", () => { definitions: { int: {type: "integer"}, }, + type: "number", $ref: "#/definitions/int", minimum: 10, } diff --git a/spec/options/options_reporting.spec.ts b/spec/options/options_reporting.spec.ts index 2f6427450b..dbcebbde2d 100644 --- a/spec/options/options_reporting.spec.ts +++ b/spec/options/options_reporting.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("reporting options", () => { describe("verbose", () => { @@ -8,7 +9,12 @@ describe("reporting options", () => { testVerbose(new _Ajv({verbose: true, allErrors: true})) function testVerbose(ajv) { - const schema = {properties: {foo: {minimum: 5}}} + const schema = { + type: "object", + properties: { + foo: {type: "number", minimum: 5}, + }, + } const validate = ajv.compile(schema) const data = {foo: 3} @@ -17,7 +23,7 @@ describe("reporting options", () => { const err = validate.errors[0] should.equal(err.schema, 5) - err.parentSchema.should.eql({minimum: 5}) + err.parentSchema.should.eql({type: "number", minimum: 5}) err.parentSchema.should.equal(schema.properties.foo) // by reference should.equal(err.data, 3) } @@ -44,6 +50,7 @@ describe("reporting options", () => { }) const schema1 = { + type: "string", allOf: [{format: "format1"}, {format: "format2"}], } diff --git a/spec/options/options_validation.spec.ts b/spec/options/options_validation.spec.ts index 2b72769db8..18097d600e 100644 --- a/spec/options/options_validation.spec.ts +++ b/spec/options/options_validation.spec.ts @@ -1,3 +1,4 @@ +import type Ajv from "../.." import _Ajv from "../ajv" require("../chai").should() const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ @@ -8,7 +9,7 @@ describe("validation options", () => { const ajv = new _Ajv({formats: {date: DATE_FORMAT}}), ajvFF = new _Ajv({formats: {date: DATE_FORMAT}, validateFormats: false}) - const schema = {format: "date"} + const schema = {type: "string", format: "date"} const invalideDateTime = "06/19/1963" // expects hyphens ajv.validate(schema, invalideDateTime).should.equal(false) @@ -24,7 +25,10 @@ describe("validation options", () => { }, }) - const validate = ajv.compile({format: "identifier"}) + const validate = ajv.compile({ + type: ["string", "number"], + format: "identifier", + }) validate("Abc1").should.equal(true) validate("foo bar").should.equal(false) @@ -47,7 +51,10 @@ describe("validation options", () => { ], }) - const validate = ajv.compile({identifier: true}) + const validate = ajv.compile({ + type: ["string", "number"], + identifier: true, + }) validate("Abc1").should.equal(true) validate("foo bar").should.equal(false) @@ -62,15 +69,15 @@ describe("validation options", () => { testUnicode(new _Ajv({unicode: false, logger: false})) testUnicode(new _Ajv({unicode: false, allErrors: true, logger: false})) - function testUnicode(ajv) { - let validateWithUnicode = ajvUnicode.compile({minLength: 2}) - let validate = ajv.compile({minLength: 2}) + function testUnicode(ajv: Ajv) { + let validateWithUnicode = ajvUnicode.compile({type: "string", minLength: 2}) + let validate = ajv.compile({type: "string", minLength: 2}) validateWithUnicode("😀").should.equal(false) validate("😀").should.equal(true) - validateWithUnicode = ajvUnicode.compile({maxLength: 1}) - validate = ajv.compile({maxLength: 1}) + validateWithUnicode = ajvUnicode.compile({type: "string", maxLength: 1}) + validate = ajv.compile({type: "string", maxLength: 1}) validateWithUnicode("😀").should.equal(true) validate("😀").should.equal(false) @@ -84,13 +91,13 @@ describe("validation options", () => { test(new _Ajv({multipleOfPrecision: 7, allErrors: true})) function test(ajv) { - let schema = {multipleOf: 0.01} + let schema = {type: "number", multipleOf: 0.01} let validate = ajv.compile(schema) validate(4.18).should.equal(true) validate(4.181).should.equal(false) - schema = {multipleOf: 0.0000001} + schema = {type: "number", multipleOf: 0.0000001} validate = ajv.compile(schema) validate(53.198098).should.equal(true) diff --git a/spec/options/ownProperties.spec.ts b/spec/options/ownProperties.spec.ts index 59f7881384..1c79883c0c 100644 --- a/spec/options/ownProperties.spec.ts +++ b/spec/options/ownProperties.spec.ts @@ -1,8 +1,10 @@ import _Ajv from "../ajv" -require("../chai").should() +import type Ajv from "../.." +import chai from "../chai" +chai.should() describe("ownProperties option", () => { - let ajv, ajvOP, ajvOP1 + let ajv: Ajv, ajvOP: Ajv, ajvOP1: Ajv beforeEach(() => { ajv = new _Ajv({allErrors: true}) @@ -12,6 +14,7 @@ describe("ownProperties option", () => { it("should only validate own properties with additionalProperties", () => { const schema = { + type: "object", properties: {a: {type: "number"}}, additionalProperties: false, } @@ -23,6 +26,7 @@ describe("ownProperties option", () => { it("should only validate own properties with properties keyword", () => { const schema = { + type: "object", properties: { a: {type: "number"}, b: {type: "number"}, @@ -36,6 +40,7 @@ describe("ownProperties option", () => { it("should only validate own properties with required keyword", () => { const schema = { + type: "object", required: ["a", "b"], } @@ -50,6 +55,7 @@ describe("ownProperties option", () => { ajvOP1 = new _Ajv({ownProperties: true, loopRequired: 1}) const schema = { + type: "object", required: ["a", "b", "c", "d"], } @@ -64,6 +70,7 @@ describe("ownProperties option", () => { ajvOP1 = new _Ajv({ownProperties: true, $data: true}) const schema = { + type: "object", required: {$data: "0/req"}, properties: { req: { @@ -83,6 +90,7 @@ describe("ownProperties option", () => { it("should only validate own properties with properties and required keyword", () => { const schema = { + type: "object", properties: { a: {type: "number"}, b: {type: "number"}, @@ -97,6 +105,7 @@ describe("ownProperties option", () => { it("should only validate own properties with dependencies keyword", () => { const schema = { + type: "object", dependencies: { a: ["c"], b: ["d"], @@ -114,6 +123,7 @@ describe("ownProperties option", () => { it("should only validate own properties with schema dependencies", () => { const schema = { + type: "object", dependencies: { a: {not: {required: ["c"]}}, b: {not: {required: ["d"]}}, @@ -131,6 +141,7 @@ describe("ownProperties option", () => { it("should only validate own properties with patternProperties", () => { const schema = { + type: "object", patternProperties: {"f.*o": {type: "integer"}}, } @@ -141,6 +152,7 @@ describe("ownProperties option", () => { it("should only validate own properties with propertyNames", () => { const schema = { + type: "object", propertyNames: { pattern: "foo", }, @@ -161,12 +173,12 @@ describe("ownProperties option", () => { if (reverse) { validate(data).should.equal(true) validateOP(data).should.equal(false) - validateOP.errors.should.have.length(errors) + validateOP.errors?.should.have.length(errors) validateOP1(data).should.equal(false) - validateOP1.errors.should.have.length(1) + validateOP1.errors?.should.have.length(1) } else { validate(data).should.equal(false) - validate.errors.should.have.length(errors) + validate.errors?.should.have.length(errors) validateOP(data).should.equal(true) validateOP1(data).should.equal(true) } diff --git a/spec/options/removeAdditional.spec.ts b/spec/options/removeAdditional.spec.ts index 581e9f2bea..14523e5448 100644 --- a/spec/options/removeAdditional.spec.ts +++ b/spec/options/removeAdditional.spec.ts @@ -7,6 +7,7 @@ describe("removeAdditional option", () => { ajv.addSchema({ $id: "//test/fooBar", + type: "object", properties: {foo: {type: "string"}, bar: {type: "string"}}, }) @@ -27,6 +28,7 @@ describe("removeAdditional option", () => { ajv.addSchema({ $id: "//test/fooBar", + type: "object", properties: {foo: {type: "string"}, bar: {type: "string"}}, additionalProperties: false, }) @@ -47,8 +49,10 @@ describe("removeAdditional option", () => { const ajv = new _Ajv({removeAdditional: true}) const schema = { + type: "object", properties: { obj: { + type: "object", additionalProperties: false, properties: { a: {type: "string"}, @@ -87,6 +91,7 @@ describe("removeAdditional option", () => { ajv.addSchema({ $id: "//test/fooBar", + type: "object", properties: {foo: {type: "string"}, bar: {type: "string"}}, additionalProperties: {type: "string"}, }) @@ -106,6 +111,7 @@ describe("removeAdditional option", () => { ajv.addSchema({ $id: "//test/fooBar2", + type: "object", properties: {foo: {type: "string"}, bar: {type: "string"}}, additionalProperties: {type: "string", pattern: "^to-be-", maxLength: 10}, }) diff --git a/spec/options/schemaId.spec.ts b/spec/options/schemaId.spec.ts index 3e8e509879..582a3c981f 100644 --- a/spec/options/schemaId.spec.ts +++ b/spec/options/schemaId.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("removed schemaId option", () => { it("should use $id and throw exception when id is used", () => { diff --git a/spec/options/strict.spec.ts b/spec/options/strict.spec.ts index a79d52602e..71cf4393a0 100644 --- a/spec/options/strict.spec.ts +++ b/spec/options/strict.spec.ts @@ -1,5 +1,6 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() describe("strict mode", () => { describe( @@ -17,6 +18,7 @@ describe("strict mode", () => { '"properties" matching "patternProperties"', testStrictMode( { + type: "object", properties: {foo: false}, patternProperties: {foo: false}, }, @@ -32,6 +34,7 @@ describe("strict mode", () => { logger: getLogger(output), }) const schema = { + type: "object", properties: {foo: false}, patternProperties: {foo: false}, } diff --git a/spec/options/strictDefaults.spec.ts b/spec/options/strictDefaults.spec.ts index 8e498cdf29..9c92df0ab4 100644 --- a/spec/options/strictDefaults.spec.ts +++ b/spec/options/strictDefaults.spec.ts @@ -1,5 +1,5 @@ import _Ajv from "../ajv" -import chai from "chai" +import chai from "../chai" const should = chai.should() describe("strict option with defaults (replaced strictDefaults)", () => { @@ -92,6 +92,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { const ajv = new _Ajv({ useDefaults: true, strict: "log", + strictTypes: false, logger: getLogger(output), }) const schema = { @@ -113,6 +114,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { oneOf: [ {enum: ["foo", "bar"]}, { + type: "object", properties: { foo: { default: true, @@ -157,6 +159,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { oneOf: [ {enum: ["foo", "bar"]}, { + type: "object", properties: { foo: { default: true, diff --git a/spec/options/strictKeywords.spec.ts b/spec/options/strictKeywords.spec.ts index 422927b9c9..cb33d2d2dd 100644 --- a/spec/options/strictKeywords.spec.ts +++ b/spec/options/strictKeywords.spec.ts @@ -1,5 +1,5 @@ import _Ajv from "../ajv" -import chai from "chai" +import chai from "../chai" const should = chai.should() describe("strict option with keywords (replaced strictKeywords)", () => { diff --git a/spec/options/strictTypes.spec.ts b/spec/options/strictTypes.spec.ts new file mode 100644 index 0000000000..43bc027a87 --- /dev/null +++ b/spec/options/strictTypes.spec.ts @@ -0,0 +1,28 @@ +import _Ajv from "../ajv" +import chai from "../chai" +const should = chai.should() + +describe("strictTypes option", () => { + const ajv = new _Ajv({strictTypes: true}) + + describe("propertyNames", () => { + it('should set default data type "string"', () => { + ajv.compile({ + type: "object", + propertyNames: {maxLength: 5}, + }) + + ajv.compile({ + type: "object", + propertyNames: {type: "string", maxLength: 5}, + }) + + should.throw(() => { + ajv.compile({ + type: "object", + propertyNames: {type: "number"}, + }) + }, /type "number" not allowed by context/) + }) + }) +}) diff --git a/spec/options/unknownFormats.spec.ts b/spec/options/unknownFormats.spec.ts index 6a6fc8e8b9..26b5d75b62 100644 --- a/spec/options/unknownFormats.spec.ts +++ b/spec/options/unknownFormats.spec.ts @@ -1,5 +1,7 @@ import _Ajv from "../ajv" -const should = require("../chai").should() +import chai from "../chai" +const should = chai.should() + const DATE_FORMAT = /^\d\d\d\d-[0-1]\d-[0-3]\d$/ describe("specifying allowed unknown formats with `formats` option", () => { diff --git a/spec/options/useDefaults.spec.ts b/spec/options/useDefaults.spec.ts index 8a824d7ae0..8c4625d944 100644 --- a/spec/options/useDefaults.spec.ts +++ b/spec/options/useDefaults.spec.ts @@ -93,6 +93,7 @@ describe("useDefaults option", () => { function test(ajv) { const schema = { + type: "object", if: {required: ["foo"]}, then: { properties: { diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index 8cb12a1b46..f8cc689886 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -1,8 +1,8 @@ import getAjvInstances from "./ajv_instances" import Ajv from "./ajv" import {AnyValidateFunction} from "../dist/types" - -const should = require("./chai").should() +import chai from "./chai" +const should = chai.should() describe("resolve", () => { let instances @@ -247,6 +247,7 @@ describe("resolve", () => { required: ["header"], properties: { header: { + type: "object", allOf: [{$ref: "header.json"}, {properties: {msgType: {enum: [0]}}}], }, }, diff --git a/spec/security/array.json b/spec/security/array.json index 449cf89ee4..429fed294c 100644 --- a/spec/security/array.json +++ b/spec/security/array.json @@ -6,14 +6,14 @@ }, "tests": [ { - "description": "uniqueItems keyword used without maxItems is unsafe", + "description": "uniqueItems keyword used without maxItems is invalid", "data": { "uniqueItems": true }, "valid": false }, { - "description": "uniqueItems keyword used with maxItems is safe", + "description": "uniqueItems keyword used with maxItems is valid", "data": { "uniqueItems": true, "maxItems": "10" @@ -21,7 +21,7 @@ "valid": true }, { - "description": "uniqueItems: false is ignored (and safe)", + "description": "uniqueItems: false is ignored (and valid)", "data": { "uniqueItems": false }, @@ -30,13 +30,13 @@ ] }, { - "description": "uniqueItems with scalar type(s) is safe to use without maxItems", + "description": "uniqueItems with scalar type(s) is valid to use without maxItems", "schema": { "$ref": "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/json-schema-secure.json#" }, "tests": [ { - "description": "uniqueItems keyword with a single scalar type is safe", + "description": "uniqueItems keyword with a single scalar type is valid", "data": { "uniqueItems": true, "items": { @@ -46,7 +46,7 @@ "valid": true }, { - "description": "uniqueItems keyword with multiple scalar types is safe", + "description": "uniqueItems keyword with multiple scalar types is valid", "data": { "uniqueItems": true, "items": { @@ -64,7 +64,7 @@ }, "tests": [ { - "description": "uniqueItems keyword with a single compound type and without maxItems is unsafe", + "description": "uniqueItems keyword with a single compound type and without maxItems is invalid", "data": { "uniqueItems": true, "items": { @@ -74,7 +74,7 @@ "valid": false }, { - "description": "uniqueItems keyword with a single compound type and with maxItems is safe", + "description": "uniqueItems keyword with a single compound type and with maxItems is valid", "data": { "uniqueItems": true, "maxItems": "10", @@ -85,7 +85,7 @@ "valid": true }, { - "description": "uniqueItems keyword with multiple types including compound type and without maxItems is unsafe", + "description": "uniqueItems keyword with multiple types including compound type and without maxItems is invalid", "data": { "uniqueItems": true, "items": { @@ -95,7 +95,7 @@ "valid": false }, { - "description": "uniqueItems keyword with multiple types including compound type and with maxItems is safe", + "description": "uniqueItems keyword with multiple types including compound type and with maxItems is valid", "data": { "uniqueItems": true, "maxItems": "10", diff --git a/spec/security/object.json b/spec/security/object.json index d23561a41d..7c14ff0393 100644 --- a/spec/security/object.json +++ b/spec/security/object.json @@ -6,7 +6,7 @@ }, "tests": [ { - "description": "patternProperties keyword used without propertyNames is unsafe", + "description": "patternProperties keyword used without propertyNames is invalid", "data": { "patternProperties": { ".*": {} @@ -15,7 +15,7 @@ "valid": false }, { - "description": "patternProperties keyword used with propertyNames is safe", + "description": "patternProperties keyword used with propertyNames is valid", "data": { "patternProperties": { ".*": {} diff --git a/spec/security/string.json b/spec/security/string.json index 5890aa288d..ee37ec06ea 100644 --- a/spec/security/string.json +++ b/spec/security/string.json @@ -6,14 +6,14 @@ }, "tests": [ { - "description": "pattern keyword used without maxLength is unsafe", + "description": "pattern keyword used without maxLength is invalid", "data": { "pattern": ".*" }, "valid": false }, { - "description": "pattern keyword used with maxLength is safe", + "description": "pattern keyword used with maxLength is valid", "data": { "pattern": ".*", "maxLength": "256" @@ -29,14 +29,14 @@ }, "tests": [ { - "description": "format keyword used without maxLength is unsafe", + "description": "format keyword used without maxLength is invalid", "data": { "format": "email" }, "valid": false }, { - "description": "format keyword used with maxLength is safe", + "description": "format keyword used with maxLength is valid", "data": { "format": "email", "maxLength": "256" diff --git a/spec/types/async-validate.spec.ts b/spec/types/async-validate.spec.ts index c092bc35aa..c9f4c4d7cb 100644 --- a/spec/types/async-validate.spec.ts +++ b/spec/types/async-validate.spec.ts @@ -12,7 +12,10 @@ describe("$async validation and type guards", () => { describe("$async: undefined", () => { it("should have result type boolean 1", () => { - const validate = ajv.compile({properties: {foo: {type: "number"}}}) + const validate = ajv.compile({ + type: "object", + properties: {foo: {type: "number"}}, + }) const data: unknown = {foo: 1} let result: boolean if ((result = validate(data))) { @@ -22,7 +25,10 @@ describe("$async validation and type guards", () => { }) it("should have result type boolean 2", () => { - const schema: SchemaObject = {properties: {foo: {type: "number"}}} + const schema: SchemaObject = { + type: "object", + properties: {foo: {type: "number"}}, + } const validate = ajv.compile(schema) const data: unknown = {foo: 1} let result: boolean @@ -33,7 +39,10 @@ describe("$async validation and type guards", () => { }) it("should have result type boolean 3", () => { - const schema: AnySchemaObject = {properties: {foo: {type: "number"}}} + const schema: AnySchemaObject = { + type: "object", + properties: {foo: {type: "number"}}, + } const validate = ajv.compile(schema) const data: unknown = {foo: 1} let result: boolean @@ -68,13 +77,21 @@ describe("$async validation and type guards", () => { describe("$async: true", () => { it("should have result type promise 1", async () => { - const validate = ajv.compile({$async: true, properties: {foo: {type: "number"}}}) + const validate = ajv.compile({ + $async: true, + type: "object", + properties: {foo: {type: "number"}}, + }) const result: Promise = validate({foo: 1}) await result.then((data) => data.should.exist) }) it("should have result type promise 2", async () => { - const schema: AsyncSchema = {$async: true, properties: {foo: {type: "number"}}} + const schema: AsyncSchema = { + $async: true, + type: "object", + properties: {foo: {type: "number"}}, + } const validate = ajv.compile(schema) const result: Promise = validate({foo: 1}) await result.then((data) => data.foo.should.equal(1)) @@ -83,7 +100,11 @@ describe("$async validation and type guards", () => { describe("$async: boolean", () => { it("should have result type boolean | promise 1", async () => { - const schema = {$async: true, properties: {foo: {type: "number"}}} + const schema = { + $async: true, + type: "object", + properties: {foo: {type: "number"}}, + } const validate = ajv.compile(schema) const data: unknown = {foo: 1} let result: boolean | Promise @@ -111,8 +132,12 @@ describe("$async validation and type guards", () => { }) describe("$async: unknown", () => { - const schema: Record = {properties: {foo: {type: "number"}}} + const schema: Record = { + type: "object", + properties: {foo: {type: "number"}}, + } const validate = ajv.compile(schema) + it("should have result type boolean", () => { const data = {foo: 1} let result: boolean From 5758319df7c95d7b817fcf2a987bd9d5e541d771 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 21 Sep 2020 12:54:05 +0100 Subject: [PATCH 251/322] fix tests failing because of strictTypes, add allowUnionTypes option --- lib/ajv.ts | 5 +- lib/compile/validate/dataType.ts | 12 +- lib/compile/validate/iterate.ts | 11 +- lib/types/index.ts | 4 +- lib/vocabularies/core/id.ts | 10 + lib/vocabularies/core/index.ts | 3 +- spec/.eslintrc.yml | 1 + spec/ajv.spec.ts | 101 +++++++--- spec/async.spec.ts | 10 +- spec/async/boolean.json | 4 + spec/async/compound.json | 7 +- spec/async/format.json | 1 + spec/async/items.json | 1 + spec/async/keyword.json | 3 + spec/async/properties.json | 1 + spec/async_validate.spec.ts | 32 +-- spec/coercion.spec.ts | 22 ++- spec/errors.spec.ts | 185 +++++++++++------- spec/extras.spec.ts | 1 + ...1_allErrors_custom_keyword_skipped.spec.ts | 1 + spec/issues/182_nan_validation.spec.ts | 2 +- spec/issues/210_mutual_recur_frags.spec.ts | 2 + spec/keyword.spec.ts | 81 +++++--- spec/options/meta_validateSchema.spec.ts | 12 +- spec/options/nullable.spec.ts | 10 +- spec/options/options_add_schemas.spec.ts | 8 +- spec/options/options_refs.spec.ts | 2 +- spec/options/options_validation.spec.ts | 2 + spec/options/schemaId.spec.ts | 19 +- spec/options/strict.spec.ts | 4 +- spec/options/strictDefaults.spec.ts | 8 +- spec/options/strictKeywords.spec.ts | 15 +- spec/options/unknownFormats.spec.ts | 14 +- spec/resolve.spec.ts | 22 ++- 34 files changed, 373 insertions(+), 243 deletions(-) create mode 100644 lib/vocabularies/core/id.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index 264d8ba432..6d164ac391 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -83,6 +83,7 @@ interface CurrentOptions { strict?: boolean | "log" strictTypes?: boolean | "log" allowMatchingProperties?: boolean // disables a strict mode restriction + allowUnionTypes?: boolean validateFormats?: boolean // validation and reporting options: $data?: boolean @@ -455,7 +456,7 @@ export default class Ajv { // If no parameter is passed all schemas but meta-schemas are removed. // If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed. // Even if schema is referenced by other schemas it still can be removed as other schemas have local references. - removeSchema(schemaKeyRef: AnySchema | string | RegExp): Ajv { + removeSchema(schemaKeyRef?: AnySchema | string | RegExp): Ajv { if (schemaKeyRef instanceof RegExp) { this._removeAllSchemas(this.schemas, schemaKeyRef) this._removeAllSchemas(this.refs, schemaKeyRef) @@ -569,7 +570,7 @@ export default class Ajv { if (!errors || errors.length === 0) return "No errors" return errors .map((e) => `${dataVar}${e.dataPath} ${e.message}`) - .reduce((text, msg) => text + msg + separator) + .reduce((text, msg) => text + separator + msg) } $dataMetaSchema(metaSchema: AnySchemaObject, keywordsJsonPointers: string[]): AnySchemaObject { diff --git a/lib/compile/validate/dataType.ts b/lib/compile/validate/dataType.ts index 1f51000c32..b2ac849075 100644 --- a/lib/compile/validate/dataType.ts +++ b/lib/compile/validate/dataType.ts @@ -15,11 +15,13 @@ import {_, str, Name} from "../codegen" export function getSchemaTypes(schema: AnySchemaObject): JSONType[] { const types = getJSONTypes(schema.type) const hasNull = types.includes("null") - if (hasNull && schema.nullable === false) { - throw new Error('{"type": "null"} contradicts {"nullable": "false"}') - } else if (!hasNull && schema.nullable === true) { - if (!types.length) throw new Error('"nullable" cannot be used without "type"') - types.push("null") + if (hasNull) { + if (schema.nullable === false) throw new Error("type: null contradicts nullable: false") + } else { + if (!types.length && schema.nullable !== undefined) { + throw new Error('"nullable" cannot be used without "type"') + } + if (schema.nullable === true) types.push("null") } return types } diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 3a28a09d7e..4e45e706c6 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -66,7 +66,7 @@ function iterateKeywords(it: SchemaObjCxt, group: RuleGroup): void { function checkStrictTypes(it: SchemaObjCxt, types: JSONType[]): void { if (it.schemaEnv.meta || !it.opts.strictTypes) return checkContextTypes(it, types) - checkMultipleTypes(it, types) + if (!it.opts.allowUnionTypes) checkMultipleTypes(it, types) checkKeywordTypes(it, it.dataTypes) } @@ -85,12 +85,8 @@ function checkContextTypes(it: SchemaObjCxt, types: JSONType[]): void { } function checkMultipleTypes(it: SchemaObjCxt, ts: JSONType[]): void { - if ( - ts.length > 1 && - !(ts.length === 2 && ts.includes("null")) && - (ts.includes("object") || ts.includes("array")) - ) { - strictTypesError(it, "multiple non-primitive types") + if (ts.length > 1 && !(ts.length === 2 && ts.includes("null"))) { + strictTypesError(it, "use allowUnionTypes to allow union type keyword") } } @@ -119,6 +115,5 @@ function includesType(ts: JSONType[], t: JSONType): boolean { function strictTypesError(it: SchemaObjCxt, msg: string): void { const schemaPath = it.schemaEnv.baseId + it.errSchemaPath msg += ` at "${schemaPath}" (strictTypes)` - // throw new Error(msg) checkStrictMode(it, msg, it.opts.strictTypes) } diff --git a/lib/types/index.ts b/lib/types/index.ts index 8370b59f6b..5835a4833b 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -175,14 +175,14 @@ export type FormatCompare = (data1: T, data2: T) => b export type AsyncFormatValidator = (data: T) => Promise export interface FormatDefinition { - type: T extends string ? "string" | undefined : "number" + type?: T extends string ? "string" | undefined : "number" validate: FormatValidator | (T extends string ? string | RegExp : never) async?: false | undefined compare?: FormatCompare } export interface AsyncFormatDefinition { - type: T extends string ? "string" | undefined : "number" + type?: T extends string ? "string" | undefined : "number" validate: AsyncFormatValidator async: true compare?: FormatCompare diff --git a/lib/vocabularies/core/id.ts b/lib/vocabularies/core/id.ts new file mode 100644 index 0000000000..aa36c4bb20 --- /dev/null +++ b/lib/vocabularies/core/id.ts @@ -0,0 +1,10 @@ +import type {CodeKeywordDefinition} from "../../types" + +const def: CodeKeywordDefinition = { + keyword: "id", + code() { + throw new Error('NOT SUPPORTED: keyword "id", use "$id" for schema ID') + }, +} + +export default def diff --git a/lib/vocabularies/core/index.ts b/lib/vocabularies/core/index.ts index 5b8fb6f364..4e641b9967 100644 --- a/lib/vocabularies/core/index.ts +++ b/lib/vocabularies/core/index.ts @@ -1,6 +1,7 @@ import type {Vocabulary} from "../../types" +import idKeyword from "./id" import refKeyword from "./ref" -const core: Vocabulary = ["$schema", "$id", "$defs", "definitions", refKeyword] +const core: Vocabulary = ["$schema", "$id", "$defs", "definitions", idKeyword, refKeyword] export default core diff --git a/spec/.eslintrc.yml b/spec/.eslintrc.yml index c31cffef6a..708a730d46 100644 --- a/spec/.eslintrc.yml +++ b/spec/.eslintrc.yml @@ -23,3 +23,4 @@ overrides: "@typescript-eslint/no-unsafe-member-access": off "@typescript-eslint/no-unsafe-return": off "@typescript-eslint/no-var-requires": off + "@typescript-eslint/ban-ts-comment": off diff --git a/spec/ajv.spec.ts b/spec/ajv.spec.ts index 6e01137d06..5d499ff48a 100644 --- a/spec/ajv.spec.ts +++ b/spec/ajv.spec.ts @@ -1,13 +1,15 @@ +import type Ajv from ".." import _Ajv from "./ajv" import {_} from "../dist/compile/codegen" +import assert from "assert" import chai from "./chai" const should = chai.should() describe("Ajv", () => { - let ajv + let ajv: Ajv beforeEach(() => { - ajv = new _Ajv() + ajv = new _Ajv({keywords: ["foo"], allowUnionTypes: true}) }) it("should create instance", () => { @@ -38,13 +40,13 @@ describe("Ajv", () => { ajv.compile({$id: "//e.com/int.json", type: "integer"}) should.throw(() => { ajv.compile({$id: "//e.com/int.json", type: "integer", minimum: 1}) - }) + }, /already exists/) }) it("should throw if invalid schema is compiled", () => { should.throw(() => { ajv.compile({type: null}) - }) + }, /should be equal to one of the allowed values/) }) it("should throw if compiled schema has an invalid JavaScript code", () => { @@ -58,7 +60,7 @@ describe("Ajv", () => { schema = {even: false} should.throw(() => { _ajv.compile(schema) - }) + }, /Unexpected token/) function badEvenCode(cxt) { const op = cxt.schema ? _`===` : _`!===` // invalid on purpose @@ -90,7 +92,7 @@ describe("Ajv", () => { ajv.validate("integer", 1).should.equal(true) should.throw(() => { ajv.validate("string", "foo") - }) + }, /no schema with key or ref/) }) it("should validate schema fragment by ref", () => { @@ -124,7 +126,7 @@ describe("Ajv", () => { it("should add and compile schema with key", () => { ajv.addSchema({type: "integer"}, "int") const validate = ajv.getSchema("int") - validate.should.be.a("function") + assert(typeof validate == "function") validate(1).should.equal(true) validate(1.1).should.equal(false) @@ -165,6 +167,8 @@ describe("Ajv", () => { const validate0 = ajv.getSchema("//e.com/int.json") const validate1 = ajv.getSchema("//e.com/str.json") + assert(typeof validate0 == "function") + assert(typeof validate1 == "function") validate0(1).should.equal(true) validate0("1").should.equal(false) @@ -181,40 +185,42 @@ describe("Ajv", () => { ajv.addSchema({type: "integer"}, "int") should.throw(() => { ajv.addSchema({type: "integer", minimum: 1}, "int") - }) + }, /already exists/) }) it("should throw on duplicate normalized key", () => { ajv.addSchema({type: "number"}, "num") should.throw(() => { ajv.addSchema({type: "integer"}, "num#") - }) + }, /already exists/) should.throw(() => { ajv.addSchema({type: "integer"}, "num#/") - }) + }, /already exists/) }) it("should allow only one schema without key and id", () => { ajv.addSchema({type: "number"}) should.throw(() => { ajv.addSchema({type: "integer"}) - }) + }, /already exists/) should.throw(() => { ajv.addSchema({type: "integer"}, "") - }) + }, /already exists/) should.throw(() => { ajv.addSchema({type: "integer"}, "#") - }) + }, /already exists/) }) it("should throw if schema is not an object", () => { should.throw(() => { + // @ts-expect-error ajv.addSchema("foo") - }) + }, /schema must be object or boolean/) }) it("should throw if schema id is not a string", () => { try { + // @ts-expect-error ajv.addSchema({$id: 1, type: "integer"}) throw new Error("should have throw exception") } catch (e) { @@ -232,6 +238,7 @@ describe("Ajv", () => { it("should return compiled schema by key", () => { ajv.addSchema({type: "integer"}, "int") const validate = ajv.getSchema("int") + assert(typeof validate == "function") validate(1).should.equal(true) validate("1").should.equal(false) }) @@ -239,6 +246,7 @@ describe("Ajv", () => { it("should return compiled schema by id or ref", () => { ajv.addSchema({$id: "//e.com/int.json", type: "integer"}) const validate = ajv.getSchema("//e.com/int.json") + assert(typeof validate == "function") validate(1).should.equal(true) validate("1").should.equal(false) }) @@ -246,12 +254,9 @@ describe("Ajv", () => { it("should return compiled schema without key or with empty key", () => { ajv.addSchema({type: "integer"}) const validate = ajv.getSchema("") + assert(typeof validate == "function") validate(1).should.equal(true) validate("1").should.equal(false) - - const v = ajv.getSchema() - v(1).should.equal(true) - v("1").should.equal(false) }) it("should return schema fragment by ref", () => { @@ -264,6 +269,7 @@ describe("Ajv", () => { }) const vInt = ajv.getSchema("http://e.com/types.json#/definitions/int") + assert(typeof vInt == "function") vInt(1).should.equal(true) vInt("1").should.equal(false) }) @@ -278,6 +284,7 @@ describe("Ajv", () => { }) const vInt = ajv.getSchema("//e.com/types.json#/definitions/int") + assert(typeof vInt == "function") vInt(1).should.equal(true) vInt("1").should.equal(false) }) @@ -292,6 +299,7 @@ describe("Ajv", () => { }) const vInt = ajv.getSchema("http://e.com/types.json#int") + assert(typeof vInt == "function") vInt(1).should.equal(true) vInt("1").should.equal(false) }) @@ -302,12 +310,14 @@ describe("Ajv", () => { const schema = {type: "integer"} ajv.addSchema(schema, "int") const v = ajv.getSchema("int") - + assert(typeof v == "function") v.should.be.a("function") + //@ts-expect-error ajv._cache.get(schema).validate.should.equal(v) ajv.removeSchema("int") should.not.exist(ajv.getSchema("int")) + //@ts-expect-error should.not.exist(ajv._cache.get(schema)) }) @@ -316,28 +326,35 @@ describe("Ajv", () => { ajv.addSchema(schema) const v = ajv.getSchema("//e.com/int.json") + assert(typeof v == "function") v.should.be.a("function") + //@ts-expect-error ajv._cache.get(schema).validate.should.equal(v) ajv.removeSchema("//e.com/int.json") should.not.exist(ajv.getSchema("//e.com/int.json")) + //@ts-expect-error should.not.exist(ajv._cache.get(schema)) }) it("should remove schema by schema object", () => { const schema = {type: "integer"} ajv.addSchema(schema) + //@ts-expect-error ajv._cache.get(schema).should.be.an("object") ajv.removeSchema(schema) + //@ts-expect-error should.not.exist(ajv._cache.get(schema)) }) it("should remove schema with id by schema object", () => { const schema = {$id: "//e.com/int.json", type: "integer"} ajv.addSchema(schema) + //@ts-expect-error ajv._cache.get(schema).should.be.an("object") ajv.removeSchema(schema) should.not.exist(ajv.getSchema("//e.com/int.json")) + //@ts-expect-error should.not.exist(ajv._cache.get(schema)) }) @@ -351,33 +368,43 @@ describe("Ajv", () => { it("should remove all schemas but meta-schemas if called without an arguments", () => { const schema1 = {$id: "//e.com/int.json", type: "integer"} ajv.addSchema(schema1) + //@ts-expect-error ajv._cache.get(schema1).should.be.an("object") const schema2 = {type: "integer"} ajv.addSchema(schema2) + //@ts-expect-error ajv._cache.get(schema2).should.be.an("object") ajv.removeSchema() + //@ts-expect-error should.not.exist(ajv._cache.get(schema1)) + //@ts-expect-error should.not.exist(ajv._cache.get(schema2)) }) it("should remove all schemas but meta-schemas with key/id matching pattern", () => { const schema1 = {$id: "//e.com/int.json", type: "integer"} ajv.addSchema(schema1) + //@ts-expect-error ajv._cache.get(schema1).should.be.an("object") const schema2 = {$id: "str.json", type: "string"} ajv.addSchema(schema2, "//e.com/str.json") + //@ts-expect-error ajv._cache.get(schema2).should.be.an("object") const schema3 = {type: "integer"} ajv.addSchema(schema3) + //@ts-expect-error ajv._cache.get(schema3).should.be.an("object") ajv.removeSchema(/e\.com/) + //@ts-expect-error should.not.exist(ajv._cache.get(schema1)) + //@ts-expect-error should.not.exist(ajv._cache.get(schema2)) + //@ts-expect-error ajv._cache.get(schema3).should.be.an("object") }) @@ -405,7 +432,7 @@ describe("Ajv", () => { it("should add format as object", () => { ajv.addFormat("identifier", { - validate: (str) => /^[a-z_$][a-z0-9_$]*$/i.test(str), + validate: (str: string) => /^[a-z_$][a-z0-9_$]*$/i.test(str), }) testFormat() }) @@ -416,7 +443,10 @@ describe("Ajv", () => { }) function testFormat() { - const validate = ajv.compile({format: "identifier"}) + const validate = ajv.compile({ + type: ["number", "string"], + format: "identifier", + }) validate("Abc1").should.equal(true) validate("123").should.equal(false) validate(123).should.equal(true) @@ -432,6 +462,7 @@ describe("Ajv", () => { }) const validate = ajv.compile({ + type: ["string", "number"], format: "positive", }) validate(-2).should.equal(false) @@ -441,7 +472,7 @@ describe("Ajv", () => { }) it("should validate numbers with format via $data", () => { - ajv = new _Ajv({$data: true}) + ajv = new _Ajv({$data: true, allowUnionTypes: true}) ajv.addFormat("positive", { type: "number", validate: function (x) { @@ -450,8 +481,12 @@ describe("Ajv", () => { }) const validate = ajv.compile({ + type: "object", properties: { - data: {format: {$data: "1/frmt"}}, + data: { + type: ["number", "string"], + format: {$data: "1/frmt"}, + }, frmt: {type: "string"}, }, }) @@ -479,6 +514,7 @@ describe("Ajv", () => { }) valid.should.equal(false) + assert(Array.isArray(ajv.errors)) ajv.errors.length.should.equal(3) ajv.errors[0].keyword.should.equal("enum") ajv.errors[1].keyword.should.equal("type") @@ -491,58 +527,59 @@ describe("Ajv", () => { $schema: "http://example.com/unknown/schema#", type: "number", }) - }) + }, /no schema with key or ref/) }) it("should throw exception if $schema is not a string", () => { should.throw(() => { ajv.validateSchema({ + //@ts-expect-error $schema: {}, type: "number", }) - }) + }, /\$schema must be a string/) }) describe("sub-schema validation outside of definitions during compilation", () => { it("maximum", () => { passValidationThrowCompile({ $ref: "#/foo", - foo: {maximum: "bar"}, + foo: {type: "number", maximum: "bar"}, }) }) it("exclusiveMaximum", () => { passValidationThrowCompile({ $ref: "#/foo", - foo: {exclusiveMaximum: "bar"}, + foo: {type: "number", exclusiveMaximum: "bar"}, }) }) it("maxItems", () => { passValidationThrowCompile({ $ref: "#/foo", - foo: {maxItems: "bar"}, + foo: {type: "array", maxItems: "bar"}, }) }) it("maxLength", () => { passValidationThrowCompile({ $ref: "#/foo", - foo: {maxLength: "bar"}, + foo: {type: "string", maxLength: "bar"}, }) }) it("maxProperties", () => { passValidationThrowCompile({ $ref: "#/foo", - foo: {maxProperties: "bar"}, + foo: {type: "object", maxProperties: "bar"}, }) }) it("multipleOf", () => { passValidationThrowCompile({ $ref: "#/foo", - foo: {maxProperties: "bar"}, + foo: {type: "number", multipleOf: "bar"}, }) }) @@ -550,7 +587,7 @@ describe("Ajv", () => { ajv.validateSchema(schema).should.equal(true) should.throw(() => { ajv.compile(schema) - }) + }, /value must be/) } }) }) diff --git a/spec/async.spec.ts b/spec/async.spec.ts index 3f0198222c..5de05c8ab2 100644 --- a/spec/async.spec.ts +++ b/spec/async.spec.ts @@ -52,6 +52,7 @@ describe("compileAsync method", () => { "http://example.com/foobar.json": { $id: "http://example.com/foobar.json", $schema: "http://example.com/foobar_meta.json", + type: "string", myFooBar: "foo", }, "http://example.com/foobar_meta.json": { @@ -139,6 +140,7 @@ describe("compileAsync method", () => { it("should correctly compile with remote schemas that have mutual references", () => { const schema = { $id: "http://example.com/root.json", + type: "object", properties: { tree: {$ref: "tree.json"}, }, @@ -279,12 +281,8 @@ describe("compileAsync method", () => { } ajv = new _Ajv() should.throw(() => { - ajv.compileAsync(schema).then(expextedSyncError, expextedSyncError) - }) - - function expextedSyncError() { - throw new Error("it should have thrown exception") - } + ajv.compileAsync(schema) + }, "options.loadSchema should be a function") }) describe("should return error via promise", () => { diff --git a/spec/async/boolean.json b/spec/async/boolean.json index db2226fb6c..f5f77d9f15 100644 --- a/spec/async/boolean.json +++ b/spec/async/boolean.json @@ -3,6 +3,7 @@ "description": "boolean schema = true in properties", "schema": { "$async": true, + "type": "object", "properties": { "foo": true } @@ -19,6 +20,7 @@ "description": "boolean schema = false in properties", "schema": { "$async": true, + "type": "object", "properties": { "foo": false } @@ -79,6 +81,7 @@ "description": "boolean schema = true in properties with $ref", "schema": { "$async": true, + "type": "object", "properties": { "foo": {"$ref": "#/definitions/foo"} }, @@ -98,6 +101,7 @@ "description": "boolean schema = false in properties with $ref", "schema": { "$async": true, + "type": "object", "properties": { "foo": {"$ref": "#/definitions/foo"} }, diff --git a/spec/async/compound.json b/spec/async/compound.json index 23c78aee10..3541227cac 100644 --- a/spec/async/compound.json +++ b/spec/async/compound.json @@ -3,12 +3,12 @@ "description": "allOf: async + sync", "schema": { "$async": true, + "type": "integer", "allOf": [ { "idExists": {"table": "users"} }, { - "type": "integer", "minimum": 3 } ] @@ -40,12 +40,12 @@ "description": "anyOf: async + sync", "schema": { "$async": true, + "type": "integer", "anyOf": [ { "idExists": {"table": "users"} }, { - "type": "integer", "minimum": 3 } ] @@ -77,12 +77,12 @@ "description": "oneOf: async + sync", "schema": { "$async": true, + "type": "integer", "oneOf": [ { "idExists": {"table": "users"} }, { - "type": "integer", "minimum": 3 } ] @@ -114,6 +114,7 @@ "description": "not with async", "schema": { "$async": true, + "type": "integer", "not": { "idExists": {"table": "users"} } diff --git a/spec/async/format.json b/spec/async/format.json index f699b556fd..3be680294c 100644 --- a/spec/async/format.json +++ b/spec/async/format.json @@ -33,6 +33,7 @@ "description": "async formats when $data ref resolves to async format name", "schema": { "$async": true, + "type": "object", "additionalProperties": { "type": "string", "format": {"$data": "0#"} diff --git a/spec/async/items.json b/spec/async/items.json index 9885610237..9b3b257733 100644 --- a/spec/async/items.json +++ b/spec/async/items.json @@ -3,6 +3,7 @@ "description": "items: async + sync", "schema": { "$async": true, + "type": "array", "items": [ { "type": "integer", diff --git a/spec/async/keyword.json b/spec/async/keyword.json index 3178dd9be2..650a9ba571 100644 --- a/spec/async/keyword.json +++ b/spec/async/keyword.json @@ -3,6 +3,7 @@ "description": "async keywords (validated)", "schema": { "$async": true, + "type": "object", "properties": { "userId": { "type": "integer", @@ -51,6 +52,7 @@ "description": "async user-defined keywords (validated with errors)", "schema": { "$async": true, + "type": "object", "properties": { "userId": { "type": "integer", @@ -99,6 +101,7 @@ "description": "async user-defined keywords (compiled)", "schema": { "$async": true, + "type": "object", "properties": { "userId": { "type": "integer", diff --git a/spec/async/properties.json b/spec/async/properties.json index e905228f32..5ae7e8a69f 100644 --- a/spec/async/properties.json +++ b/spec/async/properties.json @@ -3,6 +3,7 @@ "description": "properties: async + sync", "schema": { "$async": true, + "type": "object", "properties": { "foo": { "type": "integer", diff --git a/spec/async_validate.spec.ts b/spec/async_validate.spec.ts index 56a2922db9..d7400db543 100644 --- a/spec/async_validate.spec.ts +++ b/spec/async_validate.spec.ts @@ -37,6 +37,7 @@ describe("async schemas, formats and keywords", function () { it("should fail compilation if async schema is inside sync schema", () => { const schema: any = { + type: "object", properties: { foo: { $async: true, @@ -46,9 +47,9 @@ describe("async schemas, formats and keywords", function () { }, } - shouldThrowFunc("async schema in sync schema", () => { + should.throw(() => { ajv.compile(schema) - }) + }, "async schema in sync schema") ajv.compile({...schema, $async: true}) }) @@ -64,9 +65,9 @@ describe("async schemas, formats and keywords", function () { format: "english_word", } - shouldThrowFunc("async format in sync schema", () => { + should.throw(() => { _ajv.compile(schema) - }) + }, "async format in sync schema") schema = {...schema, $async: true} _ajv.compile(schema) }) @@ -106,9 +107,9 @@ describe("async schemas, formats and keywords", function () { }, } - shouldThrowFunc("async keyword in sync schema", () => { + should.throw(() => { _ajv.compile(schema) - }) + }, "async keyword in sync schema") schema = {...schema, $async: true} _ajv.compile(schema) @@ -196,6 +197,7 @@ describe("async schemas, formats and keywords", function () { format: "english_word", }, }, + type: "object", properties: { word: {$ref: "#/definitions/english_word"}, }, @@ -341,9 +343,9 @@ describe("async schemas, formats and keywords", function () { validate: checkWordOnServer, }) - shouldThrowFunc("async schema referenced by sync schema", () => { + should.throw(() => { ajv.compile(schema) - }) + }, "async schema referenced by sync schema") schema = {...schema, $id: "http://e.com/obj2.json#", $async: true} @@ -396,20 +398,6 @@ function checkWordOnServer(str) { : Promise.reject(new Error("unknown word")) } -function shouldThrowFunc(message, func) { - let err - should.throw(() => { - try { - func() - } catch (e) { - err = e - throw e - } - }) - - err.message.should.equal(message) -} - function shouldBeValid(p, data) { return p.then((valid) => valid.should.equal(data)) } diff --git a/spec/coercion.spec.ts b/spec/coercion.spec.ts index c48f4b62bb..09af2b9823 100644 --- a/spec/coercion.spec.ts +++ b/spec/coercion.spec.ts @@ -1,3 +1,4 @@ +import type Ajv from ".." import _Ajv from "./ajv" require("./chai").should() @@ -176,11 +177,11 @@ coercionArrayRules.array = { } describe("Type coercion", () => { - let ajv, fullAjv, instances + let ajv: Ajv, fullAjv: Ajv, instances: Ajv[] beforeEach(() => { - ajv = new _Ajv({coerceTypes: true, verbose: true}) - fullAjv = new _Ajv({coerceTypes: true, verbose: true, allErrors: true}) + ajv = new _Ajv({coerceTypes: true, verbose: true, allowUnionTypes: true}) + fullAjv = new _Ajv({coerceTypes: true, verbose: true, allErrors: true, allowUnionTypes: true}) instances = [ajv, fullAjv] }) @@ -349,7 +350,7 @@ describe("Type coercion", () => { }) it("should update data if the schema is in ref that is not inlined", () => { - instances.push(new _Ajv({coerceTypes: true, inlineRefs: false})) + instances.push(new _Ajv({coerceTypes: true, inlineRefs: false, allowUnionTypes: true})) const schema = { type: "object", @@ -394,6 +395,7 @@ describe("Type coercion", () => { }, }, }, + type: "object", properties: { foo: {$ref: "http://e.com/foo.json#"}, }, @@ -423,17 +425,18 @@ describe("Type coercion", () => { instances.forEach((_ajv) => { const validate = _ajv.compile(schema) validate(9).should.equal(false) - validate.errors.length.should.equal(1) + validate.errors?.length.should.equal(1) validate(11).should.equal(true) validate("foo").should.equal(false) - validate.errors.length.should.equal(1) + validate.errors?.length.should.equal(1) }) }) it('should check "uniqueItems" after coercion', () => { const schema = { + type: "array", items: {type: "number"}, uniqueItems: true, } @@ -443,13 +446,14 @@ describe("Type coercion", () => { validate([1, "2", 3]).should.equal(true) validate([1, "2", 2]).should.equal(false) - validate.errors.length.should.equal(1) - validate.errors[0].keyword.should.equal("uniqueItems") + validate.errors?.length.should.equal(1) + validate.errors?.[0].keyword.should.equal("uniqueItems") }) }) it('should check "contains" after coercion', () => { const schema = { + type: "array", items: {type: "number"}, contains: {const: 2}, } @@ -459,7 +463,7 @@ describe("Type coercion", () => { validate([1, "2", 3]).should.equal(true) validate([1, "3", 4]).should.equal(false) - validate.errors.pop().keyword.should.equal("contains") + validate.errors?.pop()?.keyword.should.equal("contains") }) }) diff --git a/spec/errors.spec.ts b/spec/errors.spec.ts index 34e2706ef3..7d77c9f2f9 100644 --- a/spec/errors.spec.ts +++ b/spec/errors.spec.ts @@ -1,26 +1,29 @@ -import Ajv from "./ajv" - +import type Ajv from ".." +import type {ValidateFunction} from ".." +import _Ajv from "./ajv" import chai from "./chai" const should = chai.should() describe("Validation errors", () => { - let ajv, ajvJP, fullAjv + let ajv: Ajv, ajvJP: Ajv, fullAjv: Ajv beforeEach(() => { createInstances() }) function createInstances() { - ajv = new Ajv({loopRequired: 21}) - ajvJP = new Ajv({ + ajv = new _Ajv({loopRequired: 21, strictTypes: false}) + ajvJP = new _Ajv({ loopRequired: 21, + strictTypes: false, }) - fullAjv = new Ajv({ + fullAjv = new _Ajv({ allErrors: true, verbose: true, jsPropertySyntax: true, // deprecated loopRequired: 21, logger: false, + strictTypes: false, }) } @@ -69,15 +72,22 @@ describe("Validation errors", () => { const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError(validate.errors[0], "additionalProperties", "#/additionalProperties", "", msg, { - additionalProperty: "baz", - }) + shouldBeError( + validate.errors?.[0], + "additionalProperties", + "#/additionalProperties", + "", + msg, + { + additionalProperty: "baz", + } + ) const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData) shouldBeError( - validateJP.errors[0], + validateJP.errors?.[0], "additionalProperties", "#/additionalProperties", "", @@ -89,7 +99,7 @@ describe("Validation errors", () => { shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData, 2) shouldBeError( - fullValidate.errors[0], + fullValidate.errors?.[0], "additionalProperties", "#/additionalProperties", "", @@ -97,7 +107,7 @@ describe("Validation errors", () => { {additionalProperty: "baz"} ) shouldBeError( - fullValidate.errors[1], + fullValidate.errors?.[1], "additionalProperties", "#/additionalProperties", "", @@ -134,24 +144,24 @@ describe("Validation errors", () => { const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError(validate.errors[0], "type", schPath, "/baz/quux", "should be string", { + shouldBeError(validate.errors?.[0], "type", schPath, "/baz/quux", "should be string", { type: "string", }) const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData) - shouldBeError(validateJP.errors[0], "type", schPath, "/baz/quux", "should be string", { + shouldBeError(validateJP.errors?.[0], "type", schPath, "/baz/quux", "should be string", { type: "string", }) const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData, 2) - shouldBeError(fullValidate.errors[0], "type", schPath, "['baz'].quux", "should be string", { + shouldBeError(fullValidate.errors?.[0], "type", schPath, "['baz'].quux", "should be string", { type: "string", }) - shouldBeError(fullValidate.errors[1], "type", schPath, "['boo'].quux", "should be string", { + shouldBeError(fullValidate.errors?.[1], "type", schPath, "['boo'].quux", "should be string", { type: "string", }) } @@ -198,36 +208,36 @@ describe("Validation errors", () => { const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("1"), { + shouldBeError(validate.errors?.[0], "required", schPath, "", requiredMsg("1"), { missingProperty: "1", }) shouldBeInvalid(validate, invalidData2, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("2"), { + shouldBeError(validate.errors?.[0], "required", schPath, "", requiredMsg("2"), { missingProperty: "2", }) const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("1"), { + shouldBeError(validateJP.errors?.[0], "required", schPath, "", requiredMsg("1"), { missingProperty: "1", }) shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("2"), { + shouldBeError(validateJP.errors?.[0], "required", schPath, "", requiredMsg("2"), { missingProperty: "2", }) const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("1"), { + shouldBeError(fullValidate.errors?.[0], "required", schPath, "", requiredMsg("1"), { missingProperty: "1", }) shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("2"), { + shouldBeError(fullValidate.errors?.[0], "required", schPath, "", requiredMsg("2"), { missingProperty: "2", }) - shouldBeError(fullValidate.errors[1], "required", schPath, "", requiredMsg("98"), { + shouldBeError(fullValidate.errors?.[1], "required", schPath, "", requiredMsg("98"), { missingProperty: "98", }) } @@ -263,9 +273,10 @@ describe("Validation errors", () => { } it("should not validate required twice in large schemas with loopRequired option", () => { - ajv = new Ajv({loopRequired: 1, allErrors: true}) + ajv = new _Ajv({loopRequired: 1, allErrors: true}) const schema = { + type: "object", properties: { foo: {type: "integer"}, bar: {type: "integer"}, @@ -276,13 +287,14 @@ describe("Validation errors", () => { const validate = ajv.compile(schema) validate({}).should.equal(false) - validate.errors.should.have.length(2) + validate.errors?.should.have.length(2) }) it("should not validate required twice with $data ref", () => { - ajv = new Ajv({$data: true, allErrors: true}) + ajv = new _Ajv({$data: true, allErrors: true}) const schema = { + type: "object", properties: { foo: {type: "integer"}, bar: {type: "integer"}, @@ -293,15 +305,16 @@ describe("Validation errors", () => { const validate = ajv.compile(schema) validate({requiredProperties: ["foo", "bar"]}).should.equal(false) - validate.errors.should.have.length(2) + validate.errors?.should.have.length(2) }) it("should show different error when required is $data of incorrect type", () => { - test(new Ajv({$data: true})) - test(new Ajv({$data: true, allErrors: true})) + test(new _Ajv({$data: true})) + test(new _Ajv({$data: true, allErrors: true})) function test(_ajv) { const schema = { + type: "object", required: {$data: "0/req"}, properties: { req: {}, @@ -315,7 +328,7 @@ describe("Validation errors", () => { shouldBeValid(validate, {req: ["foo", "bar"], foo: 1, bar: 2}) shouldBeInvalid(validate, {req: ["foo", "bar"], foo: 1}) shouldBeError( - validate.errors[0], + validate.errors?.[0], "required", "#/required", "", @@ -325,7 +338,7 @@ describe("Validation errors", () => { shouldBeInvalid(validate, {req: "invalid"}) shouldBeError( - validate.errors[0], + validate.errors?.[0], "required", "#/required", "", @@ -357,22 +370,36 @@ describe("Validation errors", () => { const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params("bar")) + shouldBeError(validate.errors?.[0], "dependencies", "#/dependencies", "", msg, params("bar")) shouldBeInvalid(validate, invalidData2) - shouldBeError(validate.errors[0], "dependencies", "#/dependencies", "", msg, params("foo")) + shouldBeError(validate.errors?.[0], "dependencies", "#/dependencies", "", msg, params("foo")) const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1) - shouldBeError(validateJP.errors[0], "dependencies", "#/dependencies", "", msg, params("bar")) + shouldBeError( + validateJP.errors?.[0], + "dependencies", + "#/dependencies", + "", + msg, + params("bar") + ) shouldBeInvalid(validateJP, invalidData2) - shouldBeError(validateJP.errors[0], "dependencies", "#/dependencies", "", msg, params("foo")) + shouldBeError( + validateJP.errors?.[0], + "dependencies", + "#/dependencies", + "", + msg, + params("foo") + ) const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1) shouldBeError( - fullValidate.errors[0], + fullValidate.errors?.[0], "dependencies", "#/dependencies", "", @@ -381,7 +408,7 @@ describe("Validation errors", () => { ) shouldBeInvalid(fullValidate, invalidData2, 2) shouldBeError( - fullValidate.errors[0], + fullValidate.errors?.[0], "dependencies", "#/dependencies", "", @@ -389,7 +416,7 @@ describe("Validation errors", () => { params("foo") ) shouldBeError( - fullValidate.errors[1], + fullValidate.errors?.[1], "dependencies", "#/dependencies", "", @@ -419,36 +446,36 @@ describe("Validation errors", () => { const validate = ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("bar"), { + shouldBeError(validate.errors?.[0], "required", schPath, "", requiredMsg("bar"), { missingProperty: "bar", }) shouldBeInvalid(validate, invalidData2, 1 + extraErrors) - shouldBeError(validate.errors[0], "required", schPath, "", requiredMsg("foo"), { + shouldBeError(validate.errors?.[0], "required", schPath, "", requiredMsg("foo"), { missingProperty: "foo", }) const validateJP = ajvJP.compile(schema) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("bar"), { + shouldBeError(validateJP.errors?.[0], "required", schPath, "", requiredMsg("bar"), { missingProperty: "bar", }) shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors) - shouldBeError(validateJP.errors[0], "required", schPath, "", requiredMsg("foo"), { + shouldBeError(validateJP.errors?.[0], "required", schPath, "", requiredMsg("foo"), { missingProperty: "foo", }) const fullValidate = fullAjv.compile(schema) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("bar"), { + shouldBeError(fullValidate.errors?.[0], "required", schPath, "", requiredMsg("bar"), { missingProperty: "bar", }) shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors) - shouldBeError(fullValidate.errors[0], "required", schPath, "", requiredMsg("foo"), { + shouldBeError(fullValidate.errors?.[0], "required", schPath, "", requiredMsg("foo"), { missingProperty: "foo", }) - shouldBeError(fullValidate.errors[1], "required", schPath, "", requiredMsg("baz"), { + shouldBeError(fullValidate.errors?.[1], "required", schPath, "", requiredMsg("baz"), { missingProperty: "baz", }) } @@ -474,24 +501,24 @@ describe("Validation errors", () => { let validate = ajv.compile(schema1) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") + shouldBeError(validate.errors?.[0], "minimum", "#/items/minimum", "/0", "should be >= 10") shouldBeInvalid(validate, invalidData2) - shouldBeError(validate.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") + shouldBeError(validate.errors?.[0], "minimum", "#/items/minimum", "/1", "should be >= 10") const validateJP = ajvJP.compile(schema1) shouldBeValid(validateJP, data) shouldBeInvalid(validateJP, invalidData1) - shouldBeError(validateJP.errors[0], "minimum", "#/items/minimum", "/0", "should be >= 10") + shouldBeError(validateJP.errors?.[0], "minimum", "#/items/minimum", "/0", "should be >= 10") shouldBeInvalid(validateJP, invalidData2) - shouldBeError(validateJP.errors[0], "minimum", "#/items/minimum", "/1", "should be >= 10") + shouldBeError(validateJP.errors?.[0], "minimum", "#/items/minimum", "/1", "should be >= 10") const fullValidate = fullAjv.compile(schema1) shouldBeValid(fullValidate, data) shouldBeInvalid(fullValidate, invalidData1) - shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "[0]", "should be >= 10") + shouldBeError(fullValidate.errors?.[0], "minimum", "#/items/minimum", "[0]", "should be >= 10") shouldBeInvalid(fullValidate, invalidData2, 2) - shouldBeError(fullValidate.errors[0], "minimum", "#/items/minimum", "[1]", "should be >= 10") - shouldBeError(fullValidate.errors[1], "minimum", "#/items/minimum", "[3]", "should be >= 10") + shouldBeError(fullValidate.errors?.[0], "minimum", "#/items/minimum", "[1]", "should be >= 10") + shouldBeError(fullValidate.errors?.[1], "minimum", "#/items/minimum", "[3]", "should be >= 10") const schema2 = { $id: "schema2", @@ -502,9 +529,9 @@ describe("Validation errors", () => { validate = ajv.compile(schema2) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData1) - shouldBeError(validate.errors[0], "minimum", "#/items/0/minimum", "/0", "should be >= 10") + shouldBeError(validate.errors?.[0], "minimum", "#/items/0/minimum", "/0", "should be >= 10") shouldBeInvalid(validate, invalidData2) - shouldBeError(validate.errors[0], "minimum", "#/items/2/minimum", "/2", "should be >= 12") + shouldBeError(validate.errors?.[0], "minimum", "#/items/2/minimum", "/2", "should be >= 12") }) it("should have correct schema path for additionalItems", () => { @@ -526,7 +553,7 @@ describe("Validation errors", () => { shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) shouldBeError( - validate.errors[0], + validate.errors?.[0], "additionalItems", "#/additionalItems", "", @@ -558,19 +585,19 @@ describe("Validation errors", () => { test(ajvJP, 2) test(fullAjv, 4) - function test(_ajv, numErrors) { + function test(_ajv: Ajv, numErrors: number) { const validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData, numErrors) shouldBeError( - validate.errors[0], + validate.errors?.[0], "pattern", "#/propertyNames/pattern", "", 'should match pattern "bar"' ) shouldBeError( - validate.errors[1], + validate.errors?.[1], "propertyNames", "#/propertyNames", "", @@ -578,14 +605,14 @@ describe("Validation errors", () => { ) if (numErrors === 4) { shouldBeError( - validate.errors[2], + validate.errors?.[2], "pattern", "#/propertyNames/pattern", "", 'should match pattern "bar"' ) shouldBeError( - validate.errors[3], + validate.errors?.[3], "propertyNames", "#/propertyNames", "", @@ -722,12 +749,12 @@ describe("Validation errors", () => { test(fullAjv, 2) }) - function test(_ajv, numErrors?: number) { + function test(_ajv: Ajv, numErrors?: number) { const schema = { type: ["array", "object"], minItems: 2, minProperties: 2, - minimum: 5, + minimum: 5, // this keyword would log/throw in strictTypes mode } const validate = _ajv.compile(schema) @@ -735,7 +762,7 @@ describe("Validation errors", () => { shouldBeValid(validate, {foo: 1, bar: 2}) shouldBeInvalid(validate, [1]) shouldBeInvalid(validate, {foo: 1}) - shouldBeInvalid(validate, 5) + shouldBeInvalid(validate, 5) // fails because number not allowed shouldBeInvalid(validate, 4, numErrors) } }) @@ -767,7 +794,7 @@ describe("Validation errors", () => { }) function testError(keyword, message, params) { - const err = validate.errors[0] + const err = validate.errors?.[0] shouldBeError(err, keyword, "#/" + keyword, "", message, params) } }) @@ -775,6 +802,7 @@ describe("Validation errors", () => { it("should include limits in error message with $data", () => { const schema = { + type: "object", properties: { smaller: { type: "number", @@ -784,8 +812,8 @@ describe("Validation errors", () => { }, } - ajv = new Ajv({$data: true}) - fullAjv = new Ajv({ + ajv = new _Ajv({$data: true}) + fullAjv = new _Ajv({ $data: true, allErrors: true, verbose: true, @@ -804,7 +832,7 @@ describe("Validation errors", () => { testError() function testError() { - const err = validate.errors[0] + const err = validate.errors?.[0] shouldBeError( err, "exclusiveMaximum", @@ -819,10 +847,11 @@ describe("Validation errors", () => { }) describe("if/then/else errors", () => { - let validate, numErrors + let validate: ValidateFunction, numErrors it("if/then/else should include failing keyword in message and params", () => { const schema = { + type: "number", if: {maximum: 10}, then: {multipleOf: 2}, else: {multipleOf: 5}, @@ -843,6 +872,7 @@ describe("Validation errors", () => { it("if/then should include failing keyword in message and params", () => { const schema = { + type: "number", if: {maximum: 10}, then: {multipleOf: 2}, } @@ -860,6 +890,7 @@ describe("Validation errors", () => { it("if/else should include failing keyword in message and params", () => { const schema = { + type: "number", if: {maximum: 10}, else: {multipleOf: 5}, } @@ -875,13 +906,13 @@ describe("Validation errors", () => { }) }) - function prepareTest(_ajv, schema) { + function prepareTest(_ajv: Ajv, schema) { validate = _ajv.compile(schema) numErrors = _ajv.opts.allErrors ? 2 : 1 } function testIfError(ifClause, multipleOf) { - let err = validate.errors[0] + let err = validate.errors?.[0] shouldBeError( err, "multipleOf", @@ -892,7 +923,7 @@ describe("Validation errors", () => { ) if (numErrors === 2) { - err = validate.errors[1] + err = validate.errors?.[1] shouldBeError(err, "if", "#/if", "", 'should match "' + ifClause + '" schema', { failingKeyword: ifClause, }) @@ -903,6 +934,7 @@ describe("Validation errors", () => { describe("uniqueItems errors", () => { it("should not return uniqueItems error when non-unique items are of a different type than required", () => { const schema = { + type: "array", items: {type: "number"}, uniqueItems: true, } @@ -913,7 +945,7 @@ describe("Validation errors", () => { shouldBeInvalid(validate, [1, 2, 2]) shouldBeError( - validate.errors[0], + validate.errors?.[0], "uniqueItems", "#/uniqueItems", "", @@ -927,7 +959,7 @@ describe("Validation errors", () => { if (expectedErrors === 2) testTypeError(1, _ajv.opts.jsPropertySyntax ? "[2]" : "/2") function testTypeError(i, dataPath) { - const err = validate.errors[i] + const err = validate.errors?.[i] shouldBeError(err, "type", "#/items/type", dataPath, "should be number") } }) @@ -949,7 +981,12 @@ describe("Validation errors", () => { const validate = _ajv.compile(schema) shouldBeValid(validate, data) shouldBeInvalid(validate, invalidData) - shouldBeError(validate.errors[0], "type", schPath, _ajv.opts.jsPropertySyntax ? ".foo" : "/foo") + shouldBeError( + validate.errors?.[0], + "type", + schPath, + _ajv.opts.jsPropertySyntax ? ".foo" : "/foo" + ) } function shouldBeValid(validate, data) { diff --git a/spec/extras.spec.ts b/spec/extras.spec.ts index a020b819c1..2f38f90f85 100644 --- a/spec/extras.spec.ts +++ b/spec/extras.spec.ts @@ -6,6 +6,7 @@ import {afterError, afterEach} from "./after_test" const instances = getAjvInstances(options, { $data: true, formats: {allowedUnknown: true}, + strictTypes: false, }) jsonSchemaTest(instances, { diff --git a/spec/issues/181_allErrors_custom_keyword_skipped.spec.ts b/spec/issues/181_allErrors_custom_keyword_skipped.spec.ts index f727ebec30..35a139948c 100644 --- a/spec/issues/181_allErrors_custom_keyword_skipped.spec.ts +++ b/spec/issues/181_allErrors_custom_keyword_skipped.spec.ts @@ -41,6 +41,7 @@ describe("issue #181, user-defined keyword is not validated in allErrors mode if ajv.addKeyword(def) const schema = { + type: "object", required: ["foo"], alwaysFails: true, } diff --git a/spec/issues/182_nan_validation.spec.ts b/spec/issues/182_nan_validation.spec.ts index df5890f96e..3fd9002746 100644 --- a/spec/issues/182_nan_validation.spec.ts +++ b/spec/issues/182_nan_validation.spec.ts @@ -2,7 +2,7 @@ import _Ajv from "../ajv" require("../chai").should() describe("issue #182, NaN validation", () => { - const ajv = new _Ajv() + const ajv = new _Ajv({strictTypes: false}) it("should pass minimum/maximum validation without type", () => { testNaN(ajv, {minimum: 1}, true) diff --git a/spec/issues/210_mutual_recur_frags.spec.ts b/spec/issues/210_mutual_recur_frags.spec.ts index 3dbc6fedbe..1217ffce63 100644 --- a/spec/issues/210_mutual_recur_frags.spec.ts +++ b/spec/issues/210_mutual_recur_frags.spec.ts @@ -9,6 +9,7 @@ describe("issue #210, mutual recursive $refs that are schema fragments", () => { $id: "foo", definitions: { bar: { + type: "object", properties: { baz: { anyOf: [{enum: [42]}, {$ref: "boo"}], @@ -40,6 +41,7 @@ describe("issue #210, mutual recursive $refs that are schema fragments", () => { $id: "foo", definitions: { bar: { + type: "object", properties: { baz: { anyOf: [{enum: [42]}, {$ref: "boo#/definitions/buu"}], diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index ddcfcec12f..e4be8641d6 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -20,7 +20,7 @@ describe("User-defined keywords", () => { verbose: true, inlineRefs: false, }, - {strictTypes: false} + {allowUnionTypes: true} ) ajv = instances[0] }) @@ -46,7 +46,7 @@ describe("User-defined keywords", () => { metaSchema: {type: "boolean"}, }) - shouldBeInvalidSchema({"x-even": "not_boolean"}) + shouldBeInvalidSchema({type: "number", "x-even": "not_boolean"}) function validateEven(schema, data) { return data % 2 ? !schema : schema @@ -80,9 +80,9 @@ describe("User-defined keywords", () => { additionalItems: false, }, }) - shouldBeInvalidSchema({"x-range": ["1", 2]}) - shouldBeInvalidSchema({"x-range": {}}) - shouldBeInvalidSchema({"x-range": [1, 2, 3]}) + shouldBeInvalidSchema({type: "number", "x-range": ["1", 2]}) + shouldBeInvalidSchema({type: "number", "x-range": {}}) + shouldBeInvalidSchema({type: "number", "x-range": [1, 2, 3]}) function validateRange(schema, data, parentSchema) { return parentSchema.exclusiveRange === true @@ -145,11 +145,17 @@ describe("User-defined keywords", () => { type: "number", compile: compileEven, }) - shouldBeInvalidSchema({"x-even": "not_boolean"}) + shouldBeInvalidSchema( + { + type: "number", + "x-even": "not_boolean", + }, + 'The value of "x-even" keyword must be boolean' + ) function compileEven(schema) { if (typeof schema != "boolean") { - throw new Error('The value of "even" keyword must be boolean') + throw new Error('The value of "x-even" keyword must be boolean') } return schema ? isEven : isOdd } @@ -169,7 +175,10 @@ describe("User-defined keywords", () => { compile: compileEven, metaSchema: {type: "boolean"}, }) - shouldBeInvalidSchema({"x-even": "not_boolean"}) + shouldBeInvalidSchema({ + type: "number", + "x-even": "not_boolean", + }) function compileEven(schema) { return schema ? isEven : isOdd @@ -206,7 +215,13 @@ describe("User-defined keywords", () => { schemaType: "boolean", compile: compileEven, }) - shouldBeInvalidSchema({"x-even": "not_boolean"}) + shouldBeInvalidSchema( + { + type: "number", + "x-even": "not_boolean", + }, + 'x-even value must be ["boolean"]' + ) function compileEven(schema) { if (schema) return (data) => data % 2 === 0 @@ -516,7 +531,7 @@ describe("User-defined keywords", () => { should.throw(() => { ajv.compile(schema) - }) + }, /type should be equal to one of the allowed values/) function macroInvalid(/* schema */) { return {type: "invalid"} @@ -610,7 +625,7 @@ describe("User-defined keywords", () => { verbose: true, inlineRefs: false, }, - {$data: true} + {$data: true, allowUnionTypes: true} ) ajv = instances[0] }) @@ -672,7 +687,10 @@ describe("User-defined keywords", () => { metaSchema: {type: "boolean"}, }) compileCalled.should.equal(true) - shouldBeInvalidSchema({"x-even-$data": "false"}) + shouldBeInvalidSchema({ + type: "number", + "x-even-$data": "false", + }) function validateEven(schema, data) { return data % 2 ? !schema : schema @@ -732,7 +750,10 @@ describe("User-defined keywords", () => { 2 ) macroCalled.should.equal(true) - shouldBeInvalidSchema({"x-even-$data": "false"}) + shouldBeInvalidSchema({ + type: "number", + "x-even-$data": "false", + }) function validateEven(schema, data) { return data % 2 ? !schema : schema @@ -770,7 +791,10 @@ describe("User-defined keywords", () => { }, metaSchema: {type: "boolean"}, }) - shouldBeInvalidSchema({"x-even-$data": "false"}) + shouldBeInvalidSchema({ + type: "number", + "x-even-$data": "false", + }) }) it('should fail if "macro" keyword definition has "$data" but no "code" or "validate"', () => { @@ -783,7 +807,7 @@ describe("User-defined keywords", () => { return {} }, }) - }) + }, /\$data keyword must have "code" or "validate" function/) }) it("should support schemaType with $data", () => { @@ -879,6 +903,7 @@ describe("User-defined keywords", () => { _ajv.addKeyword(definition) const schema = { + type: ["object", "array"], properties: { a: {"x-constant": 1}, b: {"x-constant": 1}, @@ -1023,11 +1048,11 @@ describe("User-defined keywords", () => { validate.errors.should.have.length(numErrors) } - function shouldBeInvalidSchema(schema) { + function shouldBeInvalidSchema(schema, msg: string | RegExp = /keyword value is invalid/) { instances.forEach((_ajv) => { should.throw(() => { _ajv.compile(schema) - }) + }, msg) }) } @@ -1042,7 +1067,7 @@ describe("User-defined keywords", () => { TEST_TYPES.forEach((dataType, index) => { should.throw(() => { _addKeyword(keywords[index], dataType) - }) + }, /already defined/) }) } @@ -1054,7 +1079,7 @@ describe("User-defined keywords", () => { _addKeyword(keyword, dataType1) should.throw(() => { _addKeyword(keyword, dataType2) - }) + }, /already defined/) }) }) } @@ -1071,15 +1096,15 @@ describe("User-defined keywords", () => { should.throw(() => { ajv.addKeyword("3-start-with-number-not-valid") - }) + }, /invalid name/) should.throw(() => { ajv.addKeyword("-start-with-hyphen-not-valid") - }) + }, /invalid name/) should.throw(() => { ajv.addKeyword("spaces not valid") - }) + }, /invalid name/) }) it("should return instance of itself", () => { @@ -1090,15 +1115,15 @@ describe("User-defined keywords", () => { it("should throw if unknown type is passed", () => { should.throw(() => { _addKeyword("user-defined1", "wrongtype") - }) + }, /type must be JSONType/) should.throw(() => { _addKeyword("user-defined2", ["number", "wrongtype"]) - }) + }, /type must be JSONType/) should.throw(() => { _addKeyword("user-defined3", ["number", undefined]) - }) + }, /type must be JSONType/) }) function _addKeyword(keyword, dataType) { @@ -1143,7 +1168,7 @@ describe("User-defined keywords", () => { validate: (_schema, data) => data > 0, }) - const schema = {positive: true} + const schema = {type: "number", positive: true} let validate = ajv.compile(schema) validate(0).should.equal(false) @@ -1157,7 +1182,7 @@ describe("User-defined keywords", () => { return data >= 0 }, }) - }) + }, /already defined/) ajv.removeKeyword("positive") ajv.removeSchema(schema) @@ -1220,7 +1245,7 @@ describe("User-defined keywords", () => { it("should NOT update data without option modifying", () => { should.throw(() => { testModifying(false) - }) + }, /expected false to equal true/) }) it("should update data with option modifying", () => { diff --git a/spec/options/meta_validateSchema.spec.ts b/spec/options/meta_validateSchema.spec.ts index 820d9f4ba9..faee97c9fd 100644 --- a/spec/options/meta_validateSchema.spec.ts +++ b/spec/options/meta_validateSchema.spec.ts @@ -9,14 +9,14 @@ describe("meta and validateSchema options", () => { function testOptionMeta(ajv) { ajv.getSchema("http://json-schema.org/draft-07/schema").should.be.a("function") - ajv.validateSchema({type: "integer"}).should.equal(true) - ajv.validateSchema({type: 123}).should.equal(false) + ajv.validateSchema({$id: "ok", type: "integer"}).should.equal(true) + ajv.validateSchema({$id: "wrong", type: 123}).should.equal(false) should.not.throw(() => { - ajv.addSchema({type: "integer"}) + ajv.addSchema({$id: "ok", type: "integer"}) }) should.throw(() => { - ajv.addSchema({type: 123}) - }) + ajv.addSchema({$id: "wrong", type: 123}) + }, /schema is invalid/) } }) @@ -32,7 +32,7 @@ describe("meta and validateSchema options", () => { let ajv = new _Ajv() should.throw(() => { ajv.addSchema({type: 123}, "integer") - }) + }, /schema is invalid/) ajv = new _Ajv({validateSchema: false}) should.not.throw(() => { diff --git a/spec/options/nullable.spec.ts b/spec/options/nullable.spec.ts index d7cb86cd8e..4ce2f1f3c0 100644 --- a/spec/options/nullable.spec.ts +++ b/spec/options/nullable.spec.ts @@ -52,7 +52,15 @@ describe("nullable keyword", () => { type: ["number", "null"], nullable: false, }) - }) + }, "type: null contradicts nullable: false") + }) + + it("should throw if nullable is used without type", () => { + should.throw(() => { + ajv.compile({ + nullable: true, + }) + }, '"nullable" cannot be used without "type"') }) function testNullable(schema) { diff --git a/spec/options/options_add_schemas.spec.ts b/spec/options/options_add_schemas.spec.ts index ddbe05ee25..f835ee3863 100644 --- a/spec/options/options_add_schemas.spec.ts +++ b/spec/options/options_add_schemas.spec.ts @@ -66,15 +66,15 @@ describe("options to add schemas", () => { it("should throw with duplicate ID", () => { ajv.compile({$id: "str", type: "string"}) should.throw(() => { - ajv.compile({$id: "str", minLength: 2}) - }) + ajv.compile({$id: "str", type: "string", minLength: 2}) + }, /already exists/) const schema = {$id: "int", type: "integer"} - const schema2 = {$id: "int", minimum: 0} + const schema2 = {$id: "int", type: "integer", minimum: 0} ajv.validate(schema, 1).should.equal(true) should.throw(() => { ajv.validate(schema2, 1) - }) + }, /already exists/) }) }) }) diff --git a/spec/options/options_refs.spec.ts b/spec/options/options_refs.spec.ts index 0e70c152bf..75eefcd500 100644 --- a/spec/options/options_refs.spec.ts +++ b/spec/options/options_refs.spec.ts @@ -98,7 +98,7 @@ describe("referenced schema options", () => { const ajv = new _Ajv() should.throw(() => { ajv.compile({$ref: "missing_reference"}) - }) + }, /can't resolve reference missing_reference/) }) }) }) diff --git a/spec/options/options_validation.spec.ts b/spec/options/options_validation.spec.ts index 18097d600e..ca6a5332f8 100644 --- a/spec/options/options_validation.spec.ts +++ b/spec/options/options_validation.spec.ts @@ -20,6 +20,7 @@ describe("validation options", () => { describe("formats", () => { it("should add formats from options", () => { const ajv = new _Ajv({ + allowUnionTypes: true, formats: { identifier: /^[a-z_$][a-z0-9_$]*$/i, }, @@ -40,6 +41,7 @@ describe("validation options", () => { describe("keywords", () => { it("should add keywords from options", () => { const ajv = new _Ajv({ + allowUnionTypes: true, keywords: [ { keyword: "identifier", diff --git a/spec/options/schemaId.spec.ts b/spec/options/schemaId.spec.ts index 582a3c981f..b74b5822f7 100644 --- a/spec/options/schemaId.spec.ts +++ b/spec/options/schemaId.spec.ts @@ -1,4 +1,6 @@ +import type Ajv from "../.." import _Ajv from "../ajv" +import assert from "assert" import chai from "../chai" const should = chai.should() @@ -12,20 +14,27 @@ describe("removed schemaId option", () => { validate("foo").should.equal(true) validate(1).should.equal(false) - should.throw(() => ajv.compile({id: "mySchema2", type: "string"})) + should.throw( + () => ajv.compile({id: "mySchema2", type: "string"}), + /NOT SUPPORTED: keyword "id"/ + ) } }) - it("should use $id and ignore id when strict: false", () => { + it("should use $id and throw exception for id when strict: false", () => { test(new _Ajv({logger: false, strict: false})) - function test(ajv) { + function test(ajv: Ajv) { ajv.addSchema({$id: "mySchema1", type: "string"}) - let validate = ajv.getSchema("mySchema1") + const validate = ajv.getSchema("mySchema1") + assert(typeof validate == "function") validate("foo").should.equal(true) validate(1).should.equal(false) - validate = ajv.compile({id: "mySchema2", type: "string"}) + should.throw( + () => ajv.compile({id: "mySchema2", type: "string"}), + /NOT SUPPORTED: keyword "id"/ + ) should.not.exist(ajv.getSchema("mySchema2")) } }) diff --git a/spec/options/strict.spec.ts b/spec/options/strict.spec.ts index 71cf4393a0..b693a6a8c5 100644 --- a/spec/options/strict.spec.ts +++ b/spec/options/strict.spec.ts @@ -66,7 +66,7 @@ function testStrictMode(schema, logPattern) { function test(ajv) { should.throw(() => { ajv.compile(schema) - }) + }, logPattern) } }) }) @@ -79,7 +79,7 @@ function testStrictMode(schema, logPattern) { logger: getLogger(output), }) ajv.compile(schema) - logPattern.test(output.warning).should.equal(true) + output.warning.should.match(logPattern) }) }) } diff --git a/spec/options/strictDefaults.spec.ts b/spec/options/strictDefaults.spec.ts index 9c92df0ab4..ee6b5fd899 100644 --- a/spec/options/strictDefaults.spec.ts +++ b/spec/options/strictDefaults.spec.ts @@ -54,11 +54,10 @@ describe("strict option with defaults (replaced strictDefaults)", () => { function test(ajv) { const schema = { default: 5, + type: "object", properties: {}, } - should.throw(() => { - ajv.compile(schema) - }) + should.throw(() => ajv.compile(schema), /default is ignored in the schema root/) } }) @@ -71,6 +70,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { oneOf: [ {enum: ["foo", "bar"]}, { + type: "object", properties: { foo: { default: true, @@ -81,7 +81,7 @@ describe("strict option with defaults (replaced strictDefaults)", () => { } should.throw(() => { ajv.compile(schema) - }) + }, /default is ignored/) } }) }) diff --git a/spec/options/strictKeywords.spec.ts b/spec/options/strictKeywords.spec.ts index cb33d2d2dd..bfc508b274 100644 --- a/spec/options/strictKeywords.spec.ts +++ b/spec/options/strictKeywords.spec.ts @@ -27,12 +27,11 @@ describe("strict option with keywords (replaced strictKeywords)", () => { function test(ajv) { const schema = { + type: "object", properties: {}, unknownKeyword: 1, } - should.throw(() => { - ajv.compile(schema) - }) + should.throw(() => ajv.compile(schema), /unknown keyword/) } }) }) @@ -61,15 +60,9 @@ describe("strict option with keywords (replaced strictKeywords)", () => { function test(ajv) { const schema = { - anyOf: [ - { - unknownKeyword: 1, - }, - ], + anyOf: [{unknownKeyword: 1}], } - should.throw(() => { - ajv.compile(schema) - }) + should.throw(() => ajv.compile(schema), /unknown keyword/) } }) }) diff --git a/spec/options/unknownFormats.spec.ts b/spec/options/unknownFormats.spec.ts index 26b5d75b62..a04af89e20 100644 --- a/spec/options/unknownFormats.spec.ts +++ b/spec/options/unknownFormats.spec.ts @@ -11,8 +11,8 @@ describe("specifying allowed unknown formats with `formats` option", () => { function test(ajv) { should.throw(() => { - ajv.compile({format: "unknown"}) - }) + ajv.compile({type: "string", format: "unknown"}) + }, /unknown format/) } }) @@ -71,20 +71,20 @@ describe("specifying allowed unknown formats with `formats` option", () => { describe("= [String]", () => { it("should pass schema compilation and be valid if allowed unknown format is used", () => { - test(new _Ajv({formats: {allowed: true}, strictTypes: false})) + test(new _Ajv({formats: {allowed: true}})) function test(ajv) { - const validate = ajv.compile({format: "allowed"}) + const validate = ajv.compile({type: "string", format: "allowed"}) validate("anything").should.equal(true) should.throw(() => { - ajv.compile({format: "unknown"}) - }) + ajv.compile({type: "string", format: "unknown"}) + }, /unknown format/) } }) it("should be valid if allowed unknown format is used via $data", () => { - test(new _Ajv({$data: true, formats: {allowed: true}})) + test(new _Ajv({$data: true, formats: {allowed: true}, allowUnionTypes: true})) function test(ajv) { ajv.addFormat("date", DATE_FORMAT) diff --git a/spec/resolve.spec.ts b/spec/resolve.spec.ts index f8cc689886..581df03d45 100644 --- a/spec/resolve.spec.ts +++ b/spec/resolve.spec.ts @@ -1,17 +1,19 @@ import getAjvInstances from "./ajv_instances" -import Ajv from "./ajv" +import _Ajv from "./ajv" +import type Ajv from ".." import {AnyValidateFunction} from "../dist/types" import chai from "./chai" const should = chai.should() describe("resolve", () => { - let instances + let instances: Ajv[] beforeEach(() => { instances = getAjvInstances({ allErrors: true, verbose: true, inlineRefs: false, + allowUnionTypes: true, }) }) @@ -73,15 +75,17 @@ describe("resolve", () => { }) should.throw(() => { ajv.compile({ + type: "object", additionalProperties: { $id: "http://example.com/1.json", type: "string", }, }) - }) + }, /resolves to more than one schema/) should.throw(() => { ajv.compile({ + type: ["object", "array"], items: { $id: "#int", type: "integer", @@ -91,7 +95,7 @@ describe("resolve", () => { type: "string", }, }) - }) + }, /resolves to more than one schema/) }) }) @@ -218,27 +222,27 @@ describe("resolve", () => { ] it("by default should inline schema if it doesn't contain refs", () => { - const ajv = new Ajv({schemas, code: {source: true}}) + const ajv = new _Ajv({schemas, code: {source: true}}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs == false", () => { - const ajv = new Ajv({schemas, inlineRefs: false, code: {source: true}}) + const ajv = new _Ajv({schemas, inlineRefs: false, code: {source: true}}) testSchemas(ajv, false) }) it("should inline schema if option inlineRefs is bigger than number of keys in referenced schema", () => { - const ajv = new Ajv({schemas, inlineRefs: 4, code: {source: true}}) + const ajv = new _Ajv({schemas, inlineRefs: 4, code: {source: true}}) testSchemas(ajv, true) }) it("should NOT inline schema if option inlineRefs is less than number of keys in referenced schema", () => { - const ajv = new Ajv({schemas, inlineRefs: 2, code: {source: true}}) + const ajv = new _Ajv({schemas, inlineRefs: 2, code: {source: true}}) testSchemas(ajv, false) }) it("should avoid schema substitution when refs are inlined (issue #77)", () => { - const ajv = new Ajv({verbose: true}) + const ajv = new _Ajv({verbose: true}) const schemaMessage = { $schema: "http://json-schema.org/draft-07/schema#", From 537de13680bef31a1217ed5a8183b5e45630d327 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 21 Sep 2020 18:03:25 +0100 Subject: [PATCH 252/322] feat: strictTuples option, docs --- README.md | 243 +++++++++++++++++++++++++-- lib/ajv.ts | 9 +- lib/vocabularies/applicator/items.ts | 21 ++- spec/async/items.json | 4 +- spec/boolean.spec.ts | 8 +- spec/errors.spec.ts | 16 +- spec/extras.spec.ts | 1 + spec/keyword.spec.ts | 1 + spec/options/useDefaults.spec.ts | 3 + 9 files changed, 273 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index ce8c8f25d9..213bc82cf1 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,10 @@ ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")) - [Command line interface](#command-line-interface) - Validation - [Strict mode](#strict-mode) + - [Prohibit ignored keywords](#prohibit-ignored-keywords) + - [Prevent unexpected validation](#prevent-unexpected-validation) + - [Strict types](#strict-types) + - [Number validation](#number-validation) - [Keywords](#validation-keywords) - [Annotation keywords](#annotation-keywords) - [Formats](#formats) @@ -271,7 +275,9 @@ Strict mode intends to prevent any unexpected behaviours or silently ignored mis The strict mode restrictions are below. To disable these restrictions use option `strict: false`. -##### Prohibit unknown keywords +### Prohibit ignored keywords + +#### Prohibit unknown keywords JSON Schema [section 6.5](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-6.5) requires to ignore unknown keywords. The motivation is to increase cross-platform portability of schemas, so that implementations that do not support certain keywords can still do partial validation. @@ -294,25 +300,45 @@ ajv.addVocabulary(["allowed1", "allowed2"]) #### Prohibit ignored "additionalItems" keyword -JSON Schema section [9.3.1.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2) requires to ignore "additionalItems" keyword if "items" keyword is absent. This is inconsistent with the interaction of "additionalProperties" and "properties", and may cause unexpected results. +JSON Schema section [9.3.1.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2) requires to ignore "additionalItems" keyword if "items" keyword is absent or if it is not an array of items. This is inconsistent with the interaction of "additionalProperties" and "properties", and may cause unexpected results. -By default Ajv fails schema compilation when "additionalItems" is used without "items. +By default Ajv fails schema compilation when "additionalItems" is used without "items" (or if "items" is not an array). -#### Prohibit ignored "if", "then", "else" keywords +#### Prohibit unconstrained tuples -JSON Schema section [9.2.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2) requires to ignore "if" (only annotations are collected) if both "then" and "else" are absent, and ignore "then"/"else" if "if" is absent. +Ajv also logs a warning if "items" is an array (for schema that defines a tuple) but neiter "minItems" nor "additionalItems"/"maxItems" keyword is present (or have a wrong value): -By default Ajv fails schema compilation in these cases. +```javascript +{ + type: "array", + items: [{type: "number"}, {type: "boolean"}] +} +``` -#### Prohibit overlap between "properties" and "patternProperties" keywords +The above schema may have a mistake, as tuples usually are expected to have a fixed size. To "fix" it: -The expectation of users (see #196, #286) is that "patternProperties" only apply to properties not already defined in "properties" keyword, but JSON Schema section [9.3.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2) defines these two keywords as independent. It means that to some properties two subschemas can be applied - one defined in "properties" keyword and another defined in "patternProperties" for the pattern matching this property. +```javascript +{ + type: "array", + items: [{type: "number"}, {type: "boolean"}], + minItems: 2, + additionalItems: false + // or + // maxItems: 2 +} +``` -By default Ajv fails schema compilation if a pattern in "patternProperties" matches a property in "properties" in the same schema. +Sometimes users accidentally create schema for unit (a tuple with one item) that only validates the first item, this restriction prevents this mistake as well. -In addition to allowing such patterns by using option `strict: false`, there is an option `allowMatchingProperties: true` to only allow this case without disabling other strict mode restrictions - there are some rare cases when this is necessary. +Use `strictTuples` option to suppress this warning (`false`) or turn it into exception (`true`). -To reiterate, neither this nor other strict mode restrictions change the validation results - they only restrict which schemas are valid. +If you use `JSONSchemaType` this mistake will also be prevented on a type level. + +#### Prohibit ignored "if", "then", "else" keywords + +JSON Schema section [9.2.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2) requires to ignore "if" (only annotations are collected) if both "then" and "else" are absent, and ignore "then"/"else" if "if" is absent. + +By default Ajv fails schema compilation in these cases. #### Prohibit unknown formats @@ -330,7 +356,186 @@ Standard JSON Schema formats are provided in [ajv-formats](https://github.com/aj With `useDefaults` option Ajv modifies validated data by assigning defaults from the schema, but there are different limitations when the defaults can be ignored (see [Assigning defaults](#assigning-defaults)). In strict mode Ajv fails schema compilation if such defaults are used in the schema. -#### Number validation +### Prevent unexpected validation + +#### Prohibit overlap between "properties" and "patternProperties" keywords + +The expectation of users (see #196, #286) is that "patternProperties" only apply to properties not already defined in "properties" keyword, but JSON Schema section [9.3.2](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2) defines these two keywords as independent. It means that to some properties two subschemas can be applied - one defined in "properties" keyword and another defined in "patternProperties" for the pattern matching this property. + +By default Ajv fails schema compilation if a pattern in "patternProperties" matches a property in "properties" in the same schema. + +In addition to allowing such patterns by using option `strict: false`, there is an option `allowMatchingProperties: true` to only allow this case without disabling other strict mode restrictions - there are some rare cases when this is necessary. + +To reiterate, neither this nor other strict mode restrictions change the validation results - they only restrict which schemas are valid. + +### Strict types + +An additional option `strictTypes` ("log" by default) imposes additional restrictions on how type keyword is used: + +#### Union types + +With `srictTypes` option "type" keywords with multiple types (other than with "null") are prohibited. + +Invalid: + +```javascript +{ + type: ["string", "number"] +} +``` + +Valid: + +```javascript +{ + type: ["object", "null"] +} +``` + +and + +```javascript +{ + type: "object", + nullable: true +} +``` + +Unions can still be defined with `anyOf` keyword. + +The motivation for this restriction is that "type" is usually not the only keyword in the schema, and mixing other keywords that apply to different types is confusing. It is also consistent with wider range of versions of OpenAPI specification and has better tooling support. E.g., this example violating `strictTypes`: + +```javascript +{ + type: ["number", "array"], + minimum: 0, + items: { + type: "number", + minimum: 0 + } +} +``` + +is equivalent to this complying example, that is more verbose but also easier to maintain: + +```javascript +{ + anyOf: [ + { + type: "number", + minimum: 0, + }, + { + type: "array", + items: { + type: "number", + minimum: 0, + }, + }, + ] +} +``` + +It also can be refactored: + +```javascript +{ + $defs: { + item: { + type: "number", + minimum: 0 + } + }, + anyOf: [ + {$ref: "#/$defs/item"}, + { + type: "array", + items: {$ref: "#/$defs/item"} + }, + ] +} +``` + +This restriction can be lifted separately from other `strictTypes` restrictions with `allowUnionTypes: true` option. + +#### Contradictory types + +Subschemas can apply to the same data instance, and it is possible to have contradictory type keywords - it usually indicate some mistake. For example: + +```javascript +{ + type: "object", + anyOf: [ + {type: "array"}, + {type: "object"} + ] +} +``` + +The schema above violates `strictTypes` as "array" type is not compatible with object. If you used `allowUnionTypes: true` option, the above schema can be fixed in this way: + +```javascript +{ + type: ["array", "object"], + anyOf: [ + {type: "array"}, + {type: "object"} + ] +} +``` + +**Please note**: type "number" can be narrowed to "integer", the opposite would violate `strictTypes`. + +#### Applicable types + +This simple JSON Schema is valid, but it violates `strictTypes`: + +```javascript +{ + properties: { + foo: {type: "number"}, + bar: {type: "string"} + } + required: ["foo", "bar"] +} +``` + +This is a very common mistake that even people experienced with JSON Schema often make - the problem here is that any value that is not an object would be valid against this schema - this is rarely intentional. + +To fix it, "type" keyword has to be added: + +```javascript +{ + type: "object", + properties: { + foo: {type: "number"}, + bar: {type: "string"} + }, + required: ["foo", "bar"] +} +``` + +You do not necessarily have to have "type" keyword in the same schema object; as long as there is "type" keyword applying to the same part of data instance in the same schema document, not via "\$ref", it will be ok: + +```javascript +{ + type: "object", + anyOf: [ + { + properties: {foo: {type: "number"}} + required: ["foo"] + }, + { + properties: {bar: {type: "string"}} + required: ["bar"] + } + ] +} +``` + +Both "properties" and "required" need `type: "object"` to satisfy `strictTypes` - it is sufficient to have it once in the parent schema, without repeating it in each schema. + +### Number validation Strict mode also affects number validation. By default Ajv fails `{"type": "number"}` (or `"integer"`) validation for `Infinity` and `NaN`. @@ -1238,6 +1443,9 @@ Option defaults: const defaultOptions = { // strict mode options strict: true, + strictTypes: "log", + strictTuples: "log", + allowUnionTypes: false, allowMatchingProperties: false, validateFormats: true, // validation and reporting options: @@ -1276,7 +1484,16 @@ const defaultOptions = { - _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](#strict-mode) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: - `true` (default) - use strict mode and throw an exception when any strict mode restriction is violated. - `"log"` - log warning when any strict mode restriction is violated. - - `false` - ignore all strict mode restrictions. + - `false` - ignore all strict mode restrictions. Also ignores `strictTypes` restrictions unless it is explicitely passed. +- _strictTypes_: By default Ajv logs warning when "type" keyword is used in a way that may be incorrect or confusing to other people - see [Strict types](#strict-types) for more details. This option does not change validation results. Option values: + - `true` - throw exception when any strictTypes restriction is violated. + - `"log"` (default, unless option strict is `false`) - log warning when any strictTypes restriction is violated. + - `false` - ignore all strictTypes restrictions violations. +- _strictTuples_: By default Ajv logs warning when "items" is array and "minItems" and "maxItems"/"additionalItems" not present or different from the number of items. See [Strict mode](#strict-mode) for more details. This option does not change validation results. Option values: + - `true` - throw exception. + - `"log"` (default, unless option strict is `false`) - log warning. + - `false` - ignore strictTuples restriction violations. +- _allowUnionTypes_: pass true to allow using multiple non-null types in "type" keyword (one of `strictTypes` restricitons). see [Strict types](#strict-types) - _allowMatchingProperties_: pass true to allow overlap between "properties" and "patternProperties". Does not affect other strict mode restrictions. See [Strict Mode](#strict-mode). - _validateFormats_: format validation. Option values: - `true` (default) - validate formats (see [Formats](#formats)). In [strict mode](#strict-mode) unknown formats will throw exception during schema compilation (and fail validation in case format keyword value is [\$data reference](#data-reference)). diff --git a/lib/ajv.ts b/lib/ajv.ts index 6d164ac391..30a1370486 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -82,6 +82,7 @@ interface CurrentOptions { // strict mode options strict?: boolean | "log" strictTypes?: boolean | "log" + strictTuples?: boolean | "log" allowMatchingProperties?: boolean // disables a strict mode restriction allowUnionTypes?: boolean validateFormats?: boolean @@ -181,6 +182,7 @@ type RequiredInstanceOptions = { [K in | "strict" | "strictTypes" + | "strictTuples" | "code" | "inlineRefs" | "loopRequired" @@ -196,9 +198,12 @@ type RequiredInstanceOptions = { export type InstanceOptions = Options & RequiredInstanceOptions function requiredOptions(o: Options): RequiredInstanceOptions { + const strict = o.strict ?? true + const strictLog = strict ? "log" : false return { - strict: o.strict ?? true, - strictTypes: o.strictTypes ?? (o.strict ?? true ? "log" : false), + strict, + strictTypes: o.strictTypes ?? strictLog, + strictTuples: o.strictTuples ?? strictLog, code: o.code ?? {}, loopRequired: o.loopRequired ?? Infinity, loopEnum: o.loopEnum ?? Infinity, diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 0941730a29..4ed72dceec 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,6 +1,6 @@ import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" -import {alwaysValidSchema} from "../util" +import {alwaysValidSchema, checkStrictMode} from "../util" import {applySubschema, Type} from "../../compile/subschema" import {_} from "../../compile/codegen" @@ -10,15 +10,20 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "array", "boolean"], before: "uniqueItems", code(cxt: KeywordCxt) { - const {gen, schema, data, it} = cxt + const {gen, schema, parentSchema, data, it} = cxt const len = gen.const("len", _`${data}.length`) if (Array.isArray(schema)) { - validateDefinedItems(schema) + validateTuple(schema) } else if (!alwaysValidSchema(it, schema)) { - validateItems() + validateArray() } - function validateDefinedItems(schArr: AnySchema[]): void { + function validateTuple(schArr: AnySchema[]): void { + if (it.opts.strictTuples && !fullTupleSchema(schema.length, parentSchema)) { + const msg = `"items" is ${schArr.length}-tuple, but minItems or maxItems/additionalItems are not specified or different` + throw new Error(msg) + checkStrictMode(it, msg, it.opts.strictTuples) + } const valid = gen.name("valid") schArr.forEach((sch: AnySchema, i: number) => { if (alwaysValidSchema(it, sch)) return @@ -38,7 +43,7 @@ const def: CodeKeywordDefinition = { }) } - function validateItems(): void { + function validateArray(): void { const valid = gen.name("valid") gen.forRange("i", 0, len, (i) => { applySubschema( @@ -58,4 +63,8 @@ const def: CodeKeywordDefinition = { }, } +function fullTupleSchema(len: number, sch: any): boolean { + return len === sch.minItems && (len === sch.maxItems || sch.additionalItems === false) +} + export default def diff --git a/spec/async/items.json b/spec/async/items.json index 9b3b257733..92fa44818d 100644 --- a/spec/async/items.json +++ b/spec/async/items.json @@ -16,7 +16,9 @@ "type": "integer", "idExists": {"table": "users"} } - ] + ], + "minItems": 3, + "additionalItems": false }, "tests": [ { diff --git a/spec/boolean.spec.ts b/spec/boolean.spec.ts index c06108ab8c..be53fddfa9 100644 --- a/spec/boolean.spec.ts +++ b/spec/boolean.spec.ts @@ -8,10 +8,10 @@ describe("boolean schemas", () => { before(() => { ajvs = [ - new _Ajv(), - new _Ajv({allErrors: true}), - new _Ajv({inlineRefs: false}), - new _Ajv({strict: false}), + new _Ajv({strictTuples: false}), + new _Ajv({allErrors: true, strictTuples: false}), + new _Ajv({inlineRefs: false, strictTuples: false}), + new _Ajv({strict: false, strictTuples: false}), ] }) diff --git a/spec/errors.spec.ts b/spec/errors.spec.ts index 7d77c9f2f9..077fb40786 100644 --- a/spec/errors.spec.ts +++ b/spec/errors.spec.ts @@ -12,18 +12,19 @@ describe("Validation errors", () => { }) function createInstances() { - ajv = new _Ajv({loopRequired: 21, strictTypes: false}) - ajvJP = new _Ajv({ + const opts = { loopRequired: 21, strictTypes: false, - }) + strictTuples: false, + } + ajv = new _Ajv(opts) + ajvJP = new _Ajv(opts) fullAjv = new _Ajv({ + ...opts, allErrors: true, verbose: true, jsPropertySyntax: true, // deprecated - loopRequired: 21, logger: false, - strictTypes: false, }) } @@ -538,11 +539,12 @@ describe("Validation errors", () => { const schema = { type: "array", items: [{type: "integer"}, {type: "integer"}], + minItems: 2, additionalItems: false, } - const data = [1, 2], - invalidData = [1, 2, 3] + const data = [1, 2] + const invalidData = [1, 2, 3] test(ajv) test(ajvJP) diff --git a/spec/extras.spec.ts b/spec/extras.spec.ts index 2f38f90f85..273b6ca176 100644 --- a/spec/extras.spec.ts +++ b/spec/extras.spec.ts @@ -7,6 +7,7 @@ const instances = getAjvInstances(options, { $data: true, formats: {allowedUnknown: true}, strictTypes: false, + strictTuples: false, }) jsonSchemaTest(instances, { diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index e4be8641d6..30c0264746 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -77,6 +77,7 @@ describe("User-defined keywords", () => { metaSchema: { type: "array", items: [{type: "number"}, {type: "number"}], + minItems: 2, additionalItems: false, }, }) diff --git a/spec/options/useDefaults.spec.ts b/spec/options/useDefaults.spec.ts index 8c4625d944..8e8dd66496 100644 --- a/spec/options/useDefaults.spec.ts +++ b/spec/options/useDefaults.spec.ts @@ -68,6 +68,7 @@ describe("useDefaults option", () => { {type: "boolean", default: false}, ], minItems: 3, + additionalItems: false, } const validate = ajv.compile(schema) @@ -171,6 +172,8 @@ describe("useDefaults option", () => { arr: { type: "array", items: [{default: "foo"}, {default: 1}, {default: 2}, {default: 3}], + minItems: 4, + additionalItems: false, }, }, } From e2dea13aeaffc09e55ede0a9c736c719f58beaba Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 21 Sep 2020 18:49:22 +0100 Subject: [PATCH 253/322] test for strictTuples --- lib/compile/validate/iterate.ts | 1 - lib/vocabularies/applicator/items.ts | 1 - spec/options/strict.spec.ts | 196 +++++++++++++++++++++++++++ spec/options/strictTypes.spec.ts | 28 ---- 4 files changed, 196 insertions(+), 30 deletions(-) delete mode 100644 spec/options/strictTypes.spec.ts diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 4e45e706c6..6fcdbe8ecf 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -98,7 +98,6 @@ function checkKeywordTypes(it: SchemaObjCxt, ts: JSONType[]): void { const {type} = rule.definition if (type.length && !type.some((t) => hasApplicableType(ts, t))) { strictTypesError(it, `missing type "${type.join(",")}" for keyword "${keyword}"`) - return } } } diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 4ed72dceec..0b826756ba 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -21,7 +21,6 @@ const def: CodeKeywordDefinition = { function validateTuple(schArr: AnySchema[]): void { if (it.opts.strictTuples && !fullTupleSchema(schema.length, parentSchema)) { const msg = `"items" is ${schArr.length}-tuple, but minItems or maxItems/additionalItems are not specified or different` - throw new Error(msg) checkStrictMode(it, msg, it.opts.strictTuples) } const valid = gen.name("valid") diff --git a/spec/options/strict.spec.ts b/spec/options/strict.spec.ts index b693a6a8c5..3c23ce2faf 100644 --- a/spec/options/strict.spec.ts +++ b/spec/options/strict.spec.ts @@ -1,3 +1,4 @@ +import {JSONSchemaType} from "../../dist/types/json-schema" import _Ajv from "../ajv" import chai from "../chai" const should = chai.should() @@ -42,6 +43,201 @@ describe("strict mode", () => { should.not.exist(output.warning) }) }) + + describe("strictTypes option", () => { + const ajv = new _Ajv({strictTypes: true}) + const ajvUT = new _Ajv({strictTypes: true, allowUnionTypes: true}) + + describe("multiple/union types", () => { + it("should prohibit multiple types", () => { + should.throw(() => { + ajv.compile({type: ["number", "string"]}) + }, /use allowUnionTypes to allow union type/) + }) + + it("should allow multiple types with option allowUnionTypes", () => { + should.not.throw(() => { + ajvUT.compile({type: ["number", "string"]}) + }) + }) + + it("should allow nullable", () => { + should.not.throw(() => { + ajv.compile({type: ["number", "null"]}) + ajv.compile({type: ["number"], nullable: true}) + }) + }) + }) + + describe("contradictory types", () => { + it("should prohibit contradictory types", () => { + should.throw(() => { + ajv.compile({ + type: "object", + anyOf: [{type: "object"}, {type: "array"}], + }) + }, /type "array" not allowed by context "object"/) + }) + + it("should allow narrowing types", () => { + should.not.throw(() => { + ajvUT.compile({ + type: ["object", "array"], + anyOf: [{type: "object"}, {type: "array"}], + }) + }) + }) + + it('should allow "integer" in "number" context', () => { + should.not.throw(() => { + ajv.compile({ + type: "number", + anyOf: [{type: "integer"}], + }) + }) + }) + + it('should prohibit "number" in "integer" context', () => { + should.throw(() => { + ajv.compile({ + type: "integer", + anyOf: [{type: "number"}], + }) + }, /type "number" not allowed by context "integer"/) + }) + }) + + describe("applicable types", () => { + it("should prohibit keywords without applicable types", () => { + should.throw(() => { + ajv.compile({ + properties: { + foo: {type: "number", minimum: 0}, + }, + }) + }, /missing type "object" for keyword "properties"/) + + should.throw(() => { + ajv.compile({ + type: "object", + properties: { + foo: {minimum: 0}, + }, + }) + }, /missing type "number" for keyword "minimum"/) + }) + + it("should allow keywords with applicable types", () => { + should.not.throw(() => { + ajv.compile({ + type: "object", + properties: { + foo: {type: "number", minimum: 0}, + }, + }) + }) + }) + + it("should allow keywords with applicable type in parent schema", () => { + should.not.throw(() => { + ajv.compile({ + type: "object", + anyOf: [ + { + properties: { + foo: {type: "number"}, + }, + }, + { + properties: { + bar: {type: "string"}, + }, + }, + ], + }) + }) + }) + }) + + describe("propertyNames", () => { + it('should set default data type "string"', () => { + ajv.compile({ + type: "object", + propertyNames: {maxLength: 5}, + }) + + ajv.compile({ + type: "object", + propertyNames: {type: "string", maxLength: 5}, + }) + + should.throw(() => { + ajv.compile({ + type: "object", + propertyNames: {type: "number"}, + }) + }, /type "number" not allowed by context/) + }) + }) + }) + + describe("option strictTuples", () => { + const ajv = new _Ajv({strictTuples: true}) + type MyTuple = [string, number] + + it("should prohibit unconstrained tuples", () => { + const schema1: JSONSchemaType = { + type: "array", + items: [{type: "string"}, {type: "number"}], + minItems: 2, + additionalItems: false, + } + should.not.throw(() => { + ajv.compile(schema1) + }) + + const schema2: JSONSchemaType = { + type: "array", + items: [{type: "string"}, {type: "number"}], + minItems: 2, + maxItems: 2, + } + should.not.throw(() => { + ajv.compile(schema2) + }) + + //@ts-expect-error + const badSchema1: JSONSchemaType = { + type: "array", + items: [{type: "string"}, {type: "number"}], + additionalItems: false, + } + should.throw(() => { + ajv.compile(badSchema1) + }, / minItems or maxItems\/additionalItems are not specified or different/) + + //@ts-expect-error + const badSchema2: JSONSchemaType = { + type: "array", + items: [{type: "string"}, {type: "number"}], + minItems: 2, + } + should.throw(() => { + ajv.compile(badSchema2) + }, / minItems or maxItems\/additionalItems are not specified or different/) + + //@ts-expect-error + const badSchema3: JSONSchemaType = { + type: "array", + items: [{type: "string"}, {type: "number"}], + minItems: 2, + maxItems: 3, + } + should.throw(() => { + ajv.compile(badSchema3) + }, / minItems or maxItems\/additionalItems are not specified or different/) + }) + }) }) function testStrictMode(schema, logPattern) { diff --git a/spec/options/strictTypes.spec.ts b/spec/options/strictTypes.spec.ts deleted file mode 100644 index 43bc027a87..0000000000 --- a/spec/options/strictTypes.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import _Ajv from "../ajv" -import chai from "../chai" -const should = chai.should() - -describe("strictTypes option", () => { - const ajv = new _Ajv({strictTypes: true}) - - describe("propertyNames", () => { - it('should set default data type "string"', () => { - ajv.compile({ - type: "object", - propertyNames: {maxLength: 5}, - }) - - ajv.compile({ - type: "object", - propertyNames: {type: "string", maxLength: 5}, - }) - - should.throw(() => { - ajv.compile({ - type: "object", - propertyNames: {type: "number"}, - }) - }, /type "number" not allowed by context/) - }) - }) -}) From 119351373c3efcdb727e381c96d94e86709e2434 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 22 Sep 2020 08:00:33 +0100 Subject: [PATCH 254/322] remove options serialilze and cache --- README.md | 16 +----------- lib/ajv.ts | 33 +++++++++--------------- lib/compile/index.ts | 3 --- spec/options/options_add_schemas.spec.ts | 16 ------------ 4 files changed, 13 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 213bc82cf1..a41efcc38e 100644 --- a/README.md +++ b/README.md @@ -1473,9 +1473,7 @@ const defaultOptions = { ownProperties: false, multipleOfPrecision: false, messages: true, - cache: new Map(), - serialize: (x) => x // (schema: object | boolean) => any - code: {es5: false, lines: false} + code: {es5: false, lines: false}, } ``` @@ -1550,18 +1548,6 @@ const defaultOptions = { - _ownProperties_: by default Ajv iterates over all enumerable object properties; when this option is `true` only own enumerable object properties (i.e. found directly on the object rather than on its prototype) are iterated. Contributed by @mbroadst. - _multipleOfPrecision_: by default `multipleOf` keyword is validated by comparing the result of division with parseInt() of that result. It works for dividers that are bigger than 1. For small dividers such as 0.01 the result of the division is usually not integer (even when it should be integer, see issue [#84](https://github.com/ajv-validator/ajv/issues/84)). If you need to use fractional dividers set this option to some positive integer N to have `multipleOf` validated using this formula: `Math.abs(Math.round(division) - division) < 1e-N` (it is slower but allows for float arithmetics deviations). - _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when messages are generated outside of Ajv code (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). -- _cache_: an optional instance of cache to store compiled schemas using schema (optionally serialized) as a key. If not passed, then a Map instance is used. Required interface for cache has the same methods as Map: - -```typescript -interface Cache { - set(key: unknown, value: object): void - get(key: unknown): object | undefined - delete(key: unknown): void - clear(): void -} -``` - -- _serialize_: an optional function to serialize schema to cache key. By default schema reference itself is used as a key. - _code_ (new in v7): code generation options: ```typescript diff --git a/lib/ajv.ts b/lib/ajv.ts index 30a1370486..fd789234aa 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -114,8 +114,6 @@ interface CurrentOptions { ownProperties?: boolean multipleOfPrecision?: boolean | number messages?: boolean - cache?: CacheInterface - serialize?: (schema: AnySchema) => unknown code?: { es5?: boolean lines?: boolean @@ -149,6 +147,8 @@ interface RemovedOptions { strictNumbers?: boolean uniqueItems?: boolean unknownFormats?: true | string[] | "ignore" + cache?: any + serialize?: (schema: AnySchema) => unknown } type OptionsInfo = { @@ -170,6 +170,8 @@ const removedOptions: OptionsInfo = { strictNumbers: "It is default now, see option `strict`.", uniqueItems: '"uniqueItems" keyword is always validated.', unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).", + cache: "Map is used as cache, schema object as key.", + serialize: "Map is used as cache, schema object as key.", } const deprecatedOptions: OptionsInfo = { @@ -189,7 +191,6 @@ type RequiredInstanceOptions = { | "loopEnum" | "meta" | "messages" - | "serialize" | "addUsedSchema" | "validateSchema" | "validateFormats"]: NonNullable @@ -210,7 +211,6 @@ function requiredOptions(o: Options): RequiredInstanceOptions { meta: o.meta ?? true, messages: o.messages ?? true, inlineRefs: o.inlineRefs ?? true, - serialize: o.serialize || ((x) => x), // "||" is to account for removed "false" option value addUsedSchema: o.addUsedSchema ?? true, validateSchema: o.validateSchema ?? true, validateFormats: o.validateFormats ?? true, @@ -223,13 +223,6 @@ export interface Logger { error(...args: unknown[]): unknown } -export interface CacheInterface { - set(key: unknown, value: SchemaEnv): void - get(key: unknown): SchemaEnv | undefined - delete(key: unknown): void - clear(): void -} - export default class Ajv { opts: InstanceOptions errors?: ErrorObject[] | null // errors from the last validation @@ -242,7 +235,7 @@ export default class Ajv { readonly RULES: ValidationRules readonly _compilations: Set = new Set() private readonly _loading: {[ref: string]: Promise | undefined} = {} - private readonly _cache: CacheInterface + private readonly _cache: Map = new Map() private readonly _metaOpts: InstanceOptions static ValidationError = ValidationError @@ -257,7 +250,6 @@ export default class Ajv { const formatOpt = opts.validateFormats opts.validateFormats = false - this._cache = opts.cache || new Map() this.RULES = getRules() checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED") checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn") @@ -475,13 +467,13 @@ export default class Ajv { return this case "string": { const sch = getSchEnv.call(this, schemaKeyRef) - if (typeof sch == "object") this._cache.delete(sch.cacheKey) + if (typeof sch == "object") this._cache.delete(sch.schema) delete this.schemas[schemaKeyRef] delete this.refs[schemaKeyRef] return this } case "object": { - const cacheKey = this.opts.serialize(schemaKeyRef) + const cacheKey = schemaKeyRef this._cache.delete(cacheKey) let id = schemaKeyRef.$id if (id) { @@ -608,7 +600,7 @@ export default class Ajv { if (typeof sch == "string") { delete schemas[keyRef] } else if (sch && !sch.meta) { - this._cache.delete(sch.cacheKey) + this._cache.delete(sch.schema) delete schemas[keyRef] } } @@ -624,13 +616,12 @@ export default class Ajv { if (typeof schema != "object" && typeof schema != "boolean") { throw new Error("schema must be object or boolean") } - const cacheKey = this.opts.serialize(schema) - let sch = this._cache.get(cacheKey) - if (sch) return sch + let sch = this._cache.get(schema) + if (sch !== undefined) return sch const localRefs = getSchemaRefs.call(this, schema) - sch = new SchemaEnv({schema, cacheKey, meta, localRefs}) - this._cache.set(sch.cacheKey, sch) + sch = new SchemaEnv({schema, meta, localRefs}) + this._cache.set(sch.schema, sch) const id = sch.baseId if (addSchema && !id.startsWith("#")) { // TODO atm it is allowed to overwrite schemas without id (instead of not adding them) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 00a88dd5cf..1b3bec3500 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -51,7 +51,6 @@ interface SchemaEnvArgs { baseId?: string localRefs?: LocalRefs meta?: boolean - cacheKey?: unknown } export class SchemaEnv implements SchemaEnvArgs { @@ -60,7 +59,6 @@ export class SchemaEnv implements SchemaEnvArgs { baseId: string // TODO possibly, it should be readonly localRefs?: LocalRefs readonly meta?: boolean - readonly cacheKey?: unknown readonly $async?: boolean readonly refs: SchemaRefs = {} validate?: AnyValidateFunction @@ -74,7 +72,6 @@ export class SchemaEnv implements SchemaEnvArgs { this.baseId = env.baseId ?? normalizeId(schema?.$id) this.localRefs = env.localRefs this.meta = env.meta - this.cacheKey = env.cacheKey this.$async = schema?.$async this.refs = {} } diff --git a/spec/options/options_add_schemas.spec.ts b/spec/options/options_add_schemas.spec.ts index f835ee3863..b56b60befa 100644 --- a/spec/options/options_add_schemas.spec.ts +++ b/spec/options/options_add_schemas.spec.ts @@ -117,20 +117,4 @@ describe("options to add schemas", () => { }) }) }) - - describe("serialize", () => { - let serializeCalled - - it("should use user-defined function to serialize schema to string", () => { - serializeCalled = undefined - const ajv = new _Ajv({serialize: serialize}) - ajv.addSchema({type: "string"}) - should.equal(serializeCalled, true) - }) - - function serialize(schema) { - serializeCalled = true - return JSON.stringify(schema) - } - }) }) From cf41c76a10b84ed23b9026ab11e8bbf238e1da5f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 22 Sep 2020 09:33:40 +0100 Subject: [PATCH 255/322] docs: move to DOCS folder, update FAQ (closes #874), update examples in JSON-SCHEMA.md (previously KEYWORDS.md), rename CUSTOM.md to KEYWORDS.md --- COERCION.md => DOCS/COERCION.md | 4 +- CONTRIBUTING.md => DOCS/CONTRIBUTING.md | 22 +- FAQ.md => DOCS/FAQ.md | 29 +- KEYWORDS.md => DOCS/JSON-SCHEMA.md | 438 +++++++++++------------- CUSTOM.md => DOCS/KEYWORDS.md | 2 +- README.md | 126 +++---- package.json | 2 +- 7 files changed, 283 insertions(+), 340 deletions(-) rename COERCION.md => DOCS/COERCION.md (97%) rename CONTRIBUTING.md => DOCS/CONTRIBUTING.md (87%) rename FAQ.md => DOCS/FAQ.md (82%) rename KEYWORDS.md => DOCS/JSON-SCHEMA.md (53%) rename CUSTOM.md => DOCS/KEYWORDS.md (99%) diff --git a/COERCION.md b/DOCS/COERCION.md similarity index 97% rename from COERCION.md rename to DOCS/COERCION.md index c2b159f391..a8fe0185cc 100644 --- a/COERCION.md +++ b/DOCS/COERCION.md @@ -1,6 +1,6 @@ # Ajv type coercion rules -To enable type coercion pass option `coerceTypes` to Ajv with `true` or `array` (it is `false` by default). See [example](https://github.com/ajv-validator/ajv#coercing-data-types). +To enable type coercion pass option `coerceTypes` to Ajv with `true` or `array` (it is `false` by default). See [example](../README.md#coercing-data-types). The coercion rules are different from JavaScript: @@ -24,7 +24,7 @@ Possible type coercions: | null | `""`→`null`
`"null"`⇸
`"abc"`⇸ | `0`→`null`
`x`⇸ | `false`→`null`
`true`⇸ | - | `[null]`→`null` | | array\* | `x`→`[x]` | `x`→`[x]` | `false`→`[false]`
`true`→`[true]` | `null`→`[null]` | - | -\* Requires option `{coerceTypes: 'array'}` +\* Requires option `{coerceTypes: "array"}` ## Coercion from string values diff --git a/CONTRIBUTING.md b/DOCS/CONTRIBUTING.md similarity index 87% rename from CONTRIBUTING.md rename to DOCS/CONTRIBUTING.md index e83384d008..937d53c3f0 100644 --- a/CONTRIBUTING.md +++ b/DOCS/CONTRIBUTING.md @@ -22,9 +22,9 @@ Ajv has a lot of features and maintaining documentation takes time. I appreciate ## Issues -Before submitting the issue please search the existing issues and also review [Frequently Asked Questions](https://github.com/ajv-validator/ajv/blob/master/FAQ.md). +Before submitting the issue please search the existing issues and also review [Frequently Asked Questions](./FAQ.md). -I would really appreciate the time you spend providing all the information and reducing both your schema and data to the smallest possible size when they still have the issue. Simplifying the issue also makes it more valuable for other users (in cases it turns out to be an incorrect usage rather than a bug). +It is really important that you spend time to provide all the relevant information and to reduce both your schema and data to the smallest possible size when they still have the issue. Simplifying the issue also makes it more valuable for other users (in cases it turns out to be an incorrect usage rather than a bug). #### Bug reports @@ -33,7 +33,7 @@ Please make sure to include the following information in the issue: 1. What version of Ajv are you using? Does the issue happen if you use the latest version? 2. Ajv options object (see https://github.com/ajv-validator/ajv#options). 3. JSON Schema and the data you are validating (please make it as small as possible to reproduce the issue). -4. Your code (please use `options`, `schema` and `data` as variables). +4. Your code sample (please use `options`, `schema` and `data` as variables). 5. Validation result, data AFTER validation, error messages. 6. What results did you expect? @@ -126,13 +126,6 @@ git submodule update --init npm test ``` -The full test suite runs for 3 minutes. If your change is trivial you can run quick test before committing (10 sec) and then disable pre-commit hook: - -```bash -npm run test-fast -git commit -nm 'type: message' -``` - `npm run build` - compiles typescript to dist folder. `npm run watch` - automatically compiles typescript when files in lib folder change @@ -144,10 +137,11 @@ To make accepting your changes faster please follow these steps: 1. Submit an [issue with the bug](https://github.com/ajv-validator/ajv/issues/new) or with the proposed change (unless the contribution is to fix the documentation typos and mistakes). 2. Please describe the proposed api and implementation plan (unless the issue is a relatively simple bug and fixing it doesn't change any api). 3. Once agreed, please write as little code as possible to achieve the desired result. -4. Please avoid unnecessary changes, refactoring or changing coding styles as part of your change (unless the change was proposed as refactoring). -5. Please follow the coding conventions even if they are not validated (and/or you use different conventions in your code). -6. Please run the tests before committing your code. -7. If tests fail in Travis after you make a PR please investigate and fix the issue. +4. Please add the tests both for the added feature and, in case the feature is some option, for the existing behaviour when this option is disabled. +5. Please avoid unnecessary changes, refactoring or changing coding styles as part of your change (unless the change was proposed as refactoring). +6. Please follow the coding conventions even if they are not validated (and/or you use different conventions in your code). +7. Please run the tests before committing your code. +8. If tests fail in Travis after you make a PR please investigate and fix the issue. #### Contributions license diff --git a/FAQ.md b/DOCS/FAQ.md similarity index 82% rename from FAQ.md rename to DOCS/FAQ.md index 18db0b8eb8..01ee304f12 100644 --- a/FAQ.md +++ b/DOCS/FAQ.md @@ -7,16 +7,18 @@ The purpose of this document is to help find answers quicker. I am happy to cont Ajv implements JSON schema specification. Before submitting the issue about the behaviour of any validation keywords please review them in: - [JSON Schema specification](https://tools.ietf.org/html/draft-handrews-json-schema-validation-00) (draft-07) -- [Validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md) in Ajv documentation +- [Validation keywords](./JSON-SCHEMA.md) in Ajv documentation - [JSON Schema tutorial](https://spacetelescope.github.io/understanding-json-schema/) (for draft-04) ##### Why Ajv validates empty object as valid? -"properties" keyword does not require the presence of any properties, you need to use "required" keyword. It also doesn't require that the data is an object, so any other type of data will also be valid. To require a specific type use "type" keyword. +"properties" keyword does not require the presence of any properties, you need to use "required" keyword. It also doesn't require that the data is an object, so any other type of data will also be valid. To require a specific type use "type" keyword. [Strict types mode](../README.md#strict-types) introduced in v7 requires presence of "type" when "properties" are used, so the mistakes are less likely. ##### Why Ajv validates only the first item of the array? -"items" keyword support [two syntaxes](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#items) - 1) when the schema applies to all items; 2) when there is a different schema for each item in the beginning of the array. This problem means you are using the second syntax. +"items" keyword support [two syntaxes](./JSON-SCHEMA.md#items) - 1) when the schema applies to all items; 2) when there is a different schema for each item in the beginning of the array. This problem means you are using the second syntax. + +In v7 with option `strictTuples` (`"log"` by default) this problem is less likely to happen, as Ajv would log warning about missing "minItems" and other keywords that are required to constrain tuple size. ## Ajv API for returning validation errors @@ -30,7 +32,7 @@ Ajv also supports asynchronous validation (with asynchronous formats and keyword ##### Would errors get overwritten in case of "concurrent" validations? -No. There is no concurrency in JavaScript - it is single-threaded. While a validation is run no other JavaScript code (that can access the same memory) can be executed. As long as the errors are used in the same execution block, the errors will not be overwritten. +No. There is no parallel execution in JavaScript, and the cooperative concurrency model of javascript makes this pattern safe. While a validation is run, no other JavaScript code that can access the same memory can be executed. As long as the errors are used in the same execution block, the errors will not be overwritten. ##### Can we change / extend API to add a method that would return errors (rather than assign them to `errors` property)? @@ -40,7 +42,7 @@ No. In many cases there is a module responsible for the validation in the applic Doing this would create a precedent where validated data is used in error messages, creating a vulnerability (e.g., when ajv is used to validate API data/parameters and error messages are logged). -Since the property name is already in the params object, in an application you can modify messages in any way you need. ajv-errors package allows modifying messages as well. +Since the property name is already in the params object, in an application you can modify the messages in any way you need. ajv-errors package allows modifying messages as well. ## Additional properties inside compound keywords anyOf, oneOf, etc. @@ -52,18 +54,19 @@ The keyword `additionalProperties` creates the restriction on validated data bas While you can expect that the schema below would allow the objects either with properties `foo` and `bar` or with properties `foo` and `baz` and all other properties will be prohibited, this schema will only allow objects with one property `foo` (an empty object and any non-objects will also be valid): -```json +```javascript { - "properties": {"foo": {"type": "number"}}, - "additionalProperties": false, - "oneOf": [ - {"properties": {"bar": {"type": "number"}}}, - {"properties": {"baz": {"type": "number"}}} + type: "object", + properties: {foo: {type: "number"}}, + additionalProperties: false, + oneOf: [ + {properties: {bar: {type: "number"}}}, + {properties: {baz: {type: "number"}}} ] } ``` -The reason for that is that `additionalProperties` keyword ignores properties inside `oneOf` keyword subschemas. That's not the limitation of Ajv or any other validator, that's how it must work according to the standard (and if you consider all the problems with the alternatives it is the only sensible way to define this keyword). +The reason for that is that `additionalProperties` keyword ignores properties inside `oneOf` keyword subschemas. That's not the limitation of Ajv or any other validator, that's how it must work according to the standard. There are several ways to implement the described logic that would allow two properties, please see the suggestions in the issues mentioned above. @@ -90,4 +93,4 @@ There were many conversations about the meaning of `$ref` in [JSON Schema GitHub There are two possible approaches: 1. Traverse schema (e.g. with json-schema-traverse) and replace every `$ref` with the referenced schema. -2. Use a specially constructed JSON Schema with a [user-defined keyword](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) to traverse and modify your schema. +2. Use a specially constructed JSON Schema with a [user-defined keyword](./KEYWORDS.md) to traverse and modify your schema. diff --git a/KEYWORDS.md b/DOCS/JSON-SCHEMA.md similarity index 53% rename from KEYWORDS.md rename to DOCS/JSON-SCHEMA.md index 73c3658350..0ad42fa2a2 100644 --- a/KEYWORDS.md +++ b/DOCS/JSON-SCHEMA.md @@ -8,13 +8,12 @@ The keywords and their values define what rules the data should satisfy to be va - [type](#type) - [Keywords for numbers](#keywords-for-numbers) - - [maximum / minimum and exclusiveMaximum / exclusiveMinimum](#maximum--minimum-and-exclusivemaximum--exclusiveminimum) (changed in draft-06) + - [maximum / minimum and exclusiveMaximum / exclusiveMinimum](#maximum--minimum-and-exclusivemaximum--exclusiveminimum) - [multipleOf](#multipleof) - [Keywords for strings](#keywords-for-strings) - [maxLength/minLength](#maxlength--minlength) - [pattern](#pattern) - [format](#format) - - [formatMaximum / formatMinimum and formatExclusiveMaximum / formatExclusiveMinimum](#formatmaximum--formatminimum-and-formatexclusivemaximum--formatexclusiveminimum-proposed) (proposed) - [Keywords for arrays](#keywords-for-arrays) - [maxItems/minItems](#maxitems--minitems) - [uniqueItems](#uniqueitems) @@ -48,19 +47,19 @@ Type can be: `number`, `integer`, `string`, `boolean`, `array`, `object` or `nul **Examples** -1. _schema_: `{ "type": "number" }` +1. _schema_: `{type: "number"}` _valid_: `1`, `1.5` _invalid_: `"abc"`, `"1"`, `[]`, `{}`, `null`, `true` -2) _schema_: `{ "type": "integer" }` +2) _schema_: `{type: "integer"}` _valid_: `1`, `2` _invalid_: `"abc"`, `"1"`, `1.5`, `[]`, `{}`, `null`, `true` -3. _schema_: `{ "type": ["number", "string"] }` +3. _schema_: `{type: ["number", "string"]}` _valid_: `1`, `1.5`, `"abc"`, `"1"` @@ -70,6 +69,8 @@ All examples above are JSON Schemas that only require data to be of certain type Most other keywords apply only to a particular type of data. If the data is of different type, the keyword will not apply and the data will be considered valid. +In v7 Ajv introduced [Strict types](../README.md/strict-types) mode that makes these mistakes less likely by requiring that types are constrained with type keyword whenever another keyword that applies to specific type is used. + ## Keywords for numbers ### `maximum` / `minimum` and `exclusiveMaximum` / `exclusiveMinimum` @@ -82,21 +83,21 @@ The value of keyword `exclusiveMaximum` (`exclusiveMinimum`) should be a number. **Examples** -1. _schema_: `{ "maximum": 5 }` +1. _schema_: `{type: "number", maximum: 5}` - _valid_: `4`, `5`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) + _valid_: `4`, `5` _invalid_: `6`, `7` -2. _schema_: `{ "minimum": 5 }` +2. _schema_: `{type: "number", minimum: 5}` - _valid_: `5`, `6`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) + _valid_: `5`, `6` _invalid_: `4`, `4.5` -3. _schema_: `{ "exclusiveMinimum": 5 }` +3. _schema_: `{type: "number", exclusiveMinimum: 5}` - _valid_: `6`, `7`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) + _valid_: `6`, `7` _invalid_: `4.5`, `5` @@ -106,15 +107,15 @@ The value of the keyword should be a number. The data to be valid should be a mu **Examples** -1. _schema_: `{ "multipleOf": 5 }` +1. _schema_: `{type: "number", multipleOf: 5}` - _valid_: `5`, `10`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) + _valid_: `5`, `10` _invalid_: `1`, `4` -2) _schema_: `{ "multipleOf": 2.5 }` +2) _schema_: `{type: "number", multipleOf: 2.5}` - _valid_: `2.5`, `5`, `7.5`, any non-number (`"abc"`, `[]`, `{}`, `null`, `true`) + _valid_: `2.5`, `5`, `7.5` _invalid_: `1`, `4` @@ -126,15 +127,15 @@ The value of the keywords should be a number. The data to be valid should have l **Examples** -1. _schema_: `{ "maxLength": 5 }` +1. _schema_: `{type: "string", maxLength: 5}` - _valid_: `"abc"`, `"abcde"`, any non-string (`1`, `[]`, `{}`, `null`, `true`) + _valid_: `"abc"`, `"abcde"` _invalid_: `"abcdef"` -2) _schema_: `{ "minLength": 2 }` +2) _schema_: `{type: "string", minLength": 2}` - _valid_: `"ab"`, `"😀😀"`, any non-string (`1`, `[]`, `{}`, `null`, `true`) + _valid_: `"ab"`, `"😀😀"` _invalid_: `"a"`, `"😀"` @@ -142,13 +143,13 @@ The value of the keywords should be a number. The data to be valid should have l The value of the keyword should be a string. The data to be valid should match the regular expression defined by the keyword value. -Ajv uses `new RegExp(value)` to create the regular expression that will be used to test data. +Ajv uses `new RegExp(value, "u")` to create the regular expression that will be used to test data. **Example** -_schema_: `{ "pattern": "[abc]+" }` +_schema_: `{type: "string", pattern: "[abc]+"}` -_valid_: `"a"`, `"abcd"`, `"cde"`, any non-string (`1`, `[]`, `{}`, `null`, `true`) +_valid_: `"a"`, `"abcd"`, `"cde"` _invalid_: `"def"`, `""` @@ -160,38 +161,12 @@ Ajv does not include any formats, they can be added with [ajv-formats](https://g **Example** -_schema_: `{ "format": "ipv4" }` +_schema_: `{type: "string", format: "ipv4"}` -_valid_: `"192.168.0.1"`, any non-string (`1`, `[]`, `{}`, `null`, `true`) +_valid_: `"192.168.0.1"` _invalid_: `"abc"` -### `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum` (proposed) - -Defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. - -The value of keyword `formatMaximum` (`formatMinimum`) should be a string. This value is the maximum (minimum) allowed value for the data to be valid as determined by `format` keyword. - -Ajv defines comparison rules for formats `"date"`, `"time"` and `"date-time"`. - -The value of keyword `formatExclusiveMaximum` (`formatExclusiveMinimum`) should be a boolean value. These keyword cannot be used without `formatMaximum` (`formatMinimum`). If this keyword value is equal to `true`, the data to be valid should not be equal to the value in `formatMaximum` (`formatMinimum`) keyword. - -**Example** - -_schema_: - -```json -{ - "format": "date", - "formatMaximum": "2016-02-06", - "formatExclusiveMaximum": true -} -``` - -_valid_: `"2015-12-31"`, `"2016-02-05"`, any non-string - -_invalid_: `"2016-02-06"`, `"2016-02-07"`, `"abc"` - ## Keywords for arrays ### `maxItems` / `minItems` @@ -200,9 +175,9 @@ The value of the keywords should be a number. The data array to be valid should **Example** -_schema_: `{ "maxItems": 3 }` +_schema_: `{type: "array", maxItems: 3}` -_valid_: `[]`, `[1]`, `["1", 2, "3"]`, any non-array (`"abc"`, `1`, `{}`, `null`, `true`) +_valid_: `[]`, `[1]`, `["1", 2, "3"]` _invalid_: `[1, 2, 3, 4]` @@ -212,96 +187,101 @@ The value of the keyword should be a boolean. If the keyword value is `true`, th **Example** -_schema_: `{ "uniqueItems": true }` +_schema_: `{type: "array", uniqueItems: true}` -_valid_: `[]`, `[1]`, `["1", 2, "3"]`, any non-array (`"abc"`, `1`, `{}`, `null`, `true`) +_valid_: `[]`, `[1]`, `["1", 2, "3"]` -_invalid_: `[1, 2, 1]`, `[{ "a": 1, "b": 2 }, { "b": 2, "a": 1 }]` +_invalid_: `[1, 2, 1]`, `[{a: 1, b: 2}, {b: 2, a: 1}]` ### `items` The value of the keyword should be an object or an array of objects. -If the keyword value is an object, then for the data array to be valid each item of the array should be valid according to the schema in this value. In this case the "additionalItems" keyword is ignored. +If the keyword value is an object, then for the data array to be valid each item of the array should be valid according to the schema in this value. In this case the `additionalItems` keyword is ignored. -If the keyword value is an array, then items with indices less than the number of items in the keyword should be valid according to the schemas with the same indices. Whether additional items are valid will depend on "additionalItems" keyword. +If the keyword value is an array, then items with indices less than the number of items in the keyword should be valid according to the schemas with the same indices. Whether additional items are valid will depend on `additionalItems` keyword. **Examples** -1. _schema_: `{ "items": { "type": "integer" } }` +1. _schema_: `{type: "array", items: {type: "integer"}}` - _valid_: `[1,2,3]`, `[]`, any non-array (`1`, `"abc"`, `{}`, `null`, `true`) + _valid_: `[1,2,3]`, `[]` _invalid_: `[1,"abc"]` 2) _schema_: - ```json + ```javascript { - "items": [{"type": "integer"}, {"type": "string"}] + type: "array", + items: [{type: "integer"}, {type: "string"}] } ``` - _valid_: `[1]`, `[1, "abc"]`, `[1, "abc", 2]`, `[]`, any non-array (`1`, `"abc"`, `{}`, `null`, `true`) + _valid_: `[1]`, `[1, "abc"]`, `[1, "abc", 2]`, `[]` _invalid_: `["abc", 1]`, `["abc"]` +The schema in example 2 will log warning by default (see `strictTuples` option), because it defines unconstrained tuple. To define a tuple with exactly 2 elements use `minItems` and `additionalItems` keywords (see example 1 in `additionalItems`). + ### `additionalItems` The value of the keyword should be a boolean or an object. -If "items" keyword is not present or it is an object, "additionalItems" keyword is ignored regardless of its value. +If `items` keyword is not present or it is an object, `additionalItems` keyword should be ignored regardless of its value. By default Ajv will throw exception in this case - see [Strict mode](../README.md#strict-mode) -If "items" keyword is an array and data array has not more items than the length of "items" keyword value, "additionalItems" keyword is also ignored. +If `items` keyword is an array and data array has not more items than the length of `items` keyword value, `additionalItems` keyword is also ignored. -If the length of data array is bigger than the length of "items" keyword value than the result of the validation depends on the value of "additionalItems" keyword: +If the length of data array is bigger than the length of "items" keyword value than the result of the validation depends on the value of `additionalItems` keyword: - `false`: data is invalid - `true`: data is valid - an object: data is valid if all additional items (i.e. items with indices greater or equal than "items" keyword value length) are valid according to the schema in "additionalItems" keyword. -**Examples** - -1. _schema_: `{ "additionalItems": { "type": "integer" } }` +The schemas in examples 2-3 log warning by default, use option `strictTuples: false` to allow) - any data is valid against such schema - "additionalItems" is ignored. +**Examples** -2) _schema_: +1. _schema_: - ```json + ```javascript { - "items": {"type": "integer"}, - "additionalItems": {"type": "string"} + type: "array", + items: [{type: "integer"}, {type: "integer"}], + minItems: 2 + additionalItems: false } ``` - _valid_: `[]`, `[1, 2]`, any non-array ("additionalItems" is ignored) + _valid_: `[1, 2]` - _invalid_: `[1, "abc"]`, (any array with some items other than integers) + _invalid_: `[]`, `[1]`, `[1, 2, 3]`, `[1, "abc"]` (any wrong number of items or wrong type) -3. _schema_: +2. _schema_: - ```json + ```javascript { - "items": [{"type": "integer"}, {"type": "integer"}], - "additionalItems": true + type: "array", + items: [{type: "integer"}, {type: "integer"}], + additionalItems: true } ``` - _valid_: `[]`, `[1, 2]`, `[1, 2, 3]`, `[1, 2, "abc"]`, any non-array + _valid_: `[]`, `[1, 2]`, `[1, 2, 3]`, `[1, 2, "abc"]` _invalid_: `["abc"]`, `[1, "abc", 3]` -4) _schema_: +3. _schema_: - ```json + ```javascript { - "items": [{"type": "integer"}, {"type": "integer"}], - "additionalItems": {"type": "string"} + type: "array", + items: [{type: "integer"}, {type: "integer"}], + additionalItems: {type: "string"} } ``` - _valid_: `[]`, `[1, 2]`, `[1, 2, "abc"]`, any non-array + _valid_: `[]`, `[1, 2]`, `[1, 2, "abc"]` _invalid_: `["abc"]`, `[1, 2, 3]` @@ -311,25 +291,12 @@ The value of the keyword is a JSON Schema. The array is valid if it contains at **Example** -_schema_: `{ "contains": { "type": "integer" } }` +_schema_: `{type: "array", contains: {type: "integer"}}` -_valid_: `[1]`, `[1, "foo"]`, any array with at least one integer, any non-array +_valid_: `[1]`, `[1, "foo"]`, any array with at least one integer _invalid_: `[]`, `["foo", "bar"]`, any array without integers -The schema from the example above is equivalent to: - -```json -{ - "not": { - "type": "array", - "items": { - "not": {"type": "integer"} - } - } -} -``` - ## Keywords for objects ### `maxProperties` / `minProperties` @@ -338,11 +305,11 @@ The value of the keywords should be a number. The data object to be valid should **Example** -_schema_: `{ "maxProperties": 2 }` +_schema_: `{type: "object", maxProperties: 2 }` -_valid_: `{}`, `{"a": 1}`, `{"a": "1", "b": 2}`, any non-object +_valid_: `{}`, `{a: 1}`, `{a: "1", b: 2}` -_invalid_: `{"a": 1, "b": 2, "c": 3}` +_invalid_: `{a: 1, b: 2, c: 3}` ### `required` @@ -350,11 +317,11 @@ The value of the keyword should be an array of unique strings. The data object t **Example** -_schema_: `{ "required": ["a", "b"] }` +_schema_: `{type: "object", required: ["a", "b"]}` -_valid_: `{"a": 1, "b": 2}`, `{"a": 1, "b": 2, "c": 3}`, any non-object +_valid_: `{a: 1, b: 2}`, `{a: 1, b: 2, c: 3}` -_invalid_: `{}`, `{"a": 1}`, `{"c": 3, "d":4}` +_invalid_: `{}`, `{a: 1}`, `{c: 3, d: 4}` ### `properties` @@ -366,21 +333,22 @@ The value of the keyword should be a map with keys equal to data object properti _schema_: -```json +```javascript { - "properties": { - "foo": {"type": "string"}, - "bar": { - "type": "number", - "minimum": 2 + type: "object", + properties: { + foo: {type: "string"}, + bar: { + type: "number", + minimum: 2 } } } ``` -_valid_: `{}`, `{"foo": "a"}`, `{"foo": "a", "bar": 2}`, any non-object +_valid_: `{}`, `{foo: "a"}`, `{foo: "a", bar: 2}` -_invalid_: `{"foo": 1}`, `{"foo": "a", "bar": 1}` +_invalid_: `{foo: 1}`, `{foo: "a", bar: 1}` ### `patternProperties` @@ -388,24 +356,28 @@ The value of this keyword should be a map where keys should be regular expressio When the value in data object property matches multiple regular expressions it should be valid according to all the schemas for all matched regular expressions. -**Please note**: `patternProperties` keyword does not require that properties matching patterns are present in the object (see examples). +**Please note**: + +1. `patternProperties` keyword does not require that properties matching patterns are present in the object (see examples). +2. By default, Ajv does not allow schemas where patterns in `patternProperties` match any property name in `properties` keyword - that leads to unexpected validation results. It can be allowed with option `allowMatchingProperties`. See [Strict mode](../README.md#strict-mode) **Example** _schema_: -```json +```javascript { - "patternProperties": { - "^fo.*$": {"type": "string"}, - "^ba.*$": {"type": "number"} + type: "object", + patternProperties: { + "^fo.*$": {type: "string"}, + "^ba.*$": {type: "number"} } } ``` -_valid_: `{}`, `{"foo": "a"}`, `{"foo": "a", "bar": 1}`, any non-object +_valid_: `{}`, `{foo: "a"}`, `{foo: "a", bar: 1}` -_invalid_: `{"foo": 1}`, `{"foo": "a", "bar": "b"}` +_invalid_: `{foo: 1}`, `{foo: "a", bar: "b"}` ### `additionalProperties` @@ -421,66 +393,69 @@ If the value is a schema for the data object to be valid the values in all "addi 1. _schema_: - ```json + ```javascript { - "properties": { - "foo": {"type": "number"} + type: "object", + properties: { + foo: {type: "number"} }, - "patternProperties": { - "^.*r$": {"type": "number"} + patternProperties: { + "^.*r$": {type: "number"} }, - "additionalProperties": false + additionalProperties: false } ``` - _valid_: `{}`, `{"foo": 1}`, `{"foo": 1, "bar": 2}`, any non-object + _valid_: `{}`, `{foo: 1}`, `{foo: 1, bar: 2}` - _invalid_: `{"a": 3}`, `{"foo": 1, "baz": 3}` + _invalid_: `{a: 3}`, `{foo: 1, baz: 3}` 2. _schema_: - ```json + ```javascript { - "properties": { - "foo": {"type": "number"} + type: "object", + properties: { + foo: {type: "number"} }, - "patternProperties": { - "^.*r$": {"type": "number"} + patternProperties: { + "^.*r$": {type: "number"} }, - "additionalProperties": {"type": "string"} + additionalProperties: {type: "string"} } ``` - _valid_: `{}`, `{"a": "b"}`, `{"foo": 1}`, `{"foo": 1, "bar": 2}`, `{"foo": 1, "bar": 2, "a": "b"}`, any non-object + _valid_: `{}`, `{a: "b"}`, `{foo: 1}`, `{foo: 1, bar: 2}`, `{foo: 1, bar: 2, a: "b"}` - _invalid_: `{"a": 3}`, `{"foo": 1, "baz": 3}` + _invalid_: `{a: 3}`, `{foo: 1, baz: 3}` 3. _schema_: - ```json + ```javascript { - "properties": { - "foo": {"type": "number"} + type: "object", + properties: { + foo: {type: "number"} }, - "additionalProperties": false, - "anyOf": [ + additionalProperties: false, + anyOf: [ { - "properties": { - "bar": {"type": "number"} + properties: { + bar: {type: "number"} } }, { - "properties": { - "baz": {"type": "number"} + properties: { + baz: {type: "number"} } } ] } ``` - _valid_: `{}`, `{"foo": 1}`, any non-object + _valid_: `{}`, `{foo: 1}` - _invalid_: `{"bar": 2}`, `{"baz": 3}`, `{"foo": 1, "bar": 2}`, etc. + _invalid_: `{bar: 2}`, `{baz: 3}`, `{foo: 1, bar: 2}`, etc. ### `dependencies` @@ -494,35 +469,37 @@ For schema dependency, if the data object contains a property that is a key in t 1. _schema (property dependency)_: - ```json + ```javascript { - "dependencies": { - "foo": ["bar", "baz"] + type: "object", + dependencies: { + foo: ["bar", "baz"] } } ``` - _valid_: `{"foo": 1, "bar": 2, "baz": 3}`, `{}`, `{"a": 1}`, any non-object + _valid_: `{foo: 1, bar: 2, baz: 3}`, `{}`, `{a: 1}` - _invalid_: `{"foo": 1}`, `{"foo": 1, "bar": 2}`, `{"foo": 1, "baz": 3}` + _invalid_: `{foo: 1}`, `{foo: 1, bar: 2}`, `{foo: 1, baz: 3}` 2) _schema (schema dependency)_: - ```json + ```javascript { - "dependencies": { - "foo": { - "properties": { - "bar": {"type": "number"} + type: "object", + dependencies: { + foo: { + properties: { + bar: {type: "number"} } } } } ``` - _valid_: `{}`, `{"foo": 1}`, `{"foo": 1, "bar": 2}`, `{"a": 1}`, any non-object + _valid_: `{}`, `{foo: 1}`, `{foo: 1, bar: 2}`, `{a: 1}` - _invalid_: `{"foo": 1, "bar": "a"}` + _invalid_: `{foo: 1, bar: "a"}` ### `propertyNames` @@ -534,37 +511,18 @@ For data object to be valid each property name in this object should be valid ac _schema_ (requires `email` format from [ajv-formats](https://github.com/ajv-validator/ajv-formats)): -```json +```javascript { - "propertyNames": {"format": "email"} + type: "object", + propertyNames: { + format: "email" + } } ``` -_valid_: `{"foo@bar.com": "any", "bar@bar.com": "any"}`, any non-object - -_invalid_: `{"foo": "any value"}` - -### `patternRequired` (proposed) - -Defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. - -The value of this keyword should be an array of strings, each string being a regular expression. For data object to be valid each regular expression in this array should match at least one property name in the data object. - -If the array contains multiple regular expressions, more than one expression can match the same property name. - -**Examples** - -1. _schema_: `{ "patternRequired": [ "f.*o" ] }` - - _valid_: `{ "foo": 1 }`, `{ "-fo-": 1 }`, `{ "foo": 1, "bar": 2 }`, any non-object - - _invalid_: `{}`, `{ "bar": 2 }`, `{ "Foo": 1 }`, - -2. _schema_: `{ "patternRequired": [ "f.*o", "b.*r" ] }` +_valid_: `{"foo@bar.com": "any", "bar@bar.com": "any"}` - _valid_: `{ "foo": 1, "bar": 2 }`, `{ "foobar": 3 }`, any non-object - - _invalid_: `{}`, `{ "foo": 1 }`, `{ "bar": 2 }` +_invalid_: `{foo: "any value"}` ## Keywords for all types @@ -574,11 +532,11 @@ The value of the keyword should be an array of unique items of any types. The da **Example** -_schema_: `{ "enum": [ 2, "foo", {"foo": "bar" }, [1, 2, 3] ] }` +_schema_: `{enum: [2, "foo", {foo: "bar" }, [1, 2, 3]]}` -_valid_: `2`, `"foo"`, `{"foo": "bar"}`, `[1, 2, 3]` +_valid_: `2`, `"foo"`, `{foo: "bar"}`, `[1, 2, 3]` -_invalid_: `1`, `"bar"`, `{"foo": "baz"}`, `[1, 2, 3, 4]`, any value not in the array +_invalid_: `1`, `"bar"`, `{foo: "baz"}`, `[1, 2, 3, 4]`, any value not in enum ### `const` @@ -586,30 +544,31 @@ The value of this keyword can be anything. The data is valid if it is deeply equ **Example** -_schema_: `{ "const": "foo" }` +_schema_: `{const: "foo"}` _valid_: `"foo"` _invalid_: any other value -The same can be achieved with `enum` keyword using the array with one item. But `const` keyword is more than just a syntax sugar for `enum`. In combination with the [\$data reference](https://github.com/ajv-validator/ajv#data-reference) it allows to define equality relations between different parts of the data. This cannot be achieved with `enum` keyword even with `$data` reference because `$data` cannot be used in place of one item - it can only be used in place of the whole array in `enum` keyword. +The same can be achieved with `enum` keyword using the array with one item. But `const` keyword is more than just a syntax sugar for `enum`. In combination with the [\$data reference](../README.md#data-reference) it allows to define equality relations between different parts of the data. This cannot be achieved with `enum` keyword even with `$data` reference because `$data` cannot be used in place of one item - it can only be used in place of the whole array in `enum` keyword. **Example** _schema_: -```json +```javascript { - "properties": { - "foo": {"type": "number"}, - "bar": {"const": {"$data": "1/foo"}} + type: "object", + properties: { + foo: {type: "number"}, + bar: {const: {$data: "1/foo"}} } } ``` -_valid_: `{ "foo": 1, "bar": 1 }`, `{}` +_valid_: `{foo: 1, bar: 1}`, `{}` -_invalid_: `{ "foo": 1 }`, `{ "bar": 1 }`, `{ "foo": 1, "bar": 2 }` +_invalid_: `{foo: 1}`, `{bar: 1}`, `{foo: 1, bar: 2}` ## Compound keywords @@ -617,29 +576,13 @@ _invalid_: `{ "foo": 1 }`, `{ "bar": 1 }`, `{ "foo": 1, "bar": 2 }` The value of the keyword should be a JSON Schema. The data is valid if it is invalid according to this schema. -**Examples** - -1. _schema_: `{ "not": { "minimum": 3 } }` - - _valid_: `1`, `2` - - _invalid_: `3`, `4`, any non-number - -2. _schema_: +**Example** - ```json - { - "not": { - "items": { - "not": {"type": "string"} - } - } - } - ``` +_schema_: `{type: "number", not: {minimum: 3}}` - _valid_: `["a"]`, `[1, "a"]`, any array containing at least one string +_valid_: `1`, `2` - _invalid_: `[]`, `[1]`, any non-array, any array not containing strings +_invalid_: `3`, `4` ### `oneOf` @@ -649,13 +592,14 @@ The value of the keyword should be an array of JSON Schemas. The data is valid i _schema_: -```json +```javascript { - "oneOf": [{"maximum": 3}, {"type": "integer"}] + type: "number", + oneOf: [{maximum: 3}, {type: "integer"}] } ``` -_valid_: `1.5`, `2.5`, `4`, `5`, any non-number +_valid_: `1.5`, `2.5`, `4`, `5` _invalid_: `2`, `3`, `4.5`, `5.5` @@ -667,13 +611,14 @@ The value of the keyword should be an array of JSON Schemas. The data is valid i _schema_: -```json +```javascript { - "anyOf": [{"maximum": 3}, {"type": "integer"}] + type: "number", + anyOf: [{maximum: 3}, {type: "integer"}] } ``` -_valid_: `1.5`, `2`, `2.5`, `3`, `4`, `5`, any non-number +_valid_: `1.5`, `2`, `2.5`, `3`, `4`, `5` _invalid_: `4.5`, `5.5` @@ -685,15 +630,16 @@ The value of the keyword should be an array of JSON Schemas. The data is valid i _schema_: -```json +```javascript { - "allOf": [{"maximum": 3}, {"type": "integer"}] + type: "number", + allOf: [{maximum: 3}, {type: "integer"}] } ``` _valid_: `2`, `3` -_invalid_: `1.5`, `2.5`, `4`, `4.5`, `5`, `5.5`, any non-number +_invalid_: `1.5`, `2.5`, `4`, `4.5`, `5`, `5.5` ### `if`/`then`/`else` @@ -709,39 +655,39 @@ If the data is invalid against the sub-schema in `if` keyword, then the validati 1. _schema_: - ```json + ```javascript { - "if": {"properties": {"power": {"minimum": 9000}}}, - "then": {"required": ["disbelief"]}, - "else": {"required": ["confidence"]} + type: "object", + if: {properties: {foo: {minimum: 10}}}, + then: {required: ["bar"]}, + else: {required: ["baz"]} } ``` _valid_: - - `{ "power": 10000, "disbelief": true }` + - `{foo: 10, bar: true }` - `{}` - - `{ "power": 1000, "confidence": true }` - - any non-object + - `{foo: 1, baz: true }` _invalid_: - - `{ "power": 10000 }` (`disbelief` is required) - - `{ "power": 10000, "confidence": true }` (`disbelief` is required) - - `{ "power": 1000 }` (`confidence` is required) + - `{foo: 10}` (`bar` is required) + - `{foo: 10, baz: true }` (`bar` is required) + - `{foo: 1}` (`baz` is required) 2) _schema_: - ```json + ```javascript { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "if": {"minimum": 100}, - "then": {"multipleOf": 100}, - "else": { - "if": {"minimum": 10}, - "then": {"multipleOf": 10} + type: "integer", + minimum: 1, + maximum: 1000, + if: {minimum: 100}, + then: {multipleOf: 100}, + else: { + if: {minimum: 10}, + then": {multipleOf: 10} } } ``` @@ -752,5 +698,5 @@ If the data is invalid against the sub-schema in `if` keyword, then the validati - `-1`, `0` (<1) - `2000` (>1000) - - `11`, `57`, `123` (any number with more than one non-zero digit) + - `11`, `57`, `123` (any integer with more than one non-zero digit) - non-integers diff --git a/CUSTOM.md b/DOCS/KEYWORDS.md similarity index 99% rename from CUSTOM.md rename to DOCS/KEYWORDS.md index 81fb6a0466..aea4aad028 100644 --- a/CUSTOM.md +++ b/DOCS/KEYWORDS.md @@ -195,7 +195,7 @@ console.log(validate([3, 4, 5])) // true, number 5 matches schema inside "contai `contains` keyword is already available in Ajv with option `v5: true`. -See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/ajv-validator/ajv/blob/master/spec/keyword.spec.js#L151). +See the example of defining recursive macro keyword `deepProperties` in the [test](../spec/keyword.spec.ts#L316). ### Define keyword with "inline" compilation function diff --git a/README.md b/README.md index a41efcc38e..d30a5e460b 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")) - [Performance](#performance) - [Features](#features) - [Getting started](#getting-started) -- [Frequently Asked Questions](https://github.com/ajv-validator/ajv/blob/master/FAQ.md) +- [Frequently Asked Questions](./DOCS/FAQ.md) - [Using in browser](#using-in-browser) - [Ajv and Content Security Policies (CSP)](#ajv-and-content-security-policies-csp) - [Command line interface](#command-line-interface) @@ -127,7 +127,7 @@ Performance of different validators by [json-schema-benchmark](https://github.co ## Features - Ajv implements full JSON Schema [draft-06/07](http://json-schema.org/) standards (draft-04 is supported in v6): - - all validation keywords (see [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md)) + - all validation keywords (see [JSON Schema validation keywords](./DOCS/JSON-SCHEMA.md)) - keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) - support of circular references between schemas @@ -543,20 +543,20 @@ Strict mode also affects number validation. By default Ajv fails `{"type": "numb Ajv supports all validation keywords from draft-07 of JSON Schema standard: -- [type](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#type) -- [for numbers](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-numbers) - maximum, minimum, exclusiveMaximum, exclusiveMinimum, multipleOf -- [for strings](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-strings) - maxLength, minLength, pattern, format -- [for arrays](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-arrays) - maxItems, minItems, uniqueItems, items, additionalItems, [contains](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#contains) -- [for objects](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-objects) - maxProperties, minProperties, required, properties, patternProperties, additionalProperties, dependencies, [propertyNames](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#propertynames) -- [for all types](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#keywords-for-all-types) - enum, [const](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#const) -- [compound keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#compound-keywords) - not, oneOf, anyOf, allOf, [if/then/else](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#ifthenelse) +- [type](./DOCS/JSON-SCHEMA.md#type) +- [for numbers](./DOCS/JSON-SCHEMA.md#keywords-for-numbers) - maximum, minimum, exclusiveMaximum, exclusiveMinimum, multipleOf +- [for strings](./DOCS/JSON-SCHEMA.md#keywords-for-strings) - maxLength, minLength, pattern, format +- [for arrays](./DOCS/JSON-SCHEMA.md#keywords-for-arrays) - maxItems, minItems, uniqueItems, items, additionalItems, [contains](./DOCS/JSON-SCHEMA.md#contains) +- [for objects](./DOCS/JSON-SCHEMA.md#keywords-for-objects) - maxProperties, minProperties, required, properties, patternProperties, additionalProperties, dependencies, [propertyNames](./DOCS/JSON-SCHEMA.md#propertynames) +- [for all types](./DOCS/JSON-SCHEMA.md#keywords-for-all-types) - enum, [const](./DOCS/JSON-SCHEMA.md#const) +- [compound keywords](./DOCS/JSON-SCHEMA.md#compound-keywords) - not, oneOf, anyOf, allOf, [if/then/else](./DOCS/JSON-SCHEMA.md#ifthenelse) With [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package Ajv also supports validation keywords from [JSON Schema extension proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) for JSON Schema standard: -- [patternRequired](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#patternrequired-proposed) - like `required` but with patterns that some property should match. -- [formatMaximum, formatMinimum, formatExclusiveMaximum, formatExclusiveMinimum](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md#formatmaximum--formatminimum-and-exclusiveformatmaximum--exclusiveformatminimum-proposed) - setting limits for date, time, etc. +- [patternRequired](./DOCS/JSON-SCHEMA.md#patternrequired-proposed) - like `required` but with patterns that some property should match. +- [formatMaximum, formatMinimum, formatExclusiveMaximum, formatExclusiveMinimum](./DOCS/JSON-SCHEMA.md#formatmaximum--formatminimum-and-exclusiveformatmaximum--exclusiveformatminimum-proposed) - setting limits for date, time, etc. -See [JSON Schema validation keywords](https://github.com/ajv-validator/ajv/blob/master/KEYWORDS.md) for more details. +See [JSON Schema validation keywords](./DOCS/JSON-SCHEMA.md) for more details. ## Annotation keywords @@ -730,16 +730,16 @@ Examples. Using `$merge`: -```json +```javascript { - "$merge": { - "source": { - "type": "object", - "properties": {"p": {"type": "string"}}, - "additionalProperties": false + $merge: { + source: { + type: "object", + properties: {p: {type: "string"}}, + additionalProperties: false }, - "with": { - "properties": {"q": {"type": "number"}} + with: { + properties: {q: {type: "number"}} } } } @@ -747,29 +747,29 @@ Using `$merge`: Using `$patch`: -```json +```javascript { - "$patch": { - "source": { - "type": "object", - "properties": {"p": {"type": "string"}}, - "additionalProperties": false + $patch: { + source: { + type: "object", + properties: {p: {type: "string"}}, + additionalProperties: false }, - "with": [{"op": "add", "path": "/properties/q", "value": {"type": "number"}}] + with: [{op: "add", path: "/properties/q", value: {type: "number"}}] } } ``` The schemas above are equivalent to this schema: -```json +```javascript { - "type": "object", - "properties": { - "p": {"type": "string"}, - "q": {"type": "number"} + type: "object", + properties: { + p: {type: "string"}, + q: {type: "number"} }, - "additionalProperties": false + additionalProperties: false } ``` @@ -824,7 +824,7 @@ console.log(validate(4)) // false Several keywords (typeof, instanceof, range and propertyNames) are defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - they can be used for your schemas and as a starting point for your own keywords. -See [User-defined keywords](https://github.com/ajv-validator/ajv/blob/master/CUSTOM.md) for more details. +See [User-defined keywords](./DOCS/KEYWORDS.md) for more details. ## Asynchronous schema compilation @@ -966,7 +966,7 @@ Some keywords in JSON Schemas can lead to very slow validation for certain data. **Please note**: The suggestions above to prevent slow validation would only work if you do NOT use `allErrors: true` in production code (using it would continue validation after validation errors). -You can validate your JSON schemas against [this meta-schema](https://github.com/ajv-validator/ajv/blob/master/lib/refs/json-schema-secure.json) to check that these recommendations are followed: +You can validate your JSON schemas against [this meta-schema](./lib/refs/json-schema-secure.json) to check that these recommendations are followed: ```javascript const isSchemaSecure = ajv.compile(require("ajv/lib/refs/json-schema-secure.json")) @@ -990,14 +990,14 @@ Certain regular expressions can lead to the exponential evaluation time even wit Please assess the regular expressions you use in the schemas on their vulnerability to this attack - see [safe-regex](https://github.com/substack/safe-regex), for example. -**Please note**: some formats that Ajv implements use [regular expressions](https://github.com/ajv-validator/ajv/blob/master/lib/compile/formats.js) that can be vulnerable to ReDoS attack, so if you use Ajv to validate data from untrusted sources **it is strongly recommended** to consider the following: +**Please note**: some formats that [ajv-formats](https://github.com/ajv-validator/ajv-formats) package implements use [regular expressions](https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts) that can be vulnerable to ReDoS attack, so if you use Ajv to validate data from untrusted sources **it is strongly recommended** to consider the following: -- making assessment of "format" implementations in Ajv. -- using `format: 'fast'` option that simplifies some of the regular expressions (although it does not guarantee that they are safe). -- replacing format implementations provided by Ajv with your own implementations of "format" keyword that either uses different regular expressions or another approach to format validation. Please see [addFormat](#api-addformat) method. +- making assessment of "format" implementations in [ajv-formats](https://github.com/ajv-validator/ajv-formats). +- passing `"fast"` option to ajv-formats plugin (see its docs) that simplifies some of the regular expressions (although it does not guarantee that they are safe). +- replacing format implementations provided by ajv-formats with your own implementations of "format" keyword that either use different regular expressions or another approach to format validation. Please see [addFormat](#api-addformat) method. - disabling format validation by ignoring "format" keyword with option `format: false` -Whatever mitigation you choose, please assume all formats provided by Ajv as potentially unsafe and make your own assessment of their suitability for your validation scenarios. +Whatever mitigation you choose, please assume all formats provided by ajv-formats as potentially unsafe and make your own assessment of their suitability for your validation scenarios. ## Filtering data @@ -1043,23 +1043,23 @@ If the option were `"failing"` then property `additional1` would have been remov **Please note**: If you use `removeAdditional` option with `additionalProperties` keyword inside `anyOf`/`oneOf` keywords your validation can fail with this schema, for example: -```json +```javascript { - "type": "object", - "oneOf": [ + type: "object", + oneOf: [ { - "properties": { - "foo": {"type": "string"} + properties: { + foo: {type: "string"} }, - "required": ["foo"], - "additionalProperties": false + required: ["foo"], + additionalProperties: false }, { - "properties": { - "bar": {"type": "integer"} + properties: { + bar: {type: "integer"} }, - "required": ["bar"], - "additionalProperties": false + required: ["bar"], + additionalProperties: false } ] } @@ -1071,15 +1071,15 @@ With the option `removeAdditional: true` the validation will pass for the object While this behaviour is unexpected (issues [#129](https://github.com/ajv-validator/ajv/issues/129), [#134](https://github.com/ajv-validator/ajv/issues/134)), it is correct. To have the expected behaviour (both objects are allowed and additional properties are removed) the schema has to be refactored in this way: -```json +```javascript { - "type": "object", - "properties": { - "foo": {"type": "string"}, - "bar": {"type": "integer"} + type: "object", + properties: { + foo: {type: "string"}, + bar: {type: "integer"} }, - "additionalProperties": false, - "oneOf": [{"required": ["foo"]}, {"required": ["bar"]}] + additionalProperties: false, + oneOf: [{required: ["foo"]}, {required: ["bar"]}] } ``` @@ -1193,7 +1193,7 @@ console.log(data) // { "foo": [1], "bar": false } The coercion rules, as you can see from the example, are different from JavaScript both to validate user input as expected and to have the coercion reversible (to correctly validate cases where different types are defined in subschemas of "anyOf" and other compound keywords). -See [Coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.md) for details. +See [Coercion rules](./DOCS/COERCION.md) for details. ## API @@ -1525,7 +1525,7 @@ const defaultOptions = { - `false` (default) - do not use defaults - `true` - insert defaults by value (object literal is used). - `"empty"` - in addition to missing or undefined, use defaults for properties and items that are equal to `null` or `""` (an empty string). -- _coerceTypes_: change data type of data to match `type` keyword. See the example in [Coercing data types](#coercing-data-types) and [coercion rules](https://github.com/ajv-validator/ajv/blob/master/COERCION.md). Option values: +- _coerceTypes_: change data type of data to match `type` keyword. See the example in [Coercing data types](#coercing-data-types) and [coercion rules](./DOCS/COERCION.md). Option values: - `false` (default) - no type coercion. - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). @@ -1781,7 +1781,7 @@ npm test `npm run watch` - automatically compiles typescript when files in lib folder change -Please see [Contributing guidelines](https://github.com/ajv-validator/ajv/blob/master/CONTRIBUTING.md) +Please see [Contributing guidelines](./DOCS/CONTRIBUTING.md) ## Changes history @@ -1799,7 +1799,7 @@ See https://github.com/ajv-validator/ajv/releases ## Code of conduct -Please review and follow the [Code of conduct](https://github.com/ajv-validator/ajv/blob/master/CODE_OF_CONDUCT.md). +Please review and follow the [Code of conduct](./CODE_OF_CONDUCT.md). Please report any unacceptable behaviour to ajv.validator@gmail.com - it will be reviewed by the project team. @@ -1809,4 +1809,4 @@ Ajv is a part of [Tidelift subscription](https://tidelift.com/subscription/pkg/n ## License -[MIT](https://github.com/ajv-validator/ajv/blob/master/LICENSE) +[MIT](./LICENSE) diff --git a/package.json b/package.json index a5228270d1..0bc215acd1 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "types": "dist/ajv.d.ts", "files": [ "lib/", + "DOCS/", "dist/", "scripts/", - "LICENSE", ".tonic_example.js" ], "scripts": { From cac624b71a3ee8976787170f99830cc2b8a7d00f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 22 Sep 2020 14:33:02 +0100 Subject: [PATCH 256/322] split readme to files, fix links --- .github/ISSUE_TEMPLATE.md | 4 +- .github/ISSUE_TEMPLATE/bug-or-error-report.md | 4 +- .github/ISSUE_TEMPLATE/change.md | 2 +- .github/ISSUE_TEMPLATE/compatibility.md | 2 +- .github/ISSUE_TEMPLATE/installation.md | 2 +- .github/ISSUE_TEMPLATE/typescript.md | 2 +- DOCS/CONTRIBUTING.md => CONTRIBUTING.md | 4 +- DOCS/COERCION.md | 2 +- DOCS/FAQ.md | 28 +- DOCS/JSON-SCHEMA.md | 9 +- DOCS/KEYWORDS.md | 8 +- DOCS/api.md | 528 +++++ DOCS/codegen.md | 67 + DOCS/security.md | 85 + DOCS/strict-mode.md | 284 +++ DOCS/validation.md | 595 ++++++ README.md | 1733 ++--------------- 17 files changed, 1737 insertions(+), 1622 deletions(-) rename DOCS/CONTRIBUTING.md => CONTRIBUTING.md (98%) create mode 100644 DOCS/api.md create mode 100644 DOCS/codegen.md create mode 100644 DOCS/security.md create mode 100644 DOCS/strict-mode.md create mode 100644 DOCS/validation.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 2d5317e14f..a56cb068d5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ + ```javascript diff --git a/.github/ISSUE_TEMPLATE/bug-or-error-report.md b/.github/ISSUE_TEMPLATE/bug-or-error-report.md index 897166f412..3727cba038 100644 --- a/.github/ISSUE_TEMPLATE/bug-or-error-report.md +++ b/.github/ISSUE_TEMPLATE/bug-or-error-report.md @@ -7,7 +7,7 @@ assignees: "" --- + ```javascript diff --git a/.github/ISSUE_TEMPLATE/change.md b/.github/ISSUE_TEMPLATE/change.md index 13eb1ed0fd..b499573205 100644 --- a/.github/ISSUE_TEMPLATE/change.md +++ b/.github/ISSUE_TEMPLATE/change.md @@ -7,7 +7,7 @@ assignees: "" ---