ata-validator 1.15.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +27 -2
- package/compat.d.ts +89 -17
- package/compat.js +367 -16
- package/index.d.ts +26 -0
- package/index.js +25 -7
- package/lib/aot-impl.js +18 -0
- package/lib/buffer-gate.js +7 -1
- package/lib/compat-errors.js +343 -0
- package/lib/interpreter.js +112 -2
- package/lib/js-compiler.js +32 -5
- package/lib/keywords.js +113 -0
- package/lib/plan-compiler.js +34 -2
- package/lib/version.js +1 -1
- package/package.json +11 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 1.16.1 - 2026-09-12
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A custom format (`formats: { name: fn }`, or `addFormat` on the compat class) next to a shape the combined code generator declines, such as an `anyOf` sibling, threw `_uf_<name> is not defined` on the first invalid document instead of reporting the format error. The error-path code generator emitted the call to the format checker without binding it; it now takes the checkers as bound parameters like the other two entry points. Verdicts were never affected, only the error path, and only on that combination. Present since custom formats were added; caught by the 1.16.0 clean-install check. `tests/test_user_format_error_path.js` covers it.
|
|
10
|
+
|
|
11
|
+
## 1.16.0 - 2026-09-12
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Custom keywords. `new Validator(schema, { keywords: { name: definition } })` registers keywords with a `validate(value, data, parentSchema)` function, a `compile(value, parentSchema)` factory, or a `macro(value, parentSchema)` that returns a schema applied in place. A definition can name the JSON Schema `type` it applies to. A schema that uses a registered keyword runs on the interpreted engine, so `anyOf`, `not`, `$ref` and the other applicators keep their meaning around the custom check; the compiled engines, the tier-0 plan and the native walker are all routed away from such schemas, and `bundleStandalone` refuses them instead of emitting a module that would ignore the keyword. Verdict paths (`isValidObject`, `isValidJSON`) agree with `validate()`. A keyword definition with none of the three forms throws, so a `code`-only definition is never accepted quietly.
|
|
16
|
+
- `ata-validator/compat` covers the reference class's surface: `compileAsync`, `removeSchema` (by key, by object, by RegExp, or everything), `validateSchema` against the vendored meta-schemas, `addFormat` in its four forms, `addKeyword` in the `validate`, `compile` and `macro` forms plus the bare-name form, `addVocabulary`, `getKeyword`, `removeKeyword`, `errorsText`, `addMetaSchema`, `ajv.errors` and `ajv.opts`; and the constructor options `allErrors`, `useDefaults` (off by default, as in the reference), `coerceTypes`, `removeAdditional`, `verbose`, `validateFormats`, `validateSchema`, `formats`, `keywords` and `schemas`. `compile` throws `schema is invalid: ...` on a schema the meta-schema rejects, and caches by schema object. A schema with no `$schema` is read as draft-07, which is what `require('ajv')` does.
|
|
17
|
+
- The shim reports errors in the reference's shape and order: keywords sorted the way the reference evaluates them rather than in declaration order, the failing branches before an `anyOf` or `oneOf`, each bad property name with its own errors and a `propertyNames` error, items before `contains` and an `if` error after a failed `then`/`else` under `allErrors`, draft-07 `dependencies` named as such, and with `allErrors` off the first failing keyword's whole group. `tests/test_ajv_parity.js` runs 32 scenarios of real consumer call shapes through both implementations and compares the results field by field; the reference is a devDependency and never ships.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `ata-validator/compat` refuses what it cannot honour instead of accepting it: `$data: true` throws at construction, a `code`-only keyword throws at `addKeyword`, and the reference's formats plugin throws with a note that its formats are built in.
|
|
22
|
+
|
|
5
23
|
## 1.13.1 - 2026-09-06
|
|
6
24
|
|
|
7
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ const v = new Validator(schema)
|
|
|
148
148
|
const result = v.validate(data)
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
The runtime API is unchanged from previous releases.
|
|
151
|
+
The runtime API is unchanged from previous releases. Code written against the default validator's class keeps working through `ata-validator/compat`, which covers `compile`, `addSchema`, `addFormat`, `addKeyword`, `errorsText` and the rest of that surface, and reports errors in the same shape and order. See [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
|
|
152
152
|
|
|
153
153
|
## Usage
|
|
154
154
|
|
|
@@ -471,6 +471,29 @@ auto result = ata::validate(schema, R"({"name": "Mert"})");
|
|
|
471
471
|
// result.valid == true
|
|
472
472
|
```
|
|
473
473
|
|
|
474
|
+
### Custom keywords
|
|
475
|
+
|
|
476
|
+
Register keywords the schema vocabulary does not have with the `keywords` option. A definition is a `validate` function, a `compile` factory, or a `macro` that returns a schema, with an optional `type` that limits which values it sees.
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
const v = new Validator(
|
|
480
|
+
{ properties: { title: { type: 'string', maxWords: 5 } } },
|
|
481
|
+
{
|
|
482
|
+
keywords: {
|
|
483
|
+
maxWords: {
|
|
484
|
+
type: 'string',
|
|
485
|
+
compile: (n) => (s) => s.split(/\s+/).length <= n,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
v.validate({ title: 'one two three four five six' })
|
|
492
|
+
// { valid: false, errors: [{ keyword: 'maxWords', instancePath: '/title', ... }] }
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
A schema that uses a custom keyword runs on the interpreted engine, where `anyOf`, `not` and `$ref` keep their meaning around the custom check, and it cannot be compiled ahead of time, since a standalone module imports nothing and cannot carry the function. `bundleStandalone` refuses such schemas rather than emitting a module that would ignore the keyword. See [docs/custom-keywords.md](docs/custom-keywords.md).
|
|
496
|
+
|
|
474
497
|
## Framework integrations
|
|
475
498
|
|
|
476
499
|
Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See [docs/integrations](docs/integrations/) for the full set.
|
|
@@ -478,7 +501,9 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
|
|
|
478
501
|
| Framework | Pattern | Recipe |
|
|
479
502
|
|---|---|---|
|
|
480
503
|
| Fastify | dedicated plugin | [`fastify-ata`](https://github.com/ata-core/fastify-ata) |
|
|
481
|
-
| Vite (build-time compile) | dedicated plugin | [
|
|
504
|
+
| Vite, Webpack, Rollup, Rolldown, esbuild, Rspack (build-time compile) | dedicated plugin | [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata) |
|
|
505
|
+
| Vite (build-time compile) | dedicated plugin | [`ata-vite`](https://github.com/ata-core/ata-vite), the Vite entry of the plugin above |
|
|
506
|
+
| Next.js (build-time compile) | webpack entry of the plugin above | `@ata-project/unplugin/webpack` in `next.config.js` |
|
|
482
507
|
| Hono | async middleware | [docs/integrations/hono.md](docs/integrations/hono.md) |
|
|
483
508
|
| Elysia | direct handler check | [docs/integrations/elysia.md](docs/integrations/elysia.md) |
|
|
484
509
|
| tRPC | Standard Schema V1 input | [docs/integrations/trpc.md](docs/integrations/trpc.md) |
|
package/compat.d.ts
CHANGED
|
@@ -1,23 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import type { JSONSchema } from './index';
|
|
2
|
+
|
|
3
|
+
declare class Ata {
|
|
4
|
+
constructor(opts?: Ata.Options);
|
|
5
|
+
readonly opts: Ata.Options & { code: object };
|
|
6
|
+
/** Errors from the last `validate()` or `validateSchema()` call. */
|
|
7
|
+
errors: Ata.ErrorObject[] | null;
|
|
8
|
+
|
|
9
|
+
compile<T = unknown>(schema: object | boolean): Ata.ValidateFunction<T>;
|
|
10
|
+
compileAsync<T = unknown>(schema: object | boolean): Promise<Ata.ValidateFunction<T>>;
|
|
11
|
+
validate(schemaKeyRef: object | boolean | string, data: unknown): boolean;
|
|
12
|
+
|
|
13
|
+
addSchema(schema: object | object[], key?: string): this;
|
|
14
|
+
addMetaSchema(schema: object, key?: string): this;
|
|
15
|
+
getSchema<T = unknown>(keyRef: string): Ata.ValidateFunction<T> | undefined;
|
|
16
|
+
removeSchema(schemaKeyRef?: object | string | RegExp): this;
|
|
17
|
+
validateSchema(schema: object | boolean, throwOrLogError?: boolean): boolean;
|
|
18
|
+
|
|
19
|
+
addFormat(name: string, format: Ata.Format): this;
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
(
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
addKeyword(definition: Ata.KeywordDefinition): this;
|
|
22
|
+
addKeyword(keyword: string, definition?: Omit<Ata.KeywordDefinition, 'keyword'>): this;
|
|
23
|
+
addVocabulary(definitions: Array<string | Ata.KeywordDefinition>): this;
|
|
24
|
+
getKeyword(keyword: string): Ata.KeywordDefinition | boolean;
|
|
25
|
+
removeKeyword(keyword: string): this;
|
|
26
|
+
|
|
27
|
+
errorsText(errors?: Ata.ErrorObject[] | null, options?: { separator?: string; dataVar?: string }): string;
|
|
13
28
|
}
|
|
14
29
|
|
|
15
|
-
declare
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
declare namespace Ata {
|
|
31
|
+
interface ErrorObject {
|
|
32
|
+
instancePath: string;
|
|
33
|
+
schemaPath: string;
|
|
34
|
+
keyword: string;
|
|
35
|
+
params: Record<string, unknown>;
|
|
36
|
+
message: string;
|
|
37
|
+
/** Present when `verbose: true`. */
|
|
38
|
+
parentSchema?: unknown;
|
|
39
|
+
/** Present on errors produced under `propertyNames`. */
|
|
40
|
+
propertyName?: string;
|
|
41
|
+
[extra: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ValidateFunction<T = unknown> {
|
|
45
|
+
(data: unknown): data is T;
|
|
46
|
+
errors: ErrorObject[] | null;
|
|
47
|
+
schema: object | boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Format =
|
|
51
|
+
| RegExp
|
|
52
|
+
| string
|
|
53
|
+
| ((data: string) => boolean)
|
|
54
|
+
| { type?: 'string' | 'number'; validate: RegExp | string | ((data: string) => boolean) };
|
|
55
|
+
|
|
56
|
+
interface KeywordDefinition {
|
|
57
|
+
keyword: string;
|
|
58
|
+
/** JSON Schema type name(s) the keyword applies to. Other types pass. */
|
|
59
|
+
type?: string | string[];
|
|
60
|
+
/** Called per value. May leave error objects on itself (`fn.errors`). */
|
|
61
|
+
validate?: (schema: unknown, data: unknown, parentSchema?: object) => boolean;
|
|
62
|
+
/** Called once per schema node; the returned function runs per value. */
|
|
63
|
+
compile?: (schema: unknown, parentSchema?: object) => (data: unknown) => boolean;
|
|
64
|
+
/** Returns a schema applied in place of the keyword. */
|
|
65
|
+
macro?: (schema: unknown, parentSchema?: object) => JSONSchema | boolean;
|
|
66
|
+
errors?: boolean;
|
|
67
|
+
[extra: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface Options {
|
|
71
|
+
allErrors?: boolean;
|
|
72
|
+
verbose?: boolean;
|
|
73
|
+
coerceTypes?: boolean | 'array';
|
|
74
|
+
useDefaults?: boolean | 'empty';
|
|
75
|
+
removeAdditional?: boolean | 'all' | 'failing';
|
|
76
|
+
validateFormats?: boolean;
|
|
77
|
+
validateSchema?: boolean | 'log';
|
|
78
|
+
formats?: Record<string, Format>;
|
|
79
|
+
keywords?: KeywordDefinition[];
|
|
80
|
+
schemas?: object[] | Record<string, object>;
|
|
81
|
+
strict?: boolean | 'log';
|
|
82
|
+
strictSchema?: boolean | 'log';
|
|
83
|
+
strictTypes?: boolean | 'log';
|
|
84
|
+
strictTuples?: boolean | 'log';
|
|
85
|
+
strictRequired?: boolean | 'log';
|
|
86
|
+
allowUnionTypes?: boolean;
|
|
87
|
+
logger?: { log(...args: unknown[]): void; warn(...args: unknown[]): void; error(...args: unknown[]): void } | false;
|
|
88
|
+
loadSchema?: (uri: string) => Promise<object>;
|
|
89
|
+
/** Refused: the constructor throws. */
|
|
90
|
+
$data?: boolean;
|
|
91
|
+
[extra: string]: unknown;
|
|
92
|
+
}
|
|
21
93
|
}
|
|
22
94
|
|
|
23
95
|
export = Ata;
|
package/compat.js
CHANGED
|
@@ -1,43 +1,394 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Drop-in for the default validator's class. `const Ajv = require('ata-validator/compat')`
|
|
4
|
+
// and the rest of the file stays as it was: `new Ajv(opts)`, `compile`,
|
|
5
|
+
// `validate`, `addSchema`, `getSchema`, `removeSchema`, `addFormat`,
|
|
6
|
+
// `addKeyword`, `errorsText`, `validateSchema`, `compileAsync`.
|
|
7
|
+
//
|
|
8
|
+
// The class reads schemas the way `require('ajv')` does: a schema that names
|
|
9
|
+
// no `$schema` is draft-07. tests/test_ajv_parity.js runs a corpus of real
|
|
10
|
+
// call shapes through both implementations and compares what comes back.
|
|
11
|
+
//
|
|
12
|
+
// What is not carried over, and refused rather than accepted quietly:
|
|
13
|
+
// - `$data` references;
|
|
14
|
+
// - keywords defined only through `code` (a code generator hook);
|
|
15
|
+
// - the reference formats plugin, whose formats are built in here.
|
|
16
|
+
// Strict-mode schema checks (`strict`, `strictTypes`, ...) are accepted and
|
|
17
|
+
// ignored: an unknown keyword is an annotation, as the specification says.
|
|
18
|
+
|
|
19
|
+
const { Validator } = require('./index');
|
|
20
|
+
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
21
|
+
const { createShaper } = require('./lib/compat-errors');
|
|
22
|
+
|
|
23
|
+
const DRAFT7 = 'http://json-schema.org/draft-07/schema#';
|
|
24
|
+
const DRAFT2020 = 'https://json-schema.org/draft/2020-12/schema';
|
|
25
|
+
|
|
26
|
+
function isDraft7Id(id) {
|
|
27
|
+
return id === DRAFT7 || id === 'http://json-schema.org/draft-07/schema';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatToFunction(name, format) {
|
|
31
|
+
if (typeof format === 'function') return format;
|
|
32
|
+
if (format instanceof RegExp) return (s) => format.test(s);
|
|
33
|
+
if (typeof format === 'string') {
|
|
34
|
+
const re = new RegExp(format, 'u');
|
|
35
|
+
return (s) => re.test(s);
|
|
36
|
+
}
|
|
37
|
+
if (format === true) return () => true;
|
|
38
|
+
if (format && typeof format === 'object') {
|
|
39
|
+
if (typeof format.validate === 'function') return format.validate;
|
|
40
|
+
if (format.validate instanceof RegExp) return (s) => format.validate.test(s);
|
|
41
|
+
if (typeof format.validate === 'string') {
|
|
42
|
+
const re = new RegExp(format.validate, 'u');
|
|
43
|
+
return (s) => re.test(s);
|
|
44
|
+
}
|
|
45
|
+
if (format.async) throw new Error(`format "${name}": async formats are not supported`);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`format "${name}": unsupported format definition`);
|
|
48
|
+
}
|
|
49
|
+
|
|
2
50
|
|
|
3
51
|
class Ata {
|
|
4
52
|
constructor(opts = {}) {
|
|
5
|
-
|
|
6
|
-
|
|
53
|
+
if (opts.$data) {
|
|
54
|
+
throw new Error('ata compat: $data references are not supported');
|
|
55
|
+
}
|
|
56
|
+
this.opts = { ...opts };
|
|
57
|
+
// The formats plugin of the reference assigns into `opts.code.formats`
|
|
58
|
+
// before registering anything; the formats it carries are built in here,
|
|
59
|
+
// so say so at that exact point instead of failing further down.
|
|
60
|
+
const code = { ...(opts.code || {}) };
|
|
61
|
+
Object.defineProperty(code, 'formats', {
|
|
62
|
+
get() { return undefined; },
|
|
63
|
+
set() {
|
|
64
|
+
throw new Error('ata compat: the formats plugin is not needed, formats such as email, uri, date-time and uuid are built in; remove addFormats()');
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
this.opts.code = code;
|
|
68
|
+
|
|
69
|
+
this.errors = null;
|
|
70
|
+
this._schemas = new Map(); // key or $id -> schema object
|
|
71
|
+
this._byKey = new Map(); // key -> $id (when both exist)
|
|
72
|
+
this._formats = Object.create(null);
|
|
73
|
+
this._keywords = Object.create(null);
|
|
74
|
+
this._compiled = new WeakMap(); // schema object -> validate function
|
|
75
|
+
this._registry = null; // Validator `schemas` option, rebuilt on change
|
|
76
|
+
this._registryEpoch = 0;
|
|
77
|
+
|
|
78
|
+
if (opts.formats) {
|
|
79
|
+
for (const name of Object.keys(opts.formats)) this.addFormat(name, opts.formats[name]);
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(opts.keywords)) {
|
|
82
|
+
for (const def of opts.keywords) this.addKeyword(def);
|
|
83
|
+
}
|
|
84
|
+
if (opts.schemas) {
|
|
85
|
+
if (Array.isArray(opts.schemas)) this.addSchema(opts.schemas);
|
|
86
|
+
else for (const key of Object.keys(opts.schemas)) this.addSchema(opts.schemas[key], key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ----- options -------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
_validatorOptions() {
|
|
93
|
+
const o = this.opts;
|
|
94
|
+
const out = {
|
|
95
|
+
useDefaults: !!o.useDefaults,
|
|
96
|
+
coerceTypes: !!o.coerceTypes,
|
|
97
|
+
removeAdditional: !!o.removeAdditional,
|
|
98
|
+
verbose: !!o.verbose,
|
|
99
|
+
};
|
|
100
|
+
if (o.validateFormats === false) out.assertFormat = false;
|
|
101
|
+
const formatNames = Object.keys(this._formats);
|
|
102
|
+
if (formatNames.length > 0) out.formats = { ...this._formats };
|
|
103
|
+
const keywordNames = Object.keys(this._keywords);
|
|
104
|
+
if (keywordNames.length > 0) {
|
|
105
|
+
const kws = {};
|
|
106
|
+
for (const name of keywordNames) if (this._keywords[name] !== true) kws[name] = this._keywords[name];
|
|
107
|
+
if (Object.keys(kws).length > 0) out.keywords = kws;
|
|
108
|
+
}
|
|
109
|
+
const registry = this._registrySnapshot();
|
|
110
|
+
if (registry) out.schemas = registry;
|
|
111
|
+
return out;
|
|
7
112
|
}
|
|
8
113
|
|
|
114
|
+
_registrySnapshot() {
|
|
115
|
+
if (this._schemas.size === 0) return null;
|
|
116
|
+
if (this._registry === null) {
|
|
117
|
+
const map = {};
|
|
118
|
+
for (const [key, schema] of this._schemas) map[key] = this._withDialect(schema);
|
|
119
|
+
this._registry = map;
|
|
120
|
+
}
|
|
121
|
+
return this._registry;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_touchRegistry() {
|
|
125
|
+
this._registry = null;
|
|
126
|
+
this._registryEpoch++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A schema with no `$schema` is read as draft-07, which is what the
|
|
130
|
+
// reference class does. The copy is shallow: only the root changes.
|
|
131
|
+
_withDialect(schema) {
|
|
132
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return schema;
|
|
133
|
+
if (typeof schema.$schema === 'string') return schema;
|
|
134
|
+
return { $schema: DRAFT7, ...schema };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ----- compile / validate --------------------------------------------
|
|
138
|
+
|
|
9
139
|
compile(schema) {
|
|
10
|
-
|
|
140
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
141
|
+
const hit = this._compiled.get(schema);
|
|
142
|
+
if (hit && hit._epoch === this._registryEpoch) return hit;
|
|
143
|
+
}
|
|
144
|
+
if (this.opts.validateSchema !== false && typeof schema === 'object' && schema !== null) {
|
|
145
|
+
if (!this.validateSchema(schema)) {
|
|
146
|
+
const message = 'schema is invalid: ' + this.errorsText();
|
|
147
|
+
if (this.opts.validateSchema === 'log') {
|
|
148
|
+
const logger = this.opts.logger;
|
|
149
|
+
if (logger && typeof logger.error === 'function') logger.error(message);
|
|
150
|
+
} else {
|
|
151
|
+
throw new Error(message);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const rootDoc = this._withDialect(schema);
|
|
156
|
+
const options = this._validatorOptions();
|
|
157
|
+
const allErrors = !!this.opts.allErrors;
|
|
158
|
+
let shaper = null;
|
|
159
|
+
if (typeof rootDoc === 'object' && rootDoc !== null) {
|
|
160
|
+
// The root is registered under a private id so the error shaper can
|
|
161
|
+
// re-validate any subschema of it by pointer, with every `$ref` in it
|
|
162
|
+
// still resolving.
|
|
163
|
+
const rootId = 'ata-compat:root';
|
|
164
|
+
options.schemas = { ...(options.schemas || {}), [rootId]: rootDoc };
|
|
165
|
+
shaper = createShaper({
|
|
166
|
+
Validator, rootId, rootDoc, options, allErrors,
|
|
167
|
+
macroKeywords: Object.keys(this._keywords).filter((k) => this._keywords[k] !== true && this._keywords[k].macro),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const v = new Validator(rootDoc, options);
|
|
11
171
|
const validate = (data) => {
|
|
12
172
|
const result = v.validate(data);
|
|
13
|
-
|
|
14
|
-
|
|
173
|
+
if (result.valid) {
|
|
174
|
+
validate.errors = null;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
const errors = shaper === null ? result.errors : shaper(result.errors, data);
|
|
178
|
+
validate.errors = shaper === null && !allErrors ? errors.slice(0, 1) : errors;
|
|
179
|
+
return false;
|
|
15
180
|
};
|
|
16
181
|
validate.errors = null;
|
|
17
182
|
validate.schema = schema;
|
|
183
|
+
validate._epoch = this._registryEpoch;
|
|
184
|
+
if (typeof schema === 'object' && schema !== null) this._compiled.set(schema, validate);
|
|
18
185
|
return validate;
|
|
19
186
|
}
|
|
20
187
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
188
|
+
compileAsync(schema) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
try { resolve(this.compile(schema)); } catch (e) { reject(e); }
|
|
191
|
+
});
|
|
24
192
|
}
|
|
25
193
|
|
|
194
|
+
validate(schemaKeyRef, data) {
|
|
195
|
+
let validate;
|
|
196
|
+
if (typeof schemaKeyRef === 'string') {
|
|
197
|
+
validate = this.getSchema(schemaKeyRef);
|
|
198
|
+
if (!validate) throw new Error(`no schema with key or ref "${schemaKeyRef}"`);
|
|
199
|
+
} else {
|
|
200
|
+
validate = this.compile(schemaKeyRef);
|
|
201
|
+
}
|
|
202
|
+
const valid = validate(data);
|
|
203
|
+
this.errors = validate.errors;
|
|
204
|
+
return valid;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ----- schema registry -----------------------------------------------
|
|
208
|
+
|
|
26
209
|
addSchema(schema, key) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
210
|
+
if (Array.isArray(schema)) {
|
|
211
|
+
for (const s of schema) this.addSchema(s);
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
const id = schema && typeof schema === 'object' && typeof schema.$id === 'string' ? schema.$id : undefined;
|
|
215
|
+
const k = key !== undefined ? String(key) : id;
|
|
216
|
+
if (k === undefined) throw new Error('schema with key or id "undefined" cannot be added: no key and no $id');
|
|
217
|
+
if (this._schemas.has(k) && this._schemas.get(k) !== schema) {
|
|
218
|
+
throw new Error(`schema with key or id "${k}" already exists`);
|
|
31
219
|
}
|
|
220
|
+
this._schemas.set(k, schema);
|
|
221
|
+
if (id !== undefined && id !== k) {
|
|
222
|
+
this._schemas.set(id, schema);
|
|
223
|
+
this._byKey.set(k, id);
|
|
224
|
+
}
|
|
225
|
+
this._touchRegistry();
|
|
32
226
|
return this;
|
|
33
227
|
}
|
|
34
228
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
229
|
+
addMetaSchema(schema, key) {
|
|
230
|
+
return this.addSchema(schema, key);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getSchema(keyRef) {
|
|
234
|
+
const schema = this._schemas.get(keyRef);
|
|
235
|
+
if (schema !== undefined) return this.compile(schema);
|
|
236
|
+
// A fragment into a registered document: compile a reference to it.
|
|
237
|
+
const hash = keyRef.indexOf('#');
|
|
238
|
+
if (hash > 0 && this._schemas.has(keyRef.slice(0, hash))) {
|
|
239
|
+
return this.compile({ $ref: keyRef });
|
|
240
|
+
}
|
|
38
241
|
return undefined;
|
|
39
242
|
}
|
|
243
|
+
|
|
244
|
+
removeSchema(schemaKeyRef) {
|
|
245
|
+
if (schemaKeyRef === undefined) {
|
|
246
|
+
this._schemas.clear();
|
|
247
|
+
this._byKey.clear();
|
|
248
|
+
this._compiled = new WeakMap();
|
|
249
|
+
this._touchRegistry();
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
if (schemaKeyRef instanceof RegExp) {
|
|
253
|
+
for (const key of Array.from(this._schemas.keys())) {
|
|
254
|
+
if (schemaKeyRef.test(key)) this._removeKey(key);
|
|
255
|
+
}
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
if (typeof schemaKeyRef === 'object' && schemaKeyRef !== null) {
|
|
259
|
+
for (const [key, schema] of Array.from(this._schemas)) {
|
|
260
|
+
if (schema === schemaKeyRef) this._removeKey(key);
|
|
261
|
+
}
|
|
262
|
+
this._compiled.delete(schemaKeyRef);
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
this._removeKey(String(schemaKeyRef));
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_removeKey(key) {
|
|
270
|
+
const schema = this._schemas.get(key);
|
|
271
|
+
if (schema === undefined) return;
|
|
272
|
+
this._schemas.delete(key);
|
|
273
|
+
const alias = this._byKey.get(key);
|
|
274
|
+
if (alias !== undefined) {
|
|
275
|
+
this._schemas.delete(alias);
|
|
276
|
+
this._byKey.delete(key);
|
|
277
|
+
}
|
|
278
|
+
for (const [k, id] of Array.from(this._byKey)) {
|
|
279
|
+
if (id === key) { this._schemas.delete(k); this._byKey.delete(k); }
|
|
280
|
+
}
|
|
281
|
+
if (typeof schema === 'object' && schema !== null) this._compiled.delete(schema);
|
|
282
|
+
this._touchRegistry();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
validateSchema(schema, throwOrLogError) {
|
|
286
|
+
if (typeof schema === 'boolean') { this.errors = null; return true; }
|
|
287
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
288
|
+
this.errors = [{ keyword: 'type', instancePath: '', schemaPath: '#/type', params: { type: 'object' }, message: 'must be object,boolean' }];
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
const declared = typeof schema.$schema === 'string' ? schema.$schema : DRAFT7;
|
|
292
|
+
const metaId = METASCHEMAS.has(declared) ? declared : isDraft7Id(declared) ? DRAFT7 : DRAFT2020;
|
|
293
|
+
const meta = metaValidator(metaId);
|
|
294
|
+
const result = meta.validate(schema);
|
|
295
|
+
if (result.valid) { this.errors = null; return true; }
|
|
296
|
+
this.errors = result.errors;
|
|
297
|
+
if (throwOrLogError) {
|
|
298
|
+
const message = 'schema is invalid: ' + this.errorsText();
|
|
299
|
+
if (this.opts.validateSchema === 'log') {
|
|
300
|
+
const logger = this.opts.logger;
|
|
301
|
+
if (logger && typeof logger.error === 'function') logger.error(message);
|
|
302
|
+
} else {
|
|
303
|
+
throw new Error(message);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ----- formats -------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
addFormat(name, format) {
|
|
312
|
+
this._formats[name] = formatToFunction(name, format);
|
|
313
|
+
this._compiled = new WeakMap();
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ----- keywords ------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
addKeyword(kwdOrDef, def) {
|
|
320
|
+
let keyword;
|
|
321
|
+
let definition;
|
|
322
|
+
if (typeof kwdOrDef === 'string') {
|
|
323
|
+
keyword = kwdOrDef;
|
|
324
|
+
definition = def;
|
|
325
|
+
} else if (kwdOrDef && typeof kwdOrDef === 'object') {
|
|
326
|
+
keyword = kwdOrDef.keyword;
|
|
327
|
+
definition = kwdOrDef;
|
|
328
|
+
}
|
|
329
|
+
if (typeof keyword !== 'string' || keyword === '') {
|
|
330
|
+
throw new Error('addKeyword: keyword must be a non-empty string');
|
|
331
|
+
}
|
|
332
|
+
if (this._keywords[keyword] !== undefined) {
|
|
333
|
+
throw new Error(`keyword "${keyword}" is already defined`);
|
|
334
|
+
}
|
|
335
|
+
if (!definition || (typeof definition === 'object' && !definition.validate && !definition.compile && !definition.macro && !definition.code)) {
|
|
336
|
+
// A bare name: the keyword is known and checks nothing.
|
|
337
|
+
this._keywords[keyword] = true;
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
if (typeof definition.code === 'function' && !definition.validate && !definition.compile && !definition.macro) {
|
|
341
|
+
throw new Error(`keyword "${keyword}": code-generating keywords are not supported; define it with validate, compile or macro`);
|
|
342
|
+
}
|
|
343
|
+
const mapped = {};
|
|
344
|
+
if (definition.type !== undefined) mapped.type = definition.type;
|
|
345
|
+
if (typeof definition.validate === 'function') mapped.validate = definition.validate;
|
|
346
|
+
if (typeof definition.compile === 'function') mapped.compile = definition.compile;
|
|
347
|
+
if (typeof definition.macro === 'function') mapped.macro = definition.macro;
|
|
348
|
+
if (mapped.validate && mapped.compile) delete mapped.validate;
|
|
349
|
+
this._keywords[keyword] = mapped;
|
|
350
|
+
this._keywordDefs = this._keywordDefs || Object.create(null);
|
|
351
|
+
this._keywordDefs[keyword] = definition;
|
|
352
|
+
this._compiled = new WeakMap();
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
addVocabulary(definitions) {
|
|
357
|
+
for (const def of definitions) this.addKeyword(def);
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
getKeyword(keyword) {
|
|
362
|
+
const def = this._keywords[keyword];
|
|
363
|
+
if (def === undefined) return false;
|
|
364
|
+
return def === true ? true : (this._keywordDefs && this._keywordDefs[keyword]) || def;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
removeKeyword(keyword) {
|
|
368
|
+
delete this._keywords[keyword];
|
|
369
|
+
if (this._keywordDefs) delete this._keywordDefs[keyword];
|
|
370
|
+
this._compiled = new WeakMap();
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ----- errors --------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
errorsText(errors = this.errors, { separator = ', ', dataVar = 'data' } = {}) {
|
|
377
|
+
if (!errors || errors.length === 0) return 'No errors';
|
|
378
|
+
return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text, msg) => text + separator + msg);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const _metaValidators = new Map();
|
|
383
|
+
function metaValidator(id) {
|
|
384
|
+
let v = _metaValidators.get(id);
|
|
385
|
+
if (!v) {
|
|
386
|
+
v = new Validator({ $ref: id }, { useDefaults: false });
|
|
387
|
+
_metaValidators.set(id, v);
|
|
388
|
+
}
|
|
389
|
+
return v;
|
|
40
390
|
}
|
|
41
391
|
|
|
42
392
|
module.exports = Ata;
|
|
43
393
|
module.exports.default = Ata;
|
|
394
|
+
module.exports.Ata = Ata;
|
package/index.d.ts
CHANGED
|
@@ -314,6 +314,26 @@ export type ValidateAndParseResult<T = unknown> =
|
|
|
314
314
|
| { valid: true; value: T; errors: ValidationError[] }
|
|
315
315
|
| { valid: false; value: unknown; errors: ValidationError[] };
|
|
316
316
|
|
|
317
|
+
export type KeywordValidate = (schemaValue: unknown, data: unknown, parentSchema: object) => boolean;
|
|
318
|
+
|
|
319
|
+
export interface KeywordDefinition {
|
|
320
|
+
/** JSON Schema type name(s) the keyword applies to. Other types pass. */
|
|
321
|
+
type?: string | string[];
|
|
322
|
+
/**
|
|
323
|
+
* Called per value with the keyword's value in the schema, the data, and
|
|
324
|
+
* the schema object that carries the keyword. May leave error objects on
|
|
325
|
+
* itself (`fn.errors`) to replace the default error.
|
|
326
|
+
*/
|
|
327
|
+
validate?: KeywordValidate;
|
|
328
|
+
/** Called once per schema node; the returned function runs per value. */
|
|
329
|
+
compile?: (schemaValue: unknown, parentSchema: object) => (data: unknown) => boolean;
|
|
330
|
+
/**
|
|
331
|
+
* Returns a schema that is applied in place. Its errors carry the keyword's
|
|
332
|
+
* path, and the keyword reports an error of its own when it fails.
|
|
333
|
+
*/
|
|
334
|
+
macro?: (schemaValue: unknown, parentSchema: object) => JSONSchema | boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
317
337
|
export interface ValidatorOptions {
|
|
318
338
|
coerceTypes?: boolean;
|
|
319
339
|
removeAdditional?: boolean;
|
|
@@ -336,6 +356,12 @@ export interface ValidatorOptions {
|
|
|
336
356
|
* the schema. Values are functions that return true when the input is valid.
|
|
337
357
|
*/
|
|
338
358
|
formats?: Record<string, FormatChecker>;
|
|
359
|
+
/**
|
|
360
|
+
* Custom keywords, keyed by name. A schema that uses one runs on the
|
|
361
|
+
* interpreted engine and cannot be compiled ahead of time. See
|
|
362
|
+
* docs/custom-keywords.md.
|
|
363
|
+
*/
|
|
364
|
+
keywords?: Record<string, KeywordDefinition | KeywordValidate>;
|
|
339
365
|
/**
|
|
340
366
|
* When true, validation errors include `parentSchema` (the schema object
|
|
341
367
|
* that produced the error). Matches ajv's `verbose: true`.
|