@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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/coverage.d.ts +30 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +81 -0
- package/dist/coverage.js.map +1 -0
- package/dist/cursor.d.ts +16 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +28 -0
- package/dist/cursor.js.map +1 -0
- package/dist/dialect.d.ts +316 -0
- package/dist/dialect.d.ts.map +1 -0
- package/dist/dialect.js +135 -0
- package/dist/dialect.js.map +1 -0
- package/dist/engine.d.ts +203 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +493 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +370 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +49 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +128 -0
- package/dist/json.js.map +1 -0
- package/dist/keywords/applicator.d.ts +57 -0
- package/dist/keywords/applicator.d.ts.map +1 -0
- package/dist/keywords/applicator.js +789 -0
- package/dist/keywords/applicator.js.map +1 -0
- package/dist/keywords/core.d.ts +42 -0
- package/dist/keywords/core.d.ts.map +1 -0
- package/dist/keywords/core.js +139 -0
- package/dist/keywords/core.js.map +1 -0
- package/dist/keywords/format.d.ts +43 -0
- package/dist/keywords/format.d.ts.map +1 -0
- package/dist/keywords/format.js +101 -0
- package/dist/keywords/format.js.map +1 -0
- package/dist/keywords/metaschemas2019.d.ts +3 -0
- package/dist/keywords/metaschemas2019.d.ts.map +1 -0
- package/dist/keywords/metaschemas2019.js +406 -0
- package/dist/keywords/metaschemas2019.js.map +1 -0
- package/dist/keywords/metaschemas2020.d.ts +3 -0
- package/dist/keywords/metaschemas2020.d.ts.map +1 -0
- package/dist/keywords/metaschemas2020.js +441 -0
- package/dist/keywords/metaschemas2020.js.map +1 -0
- package/dist/keywords/metaschemas6.d.ts +3 -0
- package/dist/keywords/metaschemas6.d.ts.map +1 -0
- package/dist/keywords/metaschemas6.js +221 -0
- package/dist/keywords/metaschemas6.js.map +1 -0
- package/dist/keywords/metaschemas7.d.ts +3 -0
- package/dist/keywords/metaschemas7.d.ts.map +1 -0
- package/dist/keywords/metaschemas7.js +248 -0
- package/dist/keywords/metaschemas7.js.map +1 -0
- package/dist/keywords/unevaluated.d.ts +14 -0
- package/dist/keywords/unevaluated.d.ts.map +1 -0
- package/dist/keywords/unevaluated.js +296 -0
- package/dist/keywords/unevaluated.js.map +1 -0
- package/dist/keywords/validation.d.ts +21 -0
- package/dist/keywords/validation.d.ts.map +1 -0
- package/dist/keywords/validation.js +332 -0
- package/dist/keywords/validation.js.map +1 -0
- package/dist/keywords/vocab2019.d.ts +36 -0
- package/dist/keywords/vocab2019.d.ts.map +1 -0
- package/dist/keywords/vocab2019.js +585 -0
- package/dist/keywords/vocab2019.js.map +1 -0
- package/dist/keywords/vocab2020.d.ts +20 -0
- package/dist/keywords/vocab2020.d.ts.map +1 -0
- package/dist/keywords/vocab2020.js +55 -0
- package/dist/keywords/vocab2020.js.map +1 -0
- package/dist/keywords/vocab7.d.ts +46 -0
- package/dist/keywords/vocab7.d.ts.map +1 -0
- package/dist/keywords/vocab7.js +305 -0
- package/dist/keywords/vocab7.js.map +1 -0
- package/dist/loader.d.ts +41 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +13 -0
- package/dist/loader.js.map +1 -0
- package/dist/lowering.d.ts +371 -0
- package/dist/lowering.d.ts.map +1 -0
- package/dist/lowering.js +59 -0
- package/dist/lowering.js.map +1 -0
- package/dist/output.d.ts +251 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +357 -0
- package/dist/output.js.map +1 -0
- package/dist/records.d.ts +71 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +150 -0
- package/dist/records.js.map +1 -0
- package/dist/ref.d.ts +10 -0
- package/dist/ref.d.ts.map +1 -0
- package/dist/ref.js +5 -0
- package/dist/ref.js.map +1 -0
- package/dist/regex.d.ts +45 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +163 -0
- package/dist/regex.js.map +1 -0
- package/dist/registry.d.ts +141 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +407 -0
- package/dist/registry.js.map +1 -0
- package/dist/result.d.ts +109 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +112 -0
- package/dist/result.js.map +1 -0
- package/dist/uri.d.ts +18 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +27 -0
- package/dist/uri.js.map +1 -0
- package/dist/walk.d.ts +25 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +65 -0
- package/dist/walk.js.map +1 -0
- package/package.json +32 -0
- package/src/coverage.ts +88 -0
- package/src/cursor.ts +42 -0
- package/src/dialect.ts +428 -0
- package/src/engine.ts +741 -0
- package/src/index.ts +699 -0
- package/src/json.ts +137 -0
- package/src/keywords/applicator.ts +895 -0
- package/src/keywords/core.ts +168 -0
- package/src/keywords/format.ts +143 -0
- package/src/keywords/metaschemas2019.ts +410 -0
- package/src/keywords/metaschemas2020.ts +443 -0
- package/src/keywords/metaschemas6.ts +226 -0
- package/src/keywords/metaschemas7.ts +253 -0
- package/src/keywords/unevaluated.ts +329 -0
- package/src/keywords/validation.ts +627 -0
- package/src/keywords/vocab2019.ts +676 -0
- package/src/keywords/vocab2020.ts +82 -0
- package/src/keywords/vocab7.ts +393 -0
- package/src/loader.ts +60 -0
- package/src/lowering.ts +414 -0
- package/src/output.ts +651 -0
- package/src/records.ts +226 -0
- package/src/ref.ts +14 -0
- package/src/regex.ts +171 -0
- package/src/registry.ts +481 -0
- package/src/result.ts +252 -0
- package/src/uri.ts +35 -0
- package/src/walk.ts +88 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Assembly of the 2020-12 dialect from its vocabularies (DESIGN.md D2).
|
|
2
|
+
|
|
3
|
+
import { KeywordBehavior, DialectRegistry } from "../dialect.js";
|
|
4
|
+
import { coreVocabulary, VOCAB_CORE, annotationOnly } from "./core.js";
|
|
5
|
+
import { applicatorVocabulary, VOCAB_APPLICATOR } from "./applicator.js";
|
|
6
|
+
import { validationVocabulary, VOCAB_VALIDATION } from "./validation.js";
|
|
7
|
+
import { unevaluatedVocabulary, VOCAB_UNEVALUATED } from "./unevaluated.js";
|
|
8
|
+
import { registerDialect2019 } from "./vocab2019.js";
|
|
9
|
+
import { registerDialect07, registerDialect06 } from "./vocab7.js";
|
|
10
|
+
|
|
11
|
+
/** 2020-12 meta-data vocabulary URI. */
|
|
12
|
+
export const VOCAB_META_DATA =
|
|
13
|
+
"https://json-schema.org/draft/2020-12/vocab/meta-data";
|
|
14
|
+
/** 2020-12 format-annotation vocabulary URI. */
|
|
15
|
+
export const VOCAB_FORMAT_ANNOTATION =
|
|
16
|
+
"https://json-schema.org/draft/2020-12/vocab/format-annotation";
|
|
17
|
+
/** 2020-12 content vocabulary URI. */
|
|
18
|
+
export const VOCAB_CONTENT =
|
|
19
|
+
"https://json-schema.org/draft/2020-12/vocab/content";
|
|
20
|
+
|
|
21
|
+
/** 2020-12 dialect URI. */
|
|
22
|
+
export const DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
23
|
+
|
|
24
|
+
const metaDataVocabulary = Object.fromEntries(
|
|
25
|
+
[
|
|
26
|
+
"title",
|
|
27
|
+
"description",
|
|
28
|
+
"default",
|
|
29
|
+
"deprecated",
|
|
30
|
+
"readOnly",
|
|
31
|
+
"writeOnly",
|
|
32
|
+
"examples",
|
|
33
|
+
].map((name) => [name, annotationOnly(`${VOCAB_META_DATA}#${name}`)]),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for {@link registerStandardDialects} (M7): `format` replaces the
|
|
38
|
+
* default annotation-only `format` behavior in every standard dialect —
|
|
39
|
+
* the `assertFormats` best-effort configuration.
|
|
40
|
+
*/
|
|
41
|
+
export interface StandardDialectOptions {
|
|
42
|
+
format?: (id: string) => KeywordBehavior;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// content* keywords are never assertions.
|
|
46
|
+
const contentVocabulary = Object.fromEntries(
|
|
47
|
+
["contentMediaType", "contentEncoding", "contentSchema"].map((name) => [
|
|
48
|
+
name,
|
|
49
|
+
annotationOnly(`${VOCAB_CONTENT}#${name}`),
|
|
50
|
+
]),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/** Registers the 2020-12, 2019-09, draft-07, and draft-06 vocabularies and dialects. */
|
|
54
|
+
export function registerStandardDialects(
|
|
55
|
+
registry: DialectRegistry,
|
|
56
|
+
options: StandardDialectOptions = {},
|
|
57
|
+
): void {
|
|
58
|
+
const format = options.format ?? annotationOnly;
|
|
59
|
+
registry.registerVocabulary(VOCAB_CORE, coreVocabulary);
|
|
60
|
+
registry.registerVocabulary(VOCAB_APPLICATOR, applicatorVocabulary);
|
|
61
|
+
registry.registerVocabulary(VOCAB_VALIDATION, validationVocabulary);
|
|
62
|
+
registry.registerVocabulary(VOCAB_UNEVALUATED, unevaluatedVocabulary);
|
|
63
|
+
registry.registerVocabulary(VOCAB_META_DATA, metaDataVocabulary);
|
|
64
|
+
registry.registerVocabulary(VOCAB_FORMAT_ANNOTATION, {
|
|
65
|
+
format: format(`${VOCAB_FORMAT_ANNOTATION}#format`),
|
|
66
|
+
});
|
|
67
|
+
registry.registerVocabulary(VOCAB_CONTENT, contentVocabulary);
|
|
68
|
+
|
|
69
|
+
registry.registerDialect(DIALECT_2020_12, [
|
|
70
|
+
VOCAB_CORE,
|
|
71
|
+
VOCAB_APPLICATOR,
|
|
72
|
+
VOCAB_VALIDATION,
|
|
73
|
+
VOCAB_UNEVALUATED,
|
|
74
|
+
VOCAB_META_DATA,
|
|
75
|
+
VOCAB_FORMAT_ANNOTATION,
|
|
76
|
+
VOCAB_CONTENT,
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
registerDialect2019(registry, format);
|
|
80
|
+
registerDialect07(registry, format);
|
|
81
|
+
registerDialect06(registry, format);
|
|
82
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// draft-07 and draft-06 dialects (DESIGN.md M4-B, D11, D18). These drafts
|
|
2
|
+
// predate `$vocabulary`, so there are no official vocabulary URIs to assemble
|
|
3
|
+
// from (contrast vocab2019.ts) — the URIs minted below are registry
|
|
4
|
+
// identities only (D2/D18), never exposed as spec-meaningful vocabulary
|
|
5
|
+
// documents. draft-06 is a strict keyword subset of draft-07 (D11 amendment),
|
|
6
|
+
// so its vocabulary objects are built by omission from the draft-07 ones.
|
|
7
|
+
|
|
8
|
+
import { isObject, JsonValue } from "../json.js";
|
|
9
|
+
import {
|
|
10
|
+
DialectRegistry,
|
|
11
|
+
KeywordBehavior,
|
|
12
|
+
StaticFacts,
|
|
13
|
+
SubschemaApplication,
|
|
14
|
+
identifiersLegacy,
|
|
15
|
+
} from "../dialect.js";
|
|
16
|
+
import { childCursor } from "../cursor.js";
|
|
17
|
+
import { LowerStmt, lowerIR } from "../lowering.js";
|
|
18
|
+
import {
|
|
19
|
+
$ref,
|
|
20
|
+
structural,
|
|
21
|
+
annotationOnly,
|
|
22
|
+
mapPositions,
|
|
23
|
+
SELF,
|
|
24
|
+
} from "./core.js";
|
|
25
|
+
import {
|
|
26
|
+
allOf,
|
|
27
|
+
anyOf,
|
|
28
|
+
oneOf,
|
|
29
|
+
not,
|
|
30
|
+
ifKeyword,
|
|
31
|
+
conditionalBranch,
|
|
32
|
+
properties,
|
|
33
|
+
patternProperties,
|
|
34
|
+
propertyNames,
|
|
35
|
+
additionalProperties,
|
|
36
|
+
} from "./applicator.js";
|
|
37
|
+
import { items2019, additionalItems } from "./vocab2019.js";
|
|
38
|
+
import { validationVocabulary } from "./validation.js";
|
|
39
|
+
|
|
40
|
+
/** draft-07 core vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
41
|
+
export const VOCAB_CORE_07 = "urn:jse:vocab:draft-07:core";
|
|
42
|
+
/** draft-07 applicator vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
43
|
+
export const VOCAB_APPLICATOR_07 = "urn:jse:vocab:draft-07:applicator";
|
|
44
|
+
/** draft-07 validation vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
45
|
+
export const VOCAB_VALIDATION_07 = "urn:jse:vocab:draft-07:validation";
|
|
46
|
+
/** draft-07 meta-data vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
47
|
+
export const VOCAB_META_DATA_07 = "urn:jse:vocab:draft-07:meta-data";
|
|
48
|
+
/** draft-07 format vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
49
|
+
export const VOCAB_FORMAT_07 = "urn:jse:vocab:draft-07:format";
|
|
50
|
+
/** draft-07 content vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
51
|
+
export const VOCAB_CONTENT_07 = "urn:jse:vocab:draft-07:content";
|
|
52
|
+
|
|
53
|
+
/** draft-06 core vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
54
|
+
export const VOCAB_CORE_06 = "urn:jse:vocab:draft-06:core";
|
|
55
|
+
/** draft-06 applicator vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
56
|
+
export const VOCAB_APPLICATOR_06 = "urn:jse:vocab:draft-06:applicator";
|
|
57
|
+
/** draft-06 validation vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
58
|
+
export const VOCAB_VALIDATION_06 = "urn:jse:vocab:draft-06:validation";
|
|
59
|
+
/** draft-06 meta-data vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
60
|
+
export const VOCAB_META_DATA_06 = "urn:jse:vocab:draft-06:meta-data";
|
|
61
|
+
/** draft-06 format vocabulary identity (registry-internal; not a spec-meaningful URI, D2/D18). */
|
|
62
|
+
export const VOCAB_FORMAT_06 = "urn:jse:vocab:draft-06:format";
|
|
63
|
+
|
|
64
|
+
/** draft-07 dialect URI. */
|
|
65
|
+
export const DIALECT_DRAFT_07 = "http://json-schema.org/draft-07/schema";
|
|
66
|
+
/** draft-06 dialect URI. */
|
|
67
|
+
export const DIALECT_DRAFT_06 = "http://json-schema.org/draft-06/schema";
|
|
68
|
+
|
|
69
|
+
const id07 = (name: string): string => `${VOCAB_APPLICATOR_07}#${name}`;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `contains` (draft-07/06): unlike 2020-12's `contains`, these drafts have
|
|
73
|
+
* no `minContains`/`maxContains` keywords at all — any occurrence of those
|
|
74
|
+
* names is just an unknown (annotation-only) keyword, never a sibling
|
|
75
|
+
* `contains` reads. So the count assertion is unconditionally "at least 1",
|
|
76
|
+
* with no min/max reads.
|
|
77
|
+
*/
|
|
78
|
+
export const containsLegacy: KeywordBehavior = {
|
|
79
|
+
id: id07("contains"),
|
|
80
|
+
// Per-item probes don't individually assert (the count does) — same
|
|
81
|
+
// asserts:false shape as 2020-12 contains' childSweep application.
|
|
82
|
+
analyze: (): StaticFacts => ({
|
|
83
|
+
...SELF,
|
|
84
|
+
applications: [
|
|
85
|
+
{ path: [], mode: "childSweep", conditional: false, asserts: false },
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
// Unconditional "at least 1" — countRange with no upper bound, matching
|
|
89
|
+
// evaluate()'s fixed message exactly (no minContains/maxContains reads
|
|
90
|
+
// exist in these drafts, so unlike 2020-12 `contains` there is no tally
|
|
91
|
+
// or params in the failure text).
|
|
92
|
+
lower: (_value, lctx) => {
|
|
93
|
+
const b = lctx.binding();
|
|
94
|
+
lctx.emit(
|
|
95
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "array"), [
|
|
96
|
+
{
|
|
97
|
+
kind: "countRange",
|
|
98
|
+
target: lctx.instance,
|
|
99
|
+
binding: b,
|
|
100
|
+
countWhen: {
|
|
101
|
+
kind: "applyExpr",
|
|
102
|
+
apply: {
|
|
103
|
+
path: [],
|
|
104
|
+
cursor: {
|
|
105
|
+
kind: "child",
|
|
106
|
+
of: { kind: "here" },
|
|
107
|
+
segment: { kind: "binding", id: b },
|
|
108
|
+
},
|
|
109
|
+
fold: "discard",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
min: 1,
|
|
113
|
+
max: Infinity,
|
|
114
|
+
outOfRangeMessage: ["no item matches the contains subschema"],
|
|
115
|
+
},
|
|
116
|
+
]),
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
evaluate: (_value, cursor, ctx) => {
|
|
120
|
+
if (!Array.isArray(cursor.value)) return true;
|
|
121
|
+
const matched: number[] = [];
|
|
122
|
+
for (let i = 0; i < cursor.value.length; i++) {
|
|
123
|
+
if (ctx.apply(["contains"], childCursor(cursor, i, cursor.value[i]!)))
|
|
124
|
+
matched.push(i);
|
|
125
|
+
}
|
|
126
|
+
if (matched.length === 0) {
|
|
127
|
+
ctx.error("no item matches the contains subschema");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Member values are exactly one of two shapes per the metaschema's `anyOf`
|
|
135
|
+
// (schema vs stringArray) — a member is never both, so reading the JS type
|
|
136
|
+
// of the value at evaluate/analyze time is sufficient to dispatch, no
|
|
137
|
+
// declared-shape tracking needed.
|
|
138
|
+
const isSchemaValue = (v: JsonValue): boolean =>
|
|
139
|
+
v === true || v === false || (isObject(v) && !Array.isArray(v));
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `dependencies` (draft-07/06): a single keyword folding what 2019-09+ split
|
|
143
|
+
* into `dependentRequired` (array-valued members) and `dependentSchemas`
|
|
144
|
+
* (schema-valued members).
|
|
145
|
+
*/
|
|
146
|
+
export const dependencies: KeywordBehavior = {
|
|
147
|
+
id: id07("dependencies"),
|
|
148
|
+
analyze: (value): StaticFacts => {
|
|
149
|
+
if (!isObject(value)) return {};
|
|
150
|
+
const schemaMembers = Object.entries(value).filter(([, v]) =>
|
|
151
|
+
isSchemaValue(v),
|
|
152
|
+
);
|
|
153
|
+
return {
|
|
154
|
+
subschemas: schemaMembers.map(([k]) => [k]),
|
|
155
|
+
// Only schema-valued members get an edge — array-valued members
|
|
156
|
+
// (dependentRequired's shape) have no subschema to plan/apply at all.
|
|
157
|
+
applications: schemaMembers.map(([k]): SubschemaApplication => ({
|
|
158
|
+
path: [k],
|
|
159
|
+
mode: "inPlace",
|
|
160
|
+
conditional: true,
|
|
161
|
+
asserts: true,
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
// Per-member dispatch is plan-time (the value shape is static, just like
|
|
166
|
+
// evaluate()'s Array.isArray/isSchemaValue reads): array-valued members
|
|
167
|
+
// lower like dependentRequired (validation.ts), schema-valued members
|
|
168
|
+
// like dependentSchemas (applicator.ts, path ["dependencies", name]).
|
|
169
|
+
lower: (value, lctx) => {
|
|
170
|
+
if (!isObject(value)) return;
|
|
171
|
+
const branches = Object.entries(value as Record<string, JsonValue>).flatMap(
|
|
172
|
+
([name, dep]): LowerStmt[] => {
|
|
173
|
+
if (Array.isArray(dep)) {
|
|
174
|
+
return [
|
|
175
|
+
lowerIR.when(
|
|
176
|
+
{ kind: "hasOwn", target: lctx.instance, key: name },
|
|
177
|
+
(dep as string[]).map((required) =>
|
|
178
|
+
lowerIR.when(
|
|
179
|
+
lowerIR.not({
|
|
180
|
+
kind: "hasOwn",
|
|
181
|
+
target: lctx.instance,
|
|
182
|
+
key: required,
|
|
183
|
+
}),
|
|
184
|
+
[
|
|
185
|
+
lowerIR.failWith(
|
|
186
|
+
{
|
|
187
|
+
property: lowerIR.constant(name),
|
|
188
|
+
missingProperty: lowerIR.constant(required),
|
|
189
|
+
},
|
|
190
|
+
`'${name}' requires '${required}' to be present`,
|
|
191
|
+
),
|
|
192
|
+
],
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
if (isSchemaValue(dep)) {
|
|
199
|
+
return [
|
|
200
|
+
lowerIR.when({ kind: "hasOwn", target: lctx.instance, key: name }, [
|
|
201
|
+
{
|
|
202
|
+
kind: "apply",
|
|
203
|
+
apply: {
|
|
204
|
+
path: [name],
|
|
205
|
+
cursor: { kind: "here" },
|
|
206
|
+
fold: "allMustPass",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
// Unreachable per the metaschema's anyOf (schema vs stringArray),
|
|
213
|
+
// mirrored defensively as evaluate()'s implicit no-op else branch.
|
|
214
|
+
return [];
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
if (branches.length === 0) return;
|
|
218
|
+
lctx.emit(lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), branches));
|
|
219
|
+
},
|
|
220
|
+
evaluate: (value, cursor, ctx) => {
|
|
221
|
+
if (!isObject(cursor.value)) return true;
|
|
222
|
+
const instance = cursor.value;
|
|
223
|
+
let ok = true;
|
|
224
|
+
for (const [name, dep] of Object.entries(
|
|
225
|
+
value as Record<string, JsonValue>,
|
|
226
|
+
)) {
|
|
227
|
+
if (!Object.hasOwn(instance, name)) continue;
|
|
228
|
+
if (Array.isArray(dep)) {
|
|
229
|
+
for (const required of dep as string[]) {
|
|
230
|
+
if (!Object.hasOwn(instance, required)) {
|
|
231
|
+
ctx.error(`'${name}' requires '${required}' to be present`, {
|
|
232
|
+
property: name,
|
|
233
|
+
missingProperty: required,
|
|
234
|
+
});
|
|
235
|
+
ok = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else if (isSchemaValue(dep)) {
|
|
239
|
+
if (!ctx.apply(["dependencies", name], cursor)) ok = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return ok;
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const core07Vocabulary: Record<string, KeywordBehavior> = {
|
|
247
|
+
$ref,
|
|
248
|
+
$id: structural(`${VOCAB_CORE_07}#$id`),
|
|
249
|
+
$schema: structural(`${VOCAB_CORE_07}#$schema`),
|
|
250
|
+
$comment: structural(`${VOCAB_CORE_07}#$comment`),
|
|
251
|
+
// definitions is draft-07/06's pre-$defs name for the same inert-container
|
|
252
|
+
// shape: subschemas exist for identification/registration but nothing
|
|
253
|
+
// evaluates them directly.
|
|
254
|
+
definitions: {
|
|
255
|
+
id: `${VOCAB_CORE_07}#definitions`,
|
|
256
|
+
structural: true,
|
|
257
|
+
analyze: mapPositions,
|
|
258
|
+
evaluate: () => true,
|
|
259
|
+
lower: () => {
|
|
260
|
+
/* contents are reachable only by reference */
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const applicator07Vocabulary: Record<string, KeywordBehavior> = {
|
|
266
|
+
allOf,
|
|
267
|
+
anyOf,
|
|
268
|
+
oneOf,
|
|
269
|
+
not,
|
|
270
|
+
if: ifKeyword,
|
|
271
|
+
then: conditionalBranch(id07("then"), "then", true),
|
|
272
|
+
else: conditionalBranch(id07("else"), "else", false),
|
|
273
|
+
dependencies,
|
|
274
|
+
properties,
|
|
275
|
+
patternProperties,
|
|
276
|
+
propertyNames,
|
|
277
|
+
additionalProperties,
|
|
278
|
+
items: items2019,
|
|
279
|
+
additionalItems,
|
|
280
|
+
contains: containsLegacy,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const metaData07Vocabulary = Object.fromEntries(
|
|
284
|
+
["title", "description", "default", "readOnly", "writeOnly", "examples"].map(
|
|
285
|
+
(name) => [name, annotationOnly(`${VOCAB_META_DATA_07}#${name}`)],
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const content07Vocabulary = Object.fromEntries(
|
|
290
|
+
["contentMediaType", "contentEncoding"].map((name) => [
|
|
291
|
+
name,
|
|
292
|
+
annotationOnly(`${VOCAB_CONTENT_07}#${name}`),
|
|
293
|
+
]),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// draft-06: draft-07 minus if/then/else, $comment, readOnly/writeOnly,
|
|
297
|
+
// contentMediaType/contentEncoding (D11 amendment — a compatible subset).
|
|
298
|
+
const { $comment: _dropComment, ...core06Rest } = core07Vocabulary;
|
|
299
|
+
const core06Vocabulary: Record<string, KeywordBehavior> = {
|
|
300
|
+
...core06Rest,
|
|
301
|
+
$id: structural(`${VOCAB_CORE_06}#$id`),
|
|
302
|
+
$schema: structural(`${VOCAB_CORE_06}#$schema`),
|
|
303
|
+
definitions: {
|
|
304
|
+
id: `${VOCAB_CORE_06}#definitions`,
|
|
305
|
+
structural: true,
|
|
306
|
+
analyze: mapPositions,
|
|
307
|
+
evaluate: () => true,
|
|
308
|
+
lower: () => {
|
|
309
|
+
/* contents are reachable only by reference */
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const {
|
|
315
|
+
if: _dropIf,
|
|
316
|
+
then: _dropThen,
|
|
317
|
+
else: _dropElse,
|
|
318
|
+
...applicator06Rest
|
|
319
|
+
} = applicator07Vocabulary;
|
|
320
|
+
const applicator06Vocabulary: Record<string, KeywordBehavior> = {
|
|
321
|
+
...applicator06Rest,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const {
|
|
325
|
+
readOnly: _dropReadOnly,
|
|
326
|
+
writeOnly: _dropWriteOnly,
|
|
327
|
+
...metaData06Rest
|
|
328
|
+
} = metaData07Vocabulary;
|
|
329
|
+
const metaData06Vocabulary = metaData06Rest;
|
|
330
|
+
|
|
331
|
+
// draft-07/06 predate minContains/maxContains/dependentRequired: those names
|
|
332
|
+
// are unknown keywords (annotation-only) in these dialects — registering the
|
|
333
|
+
// 2020-12 behaviors would silently enforce assertions these drafts don't have.
|
|
334
|
+
const {
|
|
335
|
+
minContains: _dropMinContains,
|
|
336
|
+
maxContains: _dropMaxContains,
|
|
337
|
+
dependentRequired: _dropDependentRequired,
|
|
338
|
+
...validationLegacyVocabulary
|
|
339
|
+
} = validationVocabulary;
|
|
340
|
+
|
|
341
|
+
/** Registers the draft-07 vocabularies and dialect. */
|
|
342
|
+
export function registerDialect07(
|
|
343
|
+
registry: DialectRegistry,
|
|
344
|
+
format: (id: string) => KeywordBehavior = annotationOnly,
|
|
345
|
+
): void {
|
|
346
|
+
registry.registerVocabulary(VOCAB_CORE_07, core07Vocabulary);
|
|
347
|
+
registry.registerVocabulary(VOCAB_APPLICATOR_07, applicator07Vocabulary);
|
|
348
|
+
registry.registerVocabulary(VOCAB_VALIDATION_07, validationLegacyVocabulary);
|
|
349
|
+
registry.registerVocabulary(VOCAB_META_DATA_07, metaData07Vocabulary);
|
|
350
|
+
registry.registerVocabulary(VOCAB_FORMAT_07, {
|
|
351
|
+
format: format(`${VOCAB_FORMAT_07}#format`),
|
|
352
|
+
});
|
|
353
|
+
registry.registerVocabulary(VOCAB_CONTENT_07, content07Vocabulary);
|
|
354
|
+
|
|
355
|
+
registry.registerDialect(
|
|
356
|
+
DIALECT_DRAFT_07,
|
|
357
|
+
[
|
|
358
|
+
VOCAB_CORE_07,
|
|
359
|
+
VOCAB_APPLICATOR_07,
|
|
360
|
+
VOCAB_VALIDATION_07,
|
|
361
|
+
VOCAB_META_DATA_07,
|
|
362
|
+
VOCAB_FORMAT_07,
|
|
363
|
+
VOCAB_CONTENT_07,
|
|
364
|
+
],
|
|
365
|
+
{ identifiers: identifiersLegacy, refIgnoresSiblings: true },
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Registers the draft-06 vocabularies and dialect. */
|
|
370
|
+
export function registerDialect06(
|
|
371
|
+
registry: DialectRegistry,
|
|
372
|
+
format: (id: string) => KeywordBehavior = annotationOnly,
|
|
373
|
+
): void {
|
|
374
|
+
registry.registerVocabulary(VOCAB_CORE_06, core06Vocabulary);
|
|
375
|
+
registry.registerVocabulary(VOCAB_APPLICATOR_06, applicator06Vocabulary);
|
|
376
|
+
registry.registerVocabulary(VOCAB_VALIDATION_06, validationLegacyVocabulary);
|
|
377
|
+
registry.registerVocabulary(VOCAB_META_DATA_06, metaData06Vocabulary);
|
|
378
|
+
registry.registerVocabulary(VOCAB_FORMAT_06, {
|
|
379
|
+
format: format(`${VOCAB_FORMAT_06}#format`),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
registry.registerDialect(
|
|
383
|
+
DIALECT_DRAFT_06,
|
|
384
|
+
[
|
|
385
|
+
VOCAB_CORE_06,
|
|
386
|
+
VOCAB_APPLICATOR_06,
|
|
387
|
+
VOCAB_VALIDATION_06,
|
|
388
|
+
VOCAB_META_DATA_06,
|
|
389
|
+
VOCAB_FORMAT_06,
|
|
390
|
+
],
|
|
391
|
+
{ identifiers: identifiersLegacy, refIgnoresSiblings: true },
|
|
392
|
+
);
|
|
393
|
+
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Resource loading types (DESIGN.md D7/D17). Loading is the only async
|
|
2
|
+
// boundary: loaders feed Engine.load/loadSchema, which register documents
|
|
3
|
+
// and their transitive references; compile and evaluate stay sync.
|
|
4
|
+
//
|
|
5
|
+
// D17 bridge: canonical schema locations are resource-rooted
|
|
6
|
+
// (`resourceUri#/pointer`), but loaders report source positions
|
|
7
|
+
// document-rooted, since a document can embed multiple resources via nested
|
|
8
|
+
// `$id`. The registry's documentLocation(resourceUri) gives the containing
|
|
9
|
+
// document plus the resource root's document-rooted pointer; a canonical
|
|
10
|
+
// location's document-rooted pointer is then
|
|
11
|
+
// `documentLocation(resourceUri).pointer + pointer`.
|
|
12
|
+
|
|
13
|
+
import { JsonValue } from "./json.js";
|
|
14
|
+
|
|
15
|
+
/** 1-based line/column; offset is 0-based when present. */
|
|
16
|
+
export interface SourcePosition {
|
|
17
|
+
line: number;
|
|
18
|
+
column: number;
|
|
19
|
+
offset?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A range between two source positions. */
|
|
23
|
+
export interface SourceSpan {
|
|
24
|
+
start: SourcePosition;
|
|
25
|
+
end: SourcePosition;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Key vs value spans (D17): diagnostics point at the key for missing/extra-
|
|
30
|
+
* property errors and at the value for type errors; SARIF/LSP-class
|
|
31
|
+
* consumers need both.
|
|
32
|
+
*/
|
|
33
|
+
export interface SourceRange {
|
|
34
|
+
/** span of the member key, when the value is an object member */
|
|
35
|
+
key?: SourceSpan;
|
|
36
|
+
value: SourceSpan;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A document handed to the registry by a {@link SchemaLoader}. */
|
|
40
|
+
export interface LoadedDocument {
|
|
41
|
+
value: JsonValue;
|
|
42
|
+
/**
|
|
43
|
+
* Source range for a document-rooted JSON Pointer (D17). A function rather
|
|
44
|
+
* than a materialized map so loaders can keep a parse AST and resolve
|
|
45
|
+
* lazily; returning undefined for any pointer is always allowed.
|
|
46
|
+
*/
|
|
47
|
+
getRange?: (documentRootPointer: string) => SourceRange | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Fetches a schema document by URI. Returns undefined when this loader does not handle the URI. */
|
|
51
|
+
export type SchemaLoader = (
|
|
52
|
+
uri: string,
|
|
53
|
+
) => LoadedDocument | undefined | Promise<LoadedDocument | undefined>;
|
|
54
|
+
|
|
55
|
+
/** Where a canonical schema location physically lives (D17 bridge). */
|
|
56
|
+
export interface SourceLocation {
|
|
57
|
+
documentUri: string;
|
|
58
|
+
pointer: string;
|
|
59
|
+
range?: SourceRange;
|
|
60
|
+
}
|