@astrale-os/kernel-dsl 0.2.0-beta.45 → 0.2.0-beta.46
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/dist/v1/addressing/class-key.js +5 -4
- package/dist/v1/addressing/method-key.js +3 -2
- package/dist/v1/builder/lowering/value/zod.js +11 -2
- package/dist/v1/builder/value/full-match.js +6 -3
- package/dist/v1/compiled/admission/accept.js +27 -6
- package/dist/v1/compiled/admission/semantics.js +41 -16
- package/dist/v1/compiled/loader/load.js +1 -0
- package/dist/v1/domain/model/input.d.ts +3 -0
- package/dist/v1/domain/projection/classes.js +1 -0
- package/dist/v1/domain/validation/create.d.ts +2 -0
- package/dist/v1/domain/validation/create.js +12 -0
- package/dist/v1/language/class/resolved.d.ts +3 -0
- package/dist/v1/language/index.d.ts +2 -0
- package/dist/v1/language/index.js +1 -0
- package/dist/v1/language/policy/normalization/normalize.d.ts +4 -0
- package/dist/v1/language/policy/normalization/normalize.js +51 -13
- package/dist/v1/language/value/inclusion/includes.d.ts +9 -0
- package/dist/v1/language/value/inclusion/includes.js +777 -0
- package/dist/v1/language/value/profile/rules.js +11 -32
- package/dist/v1/language/value/regexp/ast.js +3 -64
- package/dist/v1/language/value/regexp/compile.d.ts +25 -2
- package/dist/v1/language/value/regexp/compile.js +48 -12
- package/dist/v1/language/value/regexp/syntax.d.ts +11 -1
- package/dist/v1/language/value/regexp/syntax.js +14 -2
- package/dist/v1/language/value/regexp/unicode.d.ts +32 -0
- package/dist/v1/language/value/regexp/unicode.js +139 -0
- package/dist/v1/language/value/syntax/full-match.js +6 -3
- package/dist/v1/schema/compatibility/comparators/callables.d.ts +7 -0
- package/dist/v1/schema/compatibility/comparators/callables.js +13 -0
- package/dist/v1/schema/compatibility/comparators/classes.d.ts +18 -0
- package/dist/v1/schema/compatibility/comparators/classes.js +54 -0
- package/dist/v1/schema/compatibility/comparators/index.d.ts +1 -1
- package/dist/v1/schema/compatibility/comparators/index.js +14 -2
- package/dist/v1/schema/compatibility/evidence.d.ts +8 -1
- package/dist/v1/schema/compatibility/evidence.js +10 -1
- package/dist/v1/schema/compatibility/meaning/footprint.js +3 -1
- package/dist/v1/schema/compatibility/meaning/index.js +4 -2
- package/dist/v1/schema/compatibility/model.d.ts +11 -6
- package/dist/v1/schema/compatibility/references.d.ts +8 -1
- package/dist/v1/schema/compatibility/references.js +39 -6
- package/dist/v1/schema/compatibility/scope.d.ts +5 -0
- package/dist/v1/schema/compatibility/scope.js +5 -2
- package/dist/v1/schema/compatibility/structure/index.js +21 -2
- package/dist/v1/schema/compatibility/structure/requirements.js +26 -1
- package/dist/v1/schema/compatibility/subjects.js +20 -2
- package/dist/v1/schema/resolution/domain.js +1 -0
- package/dist/v1/schema/resolution/origins.d.ts +11 -4
- package/dist/v1/schema/resolution/origins.js +7 -8
- package/dist/v1/schema/validation/class.d.ts +10 -0
- package/dist/v1/schema/validation/class.js +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import { loadValueProgram } from '../compile/load.js';
|
|
2
|
+
import { isMultipleOf, jsonEqual } from '../evaluate/assertions.js';
|
|
3
|
+
import { evaluateInstance } from '../evaluate/evaluator.js';
|
|
4
|
+
import { isValueObject as isJsonObject } from '../model/object.js';
|
|
5
|
+
import { appendPointer } from '../syntax/pointer.js';
|
|
6
|
+
/**
|
|
7
|
+
* Whether `outer` admits every value `inner` admits, under the one V1 evaluator.
|
|
8
|
+
*
|
|
9
|
+
* `true` is a proof. `false` only means that no rule proved inclusion: a caller must then decide
|
|
10
|
+
* the values themselves. Each rule relates one assertion of `outer` to evidence carried by
|
|
11
|
+
* `inner`; a finite `inner` is decided exactly by evaluating its candidates.
|
|
12
|
+
*/
|
|
13
|
+
export function valueProgramIncludes(outer, inner) {
|
|
14
|
+
return new Inclusion().includes(at(outer, outer.root), at(inner, inner.root));
|
|
15
|
+
}
|
|
16
|
+
const NULL = 1;
|
|
17
|
+
const BOOLEAN = 2;
|
|
18
|
+
const OBJECT = 4;
|
|
19
|
+
const ARRAY = 8;
|
|
20
|
+
const STRING = 16;
|
|
21
|
+
const INTEGER = 32;
|
|
22
|
+
const FRACTION = 64;
|
|
23
|
+
const NUMBER = INTEGER | FRACTION;
|
|
24
|
+
const ANY = NULL | BOOLEAN | OBJECT | ARRAY | STRING | NUMBER;
|
|
25
|
+
/** Keywords the evaluator never asserts. */
|
|
26
|
+
const INERT = new Set([
|
|
27
|
+
'$anchor',
|
|
28
|
+
'$comment',
|
|
29
|
+
'$defs',
|
|
30
|
+
'$id',
|
|
31
|
+
'$schema',
|
|
32
|
+
'contentEncoding',
|
|
33
|
+
'contentMediaType',
|
|
34
|
+
'contentSchema',
|
|
35
|
+
'default',
|
|
36
|
+
'deprecated',
|
|
37
|
+
'description',
|
|
38
|
+
'examples',
|
|
39
|
+
'format',
|
|
40
|
+
'readOnly',
|
|
41
|
+
'title',
|
|
42
|
+
'writeOnly',
|
|
43
|
+
'x-astrale-path',
|
|
44
|
+
]);
|
|
45
|
+
/** Keywords whose subschemas the equivalence check compares positionally. */
|
|
46
|
+
const SUBSCHEMA_KEYWORDS = new Set([
|
|
47
|
+
'additionalProperties',
|
|
48
|
+
'allOf',
|
|
49
|
+
'anyOf',
|
|
50
|
+
'contains',
|
|
51
|
+
'dependentSchemas',
|
|
52
|
+
'else',
|
|
53
|
+
'if',
|
|
54
|
+
'items',
|
|
55
|
+
'not',
|
|
56
|
+
'oneOf',
|
|
57
|
+
'patternProperties',
|
|
58
|
+
'prefixItems',
|
|
59
|
+
'properties',
|
|
60
|
+
'propertyNames',
|
|
61
|
+
'then',
|
|
62
|
+
'unevaluatedItems',
|
|
63
|
+
'unevaluatedProperties',
|
|
64
|
+
]);
|
|
65
|
+
class Inclusion {
|
|
66
|
+
programs = new Map();
|
|
67
|
+
decided = new Map();
|
|
68
|
+
pending = new Set();
|
|
69
|
+
evidenceByAt = new Map();
|
|
70
|
+
includes(outer, inner) {
|
|
71
|
+
const key = this.pair(outer, inner);
|
|
72
|
+
const known = this.decided.get(key);
|
|
73
|
+
if (known !== undefined)
|
|
74
|
+
return known;
|
|
75
|
+
// Assuming a pending pair would make a later result depend on an unproven premise.
|
|
76
|
+
if (this.pending.has(key))
|
|
77
|
+
return false;
|
|
78
|
+
this.pending.add(key);
|
|
79
|
+
try {
|
|
80
|
+
const result = this.decide(outer, inner);
|
|
81
|
+
this.decided.set(key, result);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
this.pending.delete(key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
decide(outer, inner) {
|
|
89
|
+
const schema = schemaOf(outer);
|
|
90
|
+
if (isTop(schema))
|
|
91
|
+
return true;
|
|
92
|
+
const evidence = this.evidence(inner);
|
|
93
|
+
if (evidence.values !== undefined) {
|
|
94
|
+
return evidence.values.every((value) => !this.accepts(inner, value) || this.accepts(outer, value));
|
|
95
|
+
}
|
|
96
|
+
if (schema === false)
|
|
97
|
+
return false;
|
|
98
|
+
if (this.equivalent(outer, inner))
|
|
99
|
+
return true;
|
|
100
|
+
if (schema !== true && this.assertionsHold(outer, schema, inner, evidence))
|
|
101
|
+
return true;
|
|
102
|
+
// A disjunction admits each value through one branch, and each branch alone is weaker.
|
|
103
|
+
return evidence.conjuncts.some((conjunct) => ['anyOf', 'oneOf'].some((keyword) => {
|
|
104
|
+
const branches = childrenOf(conjunct, keyword);
|
|
105
|
+
return branches.length > 0 && branches.every((branch) => this.includes(outer, branch));
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
assertionsHold(outer, schema, inner, evidence) {
|
|
109
|
+
// Annotation-dependent keywords are decided only by equivalence.
|
|
110
|
+
if (Object.hasOwn(schema, 'unevaluatedProperties'))
|
|
111
|
+
return false;
|
|
112
|
+
if (Object.hasOwn(schema, 'unevaluatedItems'))
|
|
113
|
+
return false;
|
|
114
|
+
return Object.keys(schema).every((keyword) => this.assertionHolds(keyword, outer, schema, inner, evidence));
|
|
115
|
+
}
|
|
116
|
+
assertionHolds(keyword, outer, schema, inner, evidence) {
|
|
117
|
+
const value = schema[keyword];
|
|
118
|
+
switch (keyword) {
|
|
119
|
+
case '$ref': {
|
|
120
|
+
const target = referenceOf(outer);
|
|
121
|
+
return target !== undefined && this.includes(target, inner);
|
|
122
|
+
}
|
|
123
|
+
case 'allOf':
|
|
124
|
+
return childrenOf(outer, keyword).every((branch) => this.includes(branch, inner));
|
|
125
|
+
case 'anyOf':
|
|
126
|
+
return childrenOf(outer, keyword).some((branch) => this.includes(branch, inner));
|
|
127
|
+
case 'oneOf':
|
|
128
|
+
return false;
|
|
129
|
+
case 'not': {
|
|
130
|
+
const negated = childOf(outer, keyword);
|
|
131
|
+
return negated !== undefined && this.excludes(negated, evidence);
|
|
132
|
+
}
|
|
133
|
+
case 'if':
|
|
134
|
+
case 'then':
|
|
135
|
+
case 'else':
|
|
136
|
+
return this.conditionalHolds(outer, inner, evidence);
|
|
137
|
+
case 'type':
|
|
138
|
+
return (evidence.kinds & ~typeMask(value)) === 0;
|
|
139
|
+
case 'const':
|
|
140
|
+
case 'enum':
|
|
141
|
+
return false;
|
|
142
|
+
case 'x-astrale-value-kind':
|
|
143
|
+
return typeof value === 'string' && evidence.valueKinds.has(value);
|
|
144
|
+
case 'maximum':
|
|
145
|
+
return (typeof value !== 'number' ||
|
|
146
|
+
!has(evidence, NUMBER) ||
|
|
147
|
+
(evidence.upper !== undefined && evidence.upper.value <= value));
|
|
148
|
+
case 'exclusiveMaximum':
|
|
149
|
+
return (typeof value !== 'number' ||
|
|
150
|
+
!has(evidence, NUMBER) ||
|
|
151
|
+
(evidence.upper !== undefined &&
|
|
152
|
+
(evidence.upper.value < value ||
|
|
153
|
+
(evidence.upper.exclusive && evidence.upper.value <= value))));
|
|
154
|
+
case 'minimum':
|
|
155
|
+
return (typeof value !== 'number' ||
|
|
156
|
+
!has(evidence, NUMBER) ||
|
|
157
|
+
(evidence.lower !== undefined && evidence.lower.value >= value));
|
|
158
|
+
case 'exclusiveMinimum':
|
|
159
|
+
return (typeof value !== 'number' ||
|
|
160
|
+
!has(evidence, NUMBER) ||
|
|
161
|
+
(evidence.lower !== undefined &&
|
|
162
|
+
(evidence.lower.value > value ||
|
|
163
|
+
(evidence.lower.exclusive && evidence.lower.value >= value))));
|
|
164
|
+
case 'multipleOf':
|
|
165
|
+
return (typeof value !== 'number' ||
|
|
166
|
+
!has(evidence, NUMBER) ||
|
|
167
|
+
evidence.multipleOf.some((divisor) => isMultipleOf(divisor, value)) ||
|
|
168
|
+
(!has(evidence, FRACTION) && isMultipleOf(1, value)));
|
|
169
|
+
case 'maxLength':
|
|
170
|
+
return (typeof value !== 'number' ||
|
|
171
|
+
!has(evidence, STRING) ||
|
|
172
|
+
(evidence.maxLength !== undefined && evidence.maxLength <= value));
|
|
173
|
+
case 'minLength':
|
|
174
|
+
return (typeof value !== 'number' ||
|
|
175
|
+
!has(evidence, STRING) ||
|
|
176
|
+
value <= 0 ||
|
|
177
|
+
evidence.minLength >= value);
|
|
178
|
+
case 'pattern':
|
|
179
|
+
return (typeof value !== 'string' ||
|
|
180
|
+
!has(evidence, STRING) ||
|
|
181
|
+
evidence.patterns.has(value) ||
|
|
182
|
+
evidence.fullMatches.has(value));
|
|
183
|
+
case 'fullMatch':
|
|
184
|
+
return (typeof value !== 'string' || !has(evidence, STRING) || evidence.fullMatches.has(value));
|
|
185
|
+
case 'maxItems':
|
|
186
|
+
return (typeof value !== 'number' ||
|
|
187
|
+
!has(evidence, ARRAY) ||
|
|
188
|
+
(evidence.maxItems !== undefined && evidence.maxItems <= value));
|
|
189
|
+
case 'minItems':
|
|
190
|
+
return (typeof value !== 'number' ||
|
|
191
|
+
!has(evidence, ARRAY) ||
|
|
192
|
+
value <= 0 ||
|
|
193
|
+
evidence.minItems >= value);
|
|
194
|
+
case 'uniqueItems':
|
|
195
|
+
return (value !== true ||
|
|
196
|
+
!has(evidence, ARRAY) ||
|
|
197
|
+
evidence.uniqueItems ||
|
|
198
|
+
(evidence.maxItems !== undefined && evidence.maxItems <= 1));
|
|
199
|
+
case 'prefixItems':
|
|
200
|
+
case 'items':
|
|
201
|
+
return this.itemsHold(outer, schema, evidence);
|
|
202
|
+
case 'contains':
|
|
203
|
+
case 'minContains':
|
|
204
|
+
case 'maxContains':
|
|
205
|
+
return this.containsHolds(outer, schema, evidence);
|
|
206
|
+
case 'maxProperties':
|
|
207
|
+
return (typeof value !== 'number' ||
|
|
208
|
+
!has(evidence, OBJECT) ||
|
|
209
|
+
(evidence.maxProperties !== undefined && evidence.maxProperties <= value) ||
|
|
210
|
+
evidence.conjuncts.some((conjunct) => {
|
|
211
|
+
const names = this.closedNames(conjunct);
|
|
212
|
+
return names !== undefined && names.length <= value;
|
|
213
|
+
}));
|
|
214
|
+
case 'minProperties':
|
|
215
|
+
return (typeof value !== 'number' ||
|
|
216
|
+
!has(evidence, OBJECT) ||
|
|
217
|
+
value <= 0 ||
|
|
218
|
+
evidence.minProperties >= value ||
|
|
219
|
+
evidence.required.size >= value);
|
|
220
|
+
case 'required':
|
|
221
|
+
return (!Array.isArray(value) ||
|
|
222
|
+
!has(evidence, OBJECT) ||
|
|
223
|
+
value.every((name) => typeof name !== 'string' || evidence.required.has(name)));
|
|
224
|
+
case 'dependentRequired':
|
|
225
|
+
return (!isJsonObject(value) ||
|
|
226
|
+
!has(evidence, OBJECT) ||
|
|
227
|
+
Object.entries(value).every(([name, dependencies]) => !Array.isArray(dependencies) ||
|
|
228
|
+
this.forbids(evidence, name) ||
|
|
229
|
+
dependencies.every((dependency) => typeof dependency !== 'string' ||
|
|
230
|
+
evidence.required.has(dependency) ||
|
|
231
|
+
evidence.dependentRequired.get(name)?.has(dependency) === true)));
|
|
232
|
+
case 'properties':
|
|
233
|
+
return (!isJsonObject(value) ||
|
|
234
|
+
!has(evidence, OBJECT) ||
|
|
235
|
+
Object.keys(value).every((name) => {
|
|
236
|
+
const member = childOf(outer, keyword, name);
|
|
237
|
+
return (member === undefined ||
|
|
238
|
+
isTop(schemaOf(member)) ||
|
|
239
|
+
this.memberConstraints(evidence, name).some((constraint) => this.includes(member, constraint)));
|
|
240
|
+
}));
|
|
241
|
+
case 'patternProperties':
|
|
242
|
+
return (!isJsonObject(value) ||
|
|
243
|
+
!has(evidence, OBJECT) ||
|
|
244
|
+
Object.keys(value).every((pattern) => {
|
|
245
|
+
const member = childOf(outer, keyword, pattern);
|
|
246
|
+
if (member === undefined || isTop(schemaOf(member)))
|
|
247
|
+
return true;
|
|
248
|
+
return evidence.conjuncts.some((conjunct) => {
|
|
249
|
+
const same = childOf(conjunct, keyword, pattern);
|
|
250
|
+
if (same !== undefined && this.includes(member, same))
|
|
251
|
+
return true;
|
|
252
|
+
return this.coversNames(conjunct, member, (name) => matches(outer, pattern, name));
|
|
253
|
+
});
|
|
254
|
+
}));
|
|
255
|
+
case 'additionalProperties':
|
|
256
|
+
return this.additionalHolds(outer, schema, evidence);
|
|
257
|
+
case 'propertyNames': {
|
|
258
|
+
const names = childOf(outer, keyword);
|
|
259
|
+
if (names === undefined || isTop(schemaOf(names)) || !has(evidence, OBJECT))
|
|
260
|
+
return true;
|
|
261
|
+
return evidence.conjuncts.some((conjunct) => {
|
|
262
|
+
const own = childOf(conjunct, keyword);
|
|
263
|
+
if (own !== undefined && this.includes(names, own))
|
|
264
|
+
return true;
|
|
265
|
+
const closed = this.closedNames(conjunct);
|
|
266
|
+
return closed !== undefined && closed.every((name) => this.accepts(names, name));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
case 'dependentSchemas':
|
|
270
|
+
return (!isJsonObject(value) ||
|
|
271
|
+
!has(evidence, OBJECT) ||
|
|
272
|
+
Object.keys(value).every((name) => {
|
|
273
|
+
const dependent = childOf(outer, keyword, name);
|
|
274
|
+
if (dependent === undefined || isTop(schemaOf(dependent)))
|
|
275
|
+
return true;
|
|
276
|
+
if (this.forbids(evidence, name))
|
|
277
|
+
return true;
|
|
278
|
+
if (evidence.conjuncts.some((conjunct) => this.includes(dependent, conjunct))) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
return evidence.conjuncts.some((conjunct) => {
|
|
282
|
+
const own = childOf(conjunct, keyword, name);
|
|
283
|
+
return own !== undefined && this.includes(dependent, own);
|
|
284
|
+
});
|
|
285
|
+
}));
|
|
286
|
+
default:
|
|
287
|
+
return INERT.has(keyword);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Whether no value admitted by `evidence` satisfies `negated`. */
|
|
291
|
+
excludes(negated, evidence) {
|
|
292
|
+
if ((this.evidence(negated).kinds & evidence.kinds) === 0)
|
|
293
|
+
return true;
|
|
294
|
+
return evidence.conjuncts.some((conjunct) => {
|
|
295
|
+
const own = childOf(conjunct, 'not');
|
|
296
|
+
return own !== undefined && this.includes(own, negated);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
conditionalHolds(outer, inner, evidence) {
|
|
300
|
+
const condition = childOf(outer, 'if');
|
|
301
|
+
if (condition === undefined)
|
|
302
|
+
return true;
|
|
303
|
+
const then = childOf(outer, 'then');
|
|
304
|
+
const otherwise = childOf(outer, 'else');
|
|
305
|
+
const thenHolds = (fallback) => then === undefined ||
|
|
306
|
+
this.includes(then, inner) ||
|
|
307
|
+
(fallback !== undefined && this.includes(then, fallback));
|
|
308
|
+
const elseHolds = (fallback) => otherwise === undefined ||
|
|
309
|
+
this.includes(otherwise, inner) ||
|
|
310
|
+
(fallback !== undefined && this.includes(otherwise, fallback));
|
|
311
|
+
if (thenHolds() && elseHolds())
|
|
312
|
+
return true;
|
|
313
|
+
for (const conjunct of evidence.conjuncts) {
|
|
314
|
+
const own = childOf(conjunct, 'if');
|
|
315
|
+
if (own === undefined || !this.equivalent(own, condition))
|
|
316
|
+
continue;
|
|
317
|
+
if (thenHolds(childOf(conjunct, 'then')) && elseHolds(childOf(conjunct, 'else')))
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
if (this.includes(condition, inner))
|
|
321
|
+
return thenHolds();
|
|
322
|
+
if (this.excludes(condition, evidence))
|
|
323
|
+
return elseHolds();
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
itemsHold(outer, schema, evidence) {
|
|
327
|
+
if (!has(evidence, ARRAY))
|
|
328
|
+
return true;
|
|
329
|
+
const prefix = childrenOf(outer, 'prefixItems');
|
|
330
|
+
const rest = Object.hasOwn(schema, 'items') ? childOf(outer, 'items') : undefined;
|
|
331
|
+
const width = Math.max(prefix.length, ...evidence.conjuncts.map((conjunct) => childrenOf(conjunct, 'prefixItems').length));
|
|
332
|
+
// Every index at or beyond `width` meets `items` on both sides.
|
|
333
|
+
for (let index = 0; index <= width; index++) {
|
|
334
|
+
const required = index < prefix.length ? prefix[index] : rest;
|
|
335
|
+
if (required === undefined || isTop(schemaOf(required)))
|
|
336
|
+
continue;
|
|
337
|
+
if (evidence.maxItems !== undefined && evidence.maxItems <= index)
|
|
338
|
+
continue;
|
|
339
|
+
const admitted = evidence.conjuncts.flatMap((conjunct) => {
|
|
340
|
+
const own = childrenOf(conjunct, 'prefixItems');
|
|
341
|
+
if (index < own.length)
|
|
342
|
+
return [own[index]];
|
|
343
|
+
const items = childOf(conjunct, 'items');
|
|
344
|
+
return items === undefined ? [] : [items];
|
|
345
|
+
});
|
|
346
|
+
if (!admitted.some((constraint) => this.includes(required, constraint)))
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
containsHolds(outer, schema, evidence) {
|
|
352
|
+
const contains = childOf(outer, 'contains');
|
|
353
|
+
if (contains === undefined || !has(evidence, ARRAY))
|
|
354
|
+
return true;
|
|
355
|
+
const minimum = typeof schema.minContains === 'number' ? schema.minContains : 1;
|
|
356
|
+
const maximum = typeof schema.maxContains === 'number' ? schema.maxContains : Number.POSITIVE_INFINITY;
|
|
357
|
+
const ownContains = evidence.conjuncts.flatMap((conjunct) => {
|
|
358
|
+
const own = childOf(conjunct, 'contains');
|
|
359
|
+
if (own === undefined)
|
|
360
|
+
return [];
|
|
361
|
+
const conjunctSchema = schemaOf(conjunct);
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
at: own,
|
|
365
|
+
minimum: typeof conjunctSchema.minContains === 'number' ? conjunctSchema.minContains : 1,
|
|
366
|
+
maximum: typeof conjunctSchema.maxContains === 'number'
|
|
367
|
+
? conjunctSchema.maxContains
|
|
368
|
+
: Number.POSITIVE_INFINITY,
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
});
|
|
372
|
+
// Every inner match is an outer match, so outer counts at least as many.
|
|
373
|
+
const atLeast = minimum <= 0 ||
|
|
374
|
+
ownContains.some((own) => own.minimum >= minimum && this.includes(contains, own.at));
|
|
375
|
+
// Every outer match is an inner match, so outer counts at most as many.
|
|
376
|
+
const atMost = maximum === Number.POSITIVE_INFINITY ||
|
|
377
|
+
(evidence.maxItems !== undefined && evidence.maxItems <= maximum) ||
|
|
378
|
+
ownContains.some((own) => own.maximum <= maximum && this.includes(own.at, contains));
|
|
379
|
+
return atLeast && atMost;
|
|
380
|
+
}
|
|
381
|
+
additionalHolds(outer, schema, evidence) {
|
|
382
|
+
const additional = childOf(outer, 'additionalProperties');
|
|
383
|
+
if (additional === undefined || isTop(schemaOf(additional)) || !has(evidence, OBJECT)) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const declared = isJsonObject(schema.properties) ? schema.properties : {};
|
|
387
|
+
const patterns = isJsonObject(schema.patternProperties)
|
|
388
|
+
? Object.keys(schema.patternProperties)
|
|
389
|
+
: [];
|
|
390
|
+
const isAdditional = (name) => !Object.hasOwn(declared, name) && !patterns.some((pattern) => matches(outer, pattern, name));
|
|
391
|
+
return evidence.conjuncts.some((conjunct) => this.coversNames(conjunct, additional, isAdditional, patterns));
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Whether every property of a `conjunct`-admitted object whose name is `relevant` satisfies
|
|
395
|
+
* `required`. Names matching a pattern in `settled` are never relevant.
|
|
396
|
+
*/
|
|
397
|
+
coversNames(conjunct, required, relevant, settled = []) {
|
|
398
|
+
const schema = schemaOf(conjunct);
|
|
399
|
+
if (!isJsonObject(schema))
|
|
400
|
+
return false;
|
|
401
|
+
const rest = childOf(conjunct, 'additionalProperties');
|
|
402
|
+
if (rest === undefined || !this.includes(required, rest))
|
|
403
|
+
return false;
|
|
404
|
+
const declared = isJsonObject(schema.properties) ? Object.keys(schema.properties) : [];
|
|
405
|
+
const patterns = isJsonObject(schema.patternProperties)
|
|
406
|
+
? Object.keys(schema.patternProperties)
|
|
407
|
+
: [];
|
|
408
|
+
return (declared.every((name) => {
|
|
409
|
+
if (!relevant(name))
|
|
410
|
+
return true;
|
|
411
|
+
const member = childOf(conjunct, 'properties', name);
|
|
412
|
+
return member !== undefined && this.includes(required, member);
|
|
413
|
+
}) &&
|
|
414
|
+
patterns.every((pattern) => {
|
|
415
|
+
if (settled.includes(pattern))
|
|
416
|
+
return true;
|
|
417
|
+
const member = childOf(conjunct, 'patternProperties', pattern);
|
|
418
|
+
return member !== undefined && this.includes(required, member);
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
/** Locations one conjunct applies to the value of property `name`. */
|
|
422
|
+
memberConstraints(evidence, name) {
|
|
423
|
+
return evidence.conjuncts.flatMap((conjunct) => {
|
|
424
|
+
const schema = schemaOf(conjunct);
|
|
425
|
+
if (!isJsonObject(schema))
|
|
426
|
+
return [];
|
|
427
|
+
const constraints = [];
|
|
428
|
+
const declared = isJsonObject(schema.properties) && Object.hasOwn(schema.properties, name);
|
|
429
|
+
if (declared) {
|
|
430
|
+
const member = childOf(conjunct, 'properties', name);
|
|
431
|
+
if (member !== undefined)
|
|
432
|
+
constraints.push(member);
|
|
433
|
+
}
|
|
434
|
+
let matched = false;
|
|
435
|
+
if (isJsonObject(schema.patternProperties)) {
|
|
436
|
+
for (const pattern of Object.keys(schema.patternProperties)) {
|
|
437
|
+
if (!matches(conjunct, pattern, name))
|
|
438
|
+
continue;
|
|
439
|
+
matched = true;
|
|
440
|
+
const member = childOf(conjunct, 'patternProperties', pattern);
|
|
441
|
+
if (member !== undefined)
|
|
442
|
+
constraints.push(member);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!declared && !matched) {
|
|
446
|
+
const rest = childOf(conjunct, 'additionalProperties');
|
|
447
|
+
if (rest !== undefined)
|
|
448
|
+
constraints.push(rest);
|
|
449
|
+
}
|
|
450
|
+
return constraints;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/** Whether an admitted object never has property `name`. */
|
|
454
|
+
forbids(evidence, name) {
|
|
455
|
+
return this.memberConstraints(evidence, name).some((constraint) => this.evidence(constraint).kinds === 0);
|
|
456
|
+
}
|
|
457
|
+
/** The complete property-name set of a closed object conjunct, when it is one. */
|
|
458
|
+
closedNames(conjunct) {
|
|
459
|
+
const schema = schemaOf(conjunct);
|
|
460
|
+
if (!isJsonObject(schema))
|
|
461
|
+
return undefined;
|
|
462
|
+
if (isJsonObject(schema.patternProperties) && Object.keys(schema.patternProperties).length > 0)
|
|
463
|
+
return undefined;
|
|
464
|
+
const rest = childOf(conjunct, 'additionalProperties');
|
|
465
|
+
if (rest === undefined || this.evidence(rest).kinds !== 0)
|
|
466
|
+
return undefined;
|
|
467
|
+
return isJsonObject(schema.properties) ? Object.keys(schema.properties) : [];
|
|
468
|
+
}
|
|
469
|
+
/** Whether the two locations are bisimilar: equal assertions over paired subschemas. */
|
|
470
|
+
equivalent(left, right) {
|
|
471
|
+
const pending = [[left, right]];
|
|
472
|
+
const seen = new Set();
|
|
473
|
+
while (pending.length > 0) {
|
|
474
|
+
const [first, second] = pending.pop();
|
|
475
|
+
const key = this.pair(first, second);
|
|
476
|
+
if (seen.has(key))
|
|
477
|
+
continue;
|
|
478
|
+
seen.add(key);
|
|
479
|
+
const a = schemaOf(first);
|
|
480
|
+
const b = schemaOf(second);
|
|
481
|
+
if (typeof a === 'boolean' || typeof b === 'boolean') {
|
|
482
|
+
if (a !== b)
|
|
483
|
+
return false;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const assertions = assertionKeys(a);
|
|
487
|
+
if (assertions.length !== assertionKeys(b).length)
|
|
488
|
+
return false;
|
|
489
|
+
for (const keyword of assertions) {
|
|
490
|
+
if (!Object.hasOwn(b, keyword) || !jsonEqual(a[keyword], b[keyword]))
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
const firstChildren = subschemaChildren(first);
|
|
494
|
+
const secondChildren = subschemaChildren(second);
|
|
495
|
+
if (firstChildren.size !== secondChildren.size)
|
|
496
|
+
return false;
|
|
497
|
+
for (const [path, index] of firstChildren) {
|
|
498
|
+
const other = secondChildren.get(path);
|
|
499
|
+
if (other === undefined)
|
|
500
|
+
return false;
|
|
501
|
+
pending.push([at(first.program, index), at(second.program, other)]);
|
|
502
|
+
}
|
|
503
|
+
const firstReference = referenceOf(first);
|
|
504
|
+
const secondReference = referenceOf(second);
|
|
505
|
+
if ((firstReference === undefined) !== (secondReference === undefined))
|
|
506
|
+
return false;
|
|
507
|
+
if (firstReference !== undefined)
|
|
508
|
+
pending.push([firstReference, secondReference]);
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
evidence(location) {
|
|
513
|
+
const key = this.location(location);
|
|
514
|
+
const cached = this.evidenceByAt.get(key);
|
|
515
|
+
if (cached !== undefined)
|
|
516
|
+
return cached;
|
|
517
|
+
const conjuncts = conjunctsOf(location);
|
|
518
|
+
let kinds = ANY;
|
|
519
|
+
let values;
|
|
520
|
+
let upper;
|
|
521
|
+
let lower;
|
|
522
|
+
const multipleOf = [];
|
|
523
|
+
let maxLength;
|
|
524
|
+
let minLength = 0;
|
|
525
|
+
const patterns = new Set();
|
|
526
|
+
const fullMatches = new Set();
|
|
527
|
+
let maxItems;
|
|
528
|
+
let minItems = 0;
|
|
529
|
+
let uniqueItems = false;
|
|
530
|
+
let maxProperties;
|
|
531
|
+
let minProperties = 0;
|
|
532
|
+
const required = new Set();
|
|
533
|
+
const dependentRequired = new Map();
|
|
534
|
+
const valueKinds = new Set();
|
|
535
|
+
const narrow = (candidates) => {
|
|
536
|
+
values =
|
|
537
|
+
values === undefined
|
|
538
|
+
? [...candidates]
|
|
539
|
+
: values.filter((value) => candidates.some((candidate) => jsonEqual(value, candidate)));
|
|
540
|
+
};
|
|
541
|
+
for (const conjunct of conjuncts) {
|
|
542
|
+
const schema = schemaOf(conjunct);
|
|
543
|
+
if (schema === false)
|
|
544
|
+
kinds = 0;
|
|
545
|
+
if (!isJsonObject(schema))
|
|
546
|
+
continue;
|
|
547
|
+
kinds &= typeMask(schema.type);
|
|
548
|
+
if (Object.hasOwn(schema, 'const')) {
|
|
549
|
+
narrow([schema.const]);
|
|
550
|
+
kinds &= kindOf(schema.const);
|
|
551
|
+
}
|
|
552
|
+
if (Array.isArray(schema.enum)) {
|
|
553
|
+
narrow(schema.enum);
|
|
554
|
+
kinds &= schema.enum.reduce((mask, value) => mask | kindOf(value), 0);
|
|
555
|
+
}
|
|
556
|
+
const valueKind = schema['x-astrale-value-kind'];
|
|
557
|
+
if (typeof valueKind === 'string') {
|
|
558
|
+
valueKinds.add(valueKind);
|
|
559
|
+
if (valueKind === 'instant')
|
|
560
|
+
kinds &= STRING;
|
|
561
|
+
if (valueKind === 'blob')
|
|
562
|
+
kinds &= OBJECT;
|
|
563
|
+
}
|
|
564
|
+
upper = tighter(upper, schema.maximum, false, (a, b) => a < b);
|
|
565
|
+
upper = tighter(upper, schema.exclusiveMaximum, true, (a, b) => a < b);
|
|
566
|
+
lower = tighter(lower, schema.minimum, false, (a, b) => a > b);
|
|
567
|
+
lower = tighter(lower, schema.exclusiveMinimum, true, (a, b) => a > b);
|
|
568
|
+
if (typeof schema.multipleOf === 'number')
|
|
569
|
+
multipleOf.push(schema.multipleOf);
|
|
570
|
+
if (typeof schema.maxLength === 'number')
|
|
571
|
+
maxLength = min(maxLength, schema.maxLength);
|
|
572
|
+
if (typeof schema.minLength === 'number')
|
|
573
|
+
minLength = Math.max(minLength, schema.minLength);
|
|
574
|
+
if (typeof schema.pattern === 'string')
|
|
575
|
+
patterns.add(schema.pattern);
|
|
576
|
+
if (typeof schema.fullMatch === 'string')
|
|
577
|
+
fullMatches.add(schema.fullMatch);
|
|
578
|
+
if (typeof schema.maxItems === 'number')
|
|
579
|
+
maxItems = min(maxItems, schema.maxItems);
|
|
580
|
+
if (typeof schema.minItems === 'number')
|
|
581
|
+
minItems = Math.max(minItems, schema.minItems);
|
|
582
|
+
if (schema.uniqueItems === true)
|
|
583
|
+
uniqueItems = true;
|
|
584
|
+
if (typeof schema.maxProperties === 'number') {
|
|
585
|
+
maxProperties = min(maxProperties, schema.maxProperties);
|
|
586
|
+
}
|
|
587
|
+
if (typeof schema.minProperties === 'number') {
|
|
588
|
+
minProperties = Math.max(minProperties, schema.minProperties);
|
|
589
|
+
}
|
|
590
|
+
if (Array.isArray(schema.required)) {
|
|
591
|
+
for (const name of schema.required)
|
|
592
|
+
if (typeof name === 'string')
|
|
593
|
+
required.add(name);
|
|
594
|
+
}
|
|
595
|
+
if (isJsonObject(schema.dependentRequired)) {
|
|
596
|
+
for (const [name, dependencies] of Object.entries(schema.dependentRequired)) {
|
|
597
|
+
if (!Array.isArray(dependencies))
|
|
598
|
+
continue;
|
|
599
|
+
const known = dependentRequired.get(name) ?? new Set();
|
|
600
|
+
for (const dependency of dependencies) {
|
|
601
|
+
if (typeof dependency === 'string')
|
|
602
|
+
known.add(dependency);
|
|
603
|
+
}
|
|
604
|
+
dependentRequired.set(name, known);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const finite = values === undefined
|
|
609
|
+
? finiteKinds(kinds)
|
|
610
|
+
: values.filter((value) => (kindOf(value) & kinds) !== 0);
|
|
611
|
+
const result = Object.freeze({
|
|
612
|
+
conjuncts,
|
|
613
|
+
kinds,
|
|
614
|
+
...(finite === undefined ? {} : { values: finite }),
|
|
615
|
+
...(upper === undefined ? {} : { upper }),
|
|
616
|
+
...(lower === undefined ? {} : { lower }),
|
|
617
|
+
multipleOf,
|
|
618
|
+
...(maxLength === undefined ? {} : { maxLength }),
|
|
619
|
+
minLength,
|
|
620
|
+
patterns,
|
|
621
|
+
fullMatches,
|
|
622
|
+
...(maxItems === undefined ? {} : { maxItems }),
|
|
623
|
+
minItems,
|
|
624
|
+
uniqueItems,
|
|
625
|
+
...(maxProperties === undefined ? {} : { maxProperties }),
|
|
626
|
+
minProperties,
|
|
627
|
+
required,
|
|
628
|
+
dependentRequired,
|
|
629
|
+
valueKinds,
|
|
630
|
+
});
|
|
631
|
+
this.evidenceByAt.set(key, result);
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
accepts(location, value) {
|
|
635
|
+
return evaluateInstance(loadValueProgram(location.program, location.index), value).valid;
|
|
636
|
+
}
|
|
637
|
+
location(value) {
|
|
638
|
+
let id = this.programs.get(value.program);
|
|
639
|
+
if (id === undefined) {
|
|
640
|
+
id = this.programs.size;
|
|
641
|
+
this.programs.set(value.program, id);
|
|
642
|
+
}
|
|
643
|
+
return `${id}:${value.index}`;
|
|
644
|
+
}
|
|
645
|
+
pair(outer, inner) {
|
|
646
|
+
return `${this.location(outer)}|${this.location(inner)}`;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function at(program, index) {
|
|
650
|
+
return { program, index };
|
|
651
|
+
}
|
|
652
|
+
function schemaOf(location) {
|
|
653
|
+
const node = location.program.nodes[location.index];
|
|
654
|
+
if (node === undefined)
|
|
655
|
+
throw new TypeError('Value program location is absent.');
|
|
656
|
+
return node.schema;
|
|
657
|
+
}
|
|
658
|
+
function childOf(location, keyword, member) {
|
|
659
|
+
const base = appendPointer('', keyword);
|
|
660
|
+
const key = (member === undefined ? base : appendPointer(base, member)).slice(1);
|
|
661
|
+
const index = location.program.nodes[location.index]?.children[key];
|
|
662
|
+
return index === undefined ? undefined : at(location.program, index);
|
|
663
|
+
}
|
|
664
|
+
function childrenOf(location, keyword) {
|
|
665
|
+
const schema = schemaOf(location);
|
|
666
|
+
if (!isJsonObject(schema) || !Array.isArray(schema[keyword]))
|
|
667
|
+
return [];
|
|
668
|
+
return schema[keyword].flatMap((_, index) => {
|
|
669
|
+
const child = childOf(location, keyword, index);
|
|
670
|
+
return child === undefined ? [] : [child];
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function referenceOf(location) {
|
|
674
|
+
const index = location.program.nodes[location.index]?.reference;
|
|
675
|
+
return index === undefined ? undefined : at(location.program, index);
|
|
676
|
+
}
|
|
677
|
+
/** The location with its `$ref` targets and `allOf` branches; admission makes this acyclic. */
|
|
678
|
+
function conjunctsOf(location) {
|
|
679
|
+
const seen = new Set();
|
|
680
|
+
const conjuncts = [];
|
|
681
|
+
const visit = (current) => {
|
|
682
|
+
if (seen.has(current.index))
|
|
683
|
+
return;
|
|
684
|
+
seen.add(current.index);
|
|
685
|
+
conjuncts.push(current);
|
|
686
|
+
const reference = referenceOf(current);
|
|
687
|
+
if (reference !== undefined)
|
|
688
|
+
visit(reference);
|
|
689
|
+
for (const branch of childrenOf(current, 'allOf'))
|
|
690
|
+
visit(branch);
|
|
691
|
+
};
|
|
692
|
+
visit(location);
|
|
693
|
+
return conjuncts;
|
|
694
|
+
}
|
|
695
|
+
function matches(location, pattern, name) {
|
|
696
|
+
return loadValueProgram(location.program).matcher(pattern).test(name);
|
|
697
|
+
}
|
|
698
|
+
/** Whether a schema asserts nothing. */
|
|
699
|
+
function isTop(schema) {
|
|
700
|
+
if (schema === true)
|
|
701
|
+
return true;
|
|
702
|
+
if (schema === false)
|
|
703
|
+
return false;
|
|
704
|
+
return Object.keys(schema).every((keyword) => INERT.has(keyword) ||
|
|
705
|
+
((keyword === 'then' || keyword === 'else') && !Object.hasOwn(schema, 'if')) ||
|
|
706
|
+
((keyword === 'minContains' || keyword === 'maxContains') &&
|
|
707
|
+
!Object.hasOwn(schema, 'contains')));
|
|
708
|
+
}
|
|
709
|
+
function assertionKeys(schema) {
|
|
710
|
+
return Object.keys(schema).filter((keyword) => !INERT.has(keyword) && !SUBSCHEMA_KEYWORDS.has(keyword) && keyword !== '$ref');
|
|
711
|
+
}
|
|
712
|
+
/** Subschema children keyed by their keyword path, without `$defs` or inert annotations. */
|
|
713
|
+
function subschemaChildren(location) {
|
|
714
|
+
const children = location.program.nodes[location.index]?.children ?? {};
|
|
715
|
+
return new Map(Object.entries(children).filter(([path]) => SUBSCHEMA_KEYWORDS.has(path.split('/', 1)[0].replaceAll('~1', '/').replaceAll('~0', '~'))));
|
|
716
|
+
}
|
|
717
|
+
function has(evidence, kinds) {
|
|
718
|
+
return (evidence.kinds & kinds) !== 0;
|
|
719
|
+
}
|
|
720
|
+
function typeMask(type) {
|
|
721
|
+
if (type === undefined)
|
|
722
|
+
return ANY;
|
|
723
|
+
const names = typeof type === 'string' ? [type] : Array.isArray(type) ? type : [];
|
|
724
|
+
return names.reduce((mask, name) => {
|
|
725
|
+
switch (name) {
|
|
726
|
+
case 'null':
|
|
727
|
+
return mask | NULL;
|
|
728
|
+
case 'boolean':
|
|
729
|
+
return mask | BOOLEAN;
|
|
730
|
+
case 'object':
|
|
731
|
+
return mask | OBJECT;
|
|
732
|
+
case 'array':
|
|
733
|
+
return mask | ARRAY;
|
|
734
|
+
case 'string':
|
|
735
|
+
return mask | STRING;
|
|
736
|
+
case 'integer':
|
|
737
|
+
return mask | INTEGER;
|
|
738
|
+
case 'number':
|
|
739
|
+
return mask | NUMBER;
|
|
740
|
+
default:
|
|
741
|
+
return mask;
|
|
742
|
+
}
|
|
743
|
+
}, 0);
|
|
744
|
+
}
|
|
745
|
+
function kindOf(value) {
|
|
746
|
+
if (value === null)
|
|
747
|
+
return NULL;
|
|
748
|
+
if (typeof value === 'boolean')
|
|
749
|
+
return BOOLEAN;
|
|
750
|
+
if (typeof value === 'string')
|
|
751
|
+
return STRING;
|
|
752
|
+
if (typeof value === 'number')
|
|
753
|
+
return Number.isInteger(value) ? INTEGER : FRACTION;
|
|
754
|
+
return Array.isArray(value) ? ARRAY : OBJECT;
|
|
755
|
+
}
|
|
756
|
+
/** The finite value set of a kind mask made only of null and booleans. */
|
|
757
|
+
function finiteKinds(kinds) {
|
|
758
|
+
if ((kinds & ~(NULL | BOOLEAN)) !== 0)
|
|
759
|
+
return undefined;
|
|
760
|
+
return [
|
|
761
|
+
...((kinds & NULL) !== 0 ? [null] : []),
|
|
762
|
+
...((kinds & BOOLEAN) !== 0 ? [true, false] : []),
|
|
763
|
+
];
|
|
764
|
+
}
|
|
765
|
+
function tighter(current, value, exclusive, stricter) {
|
|
766
|
+
if (typeof value !== 'number')
|
|
767
|
+
return current;
|
|
768
|
+
if (current === undefined ||
|
|
769
|
+
stricter(value, current.value) ||
|
|
770
|
+
(value === current.value && exclusive)) {
|
|
771
|
+
return { value, exclusive };
|
|
772
|
+
}
|
|
773
|
+
return current;
|
|
774
|
+
}
|
|
775
|
+
function min(current, value) {
|
|
776
|
+
return current === undefined ? value : Math.min(current, value);
|
|
777
|
+
}
|