@weipertda/sigiljs 0.0.3 → 0.2.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.
Files changed (49) hide show
  1. package/README.md +157 -155
  2. package/docs/README.md +20 -0
  3. package/docs/arrays.md +55 -0
  4. package/docs/cli.md +48 -0
  5. package/docs/compiled-validators.md +42 -0
  6. package/docs/contributing.md +43 -0
  7. package/docs/exact-mode.md +82 -0
  8. package/docs/examples.md +92 -0
  9. package/docs/introduction.md +29 -0
  10. package/docs/license.md +23 -0
  11. package/docs/named-sigils.md +93 -0
  12. package/docs/objects.md +44 -0
  13. package/docs/optional.md +48 -0
  14. package/docs/quickstart.md +65 -0
  15. package/docs/realtype.md +36 -0
  16. package/docs/roadmap.md +40 -0
  17. package/docs/sigils.md +94 -0
  18. package/docs/{functions.md,maps.md,sets.md,tuples.md,intersection.md,rest.md,examples.md,performance.md,contributing.md,license.md +0 -0
  19. package/examples/api-response.js +21 -0
  20. package/examples/api-response.md +27 -0
  21. package/examples/api-validation.js +0 -0
  22. package/examples/api-validation.md +0 -0
  23. package/examples/basic-user.js +16 -0
  24. package/examples/basic-user.md +22 -0
  25. package/examples/basic-validation.js +0 -0
  26. package/examples/basic-validation.md +0 -0
  27. package/examples/config-file.md +16 -0
  28. package/examples/config-validation.js +18 -0
  29. package/examples/config-validation.md +24 -0
  30. package/examples/login-request.js +19 -0
  31. package/examples/login-request.md +25 -0
  32. package/examples/nested-order.js +28 -0
  33. package/examples/nested-order.md +34 -0
  34. package/examples/realtype-demo.js +6 -0
  35. package/examples/realtype-demo.md +12 -0
  36. package/examples/scripts/config-file.js +0 -0
  37. package/package.json +14 -6
  38. package/src/core/assert.js +173 -50
  39. package/src/core/compile.js +43 -9
  40. package/src/core/errors.js +29 -6
  41. package/src/core/normalize.js +1 -1
  42. package/src/core/parser.js +4 -3
  43. package/src/core/partial.js +1 -0
  44. package/src/core/registry.js +33 -0
  45. package/src/core/validate.js +3 -0
  46. package/src/index.js +1 -0
  47. package/src/playground/playground.js +1 -1
  48. package/src/sigil.js +56 -16
  49. package/CHANGELOG.md +0 -28
@@ -0,0 +1,16 @@
1
+ import { Sigil } from '../src/index.js';
2
+
3
+ const User = Sigil`
4
+ {
5
+ name: string
6
+ age?: number
7
+ tags: string[]
8
+ }
9
+ `
10
+
11
+ const data = {
12
+ name: "Alex",
13
+ tags: ["js", "bun"]
14
+ }
15
+
16
+ console.log(User.check(data))
@@ -0,0 +1,22 @@
1
+ # [examples/basic-user.js](https://github.dev/weipertda/sigiljs/blob/main/examples/basic-user.js)
2
+
3
+ ```javascript
4
+
5
+ import { Sigil } from "../src/index.js"
6
+
7
+ const User = Sigil`
8
+ {
9
+ name: string
10
+ age?: number
11
+ tags: string[]
12
+ }
13
+ `
14
+
15
+ const data = {
16
+ name: "Alex",
17
+ tags: ["js", "bun"]
18
+ }
19
+
20
+ console.log(User.check(data))
21
+
22
+ ```
File without changes
File without changes
@@ -0,0 +1,16 @@
1
+ # Configuration File Example
2
+
3
+ ```js
4
+ import { Sigil } from 'sigiljs';
5
+
6
+ const Config = Sigil`
7
+ {
8
+ port: number
9
+ host: string
10
+ debug?: boolean
11
+ }
12
+ `
13
+
14
+
15
+ Config.assert(JSON.parse(file))
16
+ ```
@@ -0,0 +1,18 @@
1
+ import { Sigil } from '../src/index.js'
2
+ import fs from 'fs'
3
+
4
+ const Config = Sigil`
5
+ {
6
+ port: number
7
+ host: string
8
+ debug?: boolean
9
+ }
10
+ `
11
+
12
+ const config = JSON.parse(
13
+ fs.readFileSync("./config.json", "utf8")
14
+ )
15
+
16
+ Config.assert(config)
17
+
18
+ console.log("Config valid")
@@ -0,0 +1,24 @@
1
+ # [examples/config-validation.js](https://github.dev/weipertda/sigiljs/blob/main/examples/scripts/config-validation.js)
2
+
3
+ ```javascript
4
+
5
+ import { Sigil } from "../src/index.js"
6
+ import fs from "fs"
7
+
8
+ const Config = Sigil`
9
+ {
10
+ port: number
11
+ host: string
12
+ debug?: boolean
13
+ }
14
+ `
15
+
16
+ const config = JSON.parse(
17
+ fs.readFileSync("./config.json", "utf8")
18
+ )
19
+
20
+ Config.assert(config)
21
+
22
+ console.log("Config valid")
23
+
24
+ ```
@@ -0,0 +1,19 @@
1
+ import { Sigil } from '../src/index.js'
2
+
3
+ const LoginRequest = Sigil`
4
+ {
5
+ email: string
6
+ password: string
7
+ }
8
+ `
9
+
10
+ function login(body) {
11
+ LoginRequest.assert(body)
12
+
13
+ console.log("Login request valid")
14
+ }
15
+
16
+ login({
17
+ email: "hello@example.com",
18
+ password: "secret"
19
+ })
@@ -0,0 +1,25 @@
1
+ # [examples/login-request.js](https://github.dev/weipertda/sigiljs/blob/main/examples/scripts/login-request.js)
2
+
3
+ ```javascript
4
+
5
+ import { Sigil } from "../src/index.js"
6
+
7
+ const LoginRequest = Sigil`
8
+ {
9
+ email: string
10
+ password: string
11
+ }
12
+ `
13
+
14
+ function login(body) {
15
+ LoginRequest.assert(body)
16
+
17
+ console.log("Login request valid")
18
+ }
19
+
20
+ login({
21
+ email: "hello@example.com",
22
+ password: "secret"
23
+ })
24
+
25
+ ```
@@ -0,0 +1,28 @@
1
+ import { Sigil } from '../src/index.js'
2
+
3
+ const Order = Sigil`
4
+ {
5
+ id: string
6
+ customer: {
7
+ name: string
8
+ email: string
9
+ }
10
+ items: {
11
+ name: string
12
+ price: number
13
+ }[]
14
+ }
15
+ `
16
+
17
+ const order = {
18
+ id: "123",
19
+ customer: {
20
+ name: "Alex",
21
+ email: "alex@example.com"
22
+ },
23
+ items: [
24
+ { name: "Keyboard", price: 99 }
25
+ ]
26
+ }
27
+
28
+ console.log(Order.check(order))
@@ -0,0 +1,34 @@
1
+ # [examples/nested-order.js](https://github.dev/weipertda/sigiljs/blob/main/examples/scripts/nested-order.js)
2
+
3
+ ```javascript
4
+
5
+ import { Sigil } from "../src/index.js"
6
+
7
+ const Order = Sigil`
8
+ {
9
+ id: string
10
+ customer: {
11
+ name: string
12
+ email: string
13
+ }
14
+ items: {
15
+ name: string
16
+ price: number
17
+ }[]
18
+ }
19
+ `
20
+
21
+ const order = {
22
+ id: "123",
23
+ customer: {
24
+ name: "Alex",
25
+ email: "alex@example.com"
26
+ },
27
+ items: [
28
+ { name: "Keyboard", price: 99 }
29
+ ]
30
+ }
31
+
32
+ console.log(Order.check(order))
33
+
34
+ ```
@@ -0,0 +1,6 @@
1
+ import { realType } from '../src/index.js'
2
+
3
+ console.log(realType([]))
4
+ console.log(realType(null))
5
+ console.log(realType(new Map()))
6
+ console.log(realType(new Date()))
@@ -0,0 +1,12 @@
1
+ # [examples/realtype-demo.js](https://github.dev/weipertda/sigiljs/blob/main/examples/scripts/realtype-demo.js)
2
+
3
+ ```javascript
4
+
5
+ import { realType } from "../src/index.js"
6
+
7
+ console.log(realType([]))
8
+ console.log(realType(null))
9
+ console.log(realType(new Map()))
10
+ console.log(realType(new Date()))
11
+
12
+ ```
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@weipertda/sigiljs",
3
- "version": "0.0.3",
4
- "description": "Runtime type sigils for JavaScript",
3
+ "version": "0.2.0",
4
+ "description": "Runtime-native type sigils for JavaScript",
5
5
  "keywords": [
6
6
  "types",
7
7
  "validation",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/weipertda/sigiljs.git"
18
+ "url": "git+https://github.com/antistructured/sigiljs.git"
19
19
  },
20
20
  "license": "MIT",
21
21
  "author": {
@@ -29,18 +29,26 @@
29
29
  },
30
30
  "main": "./src/index.js",
31
31
  "files": [
32
- "src/",
32
+ "src",
33
33
  "README.md",
34
34
  "LICENSE",
35
- "CHANGELOG.md"
35
+ "docs",
36
+ "examples"
36
37
  ],
37
38
  "scripts": {
38
39
  "format": "prettier --write src/",
39
40
  "lint": "eslint src/",
40
- "test": "bun test"
41
+ "test": "bun test",
42
+ "bench": "bun run bench/validate-user.js"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
41
46
  },
42
47
  "engines": {
43
48
  "bun": ">=1",
44
49
  "node": ">=18"
50
+ },
51
+ "devDependencies": {
52
+ "zod": "^4.4.3"
45
53
  }
46
54
  }
@@ -1,6 +1,7 @@
1
1
  import { validate } from './validate.js';
2
2
  import { SigilValidationError } from './errors.js';
3
3
  import { realType } from './realType.js';
4
+ import { resolve } from './registry.js';
4
5
 
5
6
  /**
6
7
  * Validates a value against a schema and throws a structured `SigilValidationError`
@@ -13,19 +14,37 @@ import { realType } from './realType.js';
13
14
  * @throws {SigilValidationError}
14
15
  */
15
16
  export function assert(astOrSigil, value, opts) {
16
- if (validate(astOrSigil, value, opts)) return true;
17
+ try {
18
+ if (validate(astOrSigil, value, opts)) return true;
19
+ } catch (e) {
20
+ // validate() may throw when a lazy identifier resolver can't find a named sigil.
21
+ // Re-wrap as a structured SigilValidationError so callers always get a consistent type.
22
+ throw new SigilValidationError(
23
+ e.message ?? 'Validation failed',
24
+ [],
25
+ 'unknown',
26
+ realType(value, opts),
27
+ );
28
+ }
17
29
 
18
30
  const ast = astOrSigil.normalized ?? astOrSigil.ast ?? astOrSigil;
19
31
 
20
- // Walk the AST to find the precise failure point for a rich error
21
- const err = findError(ast, value, opts, []);
22
- if (err)
32
+ let err;
33
+ try {
34
+ err = findError(ast, value, opts, []);
35
+ } catch (e) {
36
+ // findError's identifier case may also throw for unresolvable names.
23
37
  throw new SigilValidationError(
24
- err.message,
25
- err.path,
26
- err.expected,
27
- err.actual,
38
+ e.message ?? 'Validation failed',
39
+ [],
40
+ 'unknown',
41
+ realType(value, opts),
28
42
  );
43
+ }
44
+
45
+ if (err) {
46
+ throw new SigilValidationError(err.message, err.path, err.expected, err.actual);
47
+ }
29
48
 
30
49
  // Generic fallback (should rarely be reached)
31
50
  throw new SigilValidationError(
@@ -36,33 +55,72 @@ export function assert(astOrSigil, value, opts) {
36
55
  );
37
56
  }
38
57
 
58
+ // ─────────────────────────────────────────────────────────────────────────────
59
+ // Helpers
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Returns a human-readable type string for any AST node.
64
+ * Used in error messages as the "expected" description.
65
+ *
66
+ * @param {object} ast
67
+ * @returns {string}
68
+ */
69
+ function describeType(ast) {
70
+ if (!ast) return 'unknown';
71
+ switch (ast.kind) {
72
+ case 'primitive':
73
+ return ast.name;
74
+ case 'literal':
75
+ return JSON.stringify(ast.value);
76
+ case 'literal_union':
77
+ return ast.values.map((v) => JSON.stringify(v)).join(' | ');
78
+ case 'primitive_union':
79
+ return ast.names.join(' | ');
80
+ case 'union':
81
+ return ast.members.map(describeType).join(' | ');
82
+ case 'array':
83
+ return `${describeType(ast.element)}[]`;
84
+ case 'optional':
85
+ return `${describeType(ast.inner)}?`;
86
+ case 'object':
87
+ return 'object';
88
+ case 'identifier':
89
+ return ast.name;
90
+ default:
91
+ return ast.kind ?? 'unknown';
92
+ }
93
+ }
94
+
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // Core error-finding walker
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+
39
99
  /**
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.
100
+ * Recursively walks the AST to find the deepest, most-specific failure point.
101
+ *
102
+ * Returns a structured error descriptor, or null if no specific failure is found.
42
103
  *
43
104
  * @param {object} ast
44
105
  * @param {*} value
45
106
  * @param {object} opts
46
- * @param {string[]} path - Current property path (accumulated for error reporting)
107
+ * @param {string[]} path - Accumulated property path (e.g. ["user", "address", "zip"])
47
108
  * @returns {{ message: string, path: string[], expected: string, actual: string } | null}
48
109
  */
49
110
  function findError(ast, value, opts, path) {
50
111
  if (!ast) return null;
51
112
 
52
113
  switch (ast.kind) {
114
+ // ── Primitive ────────────────────────────────────────────────────────────
53
115
  case 'primitive': {
54
116
  const p = ast.name;
55
117
  if (p === 'any' || p === 'unknown') return null;
118
+
56
119
  if (p === 'never') {
57
120
  const actual = realType(value, opts);
58
- return {
59
- message: `Expected never, got ${actual}`,
60
- path,
61
- expected: 'never',
62
- actual,
63
- };
121
+ return { message: `Expected never, got ${actual}`, path, expected: 'never', actual };
64
122
  }
65
- // typeof-checkable primitives
123
+
66
124
  if (
67
125
  p === 'string' ||
68
126
  p === 'number' ||
@@ -71,29 +129,34 @@ function findError(ast, value, opts, path) {
71
129
  p === 'bigint' ||
72
130
  p === 'undefined'
73
131
  ) {
74
- const t = typeof value;
75
- return t !== p ?
76
- { message: `Expected ${p}, got ${t}`, path, expected: p, actual: t }
132
+ const actual = typeof value;
133
+ return actual !== p ?
134
+ { message: `Expected ${p}, got ${actual}`, path, expected: p, actual }
77
135
  : null;
78
136
  }
137
+
79
138
  const actual = realType(value, opts);
80
139
  return actual !== p ?
81
140
  { message: `Expected ${p}, got ${actual}`, path, expected: p, actual }
82
141
  : null;
83
142
  }
84
143
 
144
+ // ── Literal ───────────────────────────────────────────────────────────────
85
145
  case 'literal': {
86
146
  if (value !== ast.value) {
147
+ const expected = JSON.stringify(ast.value);
148
+ const actual = JSON.stringify(value);
87
149
  return {
88
- message: `Expected literal ${JSON.stringify(ast.value)}, got ${JSON.stringify(value)}`,
150
+ message: `Expected literal ${expected}, got ${actual}`,
89
151
  path,
90
- expected: String(ast.value),
152
+ expected,
91
153
  actual: String(value),
92
154
  };
93
155
  }
94
156
  return null;
95
157
  }
96
158
 
159
+ // ── Literal union ─────────────────────────────────────────────────────────
97
160
  case 'literal_union': {
98
161
  if (!ast.values.includes(value)) {
99
162
  const expected = ast.values.map((v) => JSON.stringify(v)).join(' | ');
@@ -107,6 +170,7 @@ function findError(ast, value, opts, path) {
107
170
  return null;
108
171
  }
109
172
 
173
+ // ── Primitive union ───────────────────────────────────────────────────────
110
174
  case 'primitive_union': {
111
175
  const actual = realType(value, opts);
112
176
  if (!ast.names.includes(actual)) {
@@ -121,70 +185,129 @@ function findError(ast, value, opts, path) {
121
185
  return null;
122
186
  }
123
187
 
188
+ // ── Union (mixed / complex) ───────────────────────────────────────────────
189
+ //
190
+ // Strategy: try every branch and keep the error with the deepest path
191
+ // (most specific failure). If any sub-error reaches deeper than the current
192
+ // path, return that — it gives the user the most actionable location.
193
+ // If all failures are at the same depth, fall back to a union-level message.
124
194
  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(' | ');
195
+ let bestErr = null;
196
+ for (const member of ast.members) {
197
+ const err = findError(member, value, opts, path);
198
+ if (!bestErr || (err && err.path.length > bestErr.path.length)) {
199
+ bestErr = err;
200
+ }
201
+ }
202
+ // A sub-branch had a deeper, more specific failure — surface it
203
+ if (bestErr && bestErr.path.length > path.length) {
204
+ return bestErr;
205
+ }
206
+ // All branches failed at the current depth — report the union itself
207
+ const expected = ast.members.map(describeType).join(' | ');
208
+ const actual = realType(value, opts);
127
209
  return {
128
- message: `Expected ${expected}, got ${realType(value, opts)}`,
210
+ message: `Expected ${expected}, got ${actual}`,
129
211
  path,
130
212
  expected,
131
- actual: realType(value, opts),
213
+ actual,
132
214
  };
133
215
  }
134
216
 
217
+ // ── Optional ─────────────────────────────────────────────────────────────
135
218
  case 'optional': {
136
- return value === undefined ? null : (
137
- findError(ast.inner, value, opts, path)
138
- );
219
+ return value === undefined ? null : findError(ast.inner, value, opts, path);
139
220
  }
140
221
 
222
+ // ── Array ─────────────────────────────────────────────────────────────────
141
223
  case 'array': {
142
224
  if (!Array.isArray(value)) {
143
225
  const actual = realType(value, opts);
144
- return {
145
- message: `Expected array, got ${actual}`,
146
- path,
147
- expected: 'array',
148
- actual,
149
- };
226
+ return { message: `Expected array, got ${actual}`, path, expected: 'array', actual };
150
227
  }
228
+ const elemType = describeType(ast.element);
151
229
  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;
230
+ const err = findError(ast.element, value[i], opts, [...path, String(i)]);
231
+ if (err) {
232
+ return {
233
+ ...err,
234
+ message: `Expected item [${i}] to be ${elemType}, got ${err.actual}`,
235
+ };
236
+ }
157
237
  }
158
238
  return null;
159
239
  }
160
240
 
241
+ // ── Object ────────────────────────────────────────────────────────────────
161
242
  case 'object': {
162
243
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
163
244
  const actual = realType(value, opts);
164
- return {
165
- message: `Expected object, got ${actual}`,
166
- path,
167
- expected: 'object',
168
- actual,
169
- };
245
+ return { message: `Expected object, got ${actual}`, path, expected: 'object', actual };
246
+ }
247
+
248
+ // Exact mode: report the first unexpected property
249
+ if (ast.exact) {
250
+ const allowed = new Set(ast.properties.map((p) => p.key));
251
+ for (const key in value) {
252
+ if (!allowed.has(key)) {
253
+ return {
254
+ message: `Unexpected property "${key}"`,
255
+ path: [...path, key],
256
+ expected: 'never',
257
+ actual: realType(value[key], opts),
258
+ };
259
+ }
260
+ }
170
261
  }
262
+
263
+ // Check required properties
171
264
  for (const p of ast.properties) {
172
265
  if (!p.optional && !(p.key in value)) {
266
+ const expected = describeType(p.value);
173
267
  return {
174
- message: `Missing required property "${p.key}"`,
268
+ message: `Missing required property "${p.key}" (expected ${expected})`,
175
269
  path: [...path, p.key],
176
- expected: p.value.kind,
270
+ expected,
177
271
  actual: 'undefined',
178
272
  };
179
273
  }
274
+
180
275
  if (p.key in value && value[p.key] !== undefined) {
181
276
  const err = findError(p.value, value[p.key], opts, [...path, p.key]);
182
- if (err) return err;
277
+ if (err) {
278
+ // Only override the message if the error is exactly one level deep
279
+ // (i.e., the failure IS the property itself). Deeper errors already
280
+ // carry their own enriched contextual message and must not be re-wrapped.
281
+ const isDirectFailure = err.path.length === path.length + 1;
282
+ return {
283
+ ...err,
284
+ message: isDirectFailure ?
285
+ `Expected property "${p.key}" to be ${err.expected}, got ${err.actual}`
286
+ : err.message,
287
+ };
288
+ }
183
289
  }
184
290
  }
291
+
185
292
  return null;
186
293
  }
187
294
 
295
+ // ── Identifier (named sigil reference) ───────────────────────────────────
296
+ case 'identifier': {
297
+ const name = ast.name;
298
+ const sigil = resolve(name);
299
+ if (!sigil) {
300
+ // Unknown at error-finding time — mirror the runtime throw as an error object
301
+ return {
302
+ message: `Unknown sigil reference: ${name}`,
303
+ path,
304
+ expected: name,
305
+ actual: realType(value, opts),
306
+ };
307
+ }
308
+ return findError(sigil.normalized ?? sigil.ast, value, opts, path);
309
+ }
310
+
188
311
  default:
189
312
  return null;
190
313
  }