ata-validator 1.60.0 → 1.61.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 +107 -4
- package/bin/ata.js +84 -0
- package/build.d.ts +29 -0
- package/build.mjs +2 -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 +73 -6
- package/lib/aot-impl.js +14 -19
- package/lib/bun-target.js +612 -0
- package/lib/codegen-engine.js +9 -3
- package/lib/combined-runtime.js +1 -1
- package/lib/formats.js +1 -1
- package/lib/js-compiler-errors.js +45 -10
- package/lib/js-compiler.js +92 -13
- package/lib/public-api.js +1 -0
- package/lib/tier0.js +1 -1
- package/lib/validator-core.js +36 -3
- package/lib/verify.js +321 -0
- package/lib/version.js +1 -1
- package/lite.js +1 -0
- package/lite.mjs +1 -1
- package/package.json +10 -9
package/lib/verify.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
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
|
+
// Documents for `schema`: `count` made, half of them with one rule broken.
|
|
199
|
+
function generate (schema, count, seed) {
|
|
200
|
+
const r = rng(seed)
|
|
201
|
+
const defs = { $defs: schema && schema.$defs, definitions: schema && schema.definitions }
|
|
202
|
+
const docs = []
|
|
203
|
+
for (let i = 0; i < count; i++) {
|
|
204
|
+
let d = makeValid(schema, r, defs, 0)
|
|
205
|
+
if (i % 2 === 1 && d !== undefined) d = mutate(clone(d), r)
|
|
206
|
+
docs.push(d)
|
|
207
|
+
}
|
|
208
|
+
return docs
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The project's own documents: a JSON file is one document, a JSON Lines
|
|
212
|
+
// file one per line, a JSON array is taken as one document, not several.
|
|
213
|
+
function readSamples (paths) {
|
|
214
|
+
const docs = []
|
|
215
|
+
const files = []
|
|
216
|
+
for (const p of paths) {
|
|
217
|
+
if (!fs.existsSync(p)) continue
|
|
218
|
+
if (fs.statSync(p).isDirectory()) {
|
|
219
|
+
for (const f of fs.readdirSync(p).sort()) if (/\.(json|jsonl|ndjson)$/.test(f)) files.push(path.join(p, f))
|
|
220
|
+
} else files.push(p)
|
|
221
|
+
}
|
|
222
|
+
for (const f of files) {
|
|
223
|
+
const text = fs.readFileSync(f, 'utf8')
|
|
224
|
+
if (/\.(jsonl|ndjson)$/.test(f)) {
|
|
225
|
+
text.split('\n').forEach((line, i) => { if (line.trim()) { try { docs.push({ source: `${f}:${i + 1}`, doc: JSON.parse(line) }) } catch {} } })
|
|
226
|
+
} else {
|
|
227
|
+
try { docs.push({ source: f, doc: JSON.parse(text) }) } catch { docs.push({ source: f, doc: undefined, bad: true }) }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return docs
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function errorKey (e) { return `${e.keyword || ''}@${e.instancePath || ''}` }
|
|
234
|
+
function summarize (result) {
|
|
235
|
+
if (!result || typeof result !== 'object') return 'no result'
|
|
236
|
+
const errs = Array.isArray(result.errors) ? result.errors.map(errorKey).sort() : []
|
|
237
|
+
return `${result.valid ? 'valid' : 'invalid'} ${errs.join(' ')}`
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function loadModule (file) {
|
|
241
|
+
if (/\.cjs$/.test(file)) { delete require.cache[require.resolve(file)]; return require(file) }
|
|
242
|
+
return import(pathToFileURL(file).href + `?t=${Date.now()}`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Verifies one schema against its module. Returns a report object.
|
|
246
|
+
async function verifyOne ({ schemaFile, moduleFile, schema, generate: count = 200, seed = 1, samples = [] }) {
|
|
247
|
+
const { Validator } = require('..')
|
|
248
|
+
const { schemaHash } = require('./schema-hash')
|
|
249
|
+
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 }
|
|
250
|
+
if (!fs.existsSync(moduleFile)) { report.missing = true; return report }
|
|
251
|
+
let mod
|
|
252
|
+
try { mod = await loadModule(moduleFile) } catch (e) { report.error = `module does not load: ${e.message}`; return report }
|
|
253
|
+
if (typeof mod.isValid !== 'function' || typeof mod.validate !== 'function') { report.error = 'module exports no isValid and validate'; return report }
|
|
254
|
+
report.ataVersion = mod.ataVersion || null
|
|
255
|
+
const hash = schemaHash(schema)
|
|
256
|
+
if (mod.schemaHash !== undefined && mod.schemaHash !== hash) report.stale = true
|
|
257
|
+
let runtime
|
|
258
|
+
try { runtime = new Validator(schema, {}) } catch (e) { report.error = `schema does not compile: ${e.message}`; return report }
|
|
259
|
+
const docs = generate(schema, count, seed).map((d, i) => ({ source: `generated #${i + 1}`, doc: d }))
|
|
260
|
+
report.generated = docs.length
|
|
261
|
+
const sampleDocs = readSamples(samples)
|
|
262
|
+
report.samples = sampleDocs.length
|
|
263
|
+
for (const { source, doc, bad } of [...docs, ...sampleDocs]) {
|
|
264
|
+
if (bad || doc === undefined) continue
|
|
265
|
+
report.documents++
|
|
266
|
+
const text = JSON.stringify(doc)
|
|
267
|
+
const a = runtime.validate(JSON.parse(text))
|
|
268
|
+
const b = mod.validate(JSON.parse(text))
|
|
269
|
+
const aIs = runtime.isValidObject(JSON.parse(text))
|
|
270
|
+
const bIs = mod.isValid(JSON.parse(text))
|
|
271
|
+
if (a.valid) report.valid++
|
|
272
|
+
const sa = summarize(a), sb = summarize(b)
|
|
273
|
+
if (sa === sb && aIs === bIs && aIs === a.valid) { report.agree++; continue }
|
|
274
|
+
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 })
|
|
275
|
+
else report.disagreements.push(null)
|
|
276
|
+
}
|
|
277
|
+
report.disagreements = report.disagreements.filter(Boolean).concat(report.documents - report.agree > 20 ? [{ more: report.documents - report.agree - 20 }] : [])
|
|
278
|
+
return report
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderReport (reports, { json = false } = {}) {
|
|
282
|
+
if (json) return JSON.stringify({ ok: reports.every(ok), schemas: reports }, null, 2)
|
|
283
|
+
const lines = []
|
|
284
|
+
for (const r of reports) {
|
|
285
|
+
const name = path.relative(process.cwd(), r.schema) || r.schema
|
|
286
|
+
if (r.missing) { lines.push(`${name}: no module at ${path.relative(process.cwd(), r.module)}; run ata build first`); continue }
|
|
287
|
+
if (r.error) { lines.push(`${name}: ${r.error}`); continue }
|
|
288
|
+
const dis = r.documents - r.agree
|
|
289
|
+
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})` : ''}`)
|
|
290
|
+
for (const d of r.disagreements) {
|
|
291
|
+
if (d.more) { lines.push(` ... and ${d.more} more`); continue }
|
|
292
|
+
lines.push(` ${d.source}`)
|
|
293
|
+
lines.push(` document ${d.document}`)
|
|
294
|
+
lines.push(` runtime ${d.runtime}${d.runtimeIsValid !== (d.runtime.startsWith('valid')) ? ` (isValidObject ${d.runtimeIsValid})` : ''}`)
|
|
295
|
+
lines.push(` module ${d.module}${d.moduleIsValid !== (d.module.startsWith('valid')) ? ` (isValid ${d.moduleIsValid})` : ''}`)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const bad = reports.filter((r) => !ok(r))
|
|
299
|
+
lines.push(bad.length === 0
|
|
300
|
+
? `ata verify: ${reports.length} schema${reports.length === 1 ? '' : 's'}, every module answers as the runtime does`
|
|
301
|
+
: `ata verify: ${bad.length} of ${reports.length} schema${reports.length === 1 ? '' : 's'} failed`)
|
|
302
|
+
return lines.join('\n')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function ok (r) { return !r.missing && !r.error && !r.stale && r.documents === r.agree }
|
|
306
|
+
|
|
307
|
+
// A module given as source text (a plugin's inlined module) held against the
|
|
308
|
+
// runtime the same way: written to a temporary file, loaded, verified.
|
|
309
|
+
async function verifySource (schema, src, opts = {}) {
|
|
310
|
+
const os = require('os')
|
|
311
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ata-verify-src-'))
|
|
312
|
+
const file = path.join(dir, `m.${opts.format === 'cjs' ? 'cjs' : 'mjs'}`)
|
|
313
|
+
fs.writeFileSync(file, src)
|
|
314
|
+
try {
|
|
315
|
+
return await verifyOne({ schemaFile: opts.name || 'schema', moduleFile: file, schema, generate: opts.generate, seed: opts.seed, samples: opts.samples || [] })
|
|
316
|
+
} finally {
|
|
317
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
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.0",
|
|
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",
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
"./lib/native-load.js": "./lib/native-load.browser.js",
|
|
89
89
|
"./lib/aot.js": "./lib/aot.browser.js",
|
|
90
90
|
"./lib/buffer-gate.js": "./lib/buffer-gate.browser.js",
|
|
91
|
-
"./lib/interpreter.js": "./lib/interpreter.browser.js"
|
|
91
|
+
"./lib/interpreter.js": "./lib/interpreter.browser.js",
|
|
92
|
+
"./lib/aot-order.js": false
|
|
92
93
|
},
|
|
93
94
|
"scripts": {
|
|
94
95
|
"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",
|
|
@@ -186,13 +187,13 @@
|
|
|
186
187
|
"codegen.d.ts"
|
|
187
188
|
],
|
|
188
189
|
"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.
|
|
190
|
+
"@ata-validator/native-darwin-arm64": "1.61.0",
|
|
191
|
+
"@ata-validator/native-darwin-x64": "1.61.0",
|
|
192
|
+
"@ata-validator/native-linux-arm64-gnu": "1.61.0",
|
|
193
|
+
"@ata-validator/native-linux-arm64-musl": "1.61.0",
|
|
194
|
+
"@ata-validator/native-linux-x64-gnu": "1.61.0",
|
|
195
|
+
"@ata-validator/native-linux-x64-musl": "1.61.0",
|
|
196
|
+
"@ata-validator/native-win32-x64": "1.61.0"
|
|
196
197
|
},
|
|
197
198
|
"peerDependencies": {
|
|
198
199
|
"yaml": "^2.0.0"
|