@astrale-os/kernel-dsl 0.2.0 → 1.0.0-beta.10
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/builder/callable/authoring.d.ts +10 -4
- package/dist/v1/builder/callable/authoring.js +2 -0
- package/dist/v1/builder/class/authoring.d.ts +36 -1
- package/dist/v1/builder/class/authoring.js +25 -5
- package/dist/v1/builder/index.d.ts +1 -1
- package/dist/v1/builder/lowering/callable.js +1 -0
- package/dist/v1/builder/lowering/class.js +18 -1
- package/dist/v1/builder/lowering/property-keys.d.ts +27 -0
- package/dist/v1/builder/lowering/property-keys.js +108 -0
- package/dist/v1/builder/lowering/schema/compile.js +8 -3
- package/dist/v1/builder/lowering/schema/context.d.ts +2 -0
- package/dist/v1/builder/lowering/schema/context.js +12 -0
- package/dist/v1/builder/lowering/schema/lifetime.d.ts +11 -0
- package/dist/v1/builder/lowering/schema/lifetime.js +55 -0
- package/dist/v1/builder/schema/diagnostic.d.ts +1 -1
- package/dist/v1/builder/schema/projection/callable.d.ts +7 -5
- package/dist/v1/builder/schema/projection/model.d.ts +4 -2
- package/dist/v1/builder/schema/projection/resolved-class.d.ts +2 -1
- package/dist/v1/compiled/admission/semantics/cardinality.d.ts +1 -1
- package/dist/v1/compiled/admission/semantics/cardinality.js +16 -1
- package/dist/v1/compiled/admission/semantics/class.js +31 -1
- package/dist/v1/compiled/admission/semantics/core.js +16 -0
- package/dist/v1/compiled/admission/wire/callable.js +11 -1
- package/dist/v1/compiled/admission/wire/class.js +50 -3
- package/dist/v1/compiled/compiler/compile.js +2 -0
- package/dist/v1/domain/create.js +1 -0
- package/dist/v1/domain/projection/classes.js +3 -0
- package/dist/v1/index.d.ts +2 -1
- package/dist/v1/language/callable/admission.d.ts +15 -0
- package/dist/v1/language/callable/admission.js +43 -0
- package/dist/v1/language/callable/model/callable.d.ts +10 -0
- package/dist/v1/language/callable/resolved.d.ts +5 -1
- package/dist/v1/language/class/inheritance.d.ts +18 -5
- package/dist/v1/language/class/inheritance.js +14 -1
- package/dist/v1/language/class/lifetime.d.ts +50 -11
- package/dist/v1/language/class/lifetime.js +89 -20
- package/dist/v1/language/class/model/class.d.ts +25 -1
- package/dist/v1/language/class/model/edge.d.ts +30 -1
- package/dist/v1/language/class/unique.d.ts +143 -0
- package/dist/v1/language/class/unique.js +264 -0
- package/dist/v1/language/index.d.ts +6 -2
- package/dist/v1/language/index.js +4 -1
- package/dist/v1/language/value/evaluate/validate.js +4 -0
- package/dist/v1/language/value/evaluate/validity.optimization.d.ts +14 -0
- package/dist/v1/language/value/evaluate/validity.optimization.js +342 -0
- package/dist/v1/language/value/profile/canonicalize.js +6 -5
- package/dist/v1/schema/compatibility/api.d.ts +1 -0
- package/dist/v1/schema/compatibility/api.js +1 -0
- package/dist/v1/schema/compatibility/comparators/classes.d.ts +9 -4
- package/dist/v1/schema/compatibility/comparators/classes.js +25 -4
- package/dist/v1/schema/compatibility/comparators/index.d.ts +1 -1
- package/dist/v1/schema/compatibility/extension.d.ts +11 -0
- package/dist/v1/schema/compatibility/extension.js +22 -0
- package/dist/v1/schema/compatibility/fields.d.ts +20 -4
- package/dist/v1/schema/compatibility/fields.js +13 -3
- package/dist/v1/schema/compatibility/model.d.ts +11 -3
- package/dist/v1/schema/compatibility/scope.d.ts +2 -0
- package/dist/v1/schema/compatibility/scope.js +4 -1
- package/dist/v1/schema/compatibility/structure/index.js +6 -4
- package/dist/v1/schema/compatibility/structure/requirements.js +32 -1
- package/dist/v1/schema/compatibility/versioning/surface.d.ts +1 -1
- package/dist/v1/schema/compatibility/versioning/surface.js +30 -2
- package/dist/v1/schema/diagnostic.d.ts +1 -1
- package/dist/v1/schema/normalization/domain.js +18 -0
- package/dist/v1/schema/validation/callable.d.ts +1 -1
- package/dist/v1/schema/validation/callable.js +17 -2
- package/dist/v1/schema/validation/class.js +80 -2
- package/dist/v1/schema/validation/core.js +50 -0
- package/dist/v1/schema/validation/domain.structure.js +45 -10
- package/dist/value/canonical.d.ts +5 -0
- package/dist/value/canonical.js +12 -0
- package/dist/value/json.d.ts +1 -1
- package/dist/value/json.js +1 -1
- package/package.json +1 -1
- package/dist/v1/builder/lowering/core-properties.d.ts +0 -10
- package/dist/v1/builder/lowering/core-properties.js +0 -76
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { json } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import { ClassKey } from '../../addressing/class-key.js';
|
|
3
|
+
import { compareUnicode } from '../../addressing/ordering.js';
|
|
4
|
+
import { PropertyKey } from '../../addressing/property-key.js';
|
|
5
|
+
/** The kinds of key fields, in canonical order: the endpoints, the slug, then the Properties. */
|
|
6
|
+
const KINDS = Object.freeze([
|
|
7
|
+
'source',
|
|
8
|
+
'target',
|
|
9
|
+
'slug',
|
|
10
|
+
'property',
|
|
11
|
+
]);
|
|
12
|
+
/** The canonical order of forms: kind by kind, a form before every form it prefixes. */
|
|
13
|
+
const compareForms = compareSequences(compareKinds);
|
|
14
|
+
/**
|
|
15
|
+
* Every form a key tuple may take, in canonical order: each nonempty set of field kinds. A Property
|
|
16
|
+
* kind stands for one Property or more, whatever their keys.
|
|
17
|
+
*/
|
|
18
|
+
export const EDGE_UNIQUE_FORMS = Object.freeze(Array.from({ length: (1 << KINDS.length) - 1 }, (_, index) => Object.freeze(KINDS.filter((_, kind) => ((index + 1) & (1 << kind)) !== 0))).sort(compareForms));
|
|
19
|
+
/** The one form a Node key tuple takes: its fields are Properties, whatever their keys. */
|
|
20
|
+
export const NODE_UNIQUE_FORMS = Object.freeze([
|
|
21
|
+
Object.freeze(['property']),
|
|
22
|
+
]);
|
|
23
|
+
/** Every form of every kind, which a backend enforcing any key tuple announces. */
|
|
24
|
+
export const UNIQUE_FORMS = Object.freeze({
|
|
25
|
+
edge: EDGE_UNIQUE_FORMS,
|
|
26
|
+
node: NODE_UNIQUE_FORMS,
|
|
27
|
+
});
|
|
28
|
+
/** No form of either kind: a backend that announces none enforces no key tuple. */
|
|
29
|
+
export const NO_UNIQUE_FORMS = Object.freeze({
|
|
30
|
+
edge: Object.freeze([]),
|
|
31
|
+
node: Object.freeze([]),
|
|
32
|
+
});
|
|
33
|
+
/** The kind of `field`. */
|
|
34
|
+
function kindOf(field) {
|
|
35
|
+
return typeof field === 'string' ? field : 'property';
|
|
36
|
+
}
|
|
37
|
+
function compareKinds(left, right) {
|
|
38
|
+
return KINDS.indexOf(left) - KINDS.indexOf(right);
|
|
39
|
+
}
|
|
40
|
+
/** The canonical order of fields: source, target, slug, then Properties by key. */
|
|
41
|
+
export function compareEdgeUniqueFields(left, right) {
|
|
42
|
+
return (compareKinds(kindOf(left), kindOf(right)) ||
|
|
43
|
+
(typeof left === 'string' || typeof right === 'string'
|
|
44
|
+
? 0
|
|
45
|
+
: compareUnicode(left.property, right.property)));
|
|
46
|
+
}
|
|
47
|
+
/** The canonical order of tuples: field by field, a tuple before every tuple it prefixes. */
|
|
48
|
+
export const compareEdgeUniqueTuples = compareSequences(compareEdgeUniqueFields);
|
|
49
|
+
/** The field `value` spells, detached, or undefined when it spells none. */
|
|
50
|
+
function edgeUniqueField(value) {
|
|
51
|
+
if (value === 'source' || value === 'target' || value === 'slug')
|
|
52
|
+
return value;
|
|
53
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
54
|
+
return undefined;
|
|
55
|
+
const keys = Object.keys(value);
|
|
56
|
+
const property = value.property;
|
|
57
|
+
return keys.length === 1 && keys[0] === 'property' && PropertyKey.is(property)
|
|
58
|
+
? Object.freeze({ property })
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The tuple `value` spells, detached, or undefined when it spells none: a nonempty list of fields,
|
|
63
|
+
* distinct and in canonical order, so one set of fields has one spelling.
|
|
64
|
+
*/
|
|
65
|
+
export function edgeUniqueTuple(value) {
|
|
66
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const fields = [];
|
|
69
|
+
for (const item of value) {
|
|
70
|
+
const field = edgeUniqueField(item);
|
|
71
|
+
if (field === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (fields.length > 0 && compareEdgeUniqueFields(fields.at(-1), field) >= 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
fields.push(field);
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze(fields);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The tuple the endpoints and slug `named` holds spell, in canonical order, or undefined when it
|
|
81
|
+
* holds none: the key a selector names an Edge by. A selector names no Property.
|
|
82
|
+
*/
|
|
83
|
+
export function namedEdgeUniqueTuple(named) {
|
|
84
|
+
return edgeUniqueTuple(['source', 'target', 'slug'].filter((field) => named[field] !== undefined));
|
|
85
|
+
}
|
|
86
|
+
/** The canonical order of Node tuples: Property by Property, a tuple before every one it prefixes. */
|
|
87
|
+
export const compareNodeUniqueTuples = compareSequences(compareUnicode);
|
|
88
|
+
/**
|
|
89
|
+
* The Node tuple `value` spells, detached, or undefined when it spells none: a nonempty list of
|
|
90
|
+
* Property keys, distinct and in code-point order, so one set of Properties has one spelling.
|
|
91
|
+
*/
|
|
92
|
+
export function nodeUniqueTuple(value) {
|
|
93
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
const keys = [];
|
|
96
|
+
for (const key of value) {
|
|
97
|
+
if (!PropertyKey.is(key))
|
|
98
|
+
return undefined;
|
|
99
|
+
if (keys.length > 0 && compareUnicode(keys.at(-1), key) >= 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
keys.push(key);
|
|
102
|
+
}
|
|
103
|
+
return Object.freeze(keys);
|
|
104
|
+
}
|
|
105
|
+
/** The form of `NODE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
|
|
106
|
+
export function nodeUniqueForm(value) {
|
|
107
|
+
return Array.isArray(value) && value.length === 1 && value[0] === 'property'
|
|
108
|
+
? NODE_UNIQUE_FORMS[0]
|
|
109
|
+
: undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether a backend announcing `forms` for Nodes enforces the Node tuple `value` spells: every
|
|
113
|
+
* Node tuple has the one Node form. False when `value` spells no tuple.
|
|
114
|
+
*/
|
|
115
|
+
export function enforcesNodeUniqueTuple(forms, value) {
|
|
116
|
+
return forms.length > 0 && nodeUniqueTuple(value) !== undefined;
|
|
117
|
+
}
|
|
118
|
+
/** The form of `tuple`: the kinds of its fields, each once. */
|
|
119
|
+
export function edgeUniqueFormOf(tuple) {
|
|
120
|
+
const kinds = new Set(tuple.map(kindOf));
|
|
121
|
+
return EDGE_UNIQUE_FORMS.find((form) => form.length === kinds.size && form.every((kind) => kinds.has(kind)));
|
|
122
|
+
}
|
|
123
|
+
/** The form of `EDGE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
|
|
124
|
+
export function edgeUniqueForm(value) {
|
|
125
|
+
if (!Array.isArray(value))
|
|
126
|
+
return undefined;
|
|
127
|
+
return EDGE_UNIQUE_FORMS.find((form) => form.length === value.length && form.every((kind, index) => kind === value[index]));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Whether a backend announcing `forms` enforces the tuple `value` spells: its form is one of them.
|
|
131
|
+
* False when `value` spells no tuple, so a tuple is never taken as enforced by mistake.
|
|
132
|
+
*/
|
|
133
|
+
export function enforcesEdgeUniqueTuple(forms, value) {
|
|
134
|
+
const tuple = edgeUniqueTuple(value);
|
|
135
|
+
if (tuple === undefined)
|
|
136
|
+
return false;
|
|
137
|
+
const form = edgeUniqueFormOf(tuple);
|
|
138
|
+
return forms.some((candidate) => compareForms(candidate, form) === 0);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether a Property may be a key field (D-S1): a single-kind scalar by its ordering facet, a
|
|
142
|
+
* string, number, boolean or instant, that no automatic program writes.
|
|
143
|
+
*/
|
|
144
|
+
export function keyableProperty(ordering, auto) {
|
|
145
|
+
return ordering !== undefined && auto === undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The values of the fields of `tuple` on `edge`, in order, or undefined when one of them is absent,
|
|
149
|
+
* so that an Edge without a slug or without a Property never collides.
|
|
150
|
+
*/
|
|
151
|
+
export function edgeUniqueValues(tuple, edge) {
|
|
152
|
+
return keyValues(tuple, (field) => typeof field === 'string' ? edge[field] : propertyOf(edge.props, field.property));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The value `edge` takes under `tuple`, the canonical JSON of the values of its fields, or
|
|
156
|
+
* undefined when one of them is absent: two distinct Edges of the Classes a tuple covers collide
|
|
157
|
+
* exactly when they take one value. Endpoints compare by NodeId, slugs by exact text and
|
|
158
|
+
* Properties by canonical Value.
|
|
159
|
+
*/
|
|
160
|
+
export function edgeUniqueValue(tuple, edge) {
|
|
161
|
+
return serialized(edgeUniqueValues(tuple, edge));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The values of the Properties of `tuple` on `node`, in order, or undefined when one of them is
|
|
165
|
+
* absent, so that a Node without one of them never collides, like NULL in an SQL UNIQUE.
|
|
166
|
+
*/
|
|
167
|
+
export function nodeUniqueValues(tuple, node) {
|
|
168
|
+
return keyValues(tuple, (key) => propertyOf(node.props, key));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The value `node` takes under `tuple`, the canonical JSON of the values of its Properties, or
|
|
172
|
+
* undefined when one of them is absent: two distinct Nodes of the Classes a tuple covers collide
|
|
173
|
+
* exactly when they take one value. Properties compare by canonical Value, nothing normalized.
|
|
174
|
+
*/
|
|
175
|
+
export function nodeUniqueValue(tuple, node) {
|
|
176
|
+
return serialized(nodeUniqueValues(tuple, node));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The tuples covering `candidate`: those it and each of its ancestors declare, each with the Class
|
|
180
|
+
* declaring it, ancestors found by `resolve`. A descendant never restates them.
|
|
181
|
+
*/
|
|
182
|
+
export function coveringEdgeUniqueTuples(candidate, resolve) {
|
|
183
|
+
return coveringTuples(candidate, resolve);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The Node tuples covering `candidate`: those it and each of its ancestors declare, each with the
|
|
187
|
+
* Class declaring it, ancestors found by `resolve`. A descendant never restates them.
|
|
188
|
+
*/
|
|
189
|
+
export function coveringNodeUniqueTuples(candidate, resolve) {
|
|
190
|
+
return coveringTuples(candidate, resolve);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The keys `edge` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
|
|
194
|
+
* tuple, so two Edges collide exactly when they hold one key.
|
|
195
|
+
*/
|
|
196
|
+
export function edgeUniqueKeys(tuples, edge) {
|
|
197
|
+
return heldKeys(tuples, (fields) => edgeUniqueValue(fields, edge));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The keys `node` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
|
|
201
|
+
* tuple, so two Nodes collide exactly when they hold one key.
|
|
202
|
+
*/
|
|
203
|
+
export function nodeUniqueKeys(tuples, node) {
|
|
204
|
+
return heldKeys(tuples, (fields) => nodeUniqueValue(fields, node));
|
|
205
|
+
}
|
|
206
|
+
/** The values `read` gives the fields of `tuple`, in order, or undefined when one is absent. */
|
|
207
|
+
function keyValues(tuple, read) {
|
|
208
|
+
const values = [];
|
|
209
|
+
for (const field of tuple) {
|
|
210
|
+
const value = read(field);
|
|
211
|
+
if (value === undefined)
|
|
212
|
+
return undefined;
|
|
213
|
+
values.push(value);
|
|
214
|
+
}
|
|
215
|
+
return Object.freeze(values);
|
|
216
|
+
}
|
|
217
|
+
/** The value of the Property `key` in `props`, or undefined when it is absent. */
|
|
218
|
+
function propertyOf(props, key) {
|
|
219
|
+
return Object.hasOwn(props, key) ? props[key] : undefined;
|
|
220
|
+
}
|
|
221
|
+
/** The canonical JSON of a key's values, or undefined when it has none. */
|
|
222
|
+
function serialized(values) {
|
|
223
|
+
return values === undefined ? undefined : json.serializeAdmitted(values);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The tuples `candidate` and its ancestors declare, each with its declarer. Only ancestors of the
|
|
227
|
+
* candidate's kind are visited, so every tuple is of that kind.
|
|
228
|
+
*/
|
|
229
|
+
function coveringTuples(candidate, resolve) {
|
|
230
|
+
const tuples = [];
|
|
231
|
+
const seen = new Set();
|
|
232
|
+
const visit = (value) => {
|
|
233
|
+
if (seen.has(value) || value.kind !== candidate.kind)
|
|
234
|
+
return;
|
|
235
|
+
seen.add(value);
|
|
236
|
+
for (const fields of value.unique ?? [])
|
|
237
|
+
tuples.push(Object.freeze({ class: ClassKey.of(value.ref), fields }));
|
|
238
|
+
for (const ref of value.extends) {
|
|
239
|
+
const parent = resolve(ref);
|
|
240
|
+
if (parent !== undefined)
|
|
241
|
+
visit(parent);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
visit(candidate);
|
|
245
|
+
return Object.freeze(tuples);
|
|
246
|
+
}
|
|
247
|
+
/** The keys a fact holds under `tuples`, its value under each tagged by the declaring Class. */
|
|
248
|
+
function heldKeys(tuples, valueOf) {
|
|
249
|
+
return tuples.flatMap(({ class: declaring, fields }) => {
|
|
250
|
+
const value = valueOf(fields);
|
|
251
|
+
return value === undefined ? [] : [JSON.stringify([declaring, fields, value])];
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/** The lexicographic order of sequences by `compare`, a sequence before every one it prefixes. */
|
|
255
|
+
function compareSequences(compare) {
|
|
256
|
+
return (left, right) => {
|
|
257
|
+
for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
|
|
258
|
+
const order = compare(left[index], right[index]);
|
|
259
|
+
if (order !== 0)
|
|
260
|
+
return order;
|
|
261
|
+
}
|
|
262
|
+
return left.length - right.length;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { admissionInputFields, isAdmissionMaxAge, MAXIMUM_ADMISSION_AGE_SECONDS, } from './callable/admission.js';
|
|
1
2
|
export { isMethodExecutable } from './callable/executable.js';
|
|
2
3
|
export { isBinaryInput } from './callable/model/callable.js';
|
|
3
4
|
export type { ClassRef, CoreRef, DefinitionBase, DefinitionKey, DefinitionRef, FunctionRef, MethodKey, PolicyRef, PropertyKey, ViewRef, } from '../addressing/index.js';
|
|
@@ -13,8 +14,11 @@ export type * from './callable/resolved.js';
|
|
|
13
14
|
export type * from './class/model/class.js';
|
|
14
15
|
export type { DataDefinition, DataFacet, MediaType } from './class/model/data.js';
|
|
15
16
|
export type * from './class/model/edge.js';
|
|
16
|
-
export {
|
|
17
|
-
export
|
|
17
|
+
export { admitsForeignSubclass, inheritanceOf } from './class/inheritance.js';
|
|
18
|
+
export { edgeLifetimes, lifetimeScope } from './class/lifetime.js';
|
|
19
|
+
export type { LifetimeScope } from './class/lifetime.js';
|
|
20
|
+
export { EDGE_UNIQUE_FORMS, NODE_UNIQUE_FORMS, NO_UNIQUE_FORMS, UNIQUE_FORMS, compareEdgeUniqueFields, compareEdgeUniqueTuples, compareNodeUniqueTuples, coveringEdgeUniqueTuples, coveringNodeUniqueTuples, edgeUniqueForm, edgeUniqueFormOf, edgeUniqueKeys, edgeUniqueTuple, edgeUniqueValue, edgeUniqueValues, enforcesEdgeUniqueTuple, enforcesNodeUniqueTuple, keyableProperty, namedEdgeUniqueTuple, nodeUniqueForm, nodeUniqueKeys, nodeUniqueTuple, nodeUniqueValue, nodeUniqueValues, } from './class/unique.js';
|
|
21
|
+
export type { DeclaredEdgeUniqueTuple, DeclaredNodeUniqueTuple, EdgeUniqueFacts, NodeUniqueFacts, UniqueForms, } from './class/unique.js';
|
|
18
22
|
export type * from './class/model/property.js';
|
|
19
23
|
export type * from './class/resolved.js';
|
|
20
24
|
export type * from './class/facets.js';
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export { admissionInputFields, isAdmissionMaxAge, MAXIMUM_ADMISSION_AGE_SECONDS, } from './callable/admission.js';
|
|
1
2
|
export { isMethodExecutable } from './callable/executable.js';
|
|
2
3
|
export { isBinaryInput } from './callable/model/callable.js';
|
|
3
4
|
export { valueProgramIncludes } from './value/inclusion/includes.js';
|
|
4
5
|
export { stateMachine } from './state/index.js';
|
|
5
|
-
export {
|
|
6
|
+
export { admitsForeignSubclass, inheritanceOf } from './class/inheritance.js';
|
|
7
|
+
export { edgeLifetimes, lifetimeScope } from './class/lifetime.js';
|
|
8
|
+
export { EDGE_UNIQUE_FORMS, NODE_UNIQUE_FORMS, NO_UNIQUE_FORMS, UNIQUE_FORMS, compareEdgeUniqueFields, compareEdgeUniqueTuples, compareNodeUniqueTuples, coveringEdgeUniqueTuples, coveringNodeUniqueTuples, edgeUniqueForm, edgeUniqueFormOf, edgeUniqueKeys, edgeUniqueTuple, edgeUniqueValue, edgeUniqueValues, enforcesEdgeUniqueTuple, enforcesNodeUniqueTuple, keyableProperty, namedEdgeUniqueTuple, nodeUniqueForm, nodeUniqueKeys, nodeUniqueTuple, nodeUniqueValue, nodeUniqueValues, } from './class/unique.js';
|
|
6
9
|
export { PropertyValues } from './class/property-values.js';
|
|
7
10
|
export { isClassRuleChecks, policyVariableClass, policyVariableSelector, policyObjectKey, policyObjectOrigin, projectedMemberClass, } from './policy/model/policy.js';
|
|
@@ -2,6 +2,7 @@ import { diagnostic } from '../../validation/diagnostic.js';
|
|
|
2
2
|
import { validateCanonicalJsonDomain } from '../profile/json.validate.js';
|
|
3
3
|
import { evaluateInstance } from './evaluator.js';
|
|
4
4
|
import { prepareValueSchema, } from './preparation.js';
|
|
5
|
+
import { satisfies } from './validity.optimization.js';
|
|
5
6
|
const validatorCache = new WeakMap();
|
|
6
7
|
/**
|
|
7
8
|
* Validate one complete portable value with the admitted V1 Draft 2020-12 profile.
|
|
@@ -30,6 +31,9 @@ export function validatePreparedInstance(compiled, value) {
|
|
|
30
31
|
: instanceDiagnostics(compiled, value, '');
|
|
31
32
|
}
|
|
32
33
|
function instanceDiagnostics(compiled, value, pointer) {
|
|
34
|
+
// A value the compiled check finds valid has nothing to report; any other is evaluated.
|
|
35
|
+
if (satisfies(compiled, value) === true)
|
|
36
|
+
return [];
|
|
33
37
|
const result = evaluateInstance(compiled, value);
|
|
34
38
|
if (result.valid)
|
|
35
39
|
return [];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Value as JsonValue } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import type { CompiledInstanceSchema } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a value satisfies its prepared schema, as the evaluator decides it, without building a
|
|
5
|
+
* failure, an instance pointer or an annotation: the evaluator's verdict, or undefined when this
|
|
6
|
+
* check cannot give it. Only the evaluator reports why a value fails, so a caller keeps it for
|
|
7
|
+
* every value this check does not find valid.
|
|
8
|
+
*
|
|
9
|
+
* Each schema location is compiled once into a closure over its keywords, retained with the
|
|
10
|
+
* prepared schema. A schema whose closure holds an annotation-dependent keyword
|
|
11
|
+
* (`unevaluatedItems`, `unevaluatedProperties`) is left to the evaluator, as is a value nested
|
|
12
|
+
* deeper than this check follows, and any value whose check throws.
|
|
13
|
+
*/
|
|
14
|
+
export declare function satisfies(compiled: CompiledInstanceSchema, value: JsonValue): boolean | undefined;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { isCanonicalBlobRef } from '../model/blob.js';
|
|
2
|
+
import { isInstantValue } from '../model/instant.js';
|
|
3
|
+
import { isValueObject as isJsonObject } from '../model/object.js';
|
|
4
|
+
import { codePointLength, isMultipleOf, jsonEqual, matchesType, unique } from './assertions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Whether a value satisfies its prepared schema, as the evaluator decides it, without building a
|
|
7
|
+
* failure, an instance pointer or an annotation: the evaluator's verdict, or undefined when this
|
|
8
|
+
* check cannot give it. Only the evaluator reports why a value fails, so a caller keeps it for
|
|
9
|
+
* every value this check does not find valid.
|
|
10
|
+
*
|
|
11
|
+
* Each schema location is compiled once into a closure over its keywords, retained with the
|
|
12
|
+
* prepared schema. A schema whose closure holds an annotation-dependent keyword
|
|
13
|
+
* (`unevaluatedItems`, `unevaluatedProperties`) is left to the evaluator, as is a value nested
|
|
14
|
+
* deeper than this check follows, and any value whose check throws.
|
|
15
|
+
*/
|
|
16
|
+
export function satisfies(compiled, value) {
|
|
17
|
+
let checks = compilations.get(compiled);
|
|
18
|
+
if (checks === undefined) {
|
|
19
|
+
checks = annotationFree(compiled) ? new Map() : null;
|
|
20
|
+
compilations.set(compiled, checks);
|
|
21
|
+
}
|
|
22
|
+
if (checks === null)
|
|
23
|
+
return undefined;
|
|
24
|
+
const outer = verdicts;
|
|
25
|
+
verdicts = new Map();
|
|
26
|
+
try {
|
|
27
|
+
return checkOf(compiled, checks, compiled.root)(value, 0);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
verdicts = outer;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Per prepared schema: its checks by location, or null when its closure needs annotations. */
|
|
37
|
+
const compilations = new WeakMap();
|
|
38
|
+
/** Locations one evaluation follows before leaving the value to the evaluator. */
|
|
39
|
+
const MAXIMUM_DEPTH = 1_024;
|
|
40
|
+
const UNDECIDED = new Error('The value is left to the evaluator.');
|
|
41
|
+
/**
|
|
42
|
+
* The verdicts of the current evaluation by referenced location and value. Only a reference reaches
|
|
43
|
+
* one location from several places, so each referenced location is checked once per value, as the
|
|
44
|
+
* evaluator memoizes it, and a schema that references a location twice per level stays linear. A
|
|
45
|
+
* location entered again on the same value is a cycle, left to the evaluator.
|
|
46
|
+
*/
|
|
47
|
+
let verdicts = new Map();
|
|
48
|
+
function annotationFree(compiled) {
|
|
49
|
+
for (const { schema } of compiled.locations.values()) {
|
|
50
|
+
if (isJsonObject(schema) &&
|
|
51
|
+
(Object.hasOwn(schema, 'unevaluatedItems') || Object.hasOwn(schema, 'unevaluatedProperties'))) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/** The check of one location, compiled on its first use so that references may recur. */
|
|
58
|
+
function checkOf(compiled, checks, location) {
|
|
59
|
+
let check = checks.get(location);
|
|
60
|
+
if (check === undefined) {
|
|
61
|
+
let body;
|
|
62
|
+
check = (value, depth) => {
|
|
63
|
+
if (depth > MAXIMUM_DEPTH)
|
|
64
|
+
throw UNDECIDED;
|
|
65
|
+
body ??= compileLocation(compiled, checks, location);
|
|
66
|
+
return body(value, depth);
|
|
67
|
+
};
|
|
68
|
+
checks.set(location, check);
|
|
69
|
+
}
|
|
70
|
+
return check;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The conjunction of the location's keywords, each as the evaluator applies it: an applicator
|
|
74
|
+
* checks the same value at its child location, a keyword of one type passes every other type.
|
|
75
|
+
*/
|
|
76
|
+
function compileLocation(compiled, checks, location) {
|
|
77
|
+
const schema = location.schema;
|
|
78
|
+
if (schema === true)
|
|
79
|
+
return () => true;
|
|
80
|
+
if (schema === false)
|
|
81
|
+
return () => false;
|
|
82
|
+
const child = (keyword, member) => checkOf(compiled, checks, compiled.child(location, keyword, member));
|
|
83
|
+
const parts = [];
|
|
84
|
+
if (Object.hasOwn(schema, '$ref')) {
|
|
85
|
+
const target = compiled.reference(location);
|
|
86
|
+
if (target === undefined)
|
|
87
|
+
throw UNDECIDED;
|
|
88
|
+
const check = checkOf(compiled, checks, target);
|
|
89
|
+
parts.push((value, depth) => {
|
|
90
|
+
let byValue = verdicts.get(target);
|
|
91
|
+
if (byValue === undefined)
|
|
92
|
+
verdicts.set(target, (byValue = new Map()));
|
|
93
|
+
const known = byValue.get(value);
|
|
94
|
+
if (known === null)
|
|
95
|
+
throw UNDECIDED;
|
|
96
|
+
if (known !== undefined)
|
|
97
|
+
return known;
|
|
98
|
+
byValue.set(value, null);
|
|
99
|
+
const verdict = check(value, depth + 1);
|
|
100
|
+
byValue.set(value, verdict);
|
|
101
|
+
return verdict;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
compileCompositions(schema, child, parts);
|
|
105
|
+
compileCommon(schema, parts);
|
|
106
|
+
compileNumber(schema, parts);
|
|
107
|
+
compileString(compiled, schema, parts);
|
|
108
|
+
compileArray(schema, child, parts);
|
|
109
|
+
compileObject(compiled, schema, child, parts);
|
|
110
|
+
if (parts.length === 0)
|
|
111
|
+
return () => true;
|
|
112
|
+
if (parts.length === 1)
|
|
113
|
+
return parts[0];
|
|
114
|
+
return (value, depth) => {
|
|
115
|
+
for (const part of parts)
|
|
116
|
+
if (!part(value, depth))
|
|
117
|
+
return false;
|
|
118
|
+
return true;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function compileCompositions(schema, child, parts) {
|
|
122
|
+
const allOf = schemaList(schema.allOf)?.map((_, index) => child('allOf', index));
|
|
123
|
+
if (allOf !== undefined) {
|
|
124
|
+
parts.push((value, depth) => allOf.every((check) => check(value, depth + 1)));
|
|
125
|
+
}
|
|
126
|
+
const anyOf = schemaList(schema.anyOf)?.map((_, index) => child('anyOf', index));
|
|
127
|
+
if (anyOf !== undefined) {
|
|
128
|
+
parts.push((value, depth) => anyOf.some((check) => check(value, depth + 1)));
|
|
129
|
+
}
|
|
130
|
+
const oneOf = schemaList(schema.oneOf)?.map((_, index) => child('oneOf', index));
|
|
131
|
+
if (oneOf !== undefined) {
|
|
132
|
+
parts.push((value, depth) => {
|
|
133
|
+
let admitted = 0;
|
|
134
|
+
for (const check of oneOf)
|
|
135
|
+
if (check(value, depth + 1) && ++admitted > 1)
|
|
136
|
+
return false;
|
|
137
|
+
return admitted === 1;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (isSchema(schema.not)) {
|
|
141
|
+
const not = child('not');
|
|
142
|
+
parts.push((value, depth) => !not(value, depth + 1));
|
|
143
|
+
}
|
|
144
|
+
if (isSchema(schema.if)) {
|
|
145
|
+
const condition = child('if');
|
|
146
|
+
const then = isSchema(schema.then) ? child('then') : undefined;
|
|
147
|
+
const otherwise = isSchema(schema.else) ? child('else') : undefined;
|
|
148
|
+
parts.push((value, depth) => {
|
|
149
|
+
const branch = condition(value, depth + 1) ? then : otherwise;
|
|
150
|
+
return branch === undefined || branch(value, depth + 1);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function compileCommon(schema, parts) {
|
|
155
|
+
const kind = schema['x-astrale-value-kind'];
|
|
156
|
+
if (kind === 'instant')
|
|
157
|
+
parts.push((value) => isInstantValue(value));
|
|
158
|
+
if (kind === 'blob')
|
|
159
|
+
parts.push((value) => isCanonicalBlobRef(value));
|
|
160
|
+
const type = schema.type;
|
|
161
|
+
if (typeof type === 'string')
|
|
162
|
+
parts.push((value) => matchesType(type, value));
|
|
163
|
+
else if (Array.isArray(type)) {
|
|
164
|
+
const types = type.filter((candidate) => typeof candidate === 'string');
|
|
165
|
+
parts.push((value) => types.some((candidate) => matchesType(candidate, value)));
|
|
166
|
+
}
|
|
167
|
+
if (Object.hasOwn(schema, 'const')) {
|
|
168
|
+
const expected = schema.const;
|
|
169
|
+
parts.push((value) => jsonEqual(value, expected));
|
|
170
|
+
}
|
|
171
|
+
const choices = schema.enum;
|
|
172
|
+
if (Array.isArray(choices)) {
|
|
173
|
+
parts.push((value) => choices.some((candidate) => jsonEqual(value, candidate)));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function compileNumber(schema, parts) {
|
|
177
|
+
const { multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum } = schema;
|
|
178
|
+
const bounded = typeof multipleOf === 'number' ||
|
|
179
|
+
typeof maximum === 'number' ||
|
|
180
|
+
typeof exclusiveMaximum === 'number' ||
|
|
181
|
+
typeof minimum === 'number' ||
|
|
182
|
+
typeof exclusiveMinimum === 'number';
|
|
183
|
+
if (!bounded)
|
|
184
|
+
return;
|
|
185
|
+
parts.push((value) => typeof value !== 'number' ||
|
|
186
|
+
((typeof multipleOf !== 'number' || isMultipleOf(value, multipleOf)) &&
|
|
187
|
+
(typeof maximum !== 'number' || value <= maximum) &&
|
|
188
|
+
(typeof exclusiveMaximum !== 'number' || value < exclusiveMaximum) &&
|
|
189
|
+
(typeof minimum !== 'number' || value >= minimum) &&
|
|
190
|
+
(typeof exclusiveMinimum !== 'number' || value > exclusiveMinimum)));
|
|
191
|
+
}
|
|
192
|
+
function compileString(compiled, schema, parts) {
|
|
193
|
+
const { maxLength, minLength, pattern, fullMatch } = schema;
|
|
194
|
+
if (typeof maxLength === 'number' || typeof minLength === 'number') {
|
|
195
|
+
// For n UTF-16 units, ceil(n / 2) <= code points <= n: count only when the bounds cannot
|
|
196
|
+
// decide, as the evaluator does.
|
|
197
|
+
parts.push((value) => {
|
|
198
|
+
if (typeof value !== 'string')
|
|
199
|
+
return true;
|
|
200
|
+
if ((typeof maxLength !== 'number' || value.length <= maxLength) &&
|
|
201
|
+
(typeof minLength !== 'number' || Math.ceil(value.length / 2) >= minLength)) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
const length = codePointLength(value);
|
|
205
|
+
return ((typeof maxLength !== 'number' || length <= maxLength) &&
|
|
206
|
+
(typeof minLength !== 'number' || length >= minLength));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (typeof pattern === 'string') {
|
|
210
|
+
const matcher = compiled.matcher(pattern);
|
|
211
|
+
parts.push((value) => typeof value !== 'string' || matcher.test(value));
|
|
212
|
+
}
|
|
213
|
+
if (typeof fullMatch === 'string') {
|
|
214
|
+
const matcher = compiled.matcher(fullMatch);
|
|
215
|
+
parts.push((value) => typeof value !== 'string' || matcher.matches(value));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function compileArray(schema, child, parts) {
|
|
219
|
+
const { maxItems, minItems } = schema;
|
|
220
|
+
if (typeof maxItems === 'number' || typeof minItems === 'number' || schema.uniqueItems === true) {
|
|
221
|
+
const distinct = schema.uniqueItems === true;
|
|
222
|
+
parts.push((value) => !Array.isArray(value) ||
|
|
223
|
+
((typeof maxItems !== 'number' || value.length <= maxItems) &&
|
|
224
|
+
(typeof minItems !== 'number' || value.length >= minItems) &&
|
|
225
|
+
(!distinct || unique(value))));
|
|
226
|
+
}
|
|
227
|
+
const prefix = schemaList(schema.prefixItems)?.map((_, index) => child('prefixItems', index));
|
|
228
|
+
const items = isSchema(schema.items) ? child('items') : undefined;
|
|
229
|
+
if (prefix !== undefined || items !== undefined) {
|
|
230
|
+
const leading = prefix ?? [];
|
|
231
|
+
parts.push((value, depth) => {
|
|
232
|
+
if (!Array.isArray(value))
|
|
233
|
+
return true;
|
|
234
|
+
for (let index = 0; index < value.length; index++) {
|
|
235
|
+
const check = index < leading.length ? leading[index] : items;
|
|
236
|
+
if (check !== undefined && !check(value[index], depth + 1))
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (isSchema(schema.contains)) {
|
|
243
|
+
const contains = child('contains');
|
|
244
|
+
const least = typeof schema.minContains === 'number' ? schema.minContains : 1;
|
|
245
|
+
const most = typeof schema.maxContains === 'number' ? schema.maxContains : Number.POSITIVE_INFINITY;
|
|
246
|
+
parts.push((value, depth) => {
|
|
247
|
+
if (!Array.isArray(value))
|
|
248
|
+
return true;
|
|
249
|
+
let matches = 0;
|
|
250
|
+
for (const item of value)
|
|
251
|
+
if (contains(item, depth + 1))
|
|
252
|
+
matches++;
|
|
253
|
+
return matches >= least && matches <= most;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function compileObject(compiled, schema, child, parts) {
|
|
258
|
+
const { maxProperties, minProperties } = schema;
|
|
259
|
+
const required = Array.isArray(schema.required)
|
|
260
|
+
? schema.required.filter((name) => typeof name === 'string')
|
|
261
|
+
: [];
|
|
262
|
+
const dependentRequired = isJsonObject(schema.dependentRequired)
|
|
263
|
+
? Object.entries(schema.dependentRequired).flatMap(([name, names]) => Array.isArray(names)
|
|
264
|
+
? [[name, names.filter((other) => typeof other === 'string')]]
|
|
265
|
+
: [])
|
|
266
|
+
: [];
|
|
267
|
+
if (typeof maxProperties === 'number' ||
|
|
268
|
+
typeof minProperties === 'number' ||
|
|
269
|
+
required.length > 0 ||
|
|
270
|
+
dependentRequired.length > 0) {
|
|
271
|
+
parts.push((value) => {
|
|
272
|
+
if (!isJsonObject(value))
|
|
273
|
+
return true;
|
|
274
|
+
if (typeof maxProperties === 'number' || typeof minProperties === 'number') {
|
|
275
|
+
const count = Object.keys(value).length;
|
|
276
|
+
if (typeof maxProperties === 'number' && count > maxProperties)
|
|
277
|
+
return false;
|
|
278
|
+
if (typeof minProperties === 'number' && count < minProperties)
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
for (const name of required)
|
|
282
|
+
if (!Object.hasOwn(value, name))
|
|
283
|
+
return false;
|
|
284
|
+
for (const [name, names] of dependentRequired) {
|
|
285
|
+
if (Object.hasOwn(value, name) && !names.every((other) => Object.hasOwn(value, other))) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// Each member is checked by every pattern it matches, by its declared schema, and by the
|
|
293
|
+
// additional schema when neither declares it.
|
|
294
|
+
const declared = isJsonObject(schema.properties) ? schema.properties : {};
|
|
295
|
+
const properties = new Map(Object.keys(declared).map((name) => [name, child('properties', name)]));
|
|
296
|
+
const patterns = isJsonObject(schema.patternProperties)
|
|
297
|
+
? Object.keys(schema.patternProperties).map((source) => [compiled.matcher(source), child('patternProperties', source)])
|
|
298
|
+
: [];
|
|
299
|
+
const additional = isSchema(schema.additionalProperties)
|
|
300
|
+
? child('additionalProperties')
|
|
301
|
+
: undefined;
|
|
302
|
+
const names = isSchema(schema.propertyNames) ? child('propertyNames') : undefined;
|
|
303
|
+
if (properties.size > 0 || patterns.length > 0 || additional !== undefined || names) {
|
|
304
|
+
parts.push((value, depth) => {
|
|
305
|
+
if (!isJsonObject(value))
|
|
306
|
+
return true;
|
|
307
|
+
for (const name of Object.keys(value)) {
|
|
308
|
+
const member = value[name];
|
|
309
|
+
let matched = false;
|
|
310
|
+
for (const [matcher, check] of patterns) {
|
|
311
|
+
if (!matcher.test(name))
|
|
312
|
+
continue;
|
|
313
|
+
matched = true;
|
|
314
|
+
if (!check(member, depth + 1))
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
const check = Object.hasOwn(declared, name) ? properties.get(name) : undefined;
|
|
318
|
+
if (check !== undefined) {
|
|
319
|
+
if (!check(member, depth + 1))
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
else if (!matched && additional !== undefined && !additional(member, depth + 1)) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
if (names !== undefined && !names(name, depth + 1))
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (isJsonObject(schema.dependentSchemas)) {
|
|
332
|
+
const dependent = Object.keys(schema.dependentSchemas).map((name) => [name, child('dependentSchemas', name)]);
|
|
333
|
+
parts.push((value, depth) => !isJsonObject(value) ||
|
|
334
|
+
dependent.every(([name, check]) => !Object.hasOwn(value, name) || check(value, depth + 1)));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function schemaList(input) {
|
|
338
|
+
return Array.isArray(input) ? input : undefined;
|
|
339
|
+
}
|
|
340
|
+
function isSchema(input) {
|
|
341
|
+
return typeof input === 'boolean' || isJsonObject(input);
|
|
342
|
+
}
|