@json-schema-engine/core 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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/coverage.d.ts +30 -0
  4. package/dist/coverage.d.ts.map +1 -0
  5. package/dist/coverage.js +81 -0
  6. package/dist/coverage.js.map +1 -0
  7. package/dist/cursor.d.ts +16 -0
  8. package/dist/cursor.d.ts.map +1 -0
  9. package/dist/cursor.js +28 -0
  10. package/dist/cursor.js.map +1 -0
  11. package/dist/dialect.d.ts +316 -0
  12. package/dist/dialect.d.ts.map +1 -0
  13. package/dist/dialect.js +135 -0
  14. package/dist/dialect.js.map +1 -0
  15. package/dist/engine.d.ts +203 -0
  16. package/dist/engine.d.ts.map +1 -0
  17. package/dist/engine.js +493 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/index.d.ts +182 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +370 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/json.d.ts +49 -0
  24. package/dist/json.d.ts.map +1 -0
  25. package/dist/json.js +128 -0
  26. package/dist/json.js.map +1 -0
  27. package/dist/keywords/applicator.d.ts +57 -0
  28. package/dist/keywords/applicator.d.ts.map +1 -0
  29. package/dist/keywords/applicator.js +789 -0
  30. package/dist/keywords/applicator.js.map +1 -0
  31. package/dist/keywords/core.d.ts +42 -0
  32. package/dist/keywords/core.d.ts.map +1 -0
  33. package/dist/keywords/core.js +139 -0
  34. package/dist/keywords/core.js.map +1 -0
  35. package/dist/keywords/format.d.ts +43 -0
  36. package/dist/keywords/format.d.ts.map +1 -0
  37. package/dist/keywords/format.js +101 -0
  38. package/dist/keywords/format.js.map +1 -0
  39. package/dist/keywords/metaschemas2019.d.ts +3 -0
  40. package/dist/keywords/metaschemas2019.d.ts.map +1 -0
  41. package/dist/keywords/metaschemas2019.js +406 -0
  42. package/dist/keywords/metaschemas2019.js.map +1 -0
  43. package/dist/keywords/metaschemas2020.d.ts +3 -0
  44. package/dist/keywords/metaschemas2020.d.ts.map +1 -0
  45. package/dist/keywords/metaschemas2020.js +441 -0
  46. package/dist/keywords/metaschemas2020.js.map +1 -0
  47. package/dist/keywords/metaschemas6.d.ts +3 -0
  48. package/dist/keywords/metaschemas6.d.ts.map +1 -0
  49. package/dist/keywords/metaschemas6.js +221 -0
  50. package/dist/keywords/metaschemas6.js.map +1 -0
  51. package/dist/keywords/metaschemas7.d.ts +3 -0
  52. package/dist/keywords/metaschemas7.d.ts.map +1 -0
  53. package/dist/keywords/metaschemas7.js +248 -0
  54. package/dist/keywords/metaschemas7.js.map +1 -0
  55. package/dist/keywords/unevaluated.d.ts +14 -0
  56. package/dist/keywords/unevaluated.d.ts.map +1 -0
  57. package/dist/keywords/unevaluated.js +296 -0
  58. package/dist/keywords/unevaluated.js.map +1 -0
  59. package/dist/keywords/validation.d.ts +21 -0
  60. package/dist/keywords/validation.d.ts.map +1 -0
  61. package/dist/keywords/validation.js +332 -0
  62. package/dist/keywords/validation.js.map +1 -0
  63. package/dist/keywords/vocab2019.d.ts +36 -0
  64. package/dist/keywords/vocab2019.d.ts.map +1 -0
  65. package/dist/keywords/vocab2019.js +585 -0
  66. package/dist/keywords/vocab2019.js.map +1 -0
  67. package/dist/keywords/vocab2020.d.ts +20 -0
  68. package/dist/keywords/vocab2020.d.ts.map +1 -0
  69. package/dist/keywords/vocab2020.js +55 -0
  70. package/dist/keywords/vocab2020.js.map +1 -0
  71. package/dist/keywords/vocab7.d.ts +46 -0
  72. package/dist/keywords/vocab7.d.ts.map +1 -0
  73. package/dist/keywords/vocab7.js +305 -0
  74. package/dist/keywords/vocab7.js.map +1 -0
  75. package/dist/loader.d.ts +41 -0
  76. package/dist/loader.d.ts.map +1 -0
  77. package/dist/loader.js +13 -0
  78. package/dist/loader.js.map +1 -0
  79. package/dist/lowering.d.ts +371 -0
  80. package/dist/lowering.d.ts.map +1 -0
  81. package/dist/lowering.js +59 -0
  82. package/dist/lowering.js.map +1 -0
  83. package/dist/output.d.ts +251 -0
  84. package/dist/output.d.ts.map +1 -0
  85. package/dist/output.js +357 -0
  86. package/dist/output.js.map +1 -0
  87. package/dist/records.d.ts +71 -0
  88. package/dist/records.d.ts.map +1 -0
  89. package/dist/records.js +150 -0
  90. package/dist/records.js.map +1 -0
  91. package/dist/ref.d.ts +10 -0
  92. package/dist/ref.d.ts.map +1 -0
  93. package/dist/ref.js +5 -0
  94. package/dist/ref.js.map +1 -0
  95. package/dist/regex.d.ts +45 -0
  96. package/dist/regex.d.ts.map +1 -0
  97. package/dist/regex.js +163 -0
  98. package/dist/regex.js.map +1 -0
  99. package/dist/registry.d.ts +141 -0
  100. package/dist/registry.d.ts.map +1 -0
  101. package/dist/registry.js +407 -0
  102. package/dist/registry.js.map +1 -0
  103. package/dist/result.d.ts +109 -0
  104. package/dist/result.d.ts.map +1 -0
  105. package/dist/result.js +112 -0
  106. package/dist/result.js.map +1 -0
  107. package/dist/uri.d.ts +18 -0
  108. package/dist/uri.d.ts.map +1 -0
  109. package/dist/uri.js +27 -0
  110. package/dist/uri.js.map +1 -0
  111. package/dist/walk.d.ts +25 -0
  112. package/dist/walk.d.ts.map +1 -0
  113. package/dist/walk.js +65 -0
  114. package/dist/walk.js.map +1 -0
  115. package/package.json +32 -0
  116. package/src/coverage.ts +88 -0
  117. package/src/cursor.ts +42 -0
  118. package/src/dialect.ts +428 -0
  119. package/src/engine.ts +741 -0
  120. package/src/index.ts +699 -0
  121. package/src/json.ts +137 -0
  122. package/src/keywords/applicator.ts +895 -0
  123. package/src/keywords/core.ts +168 -0
  124. package/src/keywords/format.ts +143 -0
  125. package/src/keywords/metaschemas2019.ts +410 -0
  126. package/src/keywords/metaschemas2020.ts +443 -0
  127. package/src/keywords/metaschemas6.ts +226 -0
  128. package/src/keywords/metaschemas7.ts +253 -0
  129. package/src/keywords/unevaluated.ts +329 -0
  130. package/src/keywords/validation.ts +627 -0
  131. package/src/keywords/vocab2019.ts +676 -0
  132. package/src/keywords/vocab2020.ts +82 -0
  133. package/src/keywords/vocab7.ts +393 -0
  134. package/src/loader.ts +60 -0
  135. package/src/lowering.ts +414 -0
  136. package/src/output.ts +651 -0
  137. package/src/records.ts +226 -0
  138. package/src/ref.ts +14 -0
  139. package/src/regex.ts +171 -0
  140. package/src/registry.ts +481 -0
  141. package/src/result.ts +252 -0
  142. package/src/uri.ts +35 -0
  143. package/src/walk.ts +88 -0
@@ -0,0 +1,168 @@
1
+ // Core vocabulary behaviors, plus shared behavior factories.
2
+
3
+ import { JsonValue } from "../json.js";
4
+ import { KeywordBehavior, StaticFacts } from "../dialect.js";
5
+
6
+ /** 2020-12 core vocabulary URI. */
7
+ export const VOCAB_CORE = "https://json-schema.org/draft/2020-12/vocab/core";
8
+
9
+ /** Static facts for a keyword whose value is itself a subschema (e.g. `if`, `not`). */
10
+ const SELF: StaticFacts = { subschemas: [[]] };
11
+ /** Static facts for a keyword whose value is a name-keyed map of subschemas. */
12
+ const mapPositions = (value: JsonValue): StaticFacts =>
13
+ typeof value === "object" && value !== null && !Array.isArray(value)
14
+ ? { subschemas: Object.keys(value).map((k) => [k]) }
15
+ : {};
16
+
17
+ /** Identifier/reserved keywords: no evaluation behavior, no annotation. */
18
+ export const structural = (id: string): KeywordBehavior => ({
19
+ id,
20
+ structural: true,
21
+ analyze: () => ({ produces: [] }),
22
+ evaluate: () => true,
23
+ lower: () => {
24
+ /* inert in compiled code too */
25
+ },
26
+ });
27
+
28
+ /**
29
+ * A keyword whose subschemas exist (for identification) but whose
30
+ * evaluation is driven by a sibling (`then`/`else` via `if`).
31
+ */
32
+ export const inertSubschema = (id: string): KeywordBehavior => ({
33
+ id,
34
+ analyze: () => SELF,
35
+ evaluate: () => true,
36
+ lower: () => {
37
+ /* the driving sibling owns the application */
38
+ },
39
+ });
40
+
41
+ /** EXEMPLAR (annotation-only class): the keyword's value is its annotation. */
42
+ export const annotationOnly = (id: string): KeywordBehavior => ({
43
+ id,
44
+ evaluate: (_value, _cursor, ctx) => {
45
+ ctx.annotate();
46
+ return true;
47
+ },
48
+ lower: (_value, lctx) => {
49
+ lctx.emit({ kind: "annotate" });
50
+ },
51
+ });
52
+
53
+ /**
54
+ * Placeholder for keywords owed by a later milestone: loud failure beats
55
+ * silently treating a known assertion/applicator as an annotation.
56
+ */
57
+ export const notImplemented = (
58
+ id: string,
59
+ milestone: string,
60
+ ): KeywordBehavior => ({
61
+ id,
62
+ evaluate: () => {
63
+ throw new Error(`keyword '${id}' is not implemented until ${milestone}`);
64
+ },
65
+ });
66
+
67
+ const referenceFacts = (value: JsonValue): StaticFacts =>
68
+ typeof value === "string"
69
+ ? {
70
+ references: [value],
71
+ // The resolved target applies in place, unconditionally.
72
+ applications: [
73
+ {
74
+ path: [],
75
+ ref: value,
76
+ mode: "inPlace",
77
+ conditional: false,
78
+ asserts: true,
79
+ },
80
+ ],
81
+ }
82
+ : {};
83
+
84
+ /**
85
+ * EXEMPLAR (reference class): resolve against the lexical base, apply the
86
+ * target at the same cursor. The engine owns the evaluation-path extension
87
+ * and the frame, so a reference behavior is one line.
88
+ */
89
+ export const $ref: KeywordBehavior = {
90
+ id: `${VOCAB_CORE}#$ref`,
91
+ analyze: referenceFacts,
92
+ evaluate: (value, _cursor, ctx) =>
93
+ ctx.applyResolved(ctx.resolveRef(value as string)),
94
+ lower: (value, lctx) => {
95
+ lctx.emit({
96
+ kind: "apply",
97
+ apply: {
98
+ path: [],
99
+ ref: value as string,
100
+ cursor: { kind: "here" },
101
+ fold: "allMustPass",
102
+ },
103
+ });
104
+ },
105
+ };
106
+
107
+ /** `$dynamicRef` (D8): resolves with dynamic-scope rebinding. */
108
+ export const $dynamicRef: KeywordBehavior = {
109
+ id: `${VOCAB_CORE}#$dynamicRef`,
110
+ analyze: (value) => ({
111
+ ...referenceFacts(value),
112
+ dynamicScopeSensitive: true,
113
+ }),
114
+ evaluate: (value, _cursor, ctx) =>
115
+ ctx.applyResolved(ctx.resolveDynamic(value as string)),
116
+ };
117
+
118
+ // 2019-09 core vocabulary: $recursiveRef/$recursiveAnchor are D8's degenerate
119
+ // case — resolution lives in the engine, anchor indexing in the registry's
120
+ // identifier extractor, so both behaviors are one-liners.
121
+
122
+ /** 2019-09 core vocabulary URI. */
123
+ export const VOCAB_CORE_2019 =
124
+ "https://json-schema.org/draft/2019-09/vocab/core";
125
+
126
+ /** `$recursiveRef`, 2019-09's degenerate case of `$dynamicRef` (D8). */
127
+ export const $recursiveRef: KeywordBehavior = {
128
+ id: `${VOCAB_CORE_2019}#$recursiveRef`,
129
+ analyze: (value) => ({
130
+ ...referenceFacts(value),
131
+ dynamicScopeSensitive: true,
132
+ }),
133
+ evaluate: (value, _cursor, ctx) =>
134
+ ctx.applyResolved(ctx.resolveRecursive(value as string)),
135
+ };
136
+
137
+ /** `$recursiveAnchor`: structural only, indexed by the registration walk. */
138
+ export const $recursiveAnchor: KeywordBehavior = structural(
139
+ `${VOCAB_CORE_2019}#$recursiveAnchor`,
140
+ );
141
+
142
+ /** `$defs`: a map of named subschemas, reachable only by reference. */
143
+ export const $defs: KeywordBehavior = {
144
+ id: `${VOCAB_CORE}#$defs`,
145
+ structural: true,
146
+ analyze: mapPositions,
147
+ evaluate: () => true,
148
+ lower: () => {
149
+ /* contents are reachable only by reference */
150
+ },
151
+ };
152
+
153
+ /** The 2020-12 core vocabulary's keyword behaviors, by name. */
154
+ export const coreVocabulary: Record<string, KeywordBehavior> = {
155
+ $ref,
156
+ $dynamicRef,
157
+ $defs,
158
+ $id: structural(`${VOCAB_CORE}#$id`),
159
+ $schema: structural(`${VOCAB_CORE}#$schema`),
160
+ $anchor: structural(`${VOCAB_CORE}#$anchor`),
161
+ // Dynamic-anchor indexing happens in the registration walk (D8).
162
+ $dynamicAnchor: structural(`${VOCAB_CORE}#$dynamicAnchor`),
163
+ $vocabulary: structural(`${VOCAB_CORE}#$vocabulary`),
164
+ // $comment's value MUST NOT be collected as an annotation.
165
+ $comment: structural(`${VOCAB_CORE}#$comment`),
166
+ };
167
+
168
+ export { mapPositions, SELF };
@@ -0,0 +1,143 @@
1
+ // The `format` keyword's asserting behavior (M7). Annotation-only `format`
2
+ // is the annotationOnly factory in each dialect module; this file supplies
3
+ // the assertion side: the format-table contract, the format-assertion
4
+ // vocabulary, and the best-effort configuration switch.
5
+ //
6
+ // Two assertion mechanisms with deliberately different unknown-format
7
+ // behavior:
8
+ // - format-assertion VOCABULARY: promises assertion, so a format the
9
+ // table cannot assert is refused at registration (UnknownFormatError).
10
+ // - format-annotation + the `assertFormats` configuration: best effort —
11
+ // recognized formats assert, unrecognized formats fall back to
12
+ // annotation-only.
13
+
14
+ import { JsonValue, JsonType, jsonTypeOf } from "../json.js";
15
+ import { KeywordBehavior } from "../dialect.js";
16
+ import { lowerIR } from "../lowering.js";
17
+
18
+ /** 2020-12 format-assertion vocabulary URI. */
19
+ export const VOCAB_FORMAT_ASSERTION =
20
+ "https://json-schema.org/draft/2020-12/vocab/format-assertion";
21
+
22
+ /**
23
+ * One format's definition. `types` scopes the assertion: instances of
24
+ * other types are vacuously valid (every standard JSON Schema format is
25
+ * string-scoped, but the shape admits non-string formats — the
26
+ * OpenAPI-registered int32/int64/double family applies to numbers).
27
+ */
28
+ export interface FormatDefinition {
29
+ /** instance types the format constrains; default ["string"] */
30
+ readonly types?: readonly (JsonType | "integer")[];
31
+ /** true when the (type-scoped) instance conforms */
32
+ test(value: JsonValue): boolean;
33
+ }
34
+
35
+ /** Format name → definition. */
36
+ export type FormatTable = Readonly<Record<string, FormatDefinition>>;
37
+
38
+ /**
39
+ * A schema uses a format the engine cannot assert while the
40
+ * format-assertion vocabulary is in effect. Thrown at registration:
41
+ * that vocabulary promises assertion, so unsupported formats are refused,
42
+ * never silently annotated.
43
+ */
44
+ export class UnknownFormatError extends Error {}
45
+
46
+ const appliesTo = (
47
+ types: readonly (JsonType | "integer")[],
48
+ value: JsonValue,
49
+ ): boolean =>
50
+ types.some((t) =>
51
+ t === "integer"
52
+ ? typeof value === "number" && Number.isInteger(value)
53
+ : jsonTypeOf(value) === t,
54
+ );
55
+
56
+ /**
57
+ * Builds an asserting `format` behavior over a table. `refuseUnknown`
58
+ * selects the vocabulary posture (refuse at registration) versus the
59
+ * best-effort configuration posture (annotate unknowns).
60
+ *
61
+ * Single-table contract: `lower()` resolves the format definition against
62
+ * the closed-over `table` at compile time, so it is correct only when that
63
+ * table is the compiling engine's `formats` — true by construction for the
64
+ * Engine-constructed instances, the only production call sites (both the
65
+ * `assertFormats` configuration and the format-assertion vocabulary close over
66
+ * `options.formats`, which the compiler reads back through `engine.formats`).
67
+ * A custom dialect that wires `assertingFormat` with a foreign table must drop
68
+ * `lower` (accepting the interpreter fallback) rather than compile against a
69
+ * table the artifact's runtime would not carry.
70
+ */
71
+ export function assertingFormat(
72
+ id: string,
73
+ table: FormatTable,
74
+ refuseUnknown: boolean,
75
+ ): KeywordBehavior {
76
+ return {
77
+ id,
78
+ analyze: (value) => {
79
+ if (
80
+ refuseUnknown &&
81
+ typeof value === "string" &&
82
+ !Object.hasOwn(table, value)
83
+ ) {
84
+ throw new UnknownFormatError(
85
+ `format '${value}' is not supported; the format-assertion ` +
86
+ "vocabulary requires refusing formats it cannot assert",
87
+ );
88
+ }
89
+ // Report only formats the table can assert — the exact set lower()
90
+ // emits a formatTest for (an unknown format under the best-effort
91
+ // posture falls back to annotation-only and needs no table entry). This
92
+ // keeps plan.formats == the artifact's used-format set, so the runtime's
93
+ // missing-definition guard stays unreachable via public paths.
94
+ return typeof value === "string" && Object.hasOwn(table, value)
95
+ ? { formats: [value] }
96
+ : {};
97
+ },
98
+ lower: (value, lctx) => {
99
+ // Annotate first, unconditionally — evaluate()'s annotate-before-
100
+ // assertion order (format's annotation value is the name).
101
+ lctx.emit({ kind: "annotate" });
102
+ if (typeof value !== "string") return; // metaschema's concern
103
+ // Resolve against the closed-over table (the compiling engine's, per the
104
+ // single-table contract above); the name is a schema constant. A
105
+ // refuseUnknown instance never reaches here undefined — analyze() threw
106
+ // at registration — so an undefined definition is the best-effort
107
+ // posture's unrecognized format: fall back to annotation-only, emitting
108
+ // nothing further (evaluate() parity).
109
+ const definition = Object.hasOwn(table, value) ? table[value] : undefined;
110
+ if (definition === undefined) return;
111
+ const { and, not, typeIs, when, failWith, constant, formatTest } =
112
+ lowerIR;
113
+ lctx.emit(
114
+ when(
115
+ and(
116
+ typeIs(lctx.instance, ...(definition.types ?? ["string"])),
117
+ not(formatTest(value, lctx.instance)),
118
+ ),
119
+ [
120
+ failWith(
121
+ { format: constant(value) },
122
+ "must match format '" + value + "'",
123
+ ),
124
+ ],
125
+ ),
126
+ );
127
+ },
128
+ evaluate: (value, cursor, ctx) => {
129
+ // The annotation is recorded regardless of assertion outcome
130
+ // (format's annotation value is the format name).
131
+ ctx.annotate();
132
+ if (typeof value !== "string") return true; // metaschema's concern
133
+ const definition = Object.hasOwn(table, value) ? table[value] : undefined;
134
+ if (definition === undefined) return true; // best-effort fallback
135
+ if (!appliesTo(definition.types ?? ["string"], cursor.value)) {
136
+ return true;
137
+ }
138
+ if (definition.test(cursor.value)) return true;
139
+ ctx.error(`must match format '${value}'`, { format: value });
140
+ return false;
141
+ },
142
+ };
143
+ }