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.
@@ -0,0 +1,612 @@
1
+ 'use strict'
2
+ // `ata build --target bun`: the schema as a TypeScript type, written beside
3
+ // the plain module for executables built with `bun build --compile --aot`.
4
+ //
5
+ // The type carries the shape of the schema (the JSON kind, properties and
6
+ // which are required, items and fixed tuples, literals, local definitions as
7
+ // named types) and nothing a TypeScript type cannot say. The module written
8
+ // with it answers every check through the plain module, so the answers and
9
+ // the errors are the module's; the type is for the program that imports it.
10
+ // `parse(text)` declares the type where it parses (`const d: T =
11
+ // JSON.parse(text)`), the form the compiler's documentation gives for a parse
12
+ // that makes objects with the layout of `T`; measured on the pull request
13
+ // build of 2026-10-25, that changed nothing on a ten-item order (parse 564 to
14
+ // 574 ns against 570 to 578 for a plain parse and the module, reading the
15
+ // object after it 593 to 595 against 587 to 591), so the file is a type for
16
+ // the program and nothing faster.
17
+ //
18
+ // With `tags: true`, the type is written with Bun's `Schema` tags
19
+ // (`Schema.Int`, `Schema.MinLength<n>`, `Schema.Strict`, ...) and the schema
20
+ // is split exactly in two: the keywords the tagged type carries, and a
21
+ // residual schema with every other keyword, kept inside the structure needed
22
+ // to reach it, for a checker that reads the tags. Bun's `validate<T>` and
23
+ // `JSON.parse<T>` did that in the compiler's pull request until 2026-10-25,
24
+ // when they left it to return as a change of their own; nothing in ata
25
+ // writes the tagged form until then. The split keeps the groups that see
26
+ // each other whole in the residual: `if`/`then`/`else`; `unevaluatedProperties`
27
+ // and `unevaluatedItems` with every sibling of theirs; `additionalProperties`
28
+ // with `patternProperties`; the `contains` family; `anyOf` whenever one
29
+ // branch has a residual, since a document may satisfy one branch's type and
30
+ // another's residual and neither branch.
31
+ //
32
+ // A type is built as six sets, one per JSON kind, each a list of alternatives
33
+ // of that kind. A union adds the lists; an intersection pairs them kind by
34
+ // kind, so `{ foo: string } & unknown[]` is never written: an object rule and
35
+ // an array rule meet in no value.
36
+
37
+ // Annotations: no effect on a verdict, carried by nothing here.
38
+ const IGNORED = new Set([
39
+ '$schema', '$id', '$comment', '$anchor', '$vocabulary', 'title', 'description',
40
+ 'examples', 'default', 'deprecated', 'readOnly', 'writeOnly', '$defs', 'definitions',
41
+ 'contentMediaType', 'contentEncoding', 'contentSchema'
42
+ ])
43
+ const STRING_KEYS = ['minLength', 'maxLength', 'pattern']
44
+ const NUMBER_KEYS = ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf']
45
+ const OBJECT_KEYS = ['properties', 'required', 'additionalProperties']
46
+ const ARRAY_KEYS = ['items', 'prefixItems', 'minItems', 'maxItems']
47
+ const CONSTRAINT_KEYS = [...STRING_KEYS, ...NUMBER_KEYS, ...OBJECT_KEYS, ...ARRAY_KEYS]
48
+ // Keywords the type reads. Everything else at a node goes to the residual.
49
+ const TYPED = new Set(['type', 'enum', 'const', 'anyOf', 'allOf', '$ref', ...CONSTRAINT_KEYS])
50
+ // Keywords that take the whole node with them.
51
+ const WHOLE_NODE = new Set(['unevaluatedProperties', 'unevaluatedItems', '$dynamicRef', '$dynamicAnchor', '$recursiveRef', '$recursiveAnchor'])
52
+ const KINDS = ['string', 'number', 'boolean', 'null', 'object', 'array']
53
+ // Formats ata decides with one regular expression and nothing else: the type
54
+ // carries them as that expression, so Bun's check and ata's are the same
55
+ // test. The others (email, date-time, uri, hostname, ipv6, uuid, date, time)
56
+ // are hand-written passes and stay in the residual.
57
+ function formatPattern (name) {
58
+ const f = require('./formats')
59
+ if (name === 'ipv4') return f.IPV4.source
60
+ if (name === 'duration') return f.DURATION_RE ? f.DURATION_RE.source : null
61
+ return null
62
+ }
63
+ const UNIVERSAL = { string: 'string', number: 'number', boolean: 'boolean', null: 'null', object: 'Record<string, unknown>', array: 'unknown[]' }
64
+
65
+ class Declined extends Error {
66
+ constructor (reason, pointer) {
67
+ super(`${reason} at ${pointer}`)
68
+ this.reason = reason
69
+ this.pointer = pointer
70
+ }
71
+ }
72
+
73
+ function fail (reason, ptr) { throw new Declined(reason, ptr) }
74
+
75
+ // The six sets. `alias` is a named type standing for a whole definition; it
76
+ // joins a union as a name and declines an intersection, whose members it
77
+ // would have to be distributed over.
78
+ function empty () { return { string: [], number: [], boolean: [], null: [], object: [], array: [], alias: [] } }
79
+ function universal () { const t = empty(); for (const k of KINDS) t[k].push(UNIVERSAL[k]); return t }
80
+ function single (kind, alt) { const t = empty(); t[kind].push(alt); return t }
81
+ function union (a, b) { const t = empty(); for (const k of [...KINDS, 'alias']) t[k] = dedupe([...a[k], ...b[k]]); return t }
82
+ function dedupe (xs) { return xs.filter((x, i) => xs.indexOf(x) === i) }
83
+ function isUniversal (t) { return t.alias.length === 0 && KINDS.every((k) => t[k].length === 1 && t[k][0] === UNIVERSAL[k]) }
84
+
85
+ // Intersection, or null where the two cannot be written as one type; the
86
+ // caller then keeps one side in the residual.
87
+ function intersect (a, b) {
88
+ if (a.alias.length || b.alias.length) return null
89
+ const t = empty()
90
+ for (const k of KINDS) {
91
+ for (const x of a[k]) {
92
+ for (const y of b[k]) {
93
+ const z = meet(k, x, y)
94
+ if (z === undefined) return null
95
+ if (z !== null) t[k].push(z)
96
+ }
97
+ }
98
+ t[k] = dedupe(t[k])
99
+ }
100
+ return t
101
+ }
102
+
103
+ // Two alternatives of one kind: the alternative for both, null when no value
104
+ // satisfies both, undefined when the pair cannot be written.
105
+ function meet (kind, x, y) {
106
+ if (x === UNIVERSAL[kind]) return y
107
+ if (y === UNIVERSAL[kind]) return x
108
+ if (x === y) return x
109
+ const xl = isLiteral(x), yl = isLiteral(y)
110
+ if (xl && yl) return null
111
+ if (xl || yl) return undefined
112
+ switch (kind) {
113
+ case 'string': return `${x} & ${y.replace(/^string & /, '')}`
114
+ case 'number': return `${x} & ${y.replace(/^number & /, '')}`
115
+ case 'object':
116
+ // Strict on one member of an intersection would have to see the
117
+ // others' properties; whether Bun does is not pinned down.
118
+ if (/Schema\.Strict\b/.test(x) || /Schema\.Strict\b/.test(y)) return undefined
119
+ return `${x} & ${y}`
120
+ default: return undefined
121
+ }
122
+ }
123
+
124
+ // Whether an element type needs parentheses before `[]`: a union, an
125
+ // intersection or an `&` tag at its top level. An object literal, a tuple, a
126
+ // name or a literal does not.
127
+ function needsParens (t) {
128
+ let depth = 0
129
+ let q = null
130
+ for (let i = 0; i < t.length; i++) {
131
+ const c = t[i]
132
+ if (q) { if (c === '\\') i++; else if (c === q) q = null; continue }
133
+ if (c === '"' || c === "'") q = c
134
+ else if (c === '{' || c === '[' || c === '<' || c === '(') depth++
135
+ else if (c === '}' || c === ']' || c === '>' || c === ')') depth--
136
+ else if (depth === 0 && (c === '|' || c === '&')) return true
137
+ }
138
+ return false
139
+ }
140
+
141
+ function isLiteral (alt) { return /^(".*"|-?[0-9.eE+-]+|true|false|null)$/.test(alt) }
142
+
143
+ function render (t) {
144
+ const alts = [...KINDS.flatMap((k) => t[k]), ...t.alias]
145
+ if (alts.length === 0) return 'never'
146
+ if (isUniversal(t)) return 'unknown'
147
+ if (alts.length === 1) return alts[0]
148
+ return alts.map((a) => /[&|]/.test(a) ? `(${a})` : a).join(' | ')
149
+ }
150
+
151
+ function literalOf (v) {
152
+ if (typeof v === 'string') return single('string', JSON.stringify(v))
153
+ if (typeof v === 'number' && Number.isFinite(v)) return single('number', String(v))
154
+ if (typeof v === 'boolean') return single('boolean', String(v))
155
+ if (v === null) return single('null', 'null')
156
+ return null
157
+ }
158
+
159
+ function finite (v) { return typeof v === 'number' && Number.isFinite(v) }
160
+
161
+ // Bun's Pattern runs the expression without the unicode flag; JSON Schema's
162
+ // patterns are read with it. The two agree on what they accept unless an atom
163
+ // that can match half of a surrogate pair is counted: `^.{2}$` accepts one
164
+ // astral character without the flag and refuses it with it. The expressions
165
+ // carried are the ones where no such atom is counted: ASCII only, no escapes
166
+ // whose meaning depends on the flag, and `.`, a negated class, `\W`, `\S`
167
+ // and `\D` only under a greedy `+` or `*`. Returns null when the pattern is
168
+ // carried, else what rules it out.
169
+ function patternDiffers (p) {
170
+ if (!/^[\x20-\x7e]*$/.test(p)) return 'with a character outside ASCII'
171
+ for (let i = 0; i < p.length; i++) {
172
+ const c = p[i]
173
+ if (c === '\\') {
174
+ const n = p[i + 1]
175
+ if (n === undefined) return 'ending in a backslash'
176
+ if ('pPuxbBk'.includes(n)) return `with \\${n}`
177
+ if ('WSD'.includes(n) && !greedyUnbounded(p, i + 2)) return `with \\${n} outside a greedy + or *`
178
+ i++
179
+ continue
180
+ }
181
+ if (c === '.') { if (!greedyUnbounded(p, i + 1)) return 'with . outside a greedy + or *'; continue }
182
+ if (c === '[') {
183
+ const negated = p[i + 1] === '^'
184
+ let j = i + 1 + (negated ? 1 : 0)
185
+ if (p[j] === ']') j++
186
+ for (; j < p.length && p[j] !== ']'; j++) {
187
+ if (p[j] === '\\') {
188
+ const n = p[j + 1]
189
+ if ('pPuxWSDkbB'.includes(n || '')) return `with \\${n} in a class`
190
+ j++
191
+ }
192
+ }
193
+ if (j >= p.length) return 'with an unclosed class'
194
+ if (negated && !greedyUnbounded(p, j + 1)) return 'with a negated class outside a greedy + or *'
195
+ i = j
196
+ }
197
+ }
198
+ return null
199
+ }
200
+ function greedyUnbounded (p, i) { return (p[i] === '+' || p[i] === '*') && p[i + 1] !== '?' }
201
+
202
+ // Names a definition must not take: they would shadow what the generated
203
+ // file refers to.
204
+ const RESERVED = new Set(['Schema', 'Record', 'Array', 'ReadonlyArray', 'Partial', 'Readonly', 'Pick', 'Omit', 'Required', 'Promise', 'Date', 'Object', 'String', 'Number', 'Boolean', 'Function', 'Symbol', 'JSON', 'Bun', 'ReturnType'])
205
+
206
+ // `name` must be an identifier TypeScript accepts.
207
+ function typeName (key, taken) {
208
+ let base = key.replace(/[^A-Za-z0-9_$]/g, '_').replace(/^([0-9])/, '_$1') || 'Def'
209
+ base = base.replace(/^([a-z])/, (m) => m.toUpperCase())
210
+ if (RESERVED.has(base)) base += '_'
211
+ let name = base
212
+ for (let i = 2; taken.has(name); i++) name = base + i
213
+ taken.add(name)
214
+ return name
215
+ }
216
+
217
+ function isSchema (s) { return s === true || s === false || (typeof s === 'object' && s !== null && !Array.isArray(s)) }
218
+
219
+ // The type and the residual for `schema`. Returns { name, type, aliases,
220
+ // residual, carried }: `type` the source of the root type, `aliases` the
221
+ // [name, source] definitions it refers to, `residual` the schema of what the
222
+ // type does not check (null when the type checks everything), `carried` the
223
+ // keywords the type took, by pointer. `opts.aliasPrefix` goes in front of
224
+ // every definition's name, for a file that holds several schemas. Throws
225
+ // Declined for a schema the split cannot be made for.
226
+ function schemaToBunType (root, opts = {}) {
227
+ const taken = new Set()
228
+ const name = typeName(opts.name || 'Data', taken)
229
+ // Whether formats are asserted, as a Validator does by default; a build
230
+ // that treats them as annotations (the official suite's reading) keeps
231
+ // every format out of the type.
232
+ const assertFormat = opts.assertFormat !== false
233
+ // Whether the type carries rules as Bun's `Schema` tags, with the residual
234
+ // split. Without them the type says the shape only and `residual` is null:
235
+ // the plain module checks everything.
236
+ const tags = opts.tags === true
237
+ function tag (t, s, ptr) { if (!tags) return t; carried.push(ptr); return `${t} & Schema.${s}` }
238
+ const aliases = []
239
+ const aliasByPointer = new Map()
240
+ const defResiduals = new Map() // pointer -> residual schema or null
241
+ const carried = []
242
+ let depth = 0
243
+ // Whether a definition still being built, or the root, has a residual is
244
+ // not known while a reference to it is met. The reference is first taken as
245
+ // having none; when the definition turns out to have one, it is built a
246
+ // second time with the reference kept, so that a cycle through a residual
247
+ // stays a cycle.
248
+ let rootHasResidual = false
249
+ let rootRefSeen = false
250
+ // In draft-07 and before, keywords beside $ref are ignored.
251
+ const refOverrides = typeof root.$schema === 'string' && /draft-0[467]\b/.test(root.$schema)
252
+ const restKey = (key) => `__rest__${key}`
253
+
254
+ function resolveRef (ref, ptr) {
255
+ if (ref === '#') return { pointer: '#', schema: root, key: 'Root' }
256
+ const m = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(ref)
257
+ if (!m) return fail(`$ref ${JSON.stringify(ref)} is not a local definition`, ptr)
258
+ const key = decodeURIComponent(m[2].replace(/~1/g, '/').replace(/~0/g, '~'))
259
+ const defs = root[m[1]]
260
+ if (!defs || typeof defs !== 'object' || !Object.prototype.hasOwnProperty.call(defs, key)) return fail(`$ref ${JSON.stringify(ref)} does not resolve`, ptr)
261
+ return { pointer: `#/${m[1]}/${m[2]}`, schema: defs[key], key }
262
+ }
263
+
264
+ // The alias for a definition; its type and residual are built once.
265
+ function alias (target) {
266
+ if (aliasByPointer.has(target.pointer)) return aliasByPointer.get(target.pointer)
267
+ const n = target.pointer === '#' ? name : typeName((opts.aliasPrefix || '') + target.key, taken)
268
+ aliasByPointer.set(target.pointer, n)
269
+ if (target.pointer !== '#') {
270
+ defResiduals.set(target.pointer, null)
271
+ const r = conv(target.schema, target.pointer)
272
+ aliases.push([n, render(r.t)])
273
+ if (r.res !== null) {
274
+ defResiduals.set(target.pointer, r.res)
275
+ defResiduals.set(target.pointer, conv(target.schema, target.pointer).res)
276
+ }
277
+ }
278
+ return n
279
+ }
280
+
281
+ function stringAlt (s, ptr, res) {
282
+ let t = 'string'
283
+ if (tags && typeof s.format === 'string' && assertFormat) {
284
+ const src = formatPattern(s.format)
285
+ if (src !== null && patternDiffers(src) === null) { t = tag(t, `Pattern<${JSON.stringify(src)}>`, `${ptr}/format`); delete res.format }
286
+ }
287
+ if (s.minLength !== undefined && finite(s.minLength)) t = tag(t, `MinLength<${s.minLength}>`, `${ptr}/minLength`); else if (s.minLength !== undefined) res.minLength = s.minLength
288
+ if (s.maxLength !== undefined && finite(s.maxLength)) t = tag(t, `MaxLength<${s.maxLength}>`, `${ptr}/maxLength`); else if (s.maxLength !== undefined) res.maxLength = s.maxLength
289
+ if (s.pattern !== undefined) {
290
+ if (typeof s.pattern === 'string' && patternDiffers(s.pattern) === null) t = tag(t, `Pattern<${JSON.stringify(s.pattern)}>`, `${ptr}/pattern`); else res.pattern = s.pattern
291
+ }
292
+ return t
293
+ }
294
+
295
+ function numberAlt (s, ptr, res, integer) {
296
+ let t = integer ? tag('number', 'Int', `${ptr}/type`) : 'number'
297
+ if (s.minimum !== undefined && finite(s.minimum)) t = tag(t, `Min<${s.minimum}>`, `${ptr}/minimum`); else if (s.minimum !== undefined) res.minimum = s.minimum
298
+ if (s.maximum !== undefined && finite(s.maximum)) t = tag(t, `Max<${s.maximum}>`, `${ptr}/maximum`); else if (s.maximum !== undefined) res.maximum = s.maximum
299
+ // The draft-04 boolean forms, and anything else odd, stay in the residual.
300
+ if (s.exclusiveMinimum !== undefined && finite(s.exclusiveMinimum)) t = tag(t, `Above<${s.exclusiveMinimum}>`, `${ptr}/exclusiveMinimum`); else if (s.exclusiveMinimum !== undefined) res.exclusiveMinimum = s.exclusiveMinimum
301
+ if (s.exclusiveMaximum !== undefined && finite(s.exclusiveMaximum)) t = tag(t, `Below<${s.exclusiveMaximum}>`, `${ptr}/exclusiveMaximum`); else if (s.exclusiveMaximum !== undefined) res.exclusiveMaximum = s.exclusiveMaximum
302
+ // Floating-point multiples are decided differently by every engine.
303
+ if (s.multipleOf !== undefined && Number.isInteger(s.multipleOf) && s.multipleOf >= 1) t = tag(t, `MultipleOf<${s.multipleOf}>`, `${ptr}/multipleOf`); else if (s.multipleOf !== undefined) res.multipleOf = s.multipleOf
304
+ return t
305
+ }
306
+
307
+ // The object alternative. Property subschemas' residuals go under
308
+ // res.properties; when additionalProperties cannot be carried it goes to the
309
+ // residual with every declared property name, since it applies to the keys
310
+ // not among them.
311
+ function objectAlt (s, ptr, res, keys) {
312
+ const props = s.properties
313
+ const required = s.required
314
+ const propsOk = props === undefined || (typeof props === 'object' && props !== null && !Array.isArray(props))
315
+ const reqOk = required === undefined || (Array.isArray(required) && required.every((k) => typeof k === 'string'))
316
+ if (!propsOk || !reqOk) {
317
+ if (props !== undefined) res.properties = props
318
+ if (required !== undefined) res.required = required
319
+ if (s.additionalProperties !== undefined) res.additionalProperties = s.additionalProperties
320
+ return UNIVERSAL.object
321
+ }
322
+ const names = props ? Object.keys(props) : []
323
+ if (names.includes('__proto__')) return fail('a property named __proto__ is not carried', `${ptr}/properties`)
324
+ const req = new Set(required || [])
325
+ const members = []
326
+ const propRes = {}
327
+ for (const k of names) {
328
+ const r = conv(props[k], `${ptr}/properties/${k}`)
329
+ members.push(`${JSON.stringify(k)}${req.has(k) ? '' : '?'}: ${render(r.t)}`)
330
+ if (r.res !== null) propRes[k] = r.res
331
+ }
332
+ for (const k of req) if (!names.includes(k)) members.push(`${JSON.stringify(k)}: unknown`)
333
+ if (props !== undefined) carried.push(`${ptr}/properties`)
334
+ if (required !== undefined) carried.push(`${ptr}/required`)
335
+ const ap = s.additionalProperties
336
+ const apCarried = ap === undefined || ap === true || (ap === false && !keys.includes('patternProperties'))
337
+ let t = members.length ? `{ ${members.join('; ')} }` : UNIVERSAL.object
338
+ if (apCarried) {
339
+ if (ap === false && tags) t = tag(members.length ? t : '{}', 'Strict', `${ptr}/additionalProperties`)
340
+ if (Object.keys(propRes).length) res.properties = propRes
341
+ } else {
342
+ // The residual needs the declared names to know which keys are additional.
343
+ res.additionalProperties = ap
344
+ const all = {}
345
+ for (const k of names) all[k] = Object.prototype.hasOwnProperty.call(propRes, k) ? propRes[k] : true
346
+ if (names.length) res.properties = all
347
+ }
348
+ return t
349
+ }
350
+
351
+ function arrayAlt (s, ptr, res) {
352
+ let t
353
+ if (Array.isArray(s.items)) {
354
+ // The draft-07 tuple form; its keywords stay together in the residual.
355
+ for (const k of [...ARRAY_KEYS, 'additionalItems']) if (s[k] !== undefined) res[k] = s[k]
356
+ return UNIVERSAL.array
357
+ }
358
+ if (s.prefixItems !== undefined) {
359
+ // Bun checks a tuple of fixed length only; the schema allows a shorter
360
+ // array unless minItems says otherwise, and a longer one unless items is
361
+ // false. The tuple is carried when both agree with it; otherwise the
362
+ // array keywords stay together in the residual.
363
+ const fixed = Array.isArray(s.prefixItems) && s.items === false && s.minItems === s.prefixItems.length && (s.maxItems === undefined || s.maxItems >= s.prefixItems.length)
364
+ if (!fixed) {
365
+ for (const k of ARRAY_KEYS) if (s[k] !== undefined) res[k] = s[k]
366
+ return UNIVERSAL.array
367
+ }
368
+ const parts = []
369
+ const prefRes = []
370
+ let anyRes = false
371
+ s.prefixItems.forEach((p, i) => { const r = conv(p, `${ptr}/prefixItems/${i}`); parts.push(render(r.t)); prefRes.push(r.res === null ? true : r.res); if (r.res !== null) anyRes = true })
372
+ if (anyRes) res.prefixItems = prefRes
373
+ carried.push(`${ptr}/prefixItems`, `${ptr}/items`, `${ptr}/minItems`)
374
+ if (s.maxItems !== undefined) carried.push(`${ptr}/maxItems`)
375
+ return `[${parts.join(', ')}]`
376
+ }
377
+ if (s.items === undefined || s.items === true) t = UNIVERSAL.array
378
+ else if (s.items === false) { t = '[]'; carried.push(`${ptr}/items`) } else {
379
+ const r = conv(s.items, `${ptr}/items`)
380
+ const it = render(r.t)
381
+ t = needsParens(it) ? `(${it})[]` : `${it}[]`
382
+ if (r.res !== null) res.items = r.res
383
+ carried.push(`${ptr}/items`)
384
+ }
385
+ if (s.minItems !== undefined && finite(s.minItems)) t = tag(t, `MinItems<${s.minItems}>`, `${ptr}/minItems`); else if (s.minItems !== undefined) res.minItems = s.minItems
386
+ if (s.maxItems !== undefined && finite(s.maxItems)) t = tag(t, `MaxItems<${s.maxItems}>`, `${ptr}/maxItems`); else if (s.maxItems !== undefined) res.maxItems = s.maxItems
387
+ return t
388
+ }
389
+
390
+ function hasAny (s, keys) { return keys.some((k) => s[k] !== undefined) }
391
+ function keysOf (kind) { return kind === 'string' ? STRING_KEYS : kind === 'number' || kind === 'integer' ? NUMBER_KEYS : kind === 'object' ? OBJECT_KEYS : kind === 'array' ? ARRAY_KEYS : [] }
392
+
393
+ // The alternatives for one named kind, with the rules of that kind.
394
+ function kindAlt (kind, s, ptr, res, keys) {
395
+ switch (kind) {
396
+ case 'string': return single('string', stringAlt(s, ptr, res))
397
+ case 'number': return single('number', numberAlt(s, ptr, res, false))
398
+ case 'integer': return single('number', numberAlt(s, ptr, res, true))
399
+ case 'boolean': return single('boolean', 'boolean')
400
+ case 'null': return single('null', 'null')
401
+ case 'object': return single('object', objectAlt(s, ptr, res, keys))
402
+ case 'array': return single('array', arrayAlt(s, ptr, res))
403
+ default: return null
404
+ }
405
+ }
406
+
407
+ // Everything at this node into the residual, the type `unknown`.
408
+ function whole (s) { const res = {}; for (const k of Object.keys(s)) if (!IGNORED.has(k)) res[k] = s[k]; return { t: universal(), res: Object.keys(res).length ? res : null } }
409
+
410
+ function conv (s, ptr) {
411
+ if (s === true) return { t: universal(), res: null }
412
+ if (s === false) return { t: empty(), res: null }
413
+ if (!isSchema(s)) return fail('a schema must be an object or a boolean', ptr)
414
+ if (++depth > 200) return fail('the schema nests too deep', ptr)
415
+ try {
416
+ const keys = Object.keys(s).filter((k) => !IGNORED.has(k))
417
+ if (keys.length === 0) return { t: universal(), res: null }
418
+ for (const k of keys) if (WHOLE_NODE.has(k)) return k.startsWith('$') ? fail(`keyword ${JSON.stringify(k)} is not carried`, ptr) : whole(s)
419
+ const res = {}
420
+ for (const k of keys) if (!TYPED.has(k)) res[k] = s[k]
421
+ let t = universal()
422
+
423
+ if (s.$ref !== undefined) {
424
+ if (typeof s.$ref !== 'string') return fail('$ref must be a string', ptr)
425
+ const target = resolveRef(s.$ref, ptr)
426
+ const n = alias(target)
427
+ const aliasT = empty(); aliasT.alias.push(n)
428
+ carried.push(`${ptr}/$ref`)
429
+ if (target.pointer === '#') rootRefSeen = true
430
+ const defRes = target.pointer === '#' ? (rootHasResidual ? { $ref: '#' } : null) : defResiduals.get(target.pointer)
431
+ // The residual's reference goes to the definition's residual, kept
432
+ // under its own name beside the original definitions.
433
+ if (defRes !== null && defRes !== undefined) res.$ref = target.pointer === '#' ? '#' : `#/${target.pointer.split('/')[1]}/${restKey(target.key)}`
434
+ if (refOverrides) { for (const k of keys) if (k !== '$ref') delete res[k] }
435
+ if (keys.length === 1 || refOverrides) return { t: aliasT, res: Object.keys(res).length ? res : null }
436
+ // Siblings beside $ref are conjunctive. The alias cannot be
437
+ // intersected, so the siblings' rules stay in the residual and the
438
+ // type is the alias.
439
+ for (const k of keys) if (k !== '$ref' && TYPED.has(k)) res[k] = s[k]
440
+ return { t: aliasT, res }
441
+ }
442
+
443
+ if (s.const !== undefined || s.enum !== undefined) {
444
+ let lit = null
445
+ if (s.const !== undefined && s.enum === undefined) lit = literalOf(s.const)
446
+ else if (s.enum !== undefined && s.const === undefined && Array.isArray(s.enum) && s.enum.length > 0) {
447
+ lit = empty()
448
+ for (const v of s.enum) { const l = literalOf(v); if (l === null) { lit = null; break } lit = union(lit, l) }
449
+ }
450
+ if (lit === null) {
451
+ if (s.const !== undefined) res.const = s.const
452
+ if (s.enum !== undefined) res.enum = s.enum
453
+ } else {
454
+ t = lit
455
+ carried.push(s.const !== undefined ? `${ptr}/const` : `${ptr}/enum`)
456
+ }
457
+ // The literal says everything about the value; the other rules at this
458
+ // node would have to be checked against it, which the residual does.
459
+ for (const k of ['type', ...CONSTRAINT_KEYS]) if (s[k] !== undefined) res[k] = s[k]
460
+ return finish(t, res, s, ptr)
461
+ }
462
+
463
+ if (s.type !== undefined) {
464
+ const types = Array.isArray(s.type) ? s.type : [s.type]
465
+ const known = types.every((k) => KINDS.includes(k) || k === 'integer')
466
+ if (types.length === 0 || !known) {
467
+ for (const k of ['type', ...CONSTRAINT_KEYS]) if (s[k] !== undefined) res[k] = s[k]
468
+ return finish(universal(), res, s, ptr)
469
+ }
470
+ carried.push(`${ptr}/type`)
471
+ t = empty()
472
+ for (const k of types) t = union(t, kindAlt(k, s, ptr, res, keys))
473
+ // A rule for a kind the type does not name binds nothing; it is kept
474
+ // in the residual rather than dropped, so nothing is ever lost.
475
+ const allowed = new Set(types.flatMap(keysOf))
476
+ for (const k of CONSTRAINT_KEYS) if (s[k] !== undefined && !allowed.has(k)) res[k] = s[k]
477
+ } else if (hasAny(s, CONSTRAINT_KEYS) || (tags && typeof s.format === 'string' && assertFormat && formatPattern(s.format) !== null)) {
478
+ // A rule without a type binds only the values of its kind: {minLength:
479
+ // 1} accepts every number, object and array. The six kinds, each with
480
+ // the rules of its own kind, say exactly that.
481
+ t = empty()
482
+ for (const k of KINDS) t = union(t, hasAny(s, keysOf(k)) || (tags && k === 'string' && typeof s.format === 'string' && assertFormat && formatPattern(s.format) !== null) ? kindAlt(k, s, ptr, res, keys) : single(k, UNIVERSAL[k]))
483
+ }
484
+ return finish(t, res, s, ptr)
485
+ } finally {
486
+ depth--
487
+ }
488
+ }
489
+
490
+ // anyOf and allOf on top of the node's own type, then the residual.
491
+ function finish (t, res, s, ptr) {
492
+ if (s.allOf !== undefined) {
493
+ if (Array.isArray(s.allOf) && s.allOf.length > 0) {
494
+ const rest = []
495
+ s.allOf.forEach((b, i) => {
496
+ const r = conv(b, `${ptr}/allOf/${i}`)
497
+ const m = intersect(t, r.t)
498
+ if (m === null) { rest.push(b) } else { t = m; if (r.res !== null) rest.push(r.res) }
499
+ })
500
+ if (rest.length) res.allOf = rest
501
+ carried.push(`${ptr}/allOf`)
502
+ } else res.allOf = s.allOf
503
+ }
504
+ if (s.anyOf !== undefined) {
505
+ if (Array.isArray(s.anyOf) && s.anyOf.length > 0) {
506
+ let u = empty()
507
+ let clean = true
508
+ for (let i = 0; i < s.anyOf.length; i++) {
509
+ const r = conv(s.anyOf[i], `${ptr}/anyOf/${i}`)
510
+ // Without tags the type is a shape and the union of the branches'
511
+ // shapes is it; with them a branch's residual breaks the split.
512
+ if (tags && r.res !== null) { clean = false; break }
513
+ u = union(u, r.t)
514
+ }
515
+ const m = clean ? intersect(t, u) : null
516
+ if (m !== null) { t = m; carried.push(`${ptr}/anyOf`) } else res.anyOf = s.anyOf
517
+ } else res.anyOf = s.anyOf
518
+ }
519
+ return { t, res: Object.keys(res).length ? res : null }
520
+ }
521
+
522
+ aliasByPointer.set('#', name)
523
+ let r = conv(root, '#')
524
+ if (tags && r.res !== null && rootRefSeen) { rootHasResidual = true; r = conv(root, '#') }
525
+ let residual = tags ? r.res : null
526
+ if (residual !== null) {
527
+ // Subschemas the residual copies as they are may refer to any definition,
528
+ // so the definitions travel whole; the residuals of definitions the split
529
+ // refers to travel beside them under their own names.
530
+ const text = JSON.stringify(residual)
531
+ if (/"\$ref":"#"/.test(text) && !rootHasResidual) return fail('a rule the type cannot carry refers to the root', '#')
532
+ for (const bucket of ['$defs', 'definitions']) {
533
+ if (root[bucket] && typeof root[bucket] === 'object' && text.includes(`"#/${bucket}/`)) residual[bucket] = { ...root[bucket] }
534
+ }
535
+ for (const [pointer, v] of defResiduals.entries()) {
536
+ if (v === null) continue
537
+ const m = /^#\/(\$defs|definitions)\/(.+)$/.exec(pointer)
538
+ residual[m[1]] = residual[m[1]] || {}
539
+ residual[m[1]][restKey(decodeURIComponent(m[2].replace(/~1/g, '/').replace(/~0/g, '~')))] = v
540
+ }
541
+ if (typeof root.$schema === 'string') residual = { $schema: root.$schema, ...residual }
542
+ }
543
+ if (isUniversal(r.t)) return fail('the type carries nothing of the schema', '#')
544
+ return { name, type: render(r.t), aliases, residual, carried }
545
+ }
546
+
547
+ // The TypeScript module `ata build --target bun` writes beside the plain one:
548
+ // the type, and the module's functions with the type on them. `parse`
549
+ // declares the type where it parses, which is what the compiler reads to make
550
+ // the objects with that layout; the module then checks the content.
551
+ function toBunModule ({ schemaFile, moduleImport, typeName: tn, typeSource, aliases }) {
552
+ const lines = []
553
+ lines.push(`// Generated by ata build --target bun from ${schemaFile}. Do not edit.`)
554
+ lines.push('//')
555
+ lines.push('// For executables built with `bun build --compile --aot`: the schema as a type')
556
+ lines.push(`// for the program, with the plain module (${moduleImport}) answering every`)
557
+ lines.push('// check, so the answers and the errors are the module\'s. `parse(text)` declares')
558
+ lines.push('// the type where it parses and the module checks the content; it is not faster')
559
+ lines.push('// than a plain parse and the module. Everywhere else, including plain `bun run`,')
560
+ lines.push('// the plain module does the same without this file.')
561
+ lines.push(`import * as _m from ${JSON.stringify(moduleImport)}`)
562
+ lines.push('')
563
+ for (const [n, src] of aliases) lines.push(`type ${n} = ${src}`)
564
+ lines.push(`export type ${tn} = ${typeSource}`)
565
+ lines.push('')
566
+ lines.push(`export function isValid (d: unknown): d is ${tn} { return _m.isValid(d) }`)
567
+ lines.push('export function validate (d: unknown) { return _m.validate(d) }')
568
+ lines.push('// The parsed document when the text is JSON and the document is valid, else null.')
569
+ lines.push(`export function parse (text: string): ${tn} | null {`)
570
+ lines.push(' try {')
571
+ lines.push(` const d: ${tn} = JSON.parse(text)`)
572
+ lines.push(' return _m.isValid(d) ? d : null')
573
+ lines.push(' } catch { return null }')
574
+ lines.push('}')
575
+ lines.push('')
576
+ return lines.join('\n')
577
+ }
578
+
579
+ // Writes the Bun target for one compiled module: the `.bun.ts` beside it.
580
+ // Returns what was written; throws Declined when no type can be written (a
581
+ // remote $ref, a dynamic reference, a schema the type says nothing about).
582
+ function writeBunTarget ({ schema, input, output, typeName: tn }) {
583
+ const fs = require('fs')
584
+ const path = require('path')
585
+ const mapped = schemaToBunType(schema, { name: tn })
586
+ const ext = path.extname(output)
587
+ const bunPath = output.slice(0, output.length - ext.length) + '.bun.ts'
588
+ fs.writeFileSync(bunPath, toBunModule({
589
+ schemaFile: path.relative(process.cwd(), input) || input,
590
+ moduleImport: './' + path.basename(output),
591
+ typeName: mapped.name,
592
+ typeSource: mapped.type,
593
+ aliases: mapped.aliases,
594
+ }))
595
+ return { bunPath, definitions: mapped.aliases.length, carried: mapped.carried.length }
596
+ }
597
+
598
+ // The validation keywords in a residual of the tagged split, for a report.
599
+ function countKeywords (s) {
600
+ if (typeof s !== 'object' || s === null) return 0
601
+ if (Array.isArray(s)) return s.reduce((n, x) => n + countKeywords(x), 0)
602
+ let n = 0
603
+ for (const k of Object.keys(s)) {
604
+ if (IGNORED.has(k) && k !== '$defs' && k !== 'definitions') continue
605
+ if (['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas'].includes(k)) { for (const v of Object.values(s[k] || {})) n += countKeywords(v); continue }
606
+ if (['items', 'prefixItems', 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'contains', 'additionalProperties', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems'].includes(k)) { n += (typeof s[k] === 'object' ? countKeywords(s[k]) : 0) + (['items', 'prefixItems', 'allOf', 'anyOf', 'properties'].includes(k) ? 0 : 1); continue }
607
+ n++
608
+ }
609
+ return n
610
+ }
611
+
612
+ module.exports = { schemaToBunType, toBunModule, writeBunTarget, countKeywords, Declined }
@@ -325,8 +325,14 @@ function installCodegenPaths (ctx) {
325
325
  // second time and built a second rejection around it.
326
326
  ctx.rejectBase = onReject;
327
327
  // The rich combined function, for a validator whose errors are being read
328
- // with richErrors on. See _ensureCompiled.
329
- if (!preprocess) {
328
+ // with richErrors on. See _ensureCompiled. The builders read the document
329
+ // validate() has already passed through the preprocess pass (defaults,
330
+ // coercion, removal edit it in place), so they hold with one too: a schema
331
+ // with a single `default`, which ata fills without being asked, used to
332
+ // keep its errors on the enrich-afterwards path, 666 against 313 ns for
333
+ // one `required` error on a four-field user. Only the one-pass swap below,
334
+ // which replaces validate() itself, needs the document unprocessed.
335
+ {
330
336
  ctx.richBuilder = richBuilder;
331
337
  // The plain combined function stands in where no rich one is built
332
338
  // (richErrors off, or a schema source to frame errors from): its errors
@@ -343,7 +349,7 @@ function installCodegenPaths (ctx) {
343
349
  ctx.isCombined = (fn) => fn !== null && fn === _safeCombined;
344
350
  // validate() as the combined function in its result shape: built and
345
351
  // probed here, null when it declines or throws on the probe.
346
- ctx.onePassOf = (empty, fallback) => {
352
+ ctx.onePassOf = preprocess ? null : (empty, fallback) => {
347
353
  // A collapsed branch error is built in its final plain shape here
348
354
  // (no ordering key in the runtime shape); its branch errors are put
349
355
  // in order with the rest.
@@ -114,7 +114,7 @@ function srX (S, e, srt) {
114
114
  // the standalone modules carry). The prototype test itself stays inline in
115
115
  // the generated code, read once per object: a shared helper reading the
116
116
  // prototype of every object it was asked about was megamorphic.
117
- const _PN = ({}).__proto__ === Object.prototype // eslint-disable-line no-proto
117
+ const _PN = typeof Bun === 'undefined' && ({}).__proto__ === Object.prototype // eslint-disable-line no-proto -- JavaScriptCore goes through Object.getPrototypeOf
118
118
  const _OP = Object.prototype
119
119
  const _gp = Object.getPrototypeOf
120
120
  const _hop = Object.prototype.hasOwnProperty
package/lib/formats.js CHANGED
@@ -538,4 +538,4 @@ function email(s) {
538
538
  const DURATION_RE = /^P(?:\d+W|(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)(?:T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))?|T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))$/;
539
539
  function duration (s) { return DURATION_RE.test(s); }
540
540
 
541
- module.exports = { IPV4, IPV6_FULL, ipv4Range, URI_FAST, date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uriWalk: _uriWalk, uuid, time, noReserved, jsonPointer, relativeJsonPointer, uriTemplate, email, duration, iri, iriReference, idnEmail, DURATION_RE, URI_TEMPLATE_EXPR };
541
+ module.exports = { IPV4, DURATION_RE, IPV6_FULL, ipv4Range, URI_FAST, date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uriWalk: _uriWalk, uuid, time, noReserved, jsonPointer, relativeJsonPointer, uriTemplate, email, duration, iri, iriReference, idnEmail, DURATION_RE, URI_TEMPLATE_EXPR };