@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,895 @@
|
|
|
1
|
+
// Applicator vocabulary.
|
|
2
|
+
//
|
|
3
|
+
// EXEMPLARS: `anyOf` (in-place applicator class — subschemas applied at the
|
|
4
|
+
// same cursor; see engine.ts for channel merge/discard semantics) and
|
|
5
|
+
// `properties` (child applicator class — child cursors, produces its
|
|
6
|
+
// evaluated-name annotation).
|
|
7
|
+
|
|
8
|
+
import { JsonValue, isObject } from "../json.js";
|
|
9
|
+
import {
|
|
10
|
+
KeywordBehavior,
|
|
11
|
+
StaticFacts,
|
|
12
|
+
SubschemaApplication,
|
|
13
|
+
} from "../dialect.js";
|
|
14
|
+
import { childCursor } from "../cursor.js";
|
|
15
|
+
import { LowerExpr, lowerIR } from "../lowering.js";
|
|
16
|
+
import { SELF, mapPositions } from "./core.js";
|
|
17
|
+
|
|
18
|
+
/** 2020-12 applicator vocabulary URI. */
|
|
19
|
+
export const VOCAB_APPLICATOR =
|
|
20
|
+
"https://json-schema.org/draft/2020-12/vocab/applicator";
|
|
21
|
+
|
|
22
|
+
const id = (name: string): string => `${VOCAB_APPLICATOR}#${name}`;
|
|
23
|
+
|
|
24
|
+
// Facts helpers (D9a/planner edges). `conditional` marks alternatives whose
|
|
25
|
+
// application depends on runtime branching, not merely instance shape.
|
|
26
|
+
const arrayPositions = (value: JsonValue, conditional = false): StaticFacts =>
|
|
27
|
+
Array.isArray(value)
|
|
28
|
+
? {
|
|
29
|
+
subschemas: value.map((_, i) => [i]),
|
|
30
|
+
applications: value.map((_, i): SubschemaApplication => ({
|
|
31
|
+
path: [i],
|
|
32
|
+
mode: "inPlace",
|
|
33
|
+
conditional,
|
|
34
|
+
asserts: true,
|
|
35
|
+
})),
|
|
36
|
+
}
|
|
37
|
+
: {};
|
|
38
|
+
const selfApplication = (
|
|
39
|
+
mode: SubschemaApplication["mode"],
|
|
40
|
+
conditional: boolean,
|
|
41
|
+
asserts: boolean,
|
|
42
|
+
): StaticFacts => ({
|
|
43
|
+
...SELF,
|
|
44
|
+
applications: [{ path: [], mode, conditional, asserts }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** `allOf`: every subschema must match, at the same cursor. */
|
|
48
|
+
export const allOf: KeywordBehavior = {
|
|
49
|
+
id: id("allOf"),
|
|
50
|
+
analyze: (value) => arrayPositions(value),
|
|
51
|
+
lower: (value, lctx) => {
|
|
52
|
+
(value as JsonValue[]).forEach((_, i) => {
|
|
53
|
+
lctx.emit({
|
|
54
|
+
kind: "apply",
|
|
55
|
+
apply: { path: [i], cursor: { kind: "here" }, fold: "allMustPass" },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
evaluate: (value, cursor, ctx) => {
|
|
60
|
+
let ok = true;
|
|
61
|
+
(value as JsonValue[]).forEach((_, i) => {
|
|
62
|
+
if (!ctx.apply(["allOf", i], cursor)) ok = false;
|
|
63
|
+
});
|
|
64
|
+
return ok;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** EXEMPLAR (in-place applicator class): every branch runs, even after a match (DESIGN.md §4 rule 6; see engine.ts). */
|
|
69
|
+
export const anyOf: KeywordBehavior = {
|
|
70
|
+
id: id("anyOf"),
|
|
71
|
+
analyze: (value) => arrayPositions(value, true),
|
|
72
|
+
lower: (value, lctx) => {
|
|
73
|
+
const schemas = value as JsonValue[];
|
|
74
|
+
// An empty anyOf can never match (evaluate()'s `ok` starts false and no
|
|
75
|
+
// iteration can flip it) — keywordStatements only emits a check when it
|
|
76
|
+
// sees at least one anyMayPass apply, so the empty case needs its own
|
|
77
|
+
// unconditional failure.
|
|
78
|
+
if (schemas.length === 0) {
|
|
79
|
+
lctx.emit(lowerIR.fail("no branch matched"));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
schemas.forEach((_, i) => {
|
|
83
|
+
lctx.emit({
|
|
84
|
+
kind: "apply",
|
|
85
|
+
apply: { path: [i], cursor: { kind: "here" }, fold: "anyMayPass" },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
lctx.emit({ kind: "combineCheck", message: ["no branch matched"] });
|
|
89
|
+
},
|
|
90
|
+
evaluate: (value, cursor, ctx) => {
|
|
91
|
+
let ok = false;
|
|
92
|
+
(value as JsonValue[]).forEach((_, i) => {
|
|
93
|
+
if (ctx.apply(["anyOf", i], cursor)) ok = true;
|
|
94
|
+
});
|
|
95
|
+
// Mutated inside the forEach closure above; the checker doesn't track that
|
|
96
|
+
// reassignment for a read after the callback returns.
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
98
|
+
if (!ok) ctx.error("no branch matched");
|
|
99
|
+
return ok;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** `oneOf`: exactly one subschema must match, at the same cursor. */
|
|
104
|
+
export const oneOf: KeywordBehavior = {
|
|
105
|
+
id: id("oneOf"),
|
|
106
|
+
analyze: (value) => arrayPositions(value, true),
|
|
107
|
+
lower: (value, lctx) => {
|
|
108
|
+
const schemas = value as JsonValue[];
|
|
109
|
+
// An empty oneOf can never match exactly one branch (count stays 0) —
|
|
110
|
+
// same empty-run gap as anyOf above.
|
|
111
|
+
if (schemas.length === 0) {
|
|
112
|
+
lctx.emit(
|
|
113
|
+
lowerIR.failWith(
|
|
114
|
+
{ passing: lowerIR.constant([]) },
|
|
115
|
+
"matched 0 branches, expected exactly 1",
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
schemas.forEach((_, i) => {
|
|
121
|
+
lctx.emit({
|
|
122
|
+
kind: "apply",
|
|
123
|
+
apply: { path: [i], cursor: { kind: "here" }, fold: "exactlyOne" },
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
lctx.emit({
|
|
127
|
+
kind: "combineCheck",
|
|
128
|
+
message: ["matched ", { kind: "tally" }, " branches, expected exactly 1"],
|
|
129
|
+
params: { passing: { kind: "tallyList" } },
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
evaluate: (value, cursor, ctx) => {
|
|
133
|
+
const passing: number[] = [];
|
|
134
|
+
(value as JsonValue[]).forEach((_, i) => {
|
|
135
|
+
if (ctx.apply(["oneOf", i], cursor)) passing.push(i);
|
|
136
|
+
});
|
|
137
|
+
if (passing.length !== 1)
|
|
138
|
+
ctx.error(
|
|
139
|
+
`matched ${String(passing.length)} branches, expected exactly 1`,
|
|
140
|
+
{
|
|
141
|
+
passing,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
return passing.length === 1;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** `not`: the subschema must not match. */
|
|
149
|
+
export const not: KeywordBehavior = {
|
|
150
|
+
id: id("not"),
|
|
151
|
+
analyze: () => ({
|
|
152
|
+
...SELF,
|
|
153
|
+
applications: [
|
|
154
|
+
// inverted: success fails `not`, so this edge never contributes
|
|
155
|
+
// evaluated-coverage on the parent-success path (D9a).
|
|
156
|
+
{
|
|
157
|
+
path: [],
|
|
158
|
+
mode: "inPlace",
|
|
159
|
+
conditional: false,
|
|
160
|
+
asserts: true,
|
|
161
|
+
inverted: true,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}),
|
|
165
|
+
lower: (_value, lctx) => {
|
|
166
|
+
lctx.emit({
|
|
167
|
+
kind: "apply",
|
|
168
|
+
apply: {
|
|
169
|
+
path: [],
|
|
170
|
+
cursor: { kind: "here" },
|
|
171
|
+
fold: "negate",
|
|
172
|
+
message: ["must not match the subschema"],
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
evaluate: (_value, cursor, ctx) => {
|
|
177
|
+
if (!ctx.apply(["not"], cursor)) return true;
|
|
178
|
+
ctx.error("must not match the subschema");
|
|
179
|
+
return false;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `if`: applies its subschema and communicates the outcome to `then`/`else`
|
|
185
|
+
* as dependency data (draft-03 Appendix D); the keyword itself always
|
|
186
|
+
* accepts, so a rejecting condition's errors are irrelevant (§12.2). The
|
|
187
|
+
* compiled form realizes the dependency structurally — the hoisted condition
|
|
188
|
+
* selects the sibling apply — so `then`/`else` lower nothing and the planner
|
|
189
|
+
* edges for both branches are declared here.
|
|
190
|
+
*/
|
|
191
|
+
export const ifKeyword: KeywordBehavior = {
|
|
192
|
+
id: id("if"),
|
|
193
|
+
// The sibling-context exemplar for analyze(): the branch edges depend on
|
|
194
|
+
// which siblings are present.
|
|
195
|
+
analyze: (_value, context) => {
|
|
196
|
+
const applications: SubschemaApplication[] = [
|
|
197
|
+
{ path: [], mode: "inPlace", conditional: false, asserts: false },
|
|
198
|
+
];
|
|
199
|
+
for (const branch of ["then", "else"] as const) {
|
|
200
|
+
if (context && Object.hasOwn(context.schema, branch)) {
|
|
201
|
+
applications.push({
|
|
202
|
+
path: [],
|
|
203
|
+
sibling: branch,
|
|
204
|
+
mode: "inPlace",
|
|
205
|
+
conditional: true,
|
|
206
|
+
asserts: true,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { ...SELF, produces: [id("if")], applications };
|
|
211
|
+
},
|
|
212
|
+
lower: (_value, lctx) => {
|
|
213
|
+
const hasThen = Object.hasOwn(lctx.schema, "then");
|
|
214
|
+
const hasElse = Object.hasOwn(lctx.schema, "else");
|
|
215
|
+
const condition: LowerExpr = {
|
|
216
|
+
kind: "applyExpr",
|
|
217
|
+
apply: { path: [], cursor: { kind: "here" }, fold: "discard" },
|
|
218
|
+
};
|
|
219
|
+
lctx.emit(
|
|
220
|
+
lowerIR.when(
|
|
221
|
+
condition,
|
|
222
|
+
hasThen
|
|
223
|
+
? [
|
|
224
|
+
{
|
|
225
|
+
kind: "apply",
|
|
226
|
+
apply: {
|
|
227
|
+
path: [],
|
|
228
|
+
sibling: "then",
|
|
229
|
+
cursor: { kind: "here" },
|
|
230
|
+
fold: "allMustPass",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
]
|
|
234
|
+
: [],
|
|
235
|
+
hasElse
|
|
236
|
+
? [
|
|
237
|
+
{
|
|
238
|
+
kind: "apply",
|
|
239
|
+
apply: {
|
|
240
|
+
path: [],
|
|
241
|
+
sibling: "else",
|
|
242
|
+
cursor: { kind: "here" },
|
|
243
|
+
fold: "allMustPass",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
: [],
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
evaluate: (_value, cursor, ctx) => {
|
|
252
|
+
ctx.produce(ctx.apply(["if"], cursor));
|
|
253
|
+
return true;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* `then` / `else`: consume `if`'s outcome; the selected branch applies its
|
|
259
|
+
* subschema and reports that verdict as its own, the other is inert. Without
|
|
260
|
+
* a sibling `if` there is no outcome and the keyword accepts.
|
|
261
|
+
*/
|
|
262
|
+
export const conditionalBranch = (
|
|
263
|
+
branchId: string,
|
|
264
|
+
name: "then" | "else",
|
|
265
|
+
when: boolean,
|
|
266
|
+
): KeywordBehavior => ({
|
|
267
|
+
id: branchId,
|
|
268
|
+
analyze: () => ({ ...SELF, consumes: [id("if")] }),
|
|
269
|
+
lower: () => {
|
|
270
|
+
/* if's lowering applies this sibling on the hoisted outcome */
|
|
271
|
+
},
|
|
272
|
+
evaluate: (_value, cursor, ctx) => {
|
|
273
|
+
const outcome = ctx.visible([id("if")], "adjacent");
|
|
274
|
+
if (outcome.length === 0 || outcome[0]!.data !== when) return true;
|
|
275
|
+
return ctx.apply([name], cursor);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
export const thenKeyword: KeywordBehavior = conditionalBranch(
|
|
280
|
+
id("then"),
|
|
281
|
+
"then",
|
|
282
|
+
true,
|
|
283
|
+
);
|
|
284
|
+
export const elseKeyword: KeywordBehavior = conditionalBranch(
|
|
285
|
+
id("else"),
|
|
286
|
+
"else",
|
|
287
|
+
false,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
/** `dependentSchemas`: applies a named subschema when the property is present. */
|
|
291
|
+
export const dependentSchemas: KeywordBehavior = {
|
|
292
|
+
id: id("dependentSchemas"),
|
|
293
|
+
analyze: (value) => ({
|
|
294
|
+
...mapPositions(value),
|
|
295
|
+
applications: isObject(value)
|
|
296
|
+
? Object.keys(value).map((k): SubschemaApplication => ({
|
|
297
|
+
path: [k],
|
|
298
|
+
mode: "inPlace",
|
|
299
|
+
conditional: true,
|
|
300
|
+
asserts: true,
|
|
301
|
+
}))
|
|
302
|
+
: [],
|
|
303
|
+
}),
|
|
304
|
+
lower: (value, lctx) => {
|
|
305
|
+
if (!isObject(value)) return;
|
|
306
|
+
lctx.emit(
|
|
307
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
|
|
308
|
+
...Object.keys(value).map((k) =>
|
|
309
|
+
lowerIR.when({ kind: "hasOwn", target: lctx.instance, key: k }, [
|
|
310
|
+
{
|
|
311
|
+
kind: "apply",
|
|
312
|
+
apply: {
|
|
313
|
+
path: [k],
|
|
314
|
+
cursor: { kind: "here" },
|
|
315
|
+
fold: "allMustPass",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
]),
|
|
319
|
+
),
|
|
320
|
+
]),
|
|
321
|
+
);
|
|
322
|
+
},
|
|
323
|
+
evaluate: (value, cursor, ctx) => {
|
|
324
|
+
if (!isObject(cursor.value)) return true;
|
|
325
|
+
let ok = true;
|
|
326
|
+
for (const name of Object.keys(value as Record<string, JsonValue>)) {
|
|
327
|
+
if (
|
|
328
|
+
Object.hasOwn(cursor.value, name) &&
|
|
329
|
+
!ctx.apply(["dependentSchemas", name], cursor)
|
|
330
|
+
) {
|
|
331
|
+
ok = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return ok;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** EXEMPLAR (child applicator class): child cursors, produces the matched property names. */
|
|
339
|
+
export const properties: KeywordBehavior = {
|
|
340
|
+
id: id("properties"),
|
|
341
|
+
analyze: (value) => ({
|
|
342
|
+
...mapPositions(value),
|
|
343
|
+
produces: [id("properties")],
|
|
344
|
+
evaluatesNames: isObject(value)
|
|
345
|
+
? { kind: "names", names: Object.keys(value) }
|
|
346
|
+
: { kind: "names", names: [] },
|
|
347
|
+
applications: isObject(value)
|
|
348
|
+
? Object.keys(value).map((k): SubschemaApplication => ({
|
|
349
|
+
path: [k],
|
|
350
|
+
mode: "childByKey",
|
|
351
|
+
conditional: false,
|
|
352
|
+
asserts: true,
|
|
353
|
+
}))
|
|
354
|
+
: [],
|
|
355
|
+
}),
|
|
356
|
+
lower: (value, lctx) => {
|
|
357
|
+
if (!isObject(value)) return;
|
|
358
|
+
for (const name of Object.keys(value)) {
|
|
359
|
+
lctx.emit(
|
|
360
|
+
lowerIR.when(
|
|
361
|
+
lowerIR.and(lowerIR.typeIs(lctx.instance, "object"), {
|
|
362
|
+
kind: "hasOwn",
|
|
363
|
+
target: lctx.instance,
|
|
364
|
+
key: name,
|
|
365
|
+
}),
|
|
366
|
+
[
|
|
367
|
+
{
|
|
368
|
+
kind: "apply",
|
|
369
|
+
apply: {
|
|
370
|
+
path: [name],
|
|
371
|
+
cursor: { kind: "child", of: { kind: "here" }, segment: name },
|
|
372
|
+
fold: "allMustPass",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
// Produce iff the instance is an object (an empty array otherwise), matching
|
|
380
|
+
// evaluate()'s object-type guard before ctx.produce.
|
|
381
|
+
lctx.emit(
|
|
382
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
|
|
383
|
+
{ kind: "produce", value: { kind: "collectedNames" } },
|
|
384
|
+
]),
|
|
385
|
+
);
|
|
386
|
+
},
|
|
387
|
+
evaluate: (value, cursor, ctx) => {
|
|
388
|
+
if (!isObject(cursor.value)) return true;
|
|
389
|
+
let ok = true;
|
|
390
|
+
const matched: string[] = [];
|
|
391
|
+
for (const name of Object.keys(value as Record<string, JsonValue>)) {
|
|
392
|
+
if (Object.hasOwn(cursor.value, name)) {
|
|
393
|
+
matched.push(name);
|
|
394
|
+
if (
|
|
395
|
+
!ctx.apply(
|
|
396
|
+
["properties", name],
|
|
397
|
+
childCursor(cursor, name, cursor.value[name]!),
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
ok = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Dependency data comes only from an accepting keyword (draft-03
|
|
404
|
+
// Appendix D): a rejecting producer communicates nothing.
|
|
405
|
+
if (ok) ctx.produce(matched);
|
|
406
|
+
return ok;
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/** `patternProperties`: applies to properties whose name matches a pattern; produces the matched names. */
|
|
411
|
+
export const patternProperties: KeywordBehavior = {
|
|
412
|
+
id: id("patternProperties"),
|
|
413
|
+
// Property-name patterns are declared as regexes so `rejectUnsafeRegex`
|
|
414
|
+
// can screen them at registration (see regex.ts).
|
|
415
|
+
analyze: (value) => ({
|
|
416
|
+
...mapPositions(value),
|
|
417
|
+
regexes: isObject(value) ? Object.keys(value) : [],
|
|
418
|
+
produces: [id("patternProperties")],
|
|
419
|
+
evaluatesNames: {
|
|
420
|
+
kind: "patterns",
|
|
421
|
+
patterns: isObject(value) ? Object.keys(value) : [],
|
|
422
|
+
},
|
|
423
|
+
applications: isObject(value)
|
|
424
|
+
? Object.keys(value).map((k): SubschemaApplication => ({
|
|
425
|
+
path: [k],
|
|
426
|
+
mode: "childSweep",
|
|
427
|
+
conditional: false,
|
|
428
|
+
asserts: true,
|
|
429
|
+
}))
|
|
430
|
+
: [],
|
|
431
|
+
}),
|
|
432
|
+
lower: (value, lctx) => {
|
|
433
|
+
if (!isObject(value)) return;
|
|
434
|
+
for (const pattern of Object.keys(value)) {
|
|
435
|
+
const b = lctx.binding();
|
|
436
|
+
lctx.emit(
|
|
437
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
|
|
438
|
+
{
|
|
439
|
+
kind: "forEachKey",
|
|
440
|
+
target: lctx.instance,
|
|
441
|
+
binding: b,
|
|
442
|
+
body: [
|
|
443
|
+
lowerIR.when(
|
|
444
|
+
lowerIR.regexTest(pattern, { kind: "binding", id: b }),
|
|
445
|
+
[
|
|
446
|
+
{
|
|
447
|
+
kind: "apply",
|
|
448
|
+
apply: {
|
|
449
|
+
path: [pattern],
|
|
450
|
+
cursor: {
|
|
451
|
+
kind: "child",
|
|
452
|
+
of: { kind: "here" },
|
|
453
|
+
segment: { kind: "binding", id: b },
|
|
454
|
+
},
|
|
455
|
+
fold: "allMustPass",
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
),
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
]),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
// Produce iff the instance is an object (an empty array otherwise), matching
|
|
466
|
+
// evaluate()'s object-type guard before ctx.produce.
|
|
467
|
+
lctx.emit(
|
|
468
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
|
|
469
|
+
{ kind: "produce", value: { kind: "collectedNames" } },
|
|
470
|
+
]),
|
|
471
|
+
);
|
|
472
|
+
},
|
|
473
|
+
evaluate: (value, cursor, ctx) => {
|
|
474
|
+
if (!isObject(cursor.value)) return true;
|
|
475
|
+
let ok = true;
|
|
476
|
+
const matched = new Set<string>();
|
|
477
|
+
for (const pattern of Object.keys(value as Record<string, JsonValue>)) {
|
|
478
|
+
const re = ctx.compileRegex(pattern);
|
|
479
|
+
for (const name of Object.keys(cursor.value)) {
|
|
480
|
+
if (re.test(name)) {
|
|
481
|
+
matched.add(name);
|
|
482
|
+
if (
|
|
483
|
+
!ctx.apply(
|
|
484
|
+
["patternProperties", pattern],
|
|
485
|
+
childCursor(cursor, name, cursor.value[name]!),
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
ok = false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (ok) ctx.produce([...matched]);
|
|
493
|
+
return ok;
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* `additionalProperties`: applies to properties not matched by sibling
|
|
499
|
+
* `properties`/`patternProperties` — statically derivable from the schema
|
|
500
|
+
* object, no channel involvement (contrast `unevaluatedProperties`).
|
|
501
|
+
*/
|
|
502
|
+
export const additionalProperties: KeywordBehavior = {
|
|
503
|
+
id: id("additionalProperties"),
|
|
504
|
+
// Post-success, the sibling trio covers every present name (D9a "all").
|
|
505
|
+
analyze: () => ({
|
|
506
|
+
...selfApplication("childSweep", false, true),
|
|
507
|
+
produces: [id("additionalProperties")],
|
|
508
|
+
evaluatesNames: { kind: "all" },
|
|
509
|
+
}),
|
|
510
|
+
lower: (_value, lctx) => {
|
|
511
|
+
const names = isObject(lctx.schema.properties)
|
|
512
|
+
? Object.keys(lctx.schema.properties)
|
|
513
|
+
: [];
|
|
514
|
+
const patterns = isObject(lctx.schema.patternProperties)
|
|
515
|
+
? Object.keys(lctx.schema.patternProperties)
|
|
516
|
+
: [];
|
|
517
|
+
const b = lctx.binding();
|
|
518
|
+
const covered: LowerExpr[] = [
|
|
519
|
+
...names.map((n): LowerExpr =>
|
|
520
|
+
lowerIR.cmp("===", { kind: "binding", id: b }, lowerIR.constant(n)),
|
|
521
|
+
),
|
|
522
|
+
...patterns.map((p): LowerExpr =>
|
|
523
|
+
lowerIR.regexTest(p, { kind: "binding", id: b }),
|
|
524
|
+
),
|
|
525
|
+
];
|
|
526
|
+
lctx.emit(
|
|
527
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
|
|
528
|
+
{
|
|
529
|
+
kind: "forEachKey",
|
|
530
|
+
target: lctx.instance,
|
|
531
|
+
binding: b,
|
|
532
|
+
body: [
|
|
533
|
+
lowerIR.when(
|
|
534
|
+
covered.length === 0
|
|
535
|
+
? lowerIR.constant(true)
|
|
536
|
+
: lowerIR.not(lowerIR.or(...covered)),
|
|
537
|
+
[
|
|
538
|
+
{
|
|
539
|
+
kind: "apply",
|
|
540
|
+
apply: {
|
|
541
|
+
path: [],
|
|
542
|
+
cursor: {
|
|
543
|
+
kind: "child",
|
|
544
|
+
of: { kind: "here" },
|
|
545
|
+
segment: { kind: "binding", id: b },
|
|
546
|
+
},
|
|
547
|
+
fold: "allMustPass",
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
),
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
]),
|
|
555
|
+
);
|
|
556
|
+
// Produce iff the instance is an object (an empty array otherwise), matching
|
|
557
|
+
// evaluate()'s object-type guard before ctx.produce.
|
|
558
|
+
lctx.emit(
|
|
559
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
|
|
560
|
+
{ kind: "produce", value: { kind: "collectedNames" } },
|
|
561
|
+
]),
|
|
562
|
+
);
|
|
563
|
+
},
|
|
564
|
+
evaluate: (_value, cursor, ctx) => {
|
|
565
|
+
if (!isObject(cursor.value)) return true;
|
|
566
|
+
const names = isObject(ctx.schema.properties)
|
|
567
|
+
? new Set(Object.keys(ctx.schema.properties))
|
|
568
|
+
: new Set<string>();
|
|
569
|
+
const patterns = isObject(ctx.schema.patternProperties)
|
|
570
|
+
? Object.keys(ctx.schema.patternProperties).map((p) =>
|
|
571
|
+
ctx.compileRegex(p),
|
|
572
|
+
)
|
|
573
|
+
: [];
|
|
574
|
+
let ok = true;
|
|
575
|
+
const matched: string[] = [];
|
|
576
|
+
for (const name of Object.keys(cursor.value)) {
|
|
577
|
+
if (names.has(name) || patterns.some((re) => re.test(name))) continue;
|
|
578
|
+
matched.push(name);
|
|
579
|
+
if (
|
|
580
|
+
!ctx.apply(
|
|
581
|
+
["additionalProperties"],
|
|
582
|
+
childCursor(cursor, name, cursor.value[name]!),
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
ok = false;
|
|
586
|
+
}
|
|
587
|
+
if (ok) ctx.produce(matched);
|
|
588
|
+
return ok;
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/** `prefixItems`: applies each subschema to the array item at its index; produces the largest applied index. */
|
|
593
|
+
export const prefixItems: KeywordBehavior = {
|
|
594
|
+
id: id("prefixItems"),
|
|
595
|
+
analyze: (value) => ({
|
|
596
|
+
...arrayPositions(value),
|
|
597
|
+
produces: [id("prefixItems")],
|
|
598
|
+
evaluatesIndexes: {
|
|
599
|
+
kind: "prefix",
|
|
600
|
+
count: Array.isArray(value) ? value.length : 0,
|
|
601
|
+
},
|
|
602
|
+
applications: Array.isArray(value)
|
|
603
|
+
? value.map((_, i): SubschemaApplication => ({
|
|
604
|
+
path: [i],
|
|
605
|
+
mode: "childByIndex",
|
|
606
|
+
conditional: false,
|
|
607
|
+
asserts: true,
|
|
608
|
+
}))
|
|
609
|
+
: [],
|
|
610
|
+
}),
|
|
611
|
+
lower: (value, lctx) => {
|
|
612
|
+
if (!Array.isArray(value)) return;
|
|
613
|
+
value.forEach((_, i) => {
|
|
614
|
+
lctx.emit(
|
|
615
|
+
lowerIR.when(
|
|
616
|
+
lowerIR.and(
|
|
617
|
+
lowerIR.typeIs(lctx.instance, "array"),
|
|
618
|
+
lowerIR.cmp(
|
|
619
|
+
">",
|
|
620
|
+
lowerIR.helper("lengthOf", lctx.instance),
|
|
621
|
+
lowerIR.constant(i),
|
|
622
|
+
),
|
|
623
|
+
),
|
|
624
|
+
[
|
|
625
|
+
{
|
|
626
|
+
kind: "apply",
|
|
627
|
+
apply: {
|
|
628
|
+
path: [i],
|
|
629
|
+
cursor: { kind: "child", of: { kind: "here" }, segment: i },
|
|
630
|
+
fold: "allMustPass",
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
),
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
// Annotation: largest applied index, or true when it covered the array
|
|
638
|
+
// (evaluate() produces only when at least one index applied).
|
|
639
|
+
lctx.emit({
|
|
640
|
+
kind: "produce",
|
|
641
|
+
value: { kind: "collectedIndexes", render: "largestOrTrue" },
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
evaluate: (value, cursor, ctx) => {
|
|
645
|
+
if (!Array.isArray(cursor.value)) return true;
|
|
646
|
+
// A non-array value is malformed (metaschema's job to reject, D19 scope
|
|
647
|
+
// line); no-op here so both tiers treat it identically — analyze() and
|
|
648
|
+
// lower() already contribute nothing for it.
|
|
649
|
+
if (!Array.isArray(value)) return true;
|
|
650
|
+
const schemas = value;
|
|
651
|
+
const n = Math.min(schemas.length, cursor.value.length);
|
|
652
|
+
let ok = true;
|
|
653
|
+
for (let i = 0; i < n; i++) {
|
|
654
|
+
if (
|
|
655
|
+
!ctx.apply(["prefixItems", i], childCursor(cursor, i, cursor.value[i]!))
|
|
656
|
+
)
|
|
657
|
+
ok = false;
|
|
658
|
+
}
|
|
659
|
+
// Dependency data: largest applied index, or true when it covered the
|
|
660
|
+
// array — only from an accepting keyword (Appendix D; see properties).
|
|
661
|
+
if (n > 0 && ok) ctx.produce(n === cursor.value.length ? true : n - 1);
|
|
662
|
+
return ok;
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
/** `items`: applies to array items past sibling `prefixItems`; produces `true` when it applied to any item. */
|
|
667
|
+
export const items: KeywordBehavior = {
|
|
668
|
+
id: id("items"),
|
|
669
|
+
// Coverage starts after the sibling prefixItems — the same read
|
|
670
|
+
// evaluate() performs through ctx.schema, statically (AnalyzeContext).
|
|
671
|
+
analyze: (_value, context) => ({
|
|
672
|
+
...selfApplication("childSweep", false, true),
|
|
673
|
+
produces: [id("items")],
|
|
674
|
+
evaluatesIndexes: {
|
|
675
|
+
kind: "allFrom",
|
|
676
|
+
start: Array.isArray(context?.schema.prefixItems)
|
|
677
|
+
? context.schema.prefixItems.length
|
|
678
|
+
: 0,
|
|
679
|
+
},
|
|
680
|
+
}),
|
|
681
|
+
lower: (_value, lctx) => {
|
|
682
|
+
const start = Array.isArray(lctx.schema.prefixItems)
|
|
683
|
+
? lctx.schema.prefixItems.length
|
|
684
|
+
: 0;
|
|
685
|
+
const b = lctx.binding();
|
|
686
|
+
lctx.emit(
|
|
687
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "array"), [
|
|
688
|
+
{
|
|
689
|
+
kind: "forEachIndex",
|
|
690
|
+
target: lctx.instance,
|
|
691
|
+
binding: b,
|
|
692
|
+
start,
|
|
693
|
+
body: [
|
|
694
|
+
{
|
|
695
|
+
kind: "apply",
|
|
696
|
+
apply: {
|
|
697
|
+
path: [],
|
|
698
|
+
cursor: {
|
|
699
|
+
kind: "child",
|
|
700
|
+
of: { kind: "here" },
|
|
701
|
+
segment: { kind: "binding", id: b },
|
|
702
|
+
},
|
|
703
|
+
fold: "allMustPass",
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
]),
|
|
709
|
+
);
|
|
710
|
+
// Annotation: true iff it applied to any item past the prefix.
|
|
711
|
+
lctx.emit({
|
|
712
|
+
kind: "produce",
|
|
713
|
+
value: { kind: "collectedIndexes", render: "appliedTrue" },
|
|
714
|
+
});
|
|
715
|
+
},
|
|
716
|
+
evaluate: (_value, cursor, ctx) => {
|
|
717
|
+
if (!Array.isArray(cursor.value)) return true;
|
|
718
|
+
// Applies past the sibling prefixItems (statically known per spec).
|
|
719
|
+
const start = Array.isArray(ctx.schema.prefixItems)
|
|
720
|
+
? ctx.schema.prefixItems.length
|
|
721
|
+
: 0;
|
|
722
|
+
let ok = true;
|
|
723
|
+
let applied = false;
|
|
724
|
+
for (let i = start; i < cursor.value.length; i++) {
|
|
725
|
+
applied = true;
|
|
726
|
+
if (!ctx.apply(["items"], childCursor(cursor, i, cursor.value[i]!)))
|
|
727
|
+
ok = false;
|
|
728
|
+
}
|
|
729
|
+
if (applied && ok) ctx.produce(true);
|
|
730
|
+
return ok;
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* `contains`: at least one array item must match (range configurable by
|
|
736
|
+
* sibling `minContains`/`maxContains`); produces matched indexes.
|
|
737
|
+
*/
|
|
738
|
+
export const contains: KeywordBehavior = {
|
|
739
|
+
id: id("contains"),
|
|
740
|
+
// Per-item probes don't individually assert (the count does), and which
|
|
741
|
+
// indexes end up evaluated is instance-dependent: coverage is dynamic.
|
|
742
|
+
analyze: () => ({
|
|
743
|
+
...selfApplication("childSweep", false, false),
|
|
744
|
+
produces: [id("contains")],
|
|
745
|
+
evaluatesIndexes: { kind: "dynamic" },
|
|
746
|
+
}),
|
|
747
|
+
lower: (_value, lctx) => {
|
|
748
|
+
const min =
|
|
749
|
+
typeof lctx.schema.minContains === "number" ? lctx.schema.minContains : 1;
|
|
750
|
+
const max =
|
|
751
|
+
typeof lctx.schema.maxContains === "number"
|
|
752
|
+
? lctx.schema.maxContains
|
|
753
|
+
: Infinity;
|
|
754
|
+
const b = lctx.binding();
|
|
755
|
+
lctx.emit(
|
|
756
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "array"), [
|
|
757
|
+
{
|
|
758
|
+
kind: "countRange",
|
|
759
|
+
target: lctx.instance,
|
|
760
|
+
binding: b,
|
|
761
|
+
countWhen: {
|
|
762
|
+
kind: "applyExpr",
|
|
763
|
+
apply: {
|
|
764
|
+
path: [],
|
|
765
|
+
cursor: {
|
|
766
|
+
kind: "child",
|
|
767
|
+
of: { kind: "here" },
|
|
768
|
+
segment: { kind: "binding", id: b },
|
|
769
|
+
},
|
|
770
|
+
fold: "discard",
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
collectIndexes: true,
|
|
774
|
+
min,
|
|
775
|
+
max,
|
|
776
|
+
// Mirrors evaluate()'s text exactly (list-mode parity): the
|
|
777
|
+
// tally placeholder binds to the runtime match count.
|
|
778
|
+
outOfRangeMessage: [
|
|
779
|
+
{ kind: "tally" },
|
|
780
|
+
` item(s) match the contains subschema, expected ${min}-${max}`,
|
|
781
|
+
],
|
|
782
|
+
outOfRangeParams: {
|
|
783
|
+
count: { kind: "tally" },
|
|
784
|
+
minContains: lowerIR.constant(min),
|
|
785
|
+
...(Number.isFinite(max)
|
|
786
|
+
? { maxContains: lowerIR.constant(max) }
|
|
787
|
+
: {}),
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
]),
|
|
791
|
+
);
|
|
792
|
+
// Annotation: matched indexes, or true when every item matched
|
|
793
|
+
// (evaluate() produces only when at least one item matched).
|
|
794
|
+
lctx.emit({
|
|
795
|
+
kind: "produce",
|
|
796
|
+
value: { kind: "collectedIndexes", render: "matchedOrAllTrue" },
|
|
797
|
+
});
|
|
798
|
+
},
|
|
799
|
+
evaluate: (_value, cursor, ctx) => {
|
|
800
|
+
if (!Array.isArray(cursor.value)) return true;
|
|
801
|
+
const matched: number[] = [];
|
|
802
|
+
for (let i = 0; i < cursor.value.length; i++) {
|
|
803
|
+
if (ctx.apply(["contains"], childCursor(cursor, i, cursor.value[i]!)))
|
|
804
|
+
matched.push(i);
|
|
805
|
+
}
|
|
806
|
+
// minContains/maxContains are inert siblings (validation.ts) that turn
|
|
807
|
+
// the count into a range assertion instead of contains' own >=1 default;
|
|
808
|
+
// minContains: 0 with zero matches is valid (suite: "minContains = 0").
|
|
809
|
+
const min =
|
|
810
|
+
typeof ctx.schema.minContains === "number" ? ctx.schema.minContains : 1;
|
|
811
|
+
const max =
|
|
812
|
+
typeof ctx.schema.maxContains === "number"
|
|
813
|
+
? ctx.schema.maxContains
|
|
814
|
+
: Infinity;
|
|
815
|
+
if (matched.length < min || matched.length > max) {
|
|
816
|
+
ctx.error(
|
|
817
|
+
`${matched.length} item(s) match the contains subschema, expected ${min}-${max}`,
|
|
818
|
+
{
|
|
819
|
+
count: matched.length,
|
|
820
|
+
minContains: min,
|
|
821
|
+
...(Number.isFinite(max) ? { maxContains: max } : {}),
|
|
822
|
+
},
|
|
823
|
+
);
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
// Annotation: matched indexes, or true when every item matched.
|
|
827
|
+
if (matched.length > 0)
|
|
828
|
+
ctx.produce(matched.length === cursor.value.length ? true : matched);
|
|
829
|
+
return true;
|
|
830
|
+
},
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* `propertyNames`: applies the subschema to each property name (as a string
|
|
835
|
+
* instance), not to the object itself.
|
|
836
|
+
*/
|
|
837
|
+
export const propertyNames: KeywordBehavior = {
|
|
838
|
+
id: id("propertyNames"),
|
|
839
|
+
analyze: () => ({
|
|
840
|
+
...SELF,
|
|
841
|
+
applications: [
|
|
842
|
+
{ path: [], mode: "propertyName", conditional: false, asserts: true },
|
|
843
|
+
],
|
|
844
|
+
}),
|
|
845
|
+
lower: (_value, lctx) => {
|
|
846
|
+
const b = lctx.binding();
|
|
847
|
+
lctx.emit(
|
|
848
|
+
lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
|
|
849
|
+
{
|
|
850
|
+
kind: "forEachKey",
|
|
851
|
+
target: lctx.instance,
|
|
852
|
+
binding: b,
|
|
853
|
+
body: [
|
|
854
|
+
{
|
|
855
|
+
kind: "apply",
|
|
856
|
+
apply: {
|
|
857
|
+
path: [],
|
|
858
|
+
cursor: { kind: "key", binding: b },
|
|
859
|
+
fold: "allMustPass",
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
},
|
|
864
|
+
]),
|
|
865
|
+
);
|
|
866
|
+
},
|
|
867
|
+
evaluate: (_value, cursor, ctx) => {
|
|
868
|
+
if (!isObject(cursor.value)) return true;
|
|
869
|
+
let ok = true;
|
|
870
|
+
for (const name of Object.keys(cursor.value)) {
|
|
871
|
+
if (!ctx.apply(["propertyNames"], childCursor(cursor, name, name)))
|
|
872
|
+
ok = false;
|
|
873
|
+
}
|
|
874
|
+
return ok;
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
/** The 2020-12 applicator vocabulary's keyword behaviors, by name. */
|
|
879
|
+
export const applicatorVocabulary: Record<string, KeywordBehavior> = {
|
|
880
|
+
allOf,
|
|
881
|
+
anyOf,
|
|
882
|
+
oneOf,
|
|
883
|
+
not,
|
|
884
|
+
if: ifKeyword,
|
|
885
|
+
then: thenKeyword,
|
|
886
|
+
else: elseKeyword,
|
|
887
|
+
dependentSchemas,
|
|
888
|
+
properties,
|
|
889
|
+
patternProperties,
|
|
890
|
+
additionalProperties,
|
|
891
|
+
prefixItems,
|
|
892
|
+
items,
|
|
893
|
+
contains,
|
|
894
|
+
propertyNames,
|
|
895
|
+
};
|