ata-validator 1.60.0 → 1.61.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/README.md +134 -6
- package/bin/ata.js +84 -0
- package/build.d.ts +29 -0
- package/build.mjs +2 -0
- package/compiled-bare-rich.d.ts +6 -0
- package/compiled-bare.d.ts +7 -0
- package/compiled-bare.js +5 -0
- package/compiled-bare.mjs +35 -0
- package/compiled-object.d.ts +11 -0
- package/compiled-object.js +6 -0
- package/compiled-object.mjs +3 -0
- package/compiled-plain.d.ts +16 -0
- package/compiled-plain.js +5 -0
- package/compiled-plain.mjs +3 -0
- package/compiled.d.ts +3 -1
- package/index.browser.mjs +2 -2
- package/index.bundle.mjs +2 -2
- package/index.d.ts +9 -0
- package/index.edge.mjs +2 -2
- package/index.js +48 -0
- package/index.mjs +1 -1
- package/index.node.mjs +1 -1
- package/lib/aot-build.js +81 -6
- package/lib/aot-impl.js +24 -21
- package/lib/bun-target.js +612 -0
- package/lib/codegen-engine.js +9 -3
- package/lib/combined-runtime.js +1 -1
- package/lib/compiled-bare-rich.js +54 -0
- package/lib/compiled-bare.js +44 -0
- package/lib/compiled-common.js +17 -2
- package/lib/compiled-defaults.browser.js +9 -0
- package/lib/compiled-defaults.js +8 -0
- package/lib/compiled-object.js +123 -0
- package/lib/compiled-plain.js +57 -0
- package/lib/compiled.js +2 -1
- package/lib/email-literal.js +29 -0
- package/lib/enrich-error.js +5 -1
- package/lib/error-codes.js +6 -76
- package/lib/formats-source.js +20 -3
- package/lib/formats.js +1 -1
- package/lib/js-compiler-errors.js +49 -10
- package/lib/js-compiler.js +92 -13
- package/lib/json-rejections.js +159 -0
- package/lib/native-codes.js +67 -0
- package/lib/native-message.js +8 -0
- package/lib/native.browser.js +11 -0
- package/lib/public-api.js +1 -0
- package/lib/rejections.js +13 -152
- package/lib/standard-schema.js +1 -1
- package/lib/tier0.js +1 -1
- package/lib/validator-core.js +46 -4
- package/lib/verify.js +386 -0
- package/lib/version.js +1 -1
- package/lite.js +1 -0
- package/lite.mjs +1 -1
- package/package.json +44 -9
package/lib/verify.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// `ata verify`: the proof that a compiled module answers as the runtime
|
|
3
|
+
// Validator does, run against a project's own schemas. For every schema the
|
|
4
|
+
// module written by `ata build` is loaded, documents are made from the schema
|
|
5
|
+
// (valid ones, and ones with one rule broken) and read from the project's
|
|
6
|
+
// samples, and both the module and a Validator built from the schema at
|
|
7
|
+
// verification time answer each one; the verdicts and the error lists have to
|
|
8
|
+
// agree. The module's schema hash is checked against the schema as it is now,
|
|
9
|
+
// so a module built from an older schema is reported stale.
|
|
10
|
+
//
|
|
11
|
+
// A validator that wrongly rejects gets a bug report; one that wrongly accepts
|
|
12
|
+
// does not. This command exists so a build-time module gets checked in CI the
|
|
13
|
+
// way the engine is checked here, on every change.
|
|
14
|
+
|
|
15
|
+
const fs = require('fs')
|
|
16
|
+
const path = require('path')
|
|
17
|
+
const { pathToFileURL } = require('url')
|
|
18
|
+
|
|
19
|
+
// A small deterministic generator (xorshift32), so a run can be repeated.
|
|
20
|
+
function rng (seed) {
|
|
21
|
+
let x = (seed >>> 0) || 0x9e3779b9
|
|
22
|
+
const next = () => { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; return (x >>> 0) / 4294967296 }
|
|
23
|
+
return { next, int: (n) => Math.floor(next() * n), pick: (a) => a[Math.floor(next() * a.length)], chance: (p) => next() < p }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const WORDS = ['ada', 'Zoë', 'order', 'x', 'north-west', 'a b', 'ÄÖÜ', 'sku-1001', 'TENOFF', 'hello world', '', '0', 'null', '😀', 'line\nbreak', ' ']
|
|
27
|
+
// Strings tried against a `pattern` the generator cannot read: the common
|
|
28
|
+
// shapes of identifiers, codes, addresses, dates and numbers.
|
|
29
|
+
const PATTERN_CANDIDATES = [
|
|
30
|
+
'a', 'ab', 'abc', 'abc-123', 'ABC', 'A1', 'x_y', 'hello', 'Hello World', 'sku-1001', 'SKU-1001', 'TENOFF',
|
|
31
|
+
'zoe@example.com', 'a@b.co', 'user.name+tag@example.org', '2026-10-25', '2026-10-25T10:00:00Z', '10:00:00',
|
|
32
|
+
'0', '1', '42', '-7', '3.14', '1e3', '0001', '+905551112233', '555-1234', '#fff', '#a1b2c3',
|
|
33
|
+
'https://example.com/path', 'http://x.io', 'example.com', '/a/b/c', 'v1.2.3', '1.2.3', 'en-US', 'TR',
|
|
34
|
+
'123e4567-e89b-12d3-a456-426614174000', '192.168.0.1', '::1', 'true', 'null', 'aaaaaaaaaaaaaaaa', 'ÄÖÜ', 'çş'
|
|
35
|
+
]
|
|
36
|
+
const FORMAT_EXAMPLES = {
|
|
37
|
+
email: ['zoe@example.com', 'a@b.co', 'not an email', 'x@', '@y.com'],
|
|
38
|
+
'date-time': ['2026-10-25T10:00:00Z', '2026-10-25T10:00:00+03:00', '2026-10-25 10:00', 'yesterday'],
|
|
39
|
+
date: ['2026-10-25', '2026-13-01', '2026-2-5'],
|
|
40
|
+
time: ['10:00:00Z', '25:00:00Z', '10:00'],
|
|
41
|
+
uuid: ['123e4567-e89b-12d3-a456-426614174000', '123e4567-e89b-12d3-a456', 'not-a-uuid'],
|
|
42
|
+
uri: ['https://example.com/a?b=1', 'example.com', '/relative', 'mailto:a@b.co'],
|
|
43
|
+
'uri-reference': ['https://example.com', '/path', '#frag', ' spaced'],
|
|
44
|
+
ipv4: ['192.168.0.1', '256.1.1.1', '1.2.3'],
|
|
45
|
+
ipv6: ['::1', '2001:db8::1', '2001:db8:::1', '1.2.3.4'],
|
|
46
|
+
hostname: ['example.com', 'a-b.c', '-bad.com', 'x'.repeat(70)],
|
|
47
|
+
regex: ['^a+$', '([', '\\d+'],
|
|
48
|
+
'json-pointer': ['/a/b', 'a/b', '/a~1b', '/a~2'],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A value meant to satisfy `schema`, built from its keywords; where the
|
|
52
|
+
// schema says little, something plausible of the kind.
|
|
53
|
+
function makeValid (schema, r, defs, depth) {
|
|
54
|
+
if (schema === true || schema === undefined) return r.pick([1, 'x', true, null, {}, []])
|
|
55
|
+
if (schema === false) return undefined
|
|
56
|
+
if (typeof schema !== 'object' || schema === null) return null
|
|
57
|
+
if (schema.$ref !== undefined && typeof schema.$ref === 'string') {
|
|
58
|
+
const t = resolveLocal(schema.$ref, defs)
|
|
59
|
+
if (t !== undefined && depth < 6) return makeValid(t, r, defs, depth + 1)
|
|
60
|
+
}
|
|
61
|
+
if (schema.const !== undefined) return clone(schema.const)
|
|
62
|
+
if (Array.isArray(schema.enum) && schema.enum.length) return clone(r.pick(schema.enum))
|
|
63
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length) return makeValid(r.pick(schema.oneOf), r, defs, depth + 1)
|
|
64
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length) return makeValid(r.pick(schema.anyOf), r, defs, depth + 1)
|
|
65
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length) return makeValid(mergeAllOf(schema), r, defs, depth + 1)
|
|
66
|
+
let type = schema.type
|
|
67
|
+
if (Array.isArray(type)) type = r.pick(type)
|
|
68
|
+
if (type === undefined) {
|
|
69
|
+
if (schema.properties || schema.required || schema.additionalProperties !== undefined) type = 'object'
|
|
70
|
+
else if (schema.items || schema.prefixItems || schema.minItems !== undefined) type = 'array'
|
|
71
|
+
else if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined || schema.format !== undefined) type = 'string'
|
|
72
|
+
else if (schema.minimum !== undefined || schema.maximum !== undefined || schema.multipleOf !== undefined) type = 'number'
|
|
73
|
+
else type = r.pick(['string', 'number', 'object', 'array', 'boolean', 'null'])
|
|
74
|
+
}
|
|
75
|
+
switch (type) {
|
|
76
|
+
case 'null': return null
|
|
77
|
+
case 'boolean': return r.chance(0.5)
|
|
78
|
+
case 'integer':
|
|
79
|
+
case 'number': {
|
|
80
|
+
let lo = schema.minimum !== undefined ? schema.minimum : schema.exclusiveMinimum !== undefined && typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum + 1 : -1000
|
|
81
|
+
let hi = schema.maximum !== undefined ? schema.maximum : schema.exclusiveMaximum !== undefined && typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum - 1 : 1000
|
|
82
|
+
if (hi < lo) hi = lo
|
|
83
|
+
let v = lo + r.next() * (hi - lo)
|
|
84
|
+
if (type === 'integer' || r.chance(0.6)) v = Math.round(v)
|
|
85
|
+
if (typeof schema.multipleOf === 'number' && schema.multipleOf > 0) v = Math.round(v / schema.multipleOf) * schema.multipleOf
|
|
86
|
+
if (v < lo) v = lo
|
|
87
|
+
if (v > hi) v = hi
|
|
88
|
+
return v
|
|
89
|
+
}
|
|
90
|
+
case 'string': {
|
|
91
|
+
if (schema.format && FORMAT_EXAMPLES[schema.format]) return r.pick(FORMAT_EXAMPLES[schema.format])
|
|
92
|
+
const min = schema.minLength || 0
|
|
93
|
+
const max = schema.maxLength !== undefined ? schema.maxLength : Math.max(min, 12)
|
|
94
|
+
if (schema.pattern) {
|
|
95
|
+
const simple = /^\^?\[([a-zA-Z0-9-]+)\]([+*])?\$?$/.exec(schema.pattern)
|
|
96
|
+
if (simple) {
|
|
97
|
+
const set = simple[1].includes('a-z') ? 'abcdefgh' : simple[1].includes('0-9') ? '0123456789' : simple[1].includes('A-Z') ? 'ABCDEFGH' : simple[1].replace(/-/g, '')
|
|
98
|
+
let s = ''
|
|
99
|
+
const n = Math.max(min, Math.min(max, 1 + r.int(8)))
|
|
100
|
+
for (let i = 0; i < n; i++) s += set[r.int(set.length)]
|
|
101
|
+
return s
|
|
102
|
+
}
|
|
103
|
+
// Any other pattern: a candidate that matches, when one does, so the
|
|
104
|
+
// generated documents reach past the pattern; else a word, which the
|
|
105
|
+
// schema then refuses, and that is a document too.
|
|
106
|
+
let re = null
|
|
107
|
+
try { re = new RegExp(schema.pattern, 'u') } catch {}
|
|
108
|
+
if (re) {
|
|
109
|
+
const matching = PATTERN_CANDIDATES.filter((c) => re.test(c) && [...c].length >= min && [...c].length <= max)
|
|
110
|
+
if (matching.length) return r.pick(matching)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let s = r.pick(WORDS)
|
|
114
|
+
while ([...s].length < min) s += 'a'
|
|
115
|
+
if ([...s].length > max) s = [...s].slice(0, max).join('')
|
|
116
|
+
return s
|
|
117
|
+
}
|
|
118
|
+
case 'array': {
|
|
119
|
+
const min = schema.minItems || 0
|
|
120
|
+
const max = schema.maxItems !== undefined ? schema.maxItems : Math.max(min, 4)
|
|
121
|
+
const n = depth > 4 ? min : min + r.int(Math.max(1, max - min + 1))
|
|
122
|
+
const out = []
|
|
123
|
+
for (let i = 0; i < n; i++) {
|
|
124
|
+
const sub = Array.isArray(schema.prefixItems) && i < schema.prefixItems.length ? schema.prefixItems[i] : Array.isArray(schema.items) ? (schema.items[i] !== undefined ? schema.items[i] : schema.additionalItems) : schema.items
|
|
125
|
+
if (sub === false) break
|
|
126
|
+
out.push(makeValid(sub === undefined ? true : sub, r, defs, depth + 1))
|
|
127
|
+
}
|
|
128
|
+
if (schema.uniqueItems) return out.filter((v, i, a) => a.findIndex((w) => JSON.stringify(w) === JSON.stringify(v)) === i)
|
|
129
|
+
return out
|
|
130
|
+
}
|
|
131
|
+
case 'object':
|
|
132
|
+
default: {
|
|
133
|
+
const out = {}
|
|
134
|
+
const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : {}
|
|
135
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : [])
|
|
136
|
+
for (const k of Object.keys(props)) {
|
|
137
|
+
if (k === '__proto__') continue
|
|
138
|
+
if (required.has(k) || r.chance(0.7)) out[k] = makeValid(props[k], r, defs, depth + 1)
|
|
139
|
+
}
|
|
140
|
+
for (const k of required) if (!(k in out) && k !== '__proto__') out[k] = makeValid(props[k] === undefined ? true : props[k], r, defs, depth + 1)
|
|
141
|
+
if (schema.additionalProperties !== false && !schema.patternProperties && r.chance(0.2) && depth < 4) out[`extra${r.int(3)}`] = makeValid(typeof schema.additionalProperties === 'object' ? schema.additionalProperties : true, r, defs, depth + 1)
|
|
142
|
+
if (schema.patternProperties && r.chance(0.5)) {
|
|
143
|
+
for (const [p, sub] of Object.entries(schema.patternProperties)) {
|
|
144
|
+
const m = /^\^?([a-zA-Z_]+)/.exec(p)
|
|
145
|
+
if (m) out[m[1] + r.int(9)] = makeValid(sub, r, defs, depth + 1)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mergeAllOf (schema) {
|
|
154
|
+
const merged = {}
|
|
155
|
+
for (const b of schema.allOf) if (typeof b === 'object' && b !== null) Object.assign(merged, b, { properties: { ...(merged.properties || {}), ...(b.properties || {}) }, required: [...(merged.required || []), ...(b.required || [])] })
|
|
156
|
+
for (const k of Object.keys(schema)) if (k !== 'allOf' && merged[k] === undefined) merged[k] = schema[k]
|
|
157
|
+
return merged
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveLocal (ref, defs) {
|
|
161
|
+
const m = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(ref)
|
|
162
|
+
if (!m || !defs[m[1]]) return undefined
|
|
163
|
+
const key = decodeURIComponent(m[2].replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
164
|
+
return Object.prototype.hasOwnProperty.call(defs[m[1]], key) ? defs[m[1]][key] : undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function clone (v) { return v === undefined ? v : JSON.parse(JSON.stringify(v)) }
|
|
168
|
+
|
|
169
|
+
// One rule broken somewhere in `doc`: a kind swapped, a key dropped or added,
|
|
170
|
+
// a number pushed out of range, a string lengthened or emptied, an array
|
|
171
|
+
// emptied or an element duplicated. The point is a document the schema is
|
|
172
|
+
// likely to refuse, so both engines get to disagree on refusals too.
|
|
173
|
+
function mutate (doc, r) {
|
|
174
|
+
const paths = []
|
|
175
|
+
const walk = (v, p) => {
|
|
176
|
+
paths.push(p)
|
|
177
|
+
if (Array.isArray(v)) v.forEach((x, i) => walk(x, [...p, i]))
|
|
178
|
+
else if (v && typeof v === 'object') for (const k of Object.keys(v)) walk(v[k], [...p, k])
|
|
179
|
+
}
|
|
180
|
+
walk(doc, [])
|
|
181
|
+
const p = r.pick(paths)
|
|
182
|
+
const get = (o, q) => q.reduce((a, k) => (a == null ? a : a[k]), o)
|
|
183
|
+
const set = (o, q, val) => { if (q.length === 0) return val; const parent = get(o, q.slice(0, -1)); if (parent != null) parent[q[q.length - 1]] = val; return o }
|
|
184
|
+
const del = (o, q) => { const parent = get(o, q.slice(0, -1)); if (Array.isArray(parent)) parent.splice(q[q.length - 1], 1); else if (parent) delete parent[q[q.length - 1]]; return o }
|
|
185
|
+
const v = get(doc, p)
|
|
186
|
+
switch (r.int(8)) {
|
|
187
|
+
case 0: return set(doc, p, r.pick([null, true, 'str', 0, [], {}]))
|
|
188
|
+
case 1: return p.length ? del(doc, p) : set(doc, p, null)
|
|
189
|
+
case 2: if (v && typeof v === 'object' && !Array.isArray(v)) { v[`unexpected${r.int(5)}`] = r.pick([1, 'x', null]); return doc } return set(doc, p, 'unexpected')
|
|
190
|
+
case 3: return set(doc, p, typeof v === 'number' ? r.pick([v * 1000 + 1, -v - 1, v + 0.5, Number.MAX_SAFE_INTEGER, -1]) : -1)
|
|
191
|
+
case 4: return set(doc, p, typeof v === 'string' ? r.pick(['', v.repeat(40), v + ' ', '😀' + v, v.toUpperCase()]) : '')
|
|
192
|
+
case 5: if (Array.isArray(v)) { if (v.length && r.chance(0.5)) v.push(clone(v[0])); else v.length = 0; return doc } return set(doc, p, [])
|
|
193
|
+
case 6: return set(doc, p, typeof v === 'boolean' ? !v : typeof v === 'number' ? String(v) : typeof v === 'string' ? Number(v) || 7 : v)
|
|
194
|
+
default: return JSON.parse(JSON.stringify(doc).replace(/"([a-zA-Z]+)":/, '"$1_":'))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The values on either side of each bound the schema states, for the value
|
|
199
|
+
// at one place: a module that wrote a bound off by one (`< 12` for a minimum
|
|
200
|
+
// of 13) answers every random document as the runtime does, and differs only
|
|
201
|
+
// here. A bound is where a hand edit, a stale module or a generator bug shows.
|
|
202
|
+
function boundaryValues (schema, type) {
|
|
203
|
+
const out = []
|
|
204
|
+
const int = type === 'integer'
|
|
205
|
+
const step = int ? 1 : 0.5
|
|
206
|
+
const num = (b) => typeof b === 'number' && Number.isFinite(b)
|
|
207
|
+
if (type === 'integer' || type === 'number') {
|
|
208
|
+
if (num(schema.minimum)) out.push(schema.minimum, schema.minimum - step)
|
|
209
|
+
if (num(schema.maximum)) out.push(schema.maximum, schema.maximum + step)
|
|
210
|
+
if (num(schema.exclusiveMinimum)) out.push(schema.exclusiveMinimum, schema.exclusiveMinimum + step)
|
|
211
|
+
if (num(schema.exclusiveMaximum)) out.push(schema.exclusiveMaximum, schema.exclusiveMaximum - step)
|
|
212
|
+
if (num(schema.multipleOf) && schema.multipleOf > 0) out.push(schema.multipleOf, schema.multipleOf * 1.5)
|
|
213
|
+
}
|
|
214
|
+
if (type === 'string') {
|
|
215
|
+
const str = (n) => 'abcdefghij'.repeat(Math.ceil(n / 10) + 1).slice(0, n)
|
|
216
|
+
if (num(schema.minLength)) { out.push(str(schema.minLength)); if (schema.minLength > 0) out.push(str(schema.minLength - 1)) }
|
|
217
|
+
if (num(schema.maxLength) && schema.maxLength < 4096) out.push(str(schema.maxLength), str(schema.maxLength + 1))
|
|
218
|
+
}
|
|
219
|
+
return out
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Documents that differ from a valid one at a single place, by a value on
|
|
223
|
+
// either side of a bound stated there: properties, nested objects, the first
|
|
224
|
+
// element of an array, through local references. At most `limit` of them.
|
|
225
|
+
function boundaryDocs (schema, r, defs, limit, accepts) {
|
|
226
|
+
// A base the runtime accepts, where the caller can say, so that a bound
|
|
227
|
+
// read wrong changes the verdict and not only the error list.
|
|
228
|
+
let base = makeValid(schema, r, defs, 0)
|
|
229
|
+
if (accepts) for (let i = 0; i < 50 && base !== undefined && !accepts(base); i++) base = makeValid(schema, r, defs, 0)
|
|
230
|
+
if (base === undefined) return []
|
|
231
|
+
const out = []
|
|
232
|
+
const visit = (sub, path, depth) => {
|
|
233
|
+
if (out.length >= limit || depth > 8 || !sub || typeof sub !== 'object') return
|
|
234
|
+
if (typeof sub.$ref === 'string') { const t = resolveLocal(sub.$ref, defs); if (t !== undefined) visit(t, path, depth + 1); return }
|
|
235
|
+
if (Array.isArray(sub.allOf)) { visit(mergeAllOf(sub), path, depth + 1); return }
|
|
236
|
+
const types = Array.isArray(sub.type) ? sub.type : sub.type !== undefined ? [sub.type] : []
|
|
237
|
+
for (const t of types.length ? types : ['integer', 'number', 'string']) {
|
|
238
|
+
for (const val of boundaryValues(sub, t)) {
|
|
239
|
+
if (out.length >= limit) return
|
|
240
|
+
const d = clone(base)
|
|
241
|
+
if (path.length === 0) { out.push(val); continue }
|
|
242
|
+
let o = d
|
|
243
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
244
|
+
if (o[path[i]] === undefined || o[path[i]] === null || typeof o[path[i]] !== 'object') o[path[i]] = typeof path[i + 1] === 'number' ? [] : {}
|
|
245
|
+
o = o[path[i]]
|
|
246
|
+
}
|
|
247
|
+
o[path[path.length - 1]] = val
|
|
248
|
+
out.push(d)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (sub.properties && typeof sub.properties === 'object') {
|
|
252
|
+
for (const k of Object.keys(sub.properties)) if (k !== '__proto__') visit(sub.properties[k], [...path, k], depth + 1)
|
|
253
|
+
}
|
|
254
|
+
const item = Array.isArray(sub.prefixItems) ? sub.prefixItems[0] : sub.items && typeof sub.items === 'object' && !Array.isArray(sub.items) ? sub.items : null
|
|
255
|
+
if (item) visit(item, [...path, 0], depth + 1)
|
|
256
|
+
}
|
|
257
|
+
visit(schema, [], 0)
|
|
258
|
+
return out
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Documents for `schema`: `count` made, half of them with one rule broken,
|
|
262
|
+
// then the documents on either side of each bound (up to `count` more).
|
|
263
|
+
function generate (schema, count, seed, accepts) {
|
|
264
|
+
const r = rng(seed)
|
|
265
|
+
const defs = { $defs: schema && schema.$defs, definitions: schema && schema.definitions }
|
|
266
|
+
const docs = []
|
|
267
|
+
for (let i = 0; i < count; i++) {
|
|
268
|
+
let d = makeValid(schema, r, defs, 0)
|
|
269
|
+
if (i % 2 === 1 && d !== undefined) d = mutate(clone(d), r)
|
|
270
|
+
docs.push(d)
|
|
271
|
+
}
|
|
272
|
+
for (const d of boundaryDocs(schema, r, defs, count, accepts)) docs.push(d)
|
|
273
|
+
return docs
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The project's own documents: a JSON file is one document, a JSON Lines
|
|
277
|
+
// file one per line, a JSON array is taken as one document, not several.
|
|
278
|
+
function readSamples (paths) {
|
|
279
|
+
const docs = []
|
|
280
|
+
const files = []
|
|
281
|
+
for (const p of paths) {
|
|
282
|
+
if (!fs.existsSync(p)) continue
|
|
283
|
+
if (fs.statSync(p).isDirectory()) {
|
|
284
|
+
for (const f of fs.readdirSync(p).sort()) if (/\.(json|jsonl|ndjson)$/.test(f)) files.push(path.join(p, f))
|
|
285
|
+
} else files.push(p)
|
|
286
|
+
}
|
|
287
|
+
for (const f of files) {
|
|
288
|
+
const text = fs.readFileSync(f, 'utf8')
|
|
289
|
+
if (/\.(jsonl|ndjson)$/.test(f)) {
|
|
290
|
+
text.split('\n').forEach((line, i) => { if (line.trim()) { try { docs.push({ source: `${f}:${i + 1}`, doc: JSON.parse(line) }) } catch {} } })
|
|
291
|
+
} else {
|
|
292
|
+
try { docs.push({ source: f, doc: JSON.parse(text) }) } catch { docs.push({ source: f, doc: undefined, bad: true }) }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return docs
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function errorKey (e) { return `${e.keyword || ''}@${e.instancePath || ''}` }
|
|
299
|
+
function summarize (result) {
|
|
300
|
+
if (!result || typeof result !== 'object') return 'no result'
|
|
301
|
+
const errs = Array.isArray(result.errors) ? result.errors.map(errorKey).sort() : []
|
|
302
|
+
return `${result.valid ? 'valid' : 'invalid'} ${errs.join(' ')}`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function loadModule (file) {
|
|
306
|
+
if (/\.cjs$/.test(file)) { delete require.cache[require.resolve(file)]; return require(file) }
|
|
307
|
+
return import(pathToFileURL(file).href + `?t=${Date.now()}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Verifies one schema against its module. Returns a report object.
|
|
311
|
+
async function verifyOne ({ schemaFile, moduleFile, schema, generate: count = 200, seed = 1, samples = [] }) {
|
|
312
|
+
const { Validator } = require('..')
|
|
313
|
+
const { schemaHash } = require('./schema-hash')
|
|
314
|
+
const report = { schema: schemaFile, module: moduleFile, documents: 0, generated: 0, samples: 0, valid: 0, agree: 0, disagreements: [], stale: false, missing: false, ataVersion: null, error: null }
|
|
315
|
+
if (!fs.existsSync(moduleFile)) { report.missing = true; return report }
|
|
316
|
+
let mod
|
|
317
|
+
try { mod = await loadModule(moduleFile) } catch (e) { report.error = `module does not load: ${e.message}`; return report }
|
|
318
|
+
if (typeof mod.isValid !== 'function' || typeof mod.validate !== 'function') { report.error = 'module exports no isValid and validate'; return report }
|
|
319
|
+
report.ataVersion = mod.ataVersion || null
|
|
320
|
+
const hash = schemaHash(schema)
|
|
321
|
+
if (mod.schemaHash !== undefined && mod.schemaHash !== hash) report.stale = true
|
|
322
|
+
let runtime
|
|
323
|
+
try { runtime = new Validator(schema, {}) } catch (e) { report.error = `schema does not compile: ${e.message}`; return report }
|
|
324
|
+
const docs = generate(schema, count, seed, (d) => { try { return runtime.isValidObject(clone(d)) } catch { return false } }).map((d, i) => ({ source: `generated #${i + 1}`, doc: d }))
|
|
325
|
+
report.generated = docs.length
|
|
326
|
+
const sampleDocs = readSamples(samples)
|
|
327
|
+
report.samples = sampleDocs.length
|
|
328
|
+
for (const { source, doc, bad } of [...docs, ...sampleDocs]) {
|
|
329
|
+
if (bad || doc === undefined) continue
|
|
330
|
+
report.documents++
|
|
331
|
+
const text = JSON.stringify(doc)
|
|
332
|
+
const a = runtime.validate(JSON.parse(text))
|
|
333
|
+
const b = mod.validate(JSON.parse(text))
|
|
334
|
+
const aIs = runtime.isValidObject(JSON.parse(text))
|
|
335
|
+
const bIs = mod.isValid(JSON.parse(text))
|
|
336
|
+
if (a.valid) report.valid++
|
|
337
|
+
const sa = summarize(a), sb = summarize(b)
|
|
338
|
+
if (sa === sb && aIs === bIs && aIs === a.valid) { report.agree++; continue }
|
|
339
|
+
if (report.disagreements.length < 20) report.disagreements.push({ source, document: text.length > 400 ? text.slice(0, 400) + '…' : text, runtime: sa, module: sb, runtimeIsValid: aIs, moduleIsValid: bIs })
|
|
340
|
+
else report.disagreements.push(null)
|
|
341
|
+
}
|
|
342
|
+
report.disagreements = report.disagreements.filter(Boolean).concat(report.documents - report.agree > 20 ? [{ more: report.documents - report.agree - 20 }] : [])
|
|
343
|
+
return report
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function renderReport (reports, { json = false } = {}) {
|
|
347
|
+
if (json) return JSON.stringify({ ok: reports.every(ok), schemas: reports }, null, 2)
|
|
348
|
+
const lines = []
|
|
349
|
+
for (const r of reports) {
|
|
350
|
+
const name = path.relative(process.cwd(), r.schema) || r.schema
|
|
351
|
+
if (r.missing) { lines.push(`${name}: no module at ${path.relative(process.cwd(), r.module)}; run ata build first`); continue }
|
|
352
|
+
if (r.error) { lines.push(`${name}: ${r.error}`); continue }
|
|
353
|
+
const dis = r.documents - r.agree
|
|
354
|
+
lines.push(`${name}: ${r.documents} documents (${r.generated} generated, ${r.samples} from samples), ${r.valid} valid by the runtime, ${r.agree} agree, ${dis} disagree${r.stale ? '; STALE: the module was built from a different schema' : ''}${r.ataVersion ? ` (module from ata ${r.ataVersion})` : ''}`)
|
|
355
|
+
for (const d of r.disagreements) {
|
|
356
|
+
if (d.more) { lines.push(` ... and ${d.more} more`); continue }
|
|
357
|
+
lines.push(` ${d.source}`)
|
|
358
|
+
lines.push(` document ${d.document}`)
|
|
359
|
+
lines.push(` runtime ${d.runtime}${d.runtimeIsValid !== (d.runtime.startsWith('valid')) ? ` (isValidObject ${d.runtimeIsValid})` : ''}`)
|
|
360
|
+
lines.push(` module ${d.module}${d.moduleIsValid !== (d.module.startsWith('valid')) ? ` (isValid ${d.moduleIsValid})` : ''}`)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const bad = reports.filter((r) => !ok(r))
|
|
364
|
+
lines.push(bad.length === 0
|
|
365
|
+
? `ata verify: ${reports.length} schema${reports.length === 1 ? '' : 's'}, every module answers as the runtime does`
|
|
366
|
+
: `ata verify: ${bad.length} of ${reports.length} schema${reports.length === 1 ? '' : 's'} failed`)
|
|
367
|
+
return lines.join('\n')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function ok (r) { return !r.missing && !r.error && !r.stale && r.documents === r.agree }
|
|
371
|
+
|
|
372
|
+
// A module given as source text (a plugin's inlined module) held against the
|
|
373
|
+
// runtime the same way: written to a temporary file, loaded, verified.
|
|
374
|
+
async function verifySource (schema, src, opts = {}) {
|
|
375
|
+
const os = require('os')
|
|
376
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ata-verify-src-'))
|
|
377
|
+
const file = path.join(dir, `m.${opts.format === 'cjs' ? 'cjs' : 'mjs'}`)
|
|
378
|
+
fs.writeFileSync(file, src)
|
|
379
|
+
try {
|
|
380
|
+
return await verifyOne({ schemaFile: opts.name || 'schema', moduleFile: file, schema, generate: opts.generate, seed: opts.seed, samples: opts.samples || [] })
|
|
381
|
+
} finally {
|
|
382
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = { verifyOne, verifySource, renderReport, generate, mutate, readSamples, ok }
|
package/lib/version.js
CHANGED
package/lite.js
CHANGED
package/lite.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.61.1",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -60,6 +60,25 @@
|
|
|
60
60
|
"import": "./compiled-verdict.mjs",
|
|
61
61
|
"require": "./compiled-verdict.js"
|
|
62
62
|
},
|
|
63
|
+
"./compiled-object": {
|
|
64
|
+
"types": "./compiled-object.d.ts",
|
|
65
|
+
"import": "./compiled-object.mjs",
|
|
66
|
+
"require": "./compiled-object.js"
|
|
67
|
+
},
|
|
68
|
+
"./compiled-plain": {
|
|
69
|
+
"types": "./compiled-plain.d.ts",
|
|
70
|
+
"import": "./compiled-plain.mjs",
|
|
71
|
+
"require": "./compiled-plain.js"
|
|
72
|
+
},
|
|
73
|
+
"./compiled-bare": {
|
|
74
|
+
"types": "./compiled-bare.d.ts",
|
|
75
|
+
"import": "./compiled-bare.mjs",
|
|
76
|
+
"require": "./compiled-bare.js"
|
|
77
|
+
},
|
|
78
|
+
"./compiled-bare-rich": {
|
|
79
|
+
"types": "./compiled-bare-rich.d.ts",
|
|
80
|
+
"default": "./lib/compiled-bare-rich.js"
|
|
81
|
+
},
|
|
63
82
|
"./lite": {
|
|
64
83
|
"types": "./lite.d.ts",
|
|
65
84
|
"import": "./lite.mjs",
|
|
@@ -88,7 +107,13 @@
|
|
|
88
107
|
"./lib/native-load.js": "./lib/native-load.browser.js",
|
|
89
108
|
"./lib/aot.js": "./lib/aot.browser.js",
|
|
90
109
|
"./lib/buffer-gate.js": "./lib/buffer-gate.browser.js",
|
|
91
|
-
"./lib/interpreter.js": "./lib/interpreter.browser.js"
|
|
110
|
+
"./lib/interpreter.js": "./lib/interpreter.browser.js",
|
|
111
|
+
"./lib/aot-order.js": false,
|
|
112
|
+
"./lib/compiled-defaults.js": "./lib/compiled-defaults.browser.js",
|
|
113
|
+
"./lib/native-message.js": "./lib/native.browser.js",
|
|
114
|
+
"./lib/native-codes.js": "./lib/native.browser.js",
|
|
115
|
+
"./lib/email-literal.js": false,
|
|
116
|
+
"./lib/native-positions.js": "./lib/native.browser.js"
|
|
92
117
|
},
|
|
93
118
|
"scripts": {
|
|
94
119
|
"prepublishOnly": "node tests/test_pack_purity.js && ATA_ALLOW_DOC_PLACEHOLDERS=1 node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
|
|
@@ -172,6 +197,16 @@
|
|
|
172
197
|
"compiled-verdict.js",
|
|
173
198
|
"compiled-verdict.mjs",
|
|
174
199
|
"compiled-verdict.d.ts",
|
|
200
|
+
"compiled-object.js",
|
|
201
|
+
"compiled-object.mjs",
|
|
202
|
+
"compiled-object.d.ts",
|
|
203
|
+
"compiled-plain.js",
|
|
204
|
+
"compiled-plain.mjs",
|
|
205
|
+
"compiled-plain.d.ts",
|
|
206
|
+
"compiled-bare.js",
|
|
207
|
+
"compiled-bare.mjs",
|
|
208
|
+
"compiled-bare.d.ts",
|
|
209
|
+
"compiled-bare-rich.d.ts",
|
|
175
210
|
"lite.js",
|
|
176
211
|
"lite.mjs",
|
|
177
212
|
"lite.d.ts",
|
|
@@ -186,13 +221,13 @@
|
|
|
186
221
|
"codegen.d.ts"
|
|
187
222
|
],
|
|
188
223
|
"optionalDependencies": {
|
|
189
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
190
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
191
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
192
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
193
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
194
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
195
|
-
"@ata-validator/native-win32-x64": "1.
|
|
224
|
+
"@ata-validator/native-darwin-arm64": "1.61.1",
|
|
225
|
+
"@ata-validator/native-darwin-x64": "1.61.1",
|
|
226
|
+
"@ata-validator/native-linux-arm64-gnu": "1.61.1",
|
|
227
|
+
"@ata-validator/native-linux-arm64-musl": "1.61.1",
|
|
228
|
+
"@ata-validator/native-linux-x64-gnu": "1.61.1",
|
|
229
|
+
"@ata-validator/native-linux-x64-musl": "1.61.1",
|
|
230
|
+
"@ata-validator/native-win32-x64": "1.61.1"
|
|
196
231
|
},
|
|
197
232
|
"peerDependencies": {
|
|
198
233
|
"yaml": "^2.0.0"
|