@ata-project/valibot 0.1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/index.d.ts +71 -0
- package/index.js +317 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mert Can Altin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @ata-project/valibot
|
|
2
|
+
|
|
3
|
+
Run a valibot schema on the [ata engine](https://github.com/ata-core/ata-validator).
|
|
4
|
+
The schema stays valibot, the answers always match valibot's own, and the
|
|
5
|
+
verdicts arrive in nanoseconds.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @ata-project/valibot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
valibot and ata-validator are installed alongside; valibot is a peer.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import * as v from 'valibot'
|
|
15
|
+
import { compile } from '@ata-project/valibot'
|
|
16
|
+
|
|
17
|
+
const user = v.object({
|
|
18
|
+
id: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
19
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const check = compile(user)
|
|
23
|
+
check.isValid(data) // ata answers
|
|
24
|
+
check.safeParse(data) // valibot-shaped result, valibot-produced value
|
|
25
|
+
check.parse(data) // throws a real ValiError
|
|
26
|
+
check.isValidBytes(bytes) // verdict from a Buffer or JSON string
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How it stays correct
|
|
30
|
+
|
|
31
|
+
valibot ships an official JSON Schema conversion (`@valibot/to-json-schema`).
|
|
32
|
+
The bridge asks it for the input side with unrepresentable features ignored,
|
|
33
|
+
which can only make the emitted schema looser than valibot, never stricter.
|
|
34
|
+
Then the schema is classified by walking valibot's own tree before anything
|
|
35
|
+
runs:
|
|
36
|
+
|
|
37
|
+
| Mode | When | Who answers |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `ata` | the conversion is exact | ata alone |
|
|
40
|
+
| `hybrid` | provably looser (transforms, checks, formats, native types) | ata's rejections are final, valibot confirms acceptances |
|
|
41
|
+
| `valibot` | valibot accepts what the conversion rejects (fallback, records, async) or a node is unknown | valibot |
|
|
42
|
+
|
|
43
|
+
The classification is conservative: an unrecognised node lands in `valibot`
|
|
44
|
+
mode, so a new valibot feature can make the bridge slower, never wrong.
|
|
45
|
+
`compiled.engine` tells you which mode you got, `compiled.reasons` says why.
|
|
46
|
+
|
|
47
|
+
Two valibot semantics survive nowhere in JSON Schema and are handled
|
|
48
|
+
explicitly rather than papered over. valibot's `number` accepts Infinity,
|
|
49
|
+
which JSON has no word for: schemas that constrain numbers get a scan of the
|
|
50
|
+
rejected value, and a rejection that might be Infinity-caused is handed to
|
|
51
|
+
valibot for the final word. And `v.record` runs on arrays too, so records stay
|
|
52
|
+
valibot's entirely.
|
|
53
|
+
|
|
54
|
+
The package is differential-tested against valibot on 10,249 generated values
|
|
55
|
+
across all three modes, including NaN and Infinity corners, and the whole
|
|
56
|
+
suite runs a second time with code generation blocked.
|
|
57
|
+
|
|
58
|
+
## What it costs, measured
|
|
59
|
+
|
|
60
|
+
One representative API-boundary object schema (nine fields, nested arrays of
|
|
61
|
+
objects, picklist, nullable), interleaved medians of 7 rounds on an M-series
|
|
62
|
+
Mac, Node 25, valibot 1.4.2, ata-validator 1.13.2:
|
|
63
|
+
|
|
64
|
+
| | valibot `safeParse` | this package |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| accept, verdict only | 1,051 ns | **20 ns** |
|
|
67
|
+
| reject, verdict only | 1,117 ns | **83 ns** |
|
|
68
|
+
| reject, `safeParse` | 1,117 ns | **84 ns** |
|
|
69
|
+
| accept, `safeParse` | 1,051 ns | 1,062 ns |
|
|
70
|
+
|
|
71
|
+
The last row is by design, not a gap: an accepted value's output is valibot's
|
|
72
|
+
to make. Plain `v.object` strips unknown keys, defaults fill, transforms
|
|
73
|
+
rewrite, so `safeParse` hands every accepted value to valibot and returns
|
|
74
|
+
exactly what valibot returns. What this package owns is the verdict and the
|
|
75
|
+
rejection, and a rejected `safeParse` builds its issues only when somebody
|
|
76
|
+
reads them, by running valibot once at that moment.
|
|
77
|
+
|
|
78
|
+
The reject rows carry the Infinity scan described above; on schemas with no
|
|
79
|
+
number constraints they drop to the bare engine verdict.
|
|
80
|
+
|
|
81
|
+
With code generation blocked, the way a strict CSP or a locked-down edge
|
|
82
|
+
runtime blocks it: valibot stays at its usual speed, and the bridge falls back
|
|
83
|
+
to ata's interpreted engine at 624 ns for accepts and 187 ns for rejects,
|
|
84
|
+
still ahead on both.
|
|
85
|
+
|
|
86
|
+
## Raw bytes
|
|
87
|
+
|
|
88
|
+
`isValidBytes` answers from a `Buffer`, `Uint8Array` or JSON string. On an
|
|
89
|
+
`engine: 'ata'` schema with no number constraints and the native engine
|
|
90
|
+
present, the verdict comes straight off the bytes with no `JSON.parse`; other
|
|
91
|
+
schemas parse first so number semantics stay exact. Bytes that are not valid
|
|
92
|
+
JSON return `false` rather than throwing.
|
|
93
|
+
|
|
94
|
+
## Limitations, plainly
|
|
95
|
+
|
|
96
|
+
- Accepted values in `hybrid` mode and every value in `valibot` mode run
|
|
97
|
+
valibot, so those paths are valibot-speed. The win is the rejection and the
|
|
98
|
+
pure-schema case.
|
|
99
|
+
- `validate()` reports ata's errors for the schema-representable part, which
|
|
100
|
+
are JSON Schema errors, not valibot issues. Use `safeParse` when you need
|
|
101
|
+
valibot's issue shape.
|
|
102
|
+
- The classifier reads valibot's public schema tree and the peer range is
|
|
103
|
+
pinned to valibot 1; the differential suite is the tripwire for internals
|
|
104
|
+
moving.
|
|
105
|
+
- Async schemas are not supported; `safeParse` is synchronous, as in valibot.
|
|
106
|
+
|
|
107
|
+
## Standard Schema
|
|
108
|
+
|
|
109
|
+
The compiled object implements Standard Schema V1, so anything that accepts a
|
|
110
|
+
standard schema runs the fast path without knowing either library.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { BaseIssue, GenericSchema, InferInput, InferOutput, ValiError } from 'valibot'
|
|
2
|
+
|
|
3
|
+
/** How a schema was classified, and why. */
|
|
4
|
+
export interface Analysis {
|
|
5
|
+
/**
|
|
6
|
+
* 'ata': the JSON Schema conversion is exact and ata answers alone.
|
|
7
|
+
* 'hybrid': the conversion is provably looser (transforms, checks, formats,
|
|
8
|
+
* native types), so ata's rejections are final and valibot confirms the
|
|
9
|
+
* acceptances.
|
|
10
|
+
* 'valibot': a feature makes valibot accept what the converted schema
|
|
11
|
+
* rejects (fallback, records over arrays, async) or a node is unknown;
|
|
12
|
+
* valibot answers.
|
|
13
|
+
*/
|
|
14
|
+
mode: 'ata' | 'hybrid' | 'valibot'
|
|
15
|
+
/** The nodes that forced the mode, first eight. */
|
|
16
|
+
reasons: string[]
|
|
17
|
+
/** Whether parsing can return a value that differs from the input. */
|
|
18
|
+
producesValue: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SafeParseResult<S extends GenericSchema> =
|
|
22
|
+
| { typed: true; success: true; output: InferOutput<S>; issues?: undefined }
|
|
23
|
+
| { typed: boolean; success: false; output: unknown; issues: [BaseIssue<unknown>, ...BaseIssue<unknown>[]] }
|
|
24
|
+
|
|
25
|
+
export interface CompiledValibot<S extends GenericSchema> {
|
|
26
|
+
/** The verdict, at ata speed where the classification allows it. */
|
|
27
|
+
isValid(data: unknown): boolean
|
|
28
|
+
/**
|
|
29
|
+
* Verdict on raw bytes or a JSON string. An engine:'ata' schema with no
|
|
30
|
+
* number constraints is decided without JSON.parse when the native engine
|
|
31
|
+
* is present; other schemas and pure-JS installs parse first. Bytes that
|
|
32
|
+
* are not JSON return false.
|
|
33
|
+
*/
|
|
34
|
+
isValidBytes(input: Uint8Array | string): boolean
|
|
35
|
+
/** valibot-shaped result. Accepted values run valibot, so `output` is
|
|
36
|
+
* exactly what valibot returns (unknown keys stripped, defaults filled,
|
|
37
|
+
* transforms applied). Rejections are decided by ata; the issues are built
|
|
38
|
+
* on first read. */
|
|
39
|
+
safeParse(data: unknown): SafeParseResult<S>
|
|
40
|
+
/** Like safeParse but throwing the ValiError. */
|
|
41
|
+
parse(data: unknown): InferOutput<S>
|
|
42
|
+
/** ata's error report for the schema-representable part; valibot's issues
|
|
43
|
+
* where only valibot knows why. */
|
|
44
|
+
validate(data: unknown): { valid: true; data: unknown } | { valid: false; errors: unknown[] }
|
|
45
|
+
/** The emitted JSON Schema, or null in valibot mode. */
|
|
46
|
+
schema: object | null
|
|
47
|
+
/** The original valibot schema. */
|
|
48
|
+
valibotSchema: S
|
|
49
|
+
/** Which mode the classification chose. */
|
|
50
|
+
engine: 'ata' | 'hybrid' | 'valibot'
|
|
51
|
+
/** Why, first eight reasons. */
|
|
52
|
+
reasons: string[]
|
|
53
|
+
/** Standard Schema V1. */
|
|
54
|
+
'~standard': {
|
|
55
|
+
version: 1
|
|
56
|
+
vendor: 'ata-valibot'
|
|
57
|
+
validate(value: unknown):
|
|
58
|
+
| { value: InferOutput<S> }
|
|
59
|
+
| { issues: Array<{ message: string; path?: Array<PropertyKey> }> }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CompileOptions {
|
|
64
|
+
/** Options forwarded to the ata Validator (formats stay unasserted). */
|
|
65
|
+
validator?: object
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export declare function compile<S extends GenericSchema>(schema: S, opts?: CompileOptions): CompiledValibot<S>
|
|
69
|
+
export declare function analyze(schema: unknown): Analysis
|
|
70
|
+
export declare function toJSONSchema(schema: GenericSchema): object
|
|
71
|
+
export type { InferInput, InferOutput, ValiError }
|
package/index.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Run a valibot schema on the ata engine.
|
|
4
|
+
//
|
|
5
|
+
// const check = compile(schema)
|
|
6
|
+
// check.isValid(data) // ata answers
|
|
7
|
+
// check.safeParse(data) // valibot-shaped result, valibot value
|
|
8
|
+
//
|
|
9
|
+
// valibot ships an official JSON Schema conversion (@valibot/to-json-schema),
|
|
10
|
+
// and ata is a JSON Schema engine. The conversion is asked for the input side
|
|
11
|
+
// (`typeMode: 'input'`) with unrepresentable features ignored, which can only
|
|
12
|
+
// make the emitted schema looser than valibot, never stricter. That gives the
|
|
13
|
+
// same contract as the zod bridge: a schema classified `ata` is exact and the
|
|
14
|
+
// engine answers alone; a schema with residue (transforms, checks, formats,
|
|
15
|
+
// native types) runs in `hybrid` mode, where ata's rejections are final and
|
|
16
|
+
// valibot confirms the acceptances; anything that widens acceptance beyond
|
|
17
|
+
// the emitted schema (fallback) or that the classifier has never seen hands
|
|
18
|
+
// the whole schema to valibot. Unknown nodes land in `valibot` mode, so a new
|
|
19
|
+
// valibot feature can make the bridge slower, never wrong.
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// classification
|
|
23
|
+
|
|
24
|
+
// Schema node types whose emitted schema equals valibot's acceptance, given
|
|
25
|
+
// their children do.
|
|
26
|
+
const EXACT = new Set([
|
|
27
|
+
'string', 'number', 'boolean', 'null', 'literal', 'picklist', 'enum',
|
|
28
|
+
'object', 'strict_object', 'loose_object', 'array', 'tuple', 'union',
|
|
29
|
+
'variant', 'intersect', 'optional', 'nullable', 'nullish',
|
|
30
|
+
'exact_optional', 'any', 'unknown', 'lazy',
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
// Schema node types JSON Schema cannot express: the emitted input schema is
|
|
34
|
+
// `{}` for them, strictly looser, so ata's rejections stay sound and valibot
|
|
35
|
+
// owns the acceptance.
|
|
36
|
+
const RESIDUE = new Set([
|
|
37
|
+
'date', 'bigint', 'blob', 'file', 'map', 'set', 'symbol', 'undefined',
|
|
38
|
+
'void', 'nan', 'promise', 'function', 'instance', 'custom', 'never',
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
// Pipe validations the converter emits as structural JSON Schema keywords
|
|
42
|
+
// with the same semantics ata checks. Formats (email, url, uuid and friends)
|
|
43
|
+
// are deliberately NOT here: ata's format checks are not guaranteed to match
|
|
44
|
+
// valibot's character for character, so the engine is compiled with format
|
|
45
|
+
// assertion off and every format action stays residue for valibot to confirm.
|
|
46
|
+
const EXACT_ACTIONS = new Set([
|
|
47
|
+
'min_length', 'max_length', 'length', 'min_value', 'max_value',
|
|
48
|
+
'multiple_of', 'integer', 'regex', 'min_entries', 'max_entries',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const CHILD_LISTS = ['options', 'items', 'pipe']
|
|
52
|
+
const CHILD_NODES = ['item', 'wrapped', 'key', 'value', 'rest']
|
|
53
|
+
|
|
54
|
+
function analyze (schema) {
|
|
55
|
+
const seen = new Set()
|
|
56
|
+
const reasons = []
|
|
57
|
+
let mode = 'ata'
|
|
58
|
+
let producesValue = false
|
|
59
|
+
|
|
60
|
+
const escalate = (to, why) => {
|
|
61
|
+
if (to === 'valibot') mode = 'valibot'
|
|
62
|
+
else if (to === 'hybrid' && mode === 'ata') mode = 'hybrid'
|
|
63
|
+
if (reasons.length < 8) reasons.push(why)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const walk = (node) => {
|
|
67
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return
|
|
68
|
+
seen.add(node)
|
|
69
|
+
if (node.kind !== 'schema') { escalate('valibot', 'node without a schema kind'); return }
|
|
70
|
+
const type = node.type
|
|
71
|
+
|
|
72
|
+
if (node.async) escalate('valibot', 'async schema at ' + type)
|
|
73
|
+
// v.fallback() swallows failures and returns a value, so valibot accepts
|
|
74
|
+
// input the emitted schema turns away. A fast rejection would be a wrong
|
|
75
|
+
// rejection.
|
|
76
|
+
if (node.fallback !== undefined) { escalate('valibot', 'fallback at ' + type); producesValue = true }
|
|
77
|
+
if (node.default !== undefined) producesValue = true
|
|
78
|
+
|
|
79
|
+
// valibot's record runs on anything object-typed, arrays included, while
|
|
80
|
+
// the emitted schema's `object` excludes them; a fast rejection of an
|
|
81
|
+
// array would be a wrong rejection, so records stay valibot's.
|
|
82
|
+
if (type === 'record') escalate('valibot', 'record accepts arrays')
|
|
83
|
+
else if (RESIDUE.has(type)) escalate('hybrid', type)
|
|
84
|
+
else if (!EXACT.has(type)) escalate('valibot', 'unrecognised node ' + type)
|
|
85
|
+
|
|
86
|
+
if (Array.isArray(node.pipe)) {
|
|
87
|
+
// pipe[0] is the base schema and the only stage the input-side
|
|
88
|
+
// conversion emits beyond whitelisted validations. Everything after it
|
|
89
|
+
// can only tighten what valibot accepts, so residue stays sound.
|
|
90
|
+
for (let i = 1; i < node.pipe.length; i++) {
|
|
91
|
+
const item = node.pipe[i]
|
|
92
|
+
if (!item || typeof item !== 'object') continue
|
|
93
|
+
if (item.kind === 'metadata') continue
|
|
94
|
+
if (item.kind === 'validation') {
|
|
95
|
+
if (!EXACT_ACTIONS.has(item.type)) escalate('hybrid', item.type + ' at ' + type)
|
|
96
|
+
} else if (item.kind === 'transformation') {
|
|
97
|
+
escalate('hybrid', item.type + ' at ' + type)
|
|
98
|
+
producesValue = true
|
|
99
|
+
} else if (item.kind === 'schema') {
|
|
100
|
+
escalate('hybrid', 'piped schema at ' + type)
|
|
101
|
+
producesValue = true
|
|
102
|
+
} else {
|
|
103
|
+
escalate('valibot', 'unrecognised pipe item ' + String(item.kind))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (node.entries) for (const k of Object.keys(node.entries)) walk(node.entries[k])
|
|
109
|
+
for (const k of CHILD_NODES) if (node[k] && typeof node[k] === 'object') walk(node[k])
|
|
110
|
+
for (const k of CHILD_LISTS) {
|
|
111
|
+
if (k === 'pipe') continue
|
|
112
|
+
if (Array.isArray(node[k])) for (const child of node[k]) walk(child)
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(node.pipe) && node.pipe[0] && node.pipe[0] !== node) walk(node.pipe[0])
|
|
115
|
+
if (typeof node.getter === 'function') {
|
|
116
|
+
try { walk(node.getter(undefined)) } catch { escalate('valibot', 'lazy getter threw') }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
walk(schema)
|
|
121
|
+
return { mode, reasons, producesValue }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// compile
|
|
126
|
+
|
|
127
|
+
function requireValibot () {
|
|
128
|
+
// Resolved lazily from the peer so this package never pins its own copy.
|
|
129
|
+
return require('valibot')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toJSONSchema (schema) {
|
|
133
|
+
const { toJsonSchema } = require('@valibot/to-json-schema')
|
|
134
|
+
return toJsonSchema(schema, { typeMode: 'input', errorMode: 'ignore' })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// One semantic gap survives the conversion: valibot's `number` accepts
|
|
138
|
+
// Infinity and -Infinity (it only turns away NaN), while ata follows JSON,
|
|
139
|
+
// where no such values exist. Values holding them can only be built in
|
|
140
|
+
// JavaScript, never parsed from JSON, so the engine's verdict stands unless
|
|
141
|
+
// the schema constrains numbers somewhere AND the rejected value actually
|
|
142
|
+
// carries a non-finite number, in which case valibot gets the final word.
|
|
143
|
+
function schemaChecksNumbers (node, seen) {
|
|
144
|
+
if (!node || typeof node !== 'object') return false
|
|
145
|
+
if (seen.has(node)) return false
|
|
146
|
+
seen.add(node)
|
|
147
|
+
if (node.type === 'number' || (Array.isArray(node.type) && node.type.includes('number'))) return true
|
|
148
|
+
for (const k of Object.keys(node)) {
|
|
149
|
+
const child = node[k]
|
|
150
|
+
if (Array.isArray(child)) {
|
|
151
|
+
for (const it of child) if (schemaChecksNumbers(it, seen)) return true
|
|
152
|
+
} else if (schemaChecksNumbers(child, seen)) return true
|
|
153
|
+
}
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Depth-capped so cyclic input cannot loop: past the cap the scan reports
|
|
158
|
+
// "might", and the delegate below decides inside a try/catch.
|
|
159
|
+
function hasNonFinite (value, depth) {
|
|
160
|
+
if (typeof value === 'number') return value === Infinity || value === -Infinity
|
|
161
|
+
if (value === null || typeof value !== 'object') return false
|
|
162
|
+
if (depth > 256) return true
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
for (let i = 0; i < value.length; i++) if (hasNonFinite(value[i], depth + 1)) return true
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
for (const k in value) if (hasNonFinite(value[k], depth + 1)) return true
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function confirmNonFinite (v, schema, d) {
|
|
172
|
+
try {
|
|
173
|
+
return v.safeParse(schema, d).success
|
|
174
|
+
} catch {
|
|
175
|
+
// valibot could not decide (cyclic input past the scan cap); the engine's
|
|
176
|
+
// rejection stands.
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A rejection whose issues are built on first read, by running valibot once
|
|
182
|
+
// at that moment. The getter lives on the prototype: an object-literal
|
|
183
|
+
// accessor per rejection costs about a hundred nanoseconds, which is the
|
|
184
|
+
// whole budget of the fast path. The classifier proves ata only rejects what
|
|
185
|
+
// valibot rejects; were that ever broken, the getter hands back what ata saw
|
|
186
|
+
// rather than nothing, and the differential suite is the place that fails.
|
|
187
|
+
class LazyRejection {
|
|
188
|
+
constructor (schema, engine, data) {
|
|
189
|
+
this.typed = false
|
|
190
|
+
this.success = false
|
|
191
|
+
this.output = data
|
|
192
|
+
this._schema = schema
|
|
193
|
+
this._engine = engine
|
|
194
|
+
this._issues = null
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
Object.defineProperty(LazyRejection.prototype, 'issues', {
|
|
198
|
+
enumerable: true,
|
|
199
|
+
configurable: true,
|
|
200
|
+
get () {
|
|
201
|
+
if (this._issues === null) {
|
|
202
|
+
const v = requireValibot()
|
|
203
|
+
const r = v.safeParse(this._schema, this.output)
|
|
204
|
+
this._issues = r.success
|
|
205
|
+
? this._engine.validate(this.output).errors.map((e) => ({
|
|
206
|
+
kind: 'schema', type: 'ata', input: this.output, expected: null,
|
|
207
|
+
received: 'unknown', message: e.message, path: undefined,
|
|
208
|
+
}))
|
|
209
|
+
: r.issues
|
|
210
|
+
}
|
|
211
|
+
return this._issues
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
function compile (schema, opts) {
|
|
216
|
+
const options = opts || {}
|
|
217
|
+
const v = requireValibot()
|
|
218
|
+
const analysis = analyze(schema)
|
|
219
|
+
const jsonSchema = analysis.mode === 'valibot' ? null : toJSONSchema(schema)
|
|
220
|
+
const engine = jsonSchema
|
|
221
|
+
? new (require('ata-validator').Validator)(jsonSchema, { assertFormat: false, ...options.validator })
|
|
222
|
+
: null
|
|
223
|
+
const rawFast = engine ? (d) => engine.isValidObject(d) : null
|
|
224
|
+
// A rejection is final unless the value carries Infinity, which valibot
|
|
225
|
+
// accepts where JSON has no word for it. The escape only exists for
|
|
226
|
+
// schemas that constrain numbers at all, and it only ever runs on the
|
|
227
|
+
// rejected path, so JSON-borne data never pays for it.
|
|
228
|
+
const numeric = engine ? schemaChecksNumbers(jsonSchema, new Set()) : false
|
|
229
|
+
const fast = !rawFast ? null : !numeric ? rawFast
|
|
230
|
+
: (d) => rawFast(d) || (hasNonFinite(d, 0) && confirmNonFinite(v, schema, d))
|
|
231
|
+
|
|
232
|
+
let isValid
|
|
233
|
+
if (analysis.mode === 'ata') {
|
|
234
|
+
isValid = fast
|
|
235
|
+
} else if (analysis.mode === 'hybrid') {
|
|
236
|
+
// ata turning a value down is final; ata letting it through hands it to
|
|
237
|
+
// valibot for the checks and native types the JSON Schema cannot carry.
|
|
238
|
+
isValid = (d) => fast(d) && v.safeParse(schema, d).success
|
|
239
|
+
} else {
|
|
240
|
+
isValid = (d) => v.safeParse(schema, d).success
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// The parsed value is valibot's to make: plain v.object strips unknown
|
|
244
|
+
// keys, defaults fill, transforms rewrite, so an accepted value always runs
|
|
245
|
+
// valibot and comes back exactly as valibot would return it. What ata owns
|
|
246
|
+
// is the rejection: it is decided at ata speed, and the issues are built
|
|
247
|
+
// only if somebody reads them, by running valibot once at that moment.
|
|
248
|
+
const safeParse = (d) => {
|
|
249
|
+
if (fast && !fast(d)) return new LazyRejection(schema, engine, d)
|
|
250
|
+
return v.safeParse(schema, d)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Raw bytes: an engine:'ata' schema is decided without JSON.parse, straight
|
|
254
|
+
// off the buffer by the native walker when it is present. The other modes
|
|
255
|
+
// need the materialized value for valibot, and so does a pure-JS install,
|
|
256
|
+
// so they parse and take the object path. Bytes that are not JSON are a
|
|
257
|
+
// rejection, not an exception.
|
|
258
|
+
let isValidBytes
|
|
259
|
+
if (analysis.mode === 'ata' && engine && !numeric && typeof engine.isValid === 'function') {
|
|
260
|
+
isValidBytes = (input) => engine.isValid(input)
|
|
261
|
+
} else {
|
|
262
|
+
const td = new TextDecoder()
|
|
263
|
+
isValidBytes = (input) => {
|
|
264
|
+
let value
|
|
265
|
+
try {
|
|
266
|
+
value = JSON.parse(typeof input === 'string' ? input : td.decode(input))
|
|
267
|
+
} catch {
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
return isValid(value)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ata's error report for the schema-representable part; valibot's issues
|
|
275
|
+
// where only valibot knows why.
|
|
276
|
+
const validate = (d) => {
|
|
277
|
+
if (engine) {
|
|
278
|
+
const r = engine.validate(d)
|
|
279
|
+
if (!r.valid) return { valid: false, errors: r.errors }
|
|
280
|
+
}
|
|
281
|
+
if (analysis.mode !== 'ata') {
|
|
282
|
+
const vr = v.safeParse(schema, d)
|
|
283
|
+
if (!vr.success) return { valid: false, errors: vr.issues }
|
|
284
|
+
}
|
|
285
|
+
return { valid: true, data: d }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const compiled = {
|
|
289
|
+
isValid,
|
|
290
|
+
isValidBytes,
|
|
291
|
+
safeParse,
|
|
292
|
+
parse: (d) => {
|
|
293
|
+
const r = safeParse(d)
|
|
294
|
+
if (r.success) return r.output
|
|
295
|
+
throw new v.ValiError(r.issues)
|
|
296
|
+
},
|
|
297
|
+
validate,
|
|
298
|
+
schema: jsonSchema,
|
|
299
|
+
valibotSchema: schema,
|
|
300
|
+
engine: analysis.mode,
|
|
301
|
+
reasons: analysis.reasons,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
compiled['~standard'] = {
|
|
305
|
+
version: 1,
|
|
306
|
+
vendor: 'ata-valibot',
|
|
307
|
+
validate (value) {
|
|
308
|
+
const r = safeParse(value)
|
|
309
|
+
if (r.success) return { value: r.output }
|
|
310
|
+
return { issues: r.issues.map((i) => ({ message: i.message, path: i.path && i.path.map((p) => p.key) })) }
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return compiled
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = { compile, analyze, toJSONSchema }
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ata-project/valibot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run valibot schemas on the ata engine: same answers, verdicts in nanoseconds",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ata-core/ata-valibot.git"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node test.js && node --disallow-code-generation-from-strings test.js && npm run test:types",
|
|
23
|
+
"bench": "node bench.mjs",
|
|
24
|
+
"test:types": "tsc --noEmit -p tsconfig.json"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@valibot/to-json-schema": "^1.3.0",
|
|
28
|
+
"ata-validator": "^1.13.2"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"valibot": "^1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^26.5.0",
|
|
35
|
+
"typescript": "^5.6.0",
|
|
36
|
+
"valibot": "^1.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|