@weipertda/sigiljs 0.0.1 → 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 +18 -10
  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
@@ -1,5 +1,9 @@
1
1
  import { realType } from '../core/realType.js';
2
- import { validatorCache, canonicalize } from './cache.js';
2
+ import {
3
+ canonicalize,
4
+ validatorCache,
5
+ } from './cache.js';
6
+ import { resolve } from './registry.js';
3
7
 
4
8
  /**
5
9
  * Compiles a normalized+partially-evaluated SigilJS AST into a fast validator function.
@@ -25,9 +29,10 @@ export function compile(ast) {
25
29
  /**
26
30
  * Recursively builds a validator closure from an AST node.
27
31
  * @param {object} ast
32
+ * @param {Set<string>} [visited] - Track visited identifiers for recursion safety
28
33
  * @returns {Function}
29
34
  */
30
- function build(ast) {
35
+ function build(ast, visited = new Set()) {
31
36
  switch (ast.kind) {
32
37
  case 'primitive': {
33
38
  const p = ast.name;
@@ -80,7 +85,7 @@ function build(ast) {
80
85
  }
81
86
 
82
87
  case 'union': {
83
- const fns = ast.members.map(build);
88
+ const fns = ast.members.map((m) => build(m, visited));
84
89
  const len = fns.length;
85
90
  return (v, opts) => {
86
91
  for (let i = 0; i < len; i++) {
@@ -91,7 +96,7 @@ function build(ast) {
91
96
  }
92
97
 
93
98
  case 'array': {
94
- const el = build(ast.element);
99
+ const el = build(ast.element, visited);
95
100
  return (v, opts) => {
96
101
  if (!Array.isArray(v)) return false;
97
102
  for (let i = 0; i < v.length; i++) {
@@ -102,7 +107,7 @@ function build(ast) {
102
107
  }
103
108
 
104
109
  case 'optional': {
105
- const inner = build(ast.inner);
110
+ const inner = build(ast.inner, visited);
106
111
  return (v, opts) => v === undefined || inner(v, opts);
107
112
  }
108
113
 
@@ -114,27 +119,35 @@ function build(ast) {
114
119
  hints ?
115
120
  hints.requiredKeys.map((k) => ({
116
121
  key: k,
117
- check: build(properties.find((p) => p.key === k).value),
122
+ check: build(properties.find((p) => p.key === k).value, visited),
118
123
  }))
119
124
  : properties
120
125
  .filter((p) => !p.optional)
121
- .map((p) => ({ key: p.key, check: build(p.value) }));
126
+ .map((p) => ({ key: p.key, check: build(p.value, visited) }));
122
127
  const opt =
123
128
  hints ?
124
129
  hints.optionalKeys.map((k) => ({
125
130
  key: k,
126
- check: build(properties.find((p) => p.key === k).value),
131
+ check: build(properties.find((p) => p.key === k).value, visited),
127
132
  }))
128
133
  : properties
129
134
  .filter((p) => p.optional)
130
- .map((p) => ({ key: p.key, check: build(p.value) }));
135
+ .map((p) => ({ key: p.key, check: build(p.value, visited) }));
131
136
 
132
137
  const reqLen = req.length;
133
138
  const optLen = opt.length;
139
+ const allowedKeys = ast.exact ? new Set(properties.map((p) => p.key)) : null;
134
140
 
135
141
  return (v, opts) => {
136
142
  if (typeof v !== 'object' || v === null || Array.isArray(v))
137
143
  return false;
144
+
145
+ if (ast.exact) {
146
+ for (const key in v) {
147
+ if (!allowedKeys.has(key)) return false;
148
+ }
149
+ }
150
+
138
151
  for (let i = 0; i < reqLen; i++) {
139
152
  const p = req[i];
140
153
  if (!(p.key in v) || !p.check(v[p.key], opts)) return false;
@@ -148,6 +161,27 @@ function build(ast) {
148
161
  };
149
162
  }
150
163
 
164
+ case 'identifier': {
165
+ const name = ast.name;
166
+ const sigil = resolve(name);
167
+
168
+ // If the sigil isn't registered yet, or if we are currently visiting it (circularity),
169
+ // we return a lazy wrapper that resolves the sigil at validation time.
170
+ if (!sigil || visited.has(name)) {
171
+ return (val, o) => {
172
+ const resolved = resolve(name);
173
+ if (!resolved) throw new Error(`Unknown sigil reference: ${name}`);
174
+ return resolved.check(val, o);
175
+ };
176
+ }
177
+
178
+ visited.add(name);
179
+ const target = sigil.normalized || sigil.ast;
180
+ const check = build(target, visited);
181
+ visited.delete(name);
182
+ return check;
183
+ }
184
+
151
185
  default:
152
186
  return () => false;
153
187
  }
@@ -1,14 +1,22 @@
1
1
  /**
2
2
  * Thrown by `assert()` when a value fails validation.
3
3
  *
4
- * Properties:
5
- * - message — human-readable description of the failure
6
- * - code — always "SIGIL_VALIDATION_FAILED" (machine-readable)
7
- * - path array of keys to the failing property (e.g. ["user", "email"])
8
- * - expected type string that was expected
9
- * - actual — type string that was received
4
+ * Shape:
5
+ * {
6
+ * code: "SIGIL_VALIDATION_FAILED", // always — machine-readable
7
+ * message: "Expected property \"age\" to be number, got string",
8
+ * path: ["user", "age"], // key path to the failing field
9
+ * expected: "number", // type that was required
10
+ * actual: "string" // type that was received
11
+ * }
10
12
  */
11
13
  export class SigilValidationError extends Error {
14
+ /**
15
+ * @param {string} message - Human-readable description of the failure
16
+ * @param {string[]} path - Property path to the failure (e.g. ["user", "age"])
17
+ * @param {string} expected - Type that was expected
18
+ * @param {string} actual - Type that was actually received
19
+ */
12
20
  constructor(message, path, expected, actual) {
13
21
  super(message);
14
22
  this.name = 'SigilValidationError';
@@ -23,6 +31,21 @@ export class SigilValidationError extends Error {
23
31
  }
24
32
  }
25
33
 
34
+ /**
35
+ * Returns the canonical JSON shape for structured logging / API responses.
36
+ *
37
+ * @returns {{ code: string, message: string, path: string[], expected: string, actual: string }}
38
+ */
39
+ toJSON() {
40
+ return {
41
+ code: this.code,
42
+ message: this.message,
43
+ path: this.path,
44
+ expected: this.expected,
45
+ actual: this.actual,
46
+ };
47
+ }
48
+
26
49
  /** Consistent devtools / console.log labeling */
27
50
  get [Symbol.toStringTag]() {
28
51
  return 'SigilValidationError';
@@ -43,7 +43,7 @@ export function normalize(ast) {
43
43
  optional: p.optional,
44
44
  value: normalize(p.value),
45
45
  }));
46
- return { kind: 'object', properties };
46
+ return { kind: 'object', properties, exact: ast.exact };
47
47
  }
48
48
 
49
49
  default:
@@ -29,13 +29,14 @@ const PRIMITIVES = new Set([
29
29
  * { kind: 'union', members }
30
30
  * { kind: 'array', element }
31
31
  * { kind: 'optional', inner }
32
- * { kind: 'object', properties: [{ key, optional, value }] }
32
+ * { kind: 'object', properties: [{ key, optional, value }], exact }
33
33
  * { kind: 'identifier', name }
34
34
  *
35
35
  * @param {string|Array} input
36
+ * @param {object} [options]
36
37
  * @returns {object} AST node
37
38
  */
38
- export function parse(input) {
39
+ export function parse(input, options = {}) {
39
40
  const tokens = typeof input === 'string' ? tokenize(input) : input;
40
41
  let pos = 0;
41
42
 
@@ -212,7 +213,7 @@ export function parse(input) {
212
213
  }
213
214
 
214
215
  consume('}', 'Expected closing block brace');
215
- return { kind: 'object', properties };
216
+ return { kind: 'object', properties, exact: !!options.exact };
216
217
  }
217
218
 
218
219
  return parseSigil();
@@ -54,6 +54,7 @@ export function partial(ast) {
54
54
  kind: 'object',
55
55
  properties,
56
56
  hints: { requiredKeys, optionalKeys },
57
+ exact: ast.exact,
57
58
  };
58
59
  }
59
60
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Global registry for named sigils.
3
+ * Allows sigils to be referenced by name in other sigils.
4
+ */
5
+ const registry = new Map();
6
+
7
+ /**
8
+ * Registers a sigil by name.
9
+ * @param {string} name
10
+ * @param {object} sigil
11
+ */
12
+ export function register(name, sigil) {
13
+ if (typeof name !== 'string') {
14
+ throw new Error('Sigil name must be a string');
15
+ }
16
+ registry.set(name, sigil);
17
+ }
18
+
19
+ /**
20
+ * Resolves a sigil by name.
21
+ * @param {string} name
22
+ * @returns {object|undefined}
23
+ */
24
+ export function resolve(name) {
25
+ return registry.get(name);
26
+ }
27
+
28
+ /**
29
+ * Clears the registry (mainly for testing).
30
+ */
31
+ export function clear() {
32
+ registry.clear();
33
+ }
@@ -2,6 +2,9 @@ import { compile } from './compile.js';
2
2
 
3
3
  export function validate(astOrSigil, value, opts) {
4
4
  // Can be called with a raw AST, or a Sigil object possessing `.ast` / `.normalized`
5
+ if (astOrSigil && typeof astOrSigil.validator === 'function') {
6
+ return astOrSigil.validator(value, opts);
7
+ }
5
8
  const targetAst = astOrSigil.normalized || astOrSigil.ast || astOrSigil;
6
9
  const check = compile(targetAst);
7
10
  return check(value, opts);
package/src/index.js CHANGED
@@ -4,3 +4,4 @@ export {
4
4
  realType as real,
5
5
  realType as Real,
6
6
  } from './core/realType.js';
7
+ export { SigilValidationError } from './core/errors.js';
@@ -1,4 +1,4 @@
1
- import { T } from './index.js';
1
+ import { T } from '../index.js';
2
2
 
3
3
  const jsonStr = process.argv[2];
4
4
  const sigilStr = process.argv[3];
package/src/sigil.js CHANGED
@@ -3,6 +3,7 @@ import { compile } from './core/compile.js';
3
3
  import { normalize } from './core/normalize.js';
4
4
  import { parse } from './core/parser.js';
5
5
  import { partial } from './core/partial.js';
6
+ import { register } from './core/registry.js';
6
7
  import { validate } from './core/validate.js';
7
8
 
8
9
  // Memoize fully-constructed Sigil objects by raw schema string.
@@ -16,40 +17,79 @@ const _sigilCache = new Map();
16
17
  * Each unique schema string is parsed, normalized, and compiled exactly once.
17
18
  * Subsequent calls with the same schema string return the cached sigil object.
18
19
  *
19
- * @param {TemplateStringsArray} strings
20
- * @param {...*} values - Interpolations (embedded JS values)
21
- * @returns {{ raw, ast, normalized, check, assert, compile }}
22
- *
23
- * @example
24
- * const User = Sigil`{ name: string, age?: number }`
25
- * User.check({ name: 'Alice' }) // true
26
- * User.assert({ name: 42 }) // throws SigilValidationError
20
+ * The returned sigil exposes:
21
+ * .check(value) → boolean
22
+ * .assert(value) → void | throws
23
+ * .validator → compiled validator function (same stable reference always)
24
+ * .compile() → same as .validator (method form, for back-compat)
25
+ * .raw / .ast / .normalized / .options
27
26
  */
28
- export function Sigil(strings, ...values) {
27
+ function createSigil(options, strings, ...values) {
29
28
  // Reconstruct raw string from template parts (supports interpolations)
30
29
  let raw = strings[0];
31
30
  for (let i = 0; i < values.length; i++) raw += values[i] + strings[i + 1];
32
31
 
33
- // Return memoized sigil if already compiled
34
- const cached = _sigilCache.get(raw);
32
+ // Cache key includes options to distinguish exact vs non-exact versions of same schema
33
+ const cacheKey = JSON.stringify(options) + raw;
34
+ const cached = _sigilCache.get(cacheKey);
35
35
  if (cached !== undefined) return cached;
36
36
 
37
37
  // Parse → Normalize → Partial-evaluate → Compile pipeline
38
- const ast = parse(raw);
38
+ const ast = parse(raw, options);
39
39
  const normalized = partial(normalize(ast));
40
40
 
41
- // Warm validator cache eagerly (avoids cold-start cost on first .check() call)
42
- compile(normalized);
41
+ // Pre-compile eagerly and capture the function reference.
42
+ // This warms the cache on creation and ensures sigil.validator always
43
+ // returns the exact same cached function — no re-compilation ever.
44
+ const validator = compile(normalized);
43
45
 
44
46
  const sigil = Object.freeze({
45
47
  raw,
46
48
  ast,
47
49
  normalized,
50
+ options,
51
+ validator,
48
52
  check: (value, opts) => validate(sigil, value, opts),
49
53
  assert: (value, opts) => assert(sigil, value, opts),
50
- compile: () => compile(normalized),
54
+ compile: () => validator,
51
55
  });
52
56
 
53
- _sigilCache.set(raw, sigil);
57
+ _sigilCache.set(cacheKey, sigil);
54
58
  return sigil;
55
59
  }
60
+
61
+ /**
62
+ * Default Sigil tagged template (non-strict objects).
63
+ */
64
+ export function Sigil(strings, ...values) {
65
+ return createSigil({ exact: false }, strings, ...values);
66
+ }
67
+
68
+ /**
69
+ * Strict Sigil tagged template (exact objects, fails on extra properties).
70
+ */
71
+ Sigil.exact = function (strings, ...values) {
72
+ return createSigil({ exact: true }, strings, ...values);
73
+ };
74
+
75
+ /**
76
+ * Creates a named Sigil and registers it in the global registry.
77
+ *
78
+ * @param {string} name
79
+ * @returns {Function} Tagged template function
80
+ */
81
+ Sigil.named = function (name) {
82
+ return function (strings, ...values) {
83
+ const sigil = createSigil({ exact: false, named: name }, strings, ...values);
84
+ register(name, sigil);
85
+ return sigil;
86
+ };
87
+ };
88
+
89
+ /**
90
+ * Alias for Sigil.named — preferred ergonomic name for defining reusable sigils.
91
+ *
92
+ * @param {string} name
93
+ * @returns {Function} Tagged template function
94
+ */
95
+ Sigil.define = Sigil.named;
package/CHANGELOG.md DELETED
@@ -1,28 +0,0 @@
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`.