@weipertda/sigiljs 0.0.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/package.json +46 -0
- package/src/core/assert.js +191 -0
- package/src/core/cache.js +7 -0
- package/src/core/compile.js +154 -0
- package/src/core/errors.js +30 -0
- package/src/core/normalize.js +52 -0
- package/src/core/parser.js +219 -0
- package/src/core/partial.js +63 -0
- package/src/core/realType.js +75 -0
- package/src/core/tokenizer.js +185 -0
- package/src/core/validate.js +8 -0
- package/src/core/validator.js +17 -0
- package/src/index.js +6 -0
- package/src/playground/playground.js +50 -0
- package/src/sigil.js +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [0.0.1] — 2026-03-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`realType(value, opts?)`** — cross-realm-safe runtime type detection fixing all `typeof` gaps (`null`, `array`, `nan`, `map`, `set`, `date`, `regexp`, `promise`, `asyncfunction`, `generatorfunction`, `asyncgeneratorfunction`, `weakmap`, `weakset`); extensible via hook array.
|
|
14
|
+
- **`Sigil` / `T` / `S` tagged template** — compile a schema string into a fast, memoized validator object exposing `.check()` and `.assert()`.
|
|
15
|
+
- **Schema language** — primitives, literals, unions (`|`), arrays (`[]`), optionals (`?`), and nested object schemas.
|
|
16
|
+
- **`SigilValidationError`** — structured error with `code`, `path`, `expected`, and `actual` properties.
|
|
17
|
+
- **Two-level memoization** — Sigil-level cache (identical schema strings share one object reference) and compiled-validator cache (structural AST identity).
|
|
18
|
+
- **Partial evaluation** — literal unions optimize to `Set` membership checks; pure primitive unions skip `realType` entirely; object hints pre-compute required/optional key sets.
|
|
19
|
+
- **CLI playground** — `bun run src/playground.js '<json>' '<schema>'` for shell-level validation.
|
|
20
|
+
|
|
21
|
+
### Engineering
|
|
22
|
+
- Single-source-of-truth `realType` implementation in `src/core/realType.js`.
|
|
23
|
+
- Module-scope frozen `Set` for O(1) tokenizer punctuation lookup.
|
|
24
|
+
- Array-join accumulation in tokenizer hot string loops (no `+=` reallocation).
|
|
25
|
+
- Module-scope frozen `Set` for parser primitive name lookup (not rebuilt per call).
|
|
26
|
+
- Sigil objects are `Object.freeze()`-d — immutable public API surface.
|
|
27
|
+
- `Error.captureStackTrace` on `SigilValidationError` for clean V8/Bun stack traces.
|
|
28
|
+
- Full `package.json` with `exports`, `files`, `sideEffects: false`, `engines`, `keywords`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SigilJS Contributors
|
|
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,261 @@
|
|
|
1
|
+
# SigilJS
|
|
2
|
+
|
|
3
|
+
Write types. Validate reality.
|
|
4
|
+
|
|
5
|
+
## What is SigilJS?
|
|
6
|
+
|
|
7
|
+
SigilJS is a tiny JavaScript library for describing and validating data using **sigils**.
|
|
8
|
+
|
|
9
|
+
A sigil is a small expression (type expression) that describes what your data should look like.
|
|
10
|
+
|
|
11
|
+
Sigils are compiled into fast validators, so repeated checks stay efficient.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
No TypeScript.
|
|
15
|
+
|
|
16
|
+
No dependencies.
|
|
17
|
+
|
|
18
|
+
Just JavaScript.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
|
|
26
|
+
bun add @weipertda/sigiljs
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
– or –
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
|
|
34
|
+
npm install @weipertda/sigiljs
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### Create a Sigil
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
|
|
44
|
+
import { Sigil } from "@weipertda/sigiljs"
|
|
45
|
+
|
|
46
|
+
const Email = Sigil`string`
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Validate a Value
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
|
|
54
|
+
Email.check("hello@example.com")
|
|
55
|
+
// true
|
|
56
|
+
|
|
57
|
+
Email.check(42)
|
|
58
|
+
// false
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Optional Values
|
|
63
|
+
|
|
64
|
+
Use `?` to mark optional values.
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
|
|
68
|
+
const MaybeName = Sigil`string?`
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Matches:
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
|
|
76
|
+
string
|
|
77
|
+
undefined
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### Arrays
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
|
|
87
|
+
const Tags = Sigil`string[]`
|
|
88
|
+
|
|
89
|
+
Tags.check(["js", "bun"])
|
|
90
|
+
// true
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Unions
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
|
|
100
|
+
const ID = Sigil`string | number`
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Matches either type.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Object Validation
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
|
|
112
|
+
const User = Sigil`
|
|
113
|
+
{
|
|
114
|
+
name: string
|
|
115
|
+
age?: number
|
|
116
|
+
}
|
|
117
|
+
`
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Optional properties use ` ? `.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### Nested Objects
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
|
|
129
|
+
const Order = Sigil`
|
|
130
|
+
{
|
|
131
|
+
id: string
|
|
132
|
+
customer: {
|
|
133
|
+
name: string
|
|
134
|
+
email: string
|
|
135
|
+
}
|
|
136
|
+
items: {
|
|
137
|
+
name: string
|
|
138
|
+
price: number
|
|
139
|
+
}[]
|
|
140
|
+
}
|
|
141
|
+
`
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### Runtime Type Detection
|
|
148
|
+
|
|
149
|
+
SigilJS also provides a better ` typeof `.
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
|
|
153
|
+
import { realType } from "@weipertda/sigiljs"
|
|
154
|
+
|
|
155
|
+
realType([]) // "array"
|
|
156
|
+
realType(null) // "null"
|
|
157
|
+
realType(new Map()) // "map"
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Why SigilJS?
|
|
164
|
+
|
|
165
|
+
Sigil cleanly solves four problems:
|
|
166
|
+
|
|
167
|
+
1. JavaScript's native ` typeof ` is inconsistent and too weak for real type work.
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
|
|
171
|
+
typeof []
|
|
172
|
+
// "object" 😬
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
2. TypeScript solves mostly compile-time problems, often adds friction, and disappears at runtime.
|
|
177
|
+
|
|
178
|
+
* TypeScript helps during development, but once your program runs the types are gone.
|
|
179
|
+
|
|
180
|
+
* SigilJS solves the runtime side of the problem.
|
|
181
|
+
|
|
182
|
+
* Describe your data once, then validate it anywhere.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
|
|
186
|
+
// TypeScript disappears at runtime
|
|
187
|
+
const x: string = 123;
|
|
188
|
+
|
|
189
|
+
// No runtime error
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
3. Existing runtime validation libraries are dependency-heavy, allocation-happy, or ergonomically off.
|
|
194
|
+
|
|
195
|
+
4. JavaScript lacks a native-feeling type expression system for runtime truth.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Accurate Runtime Types
|
|
200
|
+
|
|
201
|
+
Replacing the gaps in ` typeof ` — ` realType ` correctly identifies ` null `, ` NaN `, arrays, async and generator functions, maps, sets, and arbitrary custom classes through hooks.
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
|
|
205
|
+
import { realType } from '@weipertda/sigiljs';
|
|
206
|
+
|
|
207
|
+
realType('x'); // "string"
|
|
208
|
+
realType(null); // "null"
|
|
209
|
+
realType(NaN); // "nan"
|
|
210
|
+
realType([]); // "array"
|
|
211
|
+
realType(new Map()); // "map"
|
|
212
|
+
realType(async function() {}); // "asyncfunction"
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
You can even provide custom override hooks to map instances directly back to nominal strings:
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
|
|
220
|
+
realType(myThing, {
|
|
221
|
+
hooks: [ v => v instanceof MyThing ? 'mything' : null ]
|
|
222
|
+
}); // "mything"
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
SigilJS solves runtime type validation with a tiny, dependency-free, runtime-native type system.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Documentation
|
|
231
|
+
|
|
232
|
+
See the [docs/](docs/README.md) folder for full, detailed documentation __(WIP)__.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Examples
|
|
237
|
+
|
|
238
|
+
See the [examples/](examples/) folder for runnable examples __(WIP)__.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## CLI Playground
|
|
249
|
+
|
|
250
|
+
You can securely test out Sigil validator schemas against JSON inputs directly from your shell:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
|
|
254
|
+
bun run src/playground.js '{"name": "Doug"}' '{name: string, age?: number}'
|
|
255
|
+
# ✅ Validation passed
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Performance Philosophy
|
|
260
|
+
|
|
261
|
+
SigilJS embraces a Functional Core / Imperative Shell architecture. It takes your schema string, turns it into a typed token stream, drops parse grouping artifacts, flattens branches, optimizes primitive unions, and finally generates a blazingly fast validator closure mapped dynamically from the ground up to minimize allocations on the hot path. Repeated tagged template passes are thoroughly memoized.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weipertda/sigiljs",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Runtime type sigils for JavaScript",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"types",
|
|
7
|
+
"validation",
|
|
8
|
+
"runtime",
|
|
9
|
+
"schema",
|
|
10
|
+
"type-checking",
|
|
11
|
+
"zero-dependency",
|
|
12
|
+
"sigil",
|
|
13
|
+
"bun",
|
|
14
|
+
"javascript"
|
|
15
|
+
],
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Daniel Weipert",
|
|
18
|
+
"email": "weipertda@gmail.com"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/weipertda/sigiljs.git"
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.js"
|
|
29
|
+
},
|
|
30
|
+
"main": "./src/index.js",
|
|
31
|
+
"files": [
|
|
32
|
+
"src/",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"format": "prettier --write src/",
|
|
39
|
+
"lint": "eslint src/",
|
|
40
|
+
"test": "bun test"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"bun": ">=1",
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { validate } from './validate.js';
|
|
2
|
+
import { SigilValidationError } from './errors.js';
|
|
3
|
+
import { realType } from './realType.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates a value against a schema and throws a structured `SigilValidationError`
|
|
7
|
+
* if validation fails. Returns `true` if validation passes.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} astOrSigil - Raw AST, or a Sigil object with `.normalized` / `.ast`
|
|
10
|
+
* @param {*} value - The value to validate
|
|
11
|
+
* @param {object} [opts] - Optional hooks / options
|
|
12
|
+
* @returns {true}
|
|
13
|
+
* @throws {SigilValidationError}
|
|
14
|
+
*/
|
|
15
|
+
export function assert(astOrSigil, value, opts) {
|
|
16
|
+
if (validate(astOrSigil, value, opts)) return true;
|
|
17
|
+
|
|
18
|
+
const ast = astOrSigil.normalized ?? astOrSigil.ast ?? astOrSigil;
|
|
19
|
+
|
|
20
|
+
// Walk the AST to find the precise failure point for a rich error
|
|
21
|
+
const err = findError(ast, value, opts, []);
|
|
22
|
+
if (err)
|
|
23
|
+
throw new SigilValidationError(
|
|
24
|
+
err.message,
|
|
25
|
+
err.path,
|
|
26
|
+
err.expected,
|
|
27
|
+
err.actual,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Generic fallback (should rarely be reached)
|
|
31
|
+
throw new SigilValidationError(
|
|
32
|
+
'Validation failed',
|
|
33
|
+
[],
|
|
34
|
+
'match',
|
|
35
|
+
realType(value, opts),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Recursively walks the AST to find the deepest node that fails for `value`,
|
|
41
|
+
* returning a structured error descriptor or null if no specific failure is found.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} ast
|
|
44
|
+
* @param {*} value
|
|
45
|
+
* @param {object} opts
|
|
46
|
+
* @param {string[]} path - Current property path (accumulated for error reporting)
|
|
47
|
+
* @returns {{ message: string, path: string[], expected: string, actual: string } | null}
|
|
48
|
+
*/
|
|
49
|
+
function findError(ast, value, opts, path) {
|
|
50
|
+
if (!ast) return null;
|
|
51
|
+
|
|
52
|
+
switch (ast.kind) {
|
|
53
|
+
case 'primitive': {
|
|
54
|
+
const p = ast.name;
|
|
55
|
+
if (p === 'any' || p === 'unknown') return null;
|
|
56
|
+
if (p === 'never') {
|
|
57
|
+
const actual = realType(value, opts);
|
|
58
|
+
return {
|
|
59
|
+
message: `Expected never, got ${actual}`,
|
|
60
|
+
path,
|
|
61
|
+
expected: 'never',
|
|
62
|
+
actual,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// typeof-checkable primitives
|
|
66
|
+
if (
|
|
67
|
+
p === 'string' ||
|
|
68
|
+
p === 'number' ||
|
|
69
|
+
p === 'boolean' ||
|
|
70
|
+
p === 'symbol' ||
|
|
71
|
+
p === 'bigint' ||
|
|
72
|
+
p === 'undefined'
|
|
73
|
+
) {
|
|
74
|
+
const t = typeof value;
|
|
75
|
+
return t !== p ?
|
|
76
|
+
{ message: `Expected ${p}, got ${t}`, path, expected: p, actual: t }
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
const actual = realType(value, opts);
|
|
80
|
+
return actual !== p ?
|
|
81
|
+
{ message: `Expected ${p}, got ${actual}`, path, expected: p, actual }
|
|
82
|
+
: null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'literal': {
|
|
86
|
+
if (value !== ast.value) {
|
|
87
|
+
return {
|
|
88
|
+
message: `Expected literal ${JSON.stringify(ast.value)}, got ${JSON.stringify(value)}`,
|
|
89
|
+
path,
|
|
90
|
+
expected: String(ast.value),
|
|
91
|
+
actual: String(value),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'literal_union': {
|
|
98
|
+
if (!ast.values.includes(value)) {
|
|
99
|
+
const expected = ast.values.map((v) => JSON.stringify(v)).join(' | ');
|
|
100
|
+
return {
|
|
101
|
+
message: `Expected one of [${expected}], got ${JSON.stringify(value)}`,
|
|
102
|
+
path,
|
|
103
|
+
expected,
|
|
104
|
+
actual: String(value),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'primitive_union': {
|
|
111
|
+
const actual = realType(value, opts);
|
|
112
|
+
if (!ast.names.includes(actual)) {
|
|
113
|
+
const expected = ast.names.join(' | ');
|
|
114
|
+
return {
|
|
115
|
+
message: `Expected ${expected}, got ${actual}`,
|
|
116
|
+
path,
|
|
117
|
+
expected,
|
|
118
|
+
actual,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'union': {
|
|
125
|
+
// For unions we report the full set of expected types
|
|
126
|
+
const expected = ast.members.map((m) => m.name ?? m.kind).join(' | ');
|
|
127
|
+
return {
|
|
128
|
+
message: `Expected ${expected}, got ${realType(value, opts)}`,
|
|
129
|
+
path,
|
|
130
|
+
expected,
|
|
131
|
+
actual: realType(value, opts),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'optional': {
|
|
136
|
+
return value === undefined ? null : (
|
|
137
|
+
findError(ast.inner, value, opts, path)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'array': {
|
|
142
|
+
if (!Array.isArray(value)) {
|
|
143
|
+
const actual = realType(value, opts);
|
|
144
|
+
return {
|
|
145
|
+
message: `Expected array, got ${actual}`,
|
|
146
|
+
path,
|
|
147
|
+
expected: 'array',
|
|
148
|
+
actual,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
for (let i = 0; i < value.length; i++) {
|
|
152
|
+
const err = findError(ast.element, value[i], opts, [
|
|
153
|
+
...path,
|
|
154
|
+
String(i),
|
|
155
|
+
]);
|
|
156
|
+
if (err) return err;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case 'object': {
|
|
162
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
163
|
+
const actual = realType(value, opts);
|
|
164
|
+
return {
|
|
165
|
+
message: `Expected object, got ${actual}`,
|
|
166
|
+
path,
|
|
167
|
+
expected: 'object',
|
|
168
|
+
actual,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
for (const p of ast.properties) {
|
|
172
|
+
if (!p.optional && !(p.key in value)) {
|
|
173
|
+
return {
|
|
174
|
+
message: `Missing required property "${p.key}"`,
|
|
175
|
+
path: [...path, p.key],
|
|
176
|
+
expected: p.value.kind,
|
|
177
|
+
actual: 'undefined',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (p.key in value && value[p.key] !== undefined) {
|
|
181
|
+
const err = findError(p.value, value[p.key], opts, [...path, p.key]);
|
|
182
|
+
if (err) return err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
default:
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const validatorCache = new Map();
|
|
2
|
+
|
|
3
|
+
export function canonicalize(ast) {
|
|
4
|
+
// A fast and structurally stable canonicalization for caching compiled validators.
|
|
5
|
+
// Because JSON.stringify preserves key order from our normalization phase.
|
|
6
|
+
return JSON.stringify(ast);
|
|
7
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { realType } from '../core/realType.js';
|
|
2
|
+
import { validatorCache, canonicalize } from './cache.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compiles a normalized+partially-evaluated SigilJS AST into a fast validator function.
|
|
6
|
+
*
|
|
7
|
+
* Validators are memoized by structural identity (via JSON canonicalization).
|
|
8
|
+
* A validator function has the signature: (value, opts?) => boolean
|
|
9
|
+
*
|
|
10
|
+
* @param {object} ast - Normalized & partially-evaluated AST node
|
|
11
|
+
* @returns {(value: *, opts?: object) => boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function compile(ast) {
|
|
14
|
+
if (!ast) return () => true;
|
|
15
|
+
|
|
16
|
+
const key = canonicalize(ast);
|
|
17
|
+
const cached = validatorCache.get(key);
|
|
18
|
+
if (cached !== undefined) return cached;
|
|
19
|
+
|
|
20
|
+
const validator = build(ast);
|
|
21
|
+
validatorCache.set(key, validator);
|
|
22
|
+
return validator;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Recursively builds a validator closure from an AST node.
|
|
27
|
+
* @param {object} ast
|
|
28
|
+
* @returns {Function}
|
|
29
|
+
*/
|
|
30
|
+
function build(ast) {
|
|
31
|
+
switch (ast.kind) {
|
|
32
|
+
case 'primitive': {
|
|
33
|
+
const p = ast.name;
|
|
34
|
+
if (p === 'any' || p === 'unknown') return () => true;
|
|
35
|
+
if (p === 'never') return () => false;
|
|
36
|
+
// typeof-based fast-path for JS primitives (no realType overhead)
|
|
37
|
+
if (
|
|
38
|
+
p === 'string' ||
|
|
39
|
+
p === 'number' ||
|
|
40
|
+
p === 'boolean' ||
|
|
41
|
+
p === 'symbol' ||
|
|
42
|
+
p === 'bigint' ||
|
|
43
|
+
p === 'undefined'
|
|
44
|
+
) {
|
|
45
|
+
return (v) => typeof v === p;
|
|
46
|
+
}
|
|
47
|
+
return (v, opts) => realType(v, opts) === p;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case 'literal': {
|
|
51
|
+
const val = ast.value;
|
|
52
|
+
return (v) => v === val;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'literal_union': {
|
|
56
|
+
// Fast-path: Set membership for 5+ literals, Array.includes for fewer
|
|
57
|
+
if (ast.values.length < 5) {
|
|
58
|
+
const arr = ast.values;
|
|
59
|
+
return (v) => arr.includes(v);
|
|
60
|
+
}
|
|
61
|
+
const set = new Set(ast.values);
|
|
62
|
+
return (v) => set.has(v);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'primitive_union': {
|
|
66
|
+
const names = ast.names;
|
|
67
|
+
// If all names are typeof-checkable, skip realType entirely
|
|
68
|
+
const allSimple = names.every(
|
|
69
|
+
(n) =>
|
|
70
|
+
n === 'string' ||
|
|
71
|
+
n === 'number' ||
|
|
72
|
+
n === 'boolean' ||
|
|
73
|
+
n === 'symbol' ||
|
|
74
|
+
n === 'bigint' ||
|
|
75
|
+
n === 'undefined',
|
|
76
|
+
);
|
|
77
|
+
return allSimple ?
|
|
78
|
+
(v) => names.includes(typeof v)
|
|
79
|
+
: (v, opts) => names.includes(realType(v, opts));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'union': {
|
|
83
|
+
const fns = ast.members.map(build);
|
|
84
|
+
const len = fns.length;
|
|
85
|
+
return (v, opts) => {
|
|
86
|
+
for (let i = 0; i < len; i++) {
|
|
87
|
+
if (fns[i](v, opts)) return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'array': {
|
|
94
|
+
const el = build(ast.element);
|
|
95
|
+
return (v, opts) => {
|
|
96
|
+
if (!Array.isArray(v)) return false;
|
|
97
|
+
for (let i = 0; i < v.length; i++) {
|
|
98
|
+
if (!el(v[i], opts)) return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'optional': {
|
|
105
|
+
const inner = build(ast.inner);
|
|
106
|
+
return (v, opts) => v === undefined || inner(v, opts);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'object': {
|
|
110
|
+
// Use pre-computed hint arrays from partial evaluation to avoid
|
|
111
|
+
// re-filtering on every validator invocation (pure win — zero cost at compile time)
|
|
112
|
+
const { hints, properties } = ast;
|
|
113
|
+
const req =
|
|
114
|
+
hints ?
|
|
115
|
+
hints.requiredKeys.map((k) => ({
|
|
116
|
+
key: k,
|
|
117
|
+
check: build(properties.find((p) => p.key === k).value),
|
|
118
|
+
}))
|
|
119
|
+
: properties
|
|
120
|
+
.filter((p) => !p.optional)
|
|
121
|
+
.map((p) => ({ key: p.key, check: build(p.value) }));
|
|
122
|
+
const opt =
|
|
123
|
+
hints ?
|
|
124
|
+
hints.optionalKeys.map((k) => ({
|
|
125
|
+
key: k,
|
|
126
|
+
check: build(properties.find((p) => p.key === k).value),
|
|
127
|
+
}))
|
|
128
|
+
: properties
|
|
129
|
+
.filter((p) => p.optional)
|
|
130
|
+
.map((p) => ({ key: p.key, check: build(p.value) }));
|
|
131
|
+
|
|
132
|
+
const reqLen = req.length;
|
|
133
|
+
const optLen = opt.length;
|
|
134
|
+
|
|
135
|
+
return (v, opts) => {
|
|
136
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
137
|
+
return false;
|
|
138
|
+
for (let i = 0; i < reqLen; i++) {
|
|
139
|
+
const p = req[i];
|
|
140
|
+
if (!(p.key in v) || !p.check(v[p.key], opts)) return false;
|
|
141
|
+
}
|
|
142
|
+
for (let i = 0; i < optLen; i++) {
|
|
143
|
+
const p = opt[i];
|
|
144
|
+
if (p.key in v && v[p.key] !== undefined && !p.check(v[p.key], opts))
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
return () => false;
|
|
153
|
+
}
|
|
154
|
+
}
|