ata-validator 1.62.1 → 1.63.0
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/README.md +63 -8
- package/build.mjs +1 -0
- package/index.browser.mjs +23 -4
- package/index.bundle.mjs +14 -4
- package/index.edge.mjs +14 -4
- package/index.js +7 -2
- package/lib/aot-build.js +18 -0
- package/lib/codegen-engine.js +72 -3
- package/lib/compiled-common.js +2 -1
- package/lib/email-literal.js +4 -1
- package/lib/formats-source.js +39 -40
- package/lib/interpreter-core.js +17 -9
- package/lib/ir/emit-deps.mjs +131 -0
- package/lib/ir/emit.mjs +560 -0
- package/lib/ir/front.mjs +834 -0
- package/lib/ir/index.mjs +10 -0
- package/lib/ir/refs.mjs +108 -0
- package/lib/ir/run.mjs +520 -0
- package/lib/ir/table.mjs +233 -0
- package/lib/js-compiler-errors.js +9 -4
- package/lib/kernels/collapse.mjs +114 -0
- package/lib/kernels/custom.mjs +70 -0
- package/lib/kernels/fill.mjs +6 -0
- package/lib/kernels/formats-small.mjs +40 -0
- package/lib/kernels/formats.mjs +29 -0
- package/lib/kernels/index.mjs +11 -0
- package/lib/kernels/kernels.mjs +285 -0
- package/lib/kernels/leaves.mjs +28 -0
- package/lib/kernels/order.mjs +25 -0
- package/lib/kernels/plan-rich.mjs +17 -0
- package/lib/kernels/plan.d.ts +9 -0
- package/lib/kernels/plan.mjs +128 -0
- package/lib/kernels/preprocess.mjs +223 -0
- package/lib/kernels/rank.mjs +97 -0
- package/lib/kernels/rt.mjs +249 -0
- package/lib/kernels/table.mjs +182 -0
- package/lib/kernels/unique.mjs +157 -0
- package/lib/native-positions.js +1 -1
- package/lib/openapi.d.ts +2 -0
- package/lib/openapi.js +128 -0
- package/lib/plan-compiler.js +84 -7
- package/lib/public-api.js +7 -2
- package/lib/rejections.js +3 -1
- package/lib/schema-helpers.js +12 -0
- package/lib/schema-helpers.mjs +20 -0
- package/lib/schema-order.js +4 -2
- package/lib/standard-schema.js +1 -1
- package/lib/validator-core.js +35 -30
- package/lib/version.js +1 -1
- package/lite.js +7 -2
- package/package.json +26 -8
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// One definition per keyword. The check, its params, its message and
|
|
2
|
+
// its enrichment live together, and both IR backends (lib/ir/run.mjs
|
|
3
|
+
// and lib/ir/emit.mjs) read this one definition.
|
|
4
|
+
//
|
|
5
|
+
// A leaf kernel:
|
|
6
|
+
// code the public error code (a function of the value for format)
|
|
7
|
+
// on(v) the instance types the keyword constrains; others pass
|
|
8
|
+
// ok(v, n) the check. One expression over v, n and exported helpers:
|
|
9
|
+
// the specializer writes its source in with n replaced by
|
|
10
|
+
// the schema's value, so there is no second copy of it.
|
|
11
|
+
// prepare(n) optional: the value ok() receives, built once
|
|
12
|
+
// params, msg, exp functions of the schema value only: evaluated at build
|
|
13
|
+
// rank, detail, suggest the enrichment; detail and suggest are named
|
|
14
|
+
// exports, so a module imports only the ones it uses.
|
|
15
|
+
|
|
16
|
+
import { cpLen, nearest, isObj, typeName, MISSING } from './rt.mjs'
|
|
17
|
+
// The helpers here are ES modules all the way down: a CommonJS file reachable
|
|
18
|
+
// from ata-validator/kernels stays in some bundlers' output unused (see
|
|
19
|
+
// scripts/gen-kernels-cjs.js). What reads the current engine's CommonJS
|
|
20
|
+
// files at compile time lives in ./leaves.mjs.
|
|
21
|
+
import { _deq, duplicatePair } from './unique.mjs'
|
|
22
|
+
import { repr } from './rt.mjs'
|
|
23
|
+
|
|
24
|
+
// JSON equality and the duplicate search, the functions every engine shares.
|
|
25
|
+
export const deq = _deq
|
|
26
|
+
export const dupPair = duplicatePair
|
|
27
|
+
|
|
28
|
+
export const isStr = (v) => typeof v === 'string'
|
|
29
|
+
export const isNum = (v) => typeof v === 'number'
|
|
30
|
+
|
|
31
|
+
// One predicate per JSON type. isType reads this table, and the specializer
|
|
32
|
+
// writes a type's own predicate in place, so both use the same definition.
|
|
33
|
+
export const TYPES = {
|
|
34
|
+
string: (v) => typeof v === 'string',
|
|
35
|
+
integer: (v) => Number.isInteger(v),
|
|
36
|
+
number: (v) => typeof v === 'number' && Number.isFinite(v),
|
|
37
|
+
boolean: (v) => typeof v === 'boolean',
|
|
38
|
+
null: (v) => v === null,
|
|
39
|
+
object: (v) => typeof v === 'object' && v !== null && !Array.isArray(v),
|
|
40
|
+
array: (v) => Array.isArray(v),
|
|
41
|
+
}
|
|
42
|
+
export const isType = (v, t) => { const f = TYPES[t]; return f !== undefined && f(v) }
|
|
43
|
+
export const isTypes = (v, ts) => { for (let i = 0; i < ts.length; i++) if (isType(v, ts[i])) return true; return false }
|
|
44
|
+
|
|
45
|
+
export const typeDetail = (received, v, p) => `expected ${p.type}, found ${typeName(v, received)}`
|
|
46
|
+
export const typeSuggest = (v, p) => {
|
|
47
|
+
if (typeof v !== 'string') return null
|
|
48
|
+
const t = p.type
|
|
49
|
+
let ok = false; let what
|
|
50
|
+
if (t === 'integer') { ok = /^-?\d+$/.test(v); what = 'an integer' } else if (t === 'number') { ok = /^-?\d+(\.\d+)?$/.test(v); what = 'a number' } else if (t === 'boolean') { ok = v === 'true' || v === 'false'; what = 'a boolean' }
|
|
51
|
+
return ok ? { text: 'value would coerce; enable `coerceTypes` or pass ' + what, kind: 'coercion' } : null
|
|
52
|
+
}
|
|
53
|
+
const typeOk = (v, n) => typeof n === 'string' ? isType(v, n) : isTypes(v, n)
|
|
54
|
+
const typeParams = (n) => ({ type: n })
|
|
55
|
+
const typeMsg = (n) => `must be ${typeof n === 'string' ? n : n.join(', ')}`
|
|
56
|
+
export const type = {
|
|
57
|
+
kw: 'type', code: 'ATA1001', on: null, ok: typeOk, params: typeParams, msg: typeMsg, exp: (n) => String(n),
|
|
58
|
+
rank: 1, detail: typeDetail, suggest: typeSuggest,
|
|
59
|
+
}
|
|
60
|
+
// The plain shape's view of a descriptor (richErrors: false): the keyword,
|
|
61
|
+
// its check, its params and its message, the same functions, without the
|
|
62
|
+
// detail and hint the rich shape adds (lib/kernels/table.mjs).
|
|
63
|
+
export const typeP = { kw: 'type', ok: typeOk, params: typeParams, msg: typeMsg }
|
|
64
|
+
|
|
65
|
+
const minLengthOk = (v, n) => cpLen(v) >= n
|
|
66
|
+
const minLengthParams = (n) => ({ limit: n })
|
|
67
|
+
const minLengthMsg = (n) => `must NOT have fewer than ${n} characters`
|
|
68
|
+
export const minLength = {
|
|
69
|
+
kw: 'minLength', code: 'ATA2001', on: isStr, ok: minLengthOk,
|
|
70
|
+
params: minLengthParams, msg: minLengthMsg, exp: (n) => `string with ≥${n} chars`, rank: 2,
|
|
71
|
+
}
|
|
72
|
+
export const minLengthP = { kw: 'minLength', on: isStr, ok: minLengthOk, params: minLengthParams, msg: minLengthMsg }
|
|
73
|
+
const maxLengthOk = (v, n) => cpLen(v) <= n
|
|
74
|
+
const maxLengthParams = (n) => ({ limit: n })
|
|
75
|
+
const maxLengthMsg = (n) => `must NOT have more than ${n} characters`
|
|
76
|
+
export const maxLength = {
|
|
77
|
+
kw: 'maxLength', code: 'ATA2002', on: isStr, ok: maxLengthOk,
|
|
78
|
+
params: maxLengthParams, msg: maxLengthMsg, exp: (n) => `string with ≤${n} chars`, rank: 2,
|
|
79
|
+
}
|
|
80
|
+
export const maxLengthP = { kw: 'maxLength', on: isStr, ok: maxLengthOk, params: maxLengthParams, msg: maxLengthMsg }
|
|
81
|
+
const minimumOk = (v, n) => !(v < n)
|
|
82
|
+
const minimumParams = (n) => ({ comparison: '>=', limit: n })
|
|
83
|
+
const minimumMsg = (n) => `must be >= ${n}`
|
|
84
|
+
export const minimum = {
|
|
85
|
+
kw: 'minimum', code: 'ATA2003', on: isNum, ok: minimumOk,
|
|
86
|
+
params: minimumParams, msg: minimumMsg, exp: (n) => `≥${n}`, rank: 2,
|
|
87
|
+
}
|
|
88
|
+
export const minimumP = { kw: 'minimum', on: isNum, ok: minimumOk, params: minimumParams, msg: minimumMsg }
|
|
89
|
+
const maximumOk = (v, n) => !(v > n)
|
|
90
|
+
const maximumParams = (n) => ({ comparison: '<=', limit: n })
|
|
91
|
+
const maximumMsg = (n) => `must be <= ${n}`
|
|
92
|
+
export const maximum = {
|
|
93
|
+
kw: 'maximum', code: 'ATA2004', on: isNum, ok: maximumOk,
|
|
94
|
+
params: maximumParams, msg: maximumMsg, exp: (n) => `≤${n}`, rank: 2,
|
|
95
|
+
}
|
|
96
|
+
export const maximumP = { kw: 'maximum', on: isNum, ok: maximumOk, params: maximumParams, msg: maximumMsg }
|
|
97
|
+
const patternOk = (v, n) => n.test(v)
|
|
98
|
+
const patternParams = (n) => ({ pattern: n })
|
|
99
|
+
const patternMsg = (n) => `must match pattern "${n}"`
|
|
100
|
+
export const pattern = {
|
|
101
|
+
kw: 'pattern', code: 'ATA2013', on: isStr, ok: patternOk, prepare: (n) => new RegExp(n, 'u'),
|
|
102
|
+
params: patternParams, msg: patternMsg, exp: (n) => `string matching /${n}/`, rank: 2,
|
|
103
|
+
}
|
|
104
|
+
export const patternP = { kw: 'pattern', on: isStr, ok: patternOk, prepare: (n) => new RegExp(n, 'u'), params: patternParams, msg: patternMsg }
|
|
105
|
+
|
|
106
|
+
// A format's detail; the format keyword itself is in ./leaves.mjs.
|
|
107
|
+
export const formatDetail = (received, v, p) => `not a valid ${p.format}: ${received}`
|
|
108
|
+
// What a format's error says, shared by the compile-time descriptor in
|
|
109
|
+
// ./leaves.mjs and the table interpreter (./table.mjs); the check itself is
|
|
110
|
+
// the format's function, the code and hint come with it.
|
|
111
|
+
const formatOk = (v, f) => f(v)
|
|
112
|
+
const formatParams = (n) => ({ format: n })
|
|
113
|
+
const formatMsg = (n) => `must match format "${n}"`
|
|
114
|
+
export const formatK = { kw: 'format', on: isStr, ok: formatOk, params: formatParams, msg: formatMsg, exp: (n) => `format '${n}'`, rank: 2, detail: formatDetail }
|
|
115
|
+
export const formatP = { kw: 'format', on: isStr, ok: formatOk, params: formatParams, msg: formatMsg }
|
|
116
|
+
export const makeHintSuggest = (hint) => (v) => { if (typeof v !== 'string') return null; const t = hint(v); return t ? { text: t, kind: 'format' } : null }
|
|
117
|
+
|
|
118
|
+
// Structural kernels: their errors name the container, not a child.
|
|
119
|
+
export const requiredDetail = (received, v, p) => `missing required property "${p.missingProperty}"`
|
|
120
|
+
export const requiredSuggest = (v, p) => {
|
|
121
|
+
if (v === MISSING || !v || typeof v !== 'object') return null
|
|
122
|
+
const keys = Object.keys(v)
|
|
123
|
+
if (keys.length > 64) return null
|
|
124
|
+
const best = nearest(p.missingProperty, keys)
|
|
125
|
+
return best === null ? null : { text: `did you mean \`${p.missingProperty}\` instead of \`${best}\`?`, kind: 'similar-key' }
|
|
126
|
+
}
|
|
127
|
+
const requiredParams = (k) => ({ missingProperty: k })
|
|
128
|
+
const requiredMsg = (k) => `must have required property '${k}'`
|
|
129
|
+
export const required = {
|
|
130
|
+
kw: 'required', code: 'ATA7001', rank: 0,
|
|
131
|
+
params: requiredParams, msg: requiredMsg, exp: (k) => `property '${k}'`,
|
|
132
|
+
detail: requiredDetail, suggest: requiredSuggest,
|
|
133
|
+
}
|
|
134
|
+
export const requiredP = { kw: 'required', params: requiredParams, msg: requiredMsg }
|
|
135
|
+
|
|
136
|
+
export const additionalDetail = (received, v, p) => `unknown property "${p.additionalProperty}"`
|
|
137
|
+
const additionalParams = (k) => ({ additionalProperty: k })
|
|
138
|
+
const additionalMsg = () => 'must NOT have additional properties'
|
|
139
|
+
export const additionalProperties = {
|
|
140
|
+
kw: 'additionalProperties', code: 'ATA7002', rank: 0,
|
|
141
|
+
params: additionalParams, msg: additionalMsg, exp: () => undefined,
|
|
142
|
+
detail: additionalDetail,
|
|
143
|
+
}
|
|
144
|
+
export const additionalP = { kw: 'additionalProperties', params: additionalParams, msg: additionalMsg }
|
|
145
|
+
|
|
146
|
+
// The current engine has no expected text for the exclusive bounds but words
|
|
147
|
+
// their detail as if it had one: "expected undefined, found 1".
|
|
148
|
+
export const exclusiveDetail = (received) => `expected undefined, found ${received}`
|
|
149
|
+
const exclusiveMinimumOk = (v, n) => !(v <= n)
|
|
150
|
+
const exclusiveMinimumParams = (n) => ({ comparison: '>', limit: n })
|
|
151
|
+
const exclusiveMinimumMsg = (n) => `must be > ${n}`
|
|
152
|
+
export const exclusiveMinimum = {
|
|
153
|
+
kw: 'exclusiveMinimum', code: 'ATA2005', on: isNum, ok: exclusiveMinimumOk,
|
|
154
|
+
params: exclusiveMinimumParams, msg: exclusiveMinimumMsg, exp: () => undefined, rank: 2,
|
|
155
|
+
detail: exclusiveDetail,
|
|
156
|
+
}
|
|
157
|
+
export const exclusiveMinimumP = { kw: 'exclusiveMinimum', on: isNum, ok: exclusiveMinimumOk, params: exclusiveMinimumParams, msg: exclusiveMinimumMsg }
|
|
158
|
+
const exclusiveMaximumOk = (v, n) => !(v >= n)
|
|
159
|
+
const exclusiveMaximumParams = (n) => ({ comparison: '<', limit: n })
|
|
160
|
+
const exclusiveMaximumMsg = (n) => `must be < ${n}`
|
|
161
|
+
export const exclusiveMaximum = {
|
|
162
|
+
kw: 'exclusiveMaximum', code: 'ATA2006', on: isNum, ok: exclusiveMaximumOk,
|
|
163
|
+
params: exclusiveMaximumParams, msg: exclusiveMaximumMsg, exp: () => undefined, rank: 2,
|
|
164
|
+
detail: exclusiveDetail,
|
|
165
|
+
}
|
|
166
|
+
export const exclusiveMaximumP = { kw: 'exclusiveMaximum', on: isNum, ok: exclusiveMaximumOk, params: exclusiveMaximumParams, msg: exclusiveMaximumMsg }
|
|
167
|
+
const multipleOfOk = (v, n) => n !== 0 && (Number.isInteger(v / n) || Math.abs(v / n - Math.round(v / n)) < 1e-9)
|
|
168
|
+
const multipleOfParams = (n) => ({ multipleOf: n })
|
|
169
|
+
const multipleOfMsg = (n) => `must be multiple of ${n}`
|
|
170
|
+
export const multipleOf = {
|
|
171
|
+
kw: 'multipleOf', code: 'ATA2007', on: isNum, ok: multipleOfOk,
|
|
172
|
+
params: multipleOfParams, msg: multipleOfMsg, exp: () => undefined, rank: 2,
|
|
173
|
+
}
|
|
174
|
+
export const multipleOfP = { kw: 'multipleOf', on: isNum, ok: multipleOfOk, params: multipleOfParams, msg: multipleOfMsg }
|
|
175
|
+
export const isArr = (v) => Array.isArray(v)
|
|
176
|
+
export const isObjV = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
177
|
+
const minItemsOk = (v, n) => v.length >= n
|
|
178
|
+
const minItemsParams = (n) => ({ limit: n })
|
|
179
|
+
const minItemsMsg = (n) => `must NOT have fewer than ${n} items`
|
|
180
|
+
export const minItems = {
|
|
181
|
+
kw: 'minItems', code: 'ATA2008', on: isArr, ok: minItemsOk,
|
|
182
|
+
params: minItemsParams, msg: minItemsMsg, exp: () => undefined, rank: 2,
|
|
183
|
+
}
|
|
184
|
+
export const minItemsP = { kw: 'minItems', on: isArr, ok: minItemsOk, params: minItemsParams, msg: minItemsMsg }
|
|
185
|
+
const maxItemsOk = (v, n) => v.length <= n
|
|
186
|
+
const maxItemsParams = (n) => ({ limit: n })
|
|
187
|
+
const maxItemsMsg = (n) => `must NOT have more than ${n} items`
|
|
188
|
+
export const maxItems = {
|
|
189
|
+
kw: 'maxItems', code: 'ATA2009', on: isArr, ok: maxItemsOk,
|
|
190
|
+
params: maxItemsParams, msg: maxItemsMsg, exp: () => undefined, rank: 2,
|
|
191
|
+
}
|
|
192
|
+
export const maxItemsP = { kw: 'maxItems', on: isArr, ok: maxItemsOk, params: maxItemsParams, msg: maxItemsMsg }
|
|
193
|
+
const minPropertiesOk = (v, n) => Object.keys(v).length >= n
|
|
194
|
+
const minPropertiesParams = (n) => ({ limit: n })
|
|
195
|
+
const minPropertiesMsg = (n) => `must NOT have fewer than ${n} properties`
|
|
196
|
+
export const minProperties = {
|
|
197
|
+
kw: 'minProperties', code: 'ATA2010', on: isObjV, ok: minPropertiesOk,
|
|
198
|
+
params: minPropertiesParams, msg: minPropertiesMsg, exp: () => undefined, rank: 2,
|
|
199
|
+
}
|
|
200
|
+
export const minPropertiesP = { kw: 'minProperties', on: isObjV, ok: minPropertiesOk, params: minPropertiesParams, msg: minPropertiesMsg }
|
|
201
|
+
const maxPropertiesOk = (v, n) => Object.keys(v).length <= n
|
|
202
|
+
const maxPropertiesParams = (n) => ({ limit: n })
|
|
203
|
+
const maxPropertiesMsg = (n) => `must NOT have more than ${n} properties`
|
|
204
|
+
export const maxProperties = {
|
|
205
|
+
kw: 'maxProperties', code: 'ATA2011', on: isObjV, ok: maxPropertiesOk,
|
|
206
|
+
params: maxPropertiesParams, msg: maxPropertiesMsg, exp: () => undefined, rank: 2,
|
|
207
|
+
}
|
|
208
|
+
export const maxPropertiesP = { kw: 'maxProperties', on: isObjV, ok: maxPropertiesOk, params: maxPropertiesParams, msg: maxPropertiesMsg }
|
|
209
|
+
export const isAny = () => true
|
|
210
|
+
// The guard a kernel's `on` names, by identity: the IR writes the name, and a
|
|
211
|
+
// minifier that renames the functions (the front end in a browser bundle)
|
|
212
|
+
// must not change it.
|
|
213
|
+
export const GUARD_NAME = /* @__PURE__ */ (() => new Map([[isStr, 'isStr'], [isNum, 'isNum'], [isArr, 'isArr'], [isObjV, 'isObjV'], [isAny, 'isAny']]))()
|
|
214
|
+
const constOk = (v, n) => deq(v, n)
|
|
215
|
+
const constParams = (n) => ({ allowedValue: n })
|
|
216
|
+
const constMsg = () => 'must be equal to constant'
|
|
217
|
+
const constExp = (n) => repr(n)
|
|
218
|
+
export const constK = {
|
|
219
|
+
kw: 'const', code: 'ATA6002', on: isAny, ok: constOk,
|
|
220
|
+
params: constParams, msg: constMsg, exp: constExp, rank: 2,
|
|
221
|
+
}
|
|
222
|
+
export const constP = { kw: 'const', on: isAny, ok: constOk, params: constParams, msg: constMsg }
|
|
223
|
+
// const and enum whose values are all primitives: equal to one exactly when
|
|
224
|
+
// === to it (JSON numbers have no NaN, and 0 and -0 are one value to JSON
|
|
225
|
+
// equality too), so a bundle of such schemas carries no deep equality.
|
|
226
|
+
const constPrimOk = (v, n) => v === n
|
|
227
|
+
export const constPrimK = {
|
|
228
|
+
kw: 'const', code: 'ATA6002', on: isAny, ok: constPrimOk,
|
|
229
|
+
params: constParams, msg: constMsg, exp: constExp, rank: 2,
|
|
230
|
+
}
|
|
231
|
+
export const constPrimP = { kw: 'const', on: isAny, ok: constPrimOk, params: constParams, msg: constMsg }
|
|
232
|
+
export const enumSuggest = (v, p) => {
|
|
233
|
+
if (typeof v !== 'string') return null
|
|
234
|
+
const a = p.allowedValues
|
|
235
|
+
if (!Array.isArray(a) || a.length === 0 || a.length > 30) return null
|
|
236
|
+
const best = nearest(v, a)
|
|
237
|
+
return best === null ? null : { text: `did you mean \`${best}\`?`, kind: 'typo' }
|
|
238
|
+
}
|
|
239
|
+
const enumOk = (v, n) => n.some((x) => deq(v, x))
|
|
240
|
+
const enumParams = (n) => ({ allowedValues: n })
|
|
241
|
+
const enumMsg = () => 'must be equal to one of the allowed values'
|
|
242
|
+
const enumExp = (n) => `one of [${n.map(repr).join(', ')}]`
|
|
243
|
+
export const enumK = {
|
|
244
|
+
kw: 'enum', code: 'ATA6001', on: isAny, ok: enumOk,
|
|
245
|
+
params: enumParams, msg: enumMsg, exp: enumExp, rank: 2,
|
|
246
|
+
suggest: enumSuggest,
|
|
247
|
+
}
|
|
248
|
+
export const enumP = { kw: 'enum', on: isAny, ok: enumOk, params: enumParams, msg: enumMsg }
|
|
249
|
+
const enumPrimOk = (v, n) => n.indexOf(v) !== -1
|
|
250
|
+
export const enumPrimK = {
|
|
251
|
+
kw: 'enum', code: 'ATA6001', on: isAny, ok: enumPrimOk,
|
|
252
|
+
params: enumParams, msg: enumMsg, exp: enumExp, rank: 2,
|
|
253
|
+
suggest: enumSuggest,
|
|
254
|
+
}
|
|
255
|
+
export const enumPrimP = { kw: 'enum', on: isAny, ok: enumPrimOk, params: enumParams, msg: enumMsg }
|
|
256
|
+
// A keyword whose message names what it found: fail() returns the params and
|
|
257
|
+
// message of the failure, or null.
|
|
258
|
+
export const uniqueFail = (v, n) => { if (n !== true) return null; const p = dupPair(v); return p === null ? null : { params: { i: p[0], j: p[1] }, msg: `must NOT have duplicate items (items ## ${p[1]} and ${p[0]} are identical)` } }
|
|
259
|
+
export const uniqueItems = {
|
|
260
|
+
kw: 'uniqueItems', code: 'ATA2012', on: isArr, rank: 2, exp: () => undefined,
|
|
261
|
+
fail: uniqueFail,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
// anyOf and oneOf: the current engine's own collapse (lib/branch-collapse.js),
|
|
266
|
+
// which picks the closest branch by error count and severity.
|
|
267
|
+
import { __ataCollapse } from './collapse.mjs'
|
|
268
|
+
export const collapse = __ataCollapse
|
|
269
|
+
export const not = { code: 'ATA4005', keyword: 'not', msg: 'must NOT be valid', rank: 3 }
|
|
270
|
+
export const branchRank = 3
|
|
271
|
+
|
|
272
|
+
// unevaluatedProperties false: one error per property; unevaluatedItems
|
|
273
|
+
// false: one error naming the first unevaluated index, worded as a length.
|
|
274
|
+
export const unevaluatedDetail = (received, v, p) => `unevaluated property "${p.unevaluatedProperty}"`
|
|
275
|
+
export const unevaluatedProperties = { code: 'ATA7003', msg: 'must NOT have unevaluated properties', rank: 0, detail: unevaluatedDetail }
|
|
276
|
+
export const unevaluatedItems = { code: 'ATA7004', rank: 0 }
|
|
277
|
+
|
|
278
|
+
// The false schema.
|
|
279
|
+
export const falseSchema = { code: 'ATA4005', keyword: 'not', msg: 'boolean schema is false', rank: 3 }
|
|
280
|
+
// contains, minContains, maxContains: one error, keyword contains.
|
|
281
|
+
export const contains = { code: 'ATA7007', rank: 2 }
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
// Keywords that carry no constraint.
|
|
285
|
+
export const ANNOTATIONS = new Set(['$schema', '$id', '$comment', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly'])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The leaf keywords as the IR's front end and closures read them
|
|
2
|
+
// (lib/ir/front.mjs), with the format keyword, which reads the current
|
|
3
|
+
// engine's CommonJS files: its run-time checks, codes and hints. This module
|
|
4
|
+
// is not part of ata-validator/kernels; a module written by the IR imports
|
|
5
|
+
// none of it.
|
|
6
|
+
import { minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, pattern, minItems, maxItems, minProperties, maxProperties, constK, enumK, uniqueItems, formatK } from './kernels.mjs'
|
|
7
|
+
|
|
8
|
+
// Formats are the current engine's own: its run-time checks (lib/formats.js
|
|
9
|
+
// formatChecks), its codes (lib/error-codes.js codeFor) and its hints
|
|
10
|
+
// (lib/suggestions.js; generated code carries only its format's hint, see lib/ir/emit.mjs). The generated code is its code generator's
|
|
11
|
+
// (FORMAT_CODEGEN, written in by ir/emit.mjs). A format it does not know is
|
|
12
|
+
// not checked, as there.
|
|
13
|
+
import formats from '../formats.js'
|
|
14
|
+
import errorCodes from '../error-codes.js'
|
|
15
|
+
import suggestions from '../suggestions.js'
|
|
16
|
+
const formatCheck = (n) => { const fc = formats.formatChecks(); return Object.prototype.hasOwnProperty.call(fc, n) ? fc[n] : null }
|
|
17
|
+
// The closures call it; a written module carries only its format's hint (lib/ir/emit.mjs).
|
|
18
|
+
export function formatSuggest (v, p) { return suggestions.suggestFormat(p.format, v) }
|
|
19
|
+
export const format = {
|
|
20
|
+
code: (n) => errorCodes.codeFor('format', n), on: formatK.on, ok: formatK.ok,
|
|
21
|
+
// null: a format the current engine does not know, which it does not check.
|
|
22
|
+
prepare: (n) => formatCheck(n),
|
|
23
|
+
params: formatK.params, msg: formatK.msg, exp: formatK.exp, rank: formatK.rank,
|
|
24
|
+
detail: formatK.detail,
|
|
25
|
+
suggest: formatSuggest,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const LEAVES = { minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, pattern, format, minItems, maxItems, minProperties, maxProperties, const: constK, enum: enumK, uniqueItems }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The errors into schema order, a module of its own so a page that never
|
|
2
|
+
// sorts (most forms) does not carry it, and ata-validator/plan can load it
|
|
3
|
+
// when a plan asks.
|
|
4
|
+
import { rankFor } from './rank.mjs'
|
|
5
|
+
|
|
6
|
+
// The errors in schema declaration order, as the current engine orders them:
|
|
7
|
+
// by the rank of each error's schemaPath in the root schema (rankFor of
|
|
8
|
+
// lib/schema-order.js, copied in ./rank.mjs; lib/aot-order.js sorts by it too), stable, and
|
|
9
|
+
// a collapsed error's branch errors the same way, in a copy.
|
|
10
|
+
const after = (a, b) => { const m = Math.min(a.length, b.length); for (let i = 0; i < m; i++) if (a[i] !== b[i]) return a[i] > b[i]; return a.length > b.length }
|
|
11
|
+
export function ordered (root, list) {
|
|
12
|
+
const n = list.length
|
|
13
|
+
if (n === 1 && !Array.isArray(list[0].branchErrors)) return list
|
|
14
|
+
const out = new Array(n); const keys = new Array(n)
|
|
15
|
+
for (let i = 0; i < n; i++) {
|
|
16
|
+
let e = list[i]
|
|
17
|
+
if (Array.isArray(e.branchErrors) && e.branchErrors.length) { const b = ordered(root, e.branchErrors); if (b !== e.branchErrors) e = Object.assign({}, e, { branchErrors: b }) }
|
|
18
|
+
const k = rankFor(root, e.schemaPath) || []
|
|
19
|
+
let j = i - 1
|
|
20
|
+
while (j >= 0 && after(keys[j], k)) { keys[j + 1] = keys[j]; out[j + 1] = out[j]; j-- }
|
|
21
|
+
keys[j + 1] = k; out[j + 1] = e
|
|
22
|
+
}
|
|
23
|
+
for (let i = 0; i < n; i++) if (out[i] !== list[i]) return out
|
|
24
|
+
return list
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ata-validator/plan-rich: fromPlan for plans with ata's rich errors (code,
|
|
2
|
+
// expected, received, detail, suggestion), and with plain ones. A separate
|
|
3
|
+
// entry so a page reading plain plans carries none of this: the descriptors
|
|
4
|
+
// that carry the rich fields, and the format hints the module writer writes
|
|
5
|
+
// into its modules (lib/ir/emit-deps.mjs).
|
|
6
|
+
import { tableRich } from './table.mjs'
|
|
7
|
+
import { fromPlan as fromAnyPlan, PLAN_VERSION } from './plan.mjs'
|
|
8
|
+
import { type, required, additionalProperties, enumK, constK, formatK, minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, pattern, minItems, maxItems, minProperties, maxProperties } from './kernels.mjs'
|
|
9
|
+
import { FORMAT_HINTS } from '../ir/emit-deps.mjs'
|
|
10
|
+
|
|
11
|
+
const RICH = { tableRich, type, required, additionalProperties, enumK, constK, formatK, minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, pattern, minItems, maxItems, minProperties, maxProperties }
|
|
12
|
+
// A format's hint, as the module writer builds it from the same source.
|
|
13
|
+
const hint = (name) => { const h = Object.prototype.hasOwnProperty.call(FORMAT_HINTS, name) ? FORMAT_HINTS[name] : null; return h === null ? undefined : (v) => { const t = h(v); return t ? { text: t, kind: 'format' } : null } }
|
|
14
|
+
|
|
15
|
+
export { PLAN_VERSION }
|
|
16
|
+
// The validator for a plan, rich or plain.
|
|
17
|
+
export const fromPlan = (plan) => fromAnyPlan(plan, { RICH, hint, tableRich })
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A plan written by tablePlan in ata-validator/ir. */
|
|
2
|
+
export interface Plan { v: 1; ata?: string; rich: boolean; P: unknown[]; F: unknown[]; rel: boolean; sk: unknown; uses: string[] }
|
|
3
|
+
export interface PlanValidator {
|
|
4
|
+
validate(data: unknown): { valid: true; data: unknown; errors: readonly [] } | { valid: false; errors: Array<Record<string, unknown>> }
|
|
5
|
+
isValid(data: unknown): boolean
|
|
6
|
+
}
|
|
7
|
+
export declare const PLAN_VERSION: 1
|
|
8
|
+
/** The validator for a plan, once the kernels it names are loaded. */
|
|
9
|
+
export declare function fromPlan(plan: Plan): Promise<PlanValidator>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// ata-validator/plan: validators for schemas that arrive at run time, as
|
|
2
|
+
// plans a server writes with tablePlan (ata-validator/ir) instead of schemas.
|
|
3
|
+
// The heavy work (references, dialects, normalization) is done where the plan
|
|
4
|
+
// is written; the page carries the table interpreter and, loaded when a plan
|
|
5
|
+
// names them, the format checks, the error order and the rich error shape.
|
|
6
|
+
// The verdicts, errors and filled defaults are those of the server's
|
|
7
|
+
// validator for the same schema. This entry reads plans with plain errors
|
|
8
|
+
// (richErrors: false); ata-validator/plan-rich reads both, and only a page
|
|
9
|
+
// that imports it carries the rich error shape.
|
|
10
|
+
//
|
|
11
|
+
// const v = await fromPlan(await (await fetch('/plans/signup.json')).json())
|
|
12
|
+
// v.validate(data)
|
|
13
|
+
//
|
|
14
|
+
// A plan names kernels in fixed places only (each step's handler, a leaf's
|
|
15
|
+
// descriptor, check and hint); every other value is the schema's own data
|
|
16
|
+
// and is used as data.
|
|
17
|
+
import { tablePlain, tableFill, tType, tProps, tReq, tClosed, tAddl, tLeaf, tItems, tTuple, tAll, tRef, tNot, tIf, tDepReq, tFalse } from './table.mjs'
|
|
18
|
+
import { typeP, requiredP, additionalP, enumP, constP, formatP, uniqueItems, minLengthP, maxLengthP, minimumP, maximumP, exclusiveMinimumP, exclusiveMaximumP, multipleOfP, patternP, minItemsP, maxItemsP, minPropertiesP, maxPropertiesP } from './kernels.mjs'
|
|
19
|
+
|
|
20
|
+
import ATA_VERSION from '../version.js'
|
|
21
|
+
|
|
22
|
+
const CORE = { tablePlain, tableFill, tType, tProps, tReq, tClosed, tAddl, tLeaf, tItems, tTuple, tAll, tRef, tNot, tIf, tDepReq, tFalse, typeP, requiredP, additionalP, enumP, constP, formatP, uniqueItems, minLengthP, maxLengthP, minimumP, maximumP, exclusiveMinimumP, exclusiveMaximumP, multipleOfP, patternP, minItemsP, maxItemsP, minPropertiesP, maxPropertiesP }
|
|
23
|
+
export const PLAN_VERSION = 1
|
|
24
|
+
|
|
25
|
+
const warned = new Set()
|
|
26
|
+
const fail = (why) => { throw new Error('ata-validator/plan: ' + why) }
|
|
27
|
+
// Names an object literal would reorder or treat specially; a plan never
|
|
28
|
+
// carries them (lib/ir/table.mjs refuses such schemas).
|
|
29
|
+
const safeName = (k) => k !== '__proto__' && !/^(0|[1-9][0-9]*)$/.test(k)
|
|
30
|
+
|
|
31
|
+
const HANDLERS = new Set(['tType', 'tProps', 'tReq', 'tClosed', 'tAddl', 'tLeaf', 'tItems', 'tTuple', 'tAll', 'tRef', 'tNot', 'tIf', 'tDepReq', 'tFalse'])
|
|
32
|
+
const isInt = (n) => Number.isInteger(n) && n >= 0
|
|
33
|
+
const isStrings = (a) => Array.isArray(a) && a.every((x) => typeof x === 'string')
|
|
34
|
+
|
|
35
|
+
// Every step checked as it is read, so a plan that is not one the writer
|
|
36
|
+
// makes is refused here, by what is wrong with it, and never runs.
|
|
37
|
+
function decode (P, K, hints) {
|
|
38
|
+
if (!Array.isArray(P) || P.length === 0) fail('a plan has at least its root node')
|
|
39
|
+
const kernel = (n) => typeof n === 'string' && K[n] !== undefined ? K[n] : fail(`unknown kernel ${JSON.stringify(n)}`)
|
|
40
|
+
const descriptor = (n) => { const D = kernel(n); return D !== null && typeof D === 'object' && typeof D.kw === 'string' && (typeof D.ok === 'function' || typeof D.fail === 'function') ? D : fail(`${JSON.stringify(n)} is not a keyword descriptor`) }
|
|
41
|
+
const ref = (r) => typeof r === 'number' ? (isInt(r) && r < P.length ? r : fail('a node index out of range')) : node(r)
|
|
42
|
+
// The node's properties step, for tReq, tClosed and tAddl to name.
|
|
43
|
+
const propsStep = (N, j) => j === -1 || (isInt(j) && j < N.length && Array.isArray(N[j]) && N[j][0] === 'tProps') ? j : fail('a step names a properties step that is not one')
|
|
44
|
+
function node (N) {
|
|
45
|
+
if (!Array.isArray(N)) fail('a node is not a list of steps')
|
|
46
|
+
return N.map((t) => step(t, N))
|
|
47
|
+
}
|
|
48
|
+
function step (t, N) {
|
|
49
|
+
if (!Array.isArray(t) || !HANDLERS.has(t[0])) fail(`a step whose handler is ${JSON.stringify(Array.isArray(t) ? t[0] : t)}`)
|
|
50
|
+
const out = t.slice(); out[0] = kernel(t[0])
|
|
51
|
+
switch (t[0]) {
|
|
52
|
+
// A list may hold what draft 3 allowed there; the check only reads it.
|
|
53
|
+
case 'tType': if (!(typeof t[1] === 'string' || Array.isArray(t[1]))) fail('tType takes a type or a list'); break
|
|
54
|
+
case 'tProps': {
|
|
55
|
+
if (t[1] === null || typeof t[1] !== 'object' || Array.isArray(t[1])) fail('tProps takes an object')
|
|
56
|
+
const o = {}
|
|
57
|
+
for (const k of Object.keys(t[1])) { if (!safeName(k)) fail(`a property name ${JSON.stringify(k)} a plan cannot carry`); o[k] = ref(t[1][k]) }
|
|
58
|
+
out[1] = o; break
|
|
59
|
+
}
|
|
60
|
+
case 'tReq':
|
|
61
|
+
if (isStrings(t[1])) break
|
|
62
|
+
if (!(isInt(t[1]) && t[1] < 2 ** 31)) fail('tReq takes names or a mask')
|
|
63
|
+
if (propsStep(N, t[2]) === -1) fail('a required mask with no properties step'); break
|
|
64
|
+
case 'tClosed': propsStep(N, t[1]); break
|
|
65
|
+
case 'tAddl': out[1] = ref(t[1]); propsStep(N, t[2]); break
|
|
66
|
+
case 'tItems': out[1] = ref(t[1]); if (!isInt(t[2])) fail('tItems takes a first index'); break
|
|
67
|
+
case 'tRef': case 'tNot': out[1] = ref(t[1]); break
|
|
68
|
+
case 'tTuple': case 'tAll': if (!Array.isArray(t[1])) fail(t[0] + ' takes a list of nodes'); out[1] = t[1].map(ref); break
|
|
69
|
+
case 'tIf': out[1] = ref(t[1]); out[2] = t[2] === -1 ? -1 : ref(t[2]); out[3] = t[3] === -1 ? -1 : ref(t[3]); break
|
|
70
|
+
case 'tDepReq':
|
|
71
|
+
if (!Array.isArray(t[1]) || t[1].length % 2 !== 0) fail('tDepReq takes names and lists of names')
|
|
72
|
+
for (let j = 0; j < t[1].length; j += 2) if (typeof t[1][j] !== 'string' || !isStrings(t[1][j + 1])) fail('tDepReq takes names and lists of names')
|
|
73
|
+
break
|
|
74
|
+
case 'tFalse': break
|
|
75
|
+
case 'tLeaf':
|
|
76
|
+
out[1] = descriptor(t[1])
|
|
77
|
+
if (t.length === 6) {
|
|
78
|
+
if (out[1].kw !== 'format' || typeof t[3] !== 'string' || !/^fmt_\w+$/.test(t[3]) || typeof t[4] !== 'string') fail('a format step is not one')
|
|
79
|
+
out[3] = kernel(t[3]); out[5] = t[5] == null ? undefined : hints(t[5])
|
|
80
|
+
} else if (t.length !== 3) fail('a leaf step has a descriptor and its argument')
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
return P.map(node)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The defaults table: [defaults [k, v, ...], properties [k, f, ...],
|
|
89
|
+
// prefixItems [f or -1, ...], items f or -1, dependentSchemas [k, f, ...],
|
|
90
|
+
// in place [f, ...]], every f an index into it.
|
|
91
|
+
function checkFill (F) {
|
|
92
|
+
if (!Array.isArray(F)) fail('the defaults table is not a list')
|
|
93
|
+
const f = (i) => i === -1 || (isInt(i) && i < F.length) || fail('a defaults index out of range')
|
|
94
|
+
const pairs = (a, idx) => { if (!Array.isArray(a) || a.length % 2 !== 0) fail('a defaults entry is not pairs'); for (let j = 0; j < a.length; j += 2) { if (typeof a[j] !== 'string') fail('a defaults name is not a string'); if (idx) f(a[j + 1]) } }
|
|
95
|
+
for (const e of F) {
|
|
96
|
+
if (!Array.isArray(e) || e.length !== 6) fail('a defaults entry has six parts')
|
|
97
|
+
pairs(e[0], false); pairs(e[1], true)
|
|
98
|
+
if (!Array.isArray(e[2])) fail('a defaults entry is not well formed'); e[2].forEach(f)
|
|
99
|
+
f(e[3]); pairs(e[4], true)
|
|
100
|
+
if (!Array.isArray(e[5])) fail('a defaults entry is not well formed'); e[5].forEach(f)
|
|
101
|
+
}
|
|
102
|
+
return F
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The validator for a plan, once the kernels it names are loaded. `rich`:
|
|
106
|
+
// what ata-validator/plan-rich passes in.
|
|
107
|
+
export async function fromPlan (plan, rich = null) {
|
|
108
|
+
if (plan === null || typeof plan !== 'object' || plan.v !== PLAN_VERSION) fail(`a plan of version ${PLAN_VERSION} is expected`)
|
|
109
|
+
const uses = Array.isArray(plan.uses) ? plan.uses : fail('the plan does not list its kernels')
|
|
110
|
+
// The page and the server decide alike when both run the same ata-validator.
|
|
111
|
+
if (typeof plan.ata === 'string' && plan.ata !== ATA_VERSION && !warned.has(plan.ata)) {
|
|
112
|
+
warned.add(plan.ata)
|
|
113
|
+
console.warn(`ata-validator/plan: this plan was written by ata-validator ${plan.ata} and is read by ${ATA_VERSION}; a page and a server agree on every document only on the same version.`)
|
|
114
|
+
}
|
|
115
|
+
const K = Object.assign(Object.create(null), CORE)
|
|
116
|
+
const loads = []
|
|
117
|
+
if (plan.rich) { if (rich === null) fail('this plan has rich errors: read it with ata-validator/plan-rich'); Object.assign(K, rich.RICH) }
|
|
118
|
+
if (uses.some((n) => /^fmt_\w+_s$/.test(n))) loads.push(import('./formats-small.mjs').then((m) => Object.assign(K, m)))
|
|
119
|
+
if (uses.some((n) => /^fmt_\w+$/.test(n) && !/_s$/.test(n))) loads.push(import('./formats.mjs').then((m) => Object.assign(K, m)))
|
|
120
|
+
let order
|
|
121
|
+
if (plan.sk) loads.push(import('./order.mjs').then((m) => { order = (e) => m.ordered(plan.sk, e) }))
|
|
122
|
+
await Promise.all(loads)
|
|
123
|
+
for (const n of uses) if (n !== 'ordered' && K[n] === undefined) fail(`this plan needs "${n}", which this version of ata-validator does not have`)
|
|
124
|
+
const P = decode(plan.P, K, (name) => rich ? rich.hint(name) : undefined)
|
|
125
|
+
const F = checkFill(plan.F === undefined ? [] : plan.F)
|
|
126
|
+
const fill = F.length ? tableFill : null
|
|
127
|
+
return plan.rich ? rich.tableRich(P, F, fill, plan.rel, order) : tablePlain(P, F, fill, order)
|
|
128
|
+
}
|