@astrale-os/kernel-dsl 1.0.0-beta.10 → 1.0.0-beta.12
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 +3 -3
- package/dist/v1/addressing/key.d.ts +4 -1
- package/dist/v1/addressing/key.js +21 -2
- package/dist/v1/builder/callable/authoring.d.ts +1 -1
- package/dist/v1/builder/class/authoring.d.ts +6 -256
- package/dist/v1/builder/class/handle.d.ts +27 -0
- package/dist/v1/builder/class/handle.js +1 -0
- package/dist/v1/builder/class/input.d.ts +129 -0
- package/dist/v1/builder/class/input.js +1 -0
- package/dist/v1/builder/class/projection.d.ts +129 -0
- package/dist/v1/builder/class/projection.js +1 -0
- package/dist/v1/builder/core/authoring.d.ts +1 -1
- package/dist/v1/builder/index.d.ts +3 -1
- package/dist/v1/builder/lowering/class.d.ts +1 -1
- package/dist/v1/builder/lowering/schema/context.d.ts +2 -1
- package/dist/v1/builder/lowering/value/path.d.ts +1 -1
- package/dist/v1/builder/policy/authoring.d.ts +1 -1
- package/dist/v1/builder/schema/input.d.ts +1 -1
- package/dist/v1/builder/schema/projection/class.d.ts +2 -1
- package/dist/v1/builder/schema/projection/core-input.d.ts +2 -1
- package/dist/v1/builder/schema/projection/identity.d.ts +2 -1
- package/dist/v1/builder/schema/projection/property.d.ts +1 -1
- package/dist/v1/builder/value/path-metadata.d.ts +1 -1
- package/dist/v1/builder/value/path.d.ts +1 -1
- package/dist/v1/language/index.d.ts +1 -0
- package/dist/v1/language/index.js +1 -0
- package/dist/v1/language/validation/diagnostic.d.ts +1 -1
- package/dist/v1/language/value/inclusion/arrays.d.ts +3 -0
- package/dist/v1/language/value/inclusion/arrays.js +85 -0
- package/dist/v1/language/value/inclusion/assertions.d.ts +18 -0
- package/dist/v1/language/value/inclusion/assertions.js +29 -0
- package/dist/v1/language/value/inclusion/combinators.d.ts +3 -0
- package/dist/v1/language/value/inclusion/combinators.js +67 -0
- package/dist/v1/language/value/inclusion/equivalence.d.ts +4 -0
- package/dist/v1/language/value/inclusion/equivalence.js +74 -0
- package/dist/v1/language/value/inclusion/evidence.d.ts +34 -0
- package/dist/v1/language/value/inclusion/evidence.js +138 -0
- package/dist/v1/language/value/inclusion/includes.d.ts +17 -0
- package/dist/v1/language/value/inclusion/includes.js +25 -719
- package/dist/v1/language/value/inclusion/keywords.d.ts +5 -0
- package/dist/v1/language/value/inclusion/keywords.js +31 -0
- package/dist/v1/language/value/inclusion/kinds.d.ts +17 -0
- package/dist/v1/language/value/inclusion/kinds.js +57 -0
- package/dist/v1/language/value/inclusion/location.d.ts +24 -0
- package/dist/v1/language/value/inclusion/location.js +63 -0
- package/dist/v1/language/value/inclusion/numbers.d.ts +3 -0
- package/dist/v1/language/value/inclusion/numbers.js +41 -0
- package/dist/v1/language/value/inclusion/objects.d.ts +3 -0
- package/dist/v1/language/value/inclusion/objects.js +190 -0
- package/dist/v1/language/value/inclusion/strings.d.ts +3 -0
- package/dist/v1/language/value/inclusion/strings.js +29 -0
- package/dist/v1/language/value/inclusion/types.d.ts +3 -0
- package/dist/v1/language/value/inclusion/types.js +11 -0
- package/dist/v1/language/value/index.d.ts +1 -1
- package/dist/v1/language/value/index.js +1 -1
- package/dist/v1/language/value/ordering.d.ts +9 -0
- package/dist/v1/language/value/ordering.js +72 -9
- package/dist/v1/schema/validation/value-api.d.ts +9 -0
- package/dist/v1/schema/validation/value-api.js +23 -1
- package/dist/value/canonical.d.ts +3 -2
- package/dist/value/canonical.js +30 -6
- package/dist/value/decode.d.ts +7 -0
- package/dist/value/decode.js +159 -9
- package/dist/value/index.d.ts +1 -1
- package/dist/value/json.d.ts +1 -1
- package/dist/value/json.js +1 -1
- package/dist/value/types.d.ts +6 -0
- package/dist/value/unicode.d.ts +2 -1
- package/dist/value/unicode.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Keywords the evaluator never asserts. */
|
|
2
|
+
export const INERT = new Set([
|
|
3
|
+
'$anchor',
|
|
4
|
+
'$comment',
|
|
5
|
+
'$defs',
|
|
6
|
+
'$id',
|
|
7
|
+
'$schema',
|
|
8
|
+
'contentEncoding',
|
|
9
|
+
'contentMediaType',
|
|
10
|
+
'contentSchema',
|
|
11
|
+
'default',
|
|
12
|
+
'deprecated',
|
|
13
|
+
'description',
|
|
14
|
+
'examples',
|
|
15
|
+
'format',
|
|
16
|
+
'readOnly',
|
|
17
|
+
'title',
|
|
18
|
+
'writeOnly',
|
|
19
|
+
'x-astrale-path',
|
|
20
|
+
]);
|
|
21
|
+
/** Whether a schema asserts nothing. */
|
|
22
|
+
export function isTop(schema) {
|
|
23
|
+
if (schema === true)
|
|
24
|
+
return true;
|
|
25
|
+
if (schema === false)
|
|
26
|
+
return false;
|
|
27
|
+
return Object.keys(schema).every((keyword) => INERT.has(keyword) ||
|
|
28
|
+
((keyword === 'then' || keyword === 'else') && !Object.hasOwn(schema, 'if')) ||
|
|
29
|
+
((keyword === 'minContains' || keyword === 'maxContains') &&
|
|
30
|
+
!Object.hasOwn(schema, 'contains')));
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Value as JsonValue } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
/** Bit masks over the JSON kinds a schema may admit; integers and fractions split `number`. */
|
|
3
|
+
export declare const NULL = 1;
|
|
4
|
+
export declare const BOOLEAN = 2;
|
|
5
|
+
export declare const OBJECT = 4;
|
|
6
|
+
export declare const ARRAY = 8;
|
|
7
|
+
export declare const STRING = 16;
|
|
8
|
+
export declare const INTEGER = 32;
|
|
9
|
+
export declare const FRACTION = 64;
|
|
10
|
+
export declare const NUMBER: number;
|
|
11
|
+
export declare const ANY: number;
|
|
12
|
+
/** The kinds a `type` keyword value admits; an absent `type` admits every kind. */
|
|
13
|
+
export declare function typeMask(type: JsonValue | undefined): number;
|
|
14
|
+
/** The single kind of one JSON value. */
|
|
15
|
+
export declare function kindOf(value: JsonValue): number;
|
|
16
|
+
/** The finite value set of a kind mask made only of null and booleans. */
|
|
17
|
+
export declare function finiteKinds(kinds: number): readonly JsonValue[] | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Bit masks over the JSON kinds a schema may admit; integers and fractions split `number`. */
|
|
2
|
+
export const NULL = 1;
|
|
3
|
+
export const BOOLEAN = 2;
|
|
4
|
+
export const OBJECT = 4;
|
|
5
|
+
export const ARRAY = 8;
|
|
6
|
+
export const STRING = 16;
|
|
7
|
+
export const INTEGER = 32;
|
|
8
|
+
export const FRACTION = 64;
|
|
9
|
+
export const NUMBER = INTEGER | FRACTION;
|
|
10
|
+
export const ANY = NULL | BOOLEAN | OBJECT | ARRAY | STRING | NUMBER;
|
|
11
|
+
/** The kinds a `type` keyword value admits; an absent `type` admits every kind. */
|
|
12
|
+
export function typeMask(type) {
|
|
13
|
+
if (type === undefined)
|
|
14
|
+
return ANY;
|
|
15
|
+
const names = typeof type === 'string' ? [type] : Array.isArray(type) ? type : [];
|
|
16
|
+
return names.reduce((mask, name) => {
|
|
17
|
+
switch (name) {
|
|
18
|
+
case 'null':
|
|
19
|
+
return mask | NULL;
|
|
20
|
+
case 'boolean':
|
|
21
|
+
return mask | BOOLEAN;
|
|
22
|
+
case 'object':
|
|
23
|
+
return mask | OBJECT;
|
|
24
|
+
case 'array':
|
|
25
|
+
return mask | ARRAY;
|
|
26
|
+
case 'string':
|
|
27
|
+
return mask | STRING;
|
|
28
|
+
case 'integer':
|
|
29
|
+
return mask | INTEGER;
|
|
30
|
+
case 'number':
|
|
31
|
+
return mask | NUMBER;
|
|
32
|
+
default:
|
|
33
|
+
return mask;
|
|
34
|
+
}
|
|
35
|
+
}, 0);
|
|
36
|
+
}
|
|
37
|
+
/** The single kind of one JSON value. */
|
|
38
|
+
export function kindOf(value) {
|
|
39
|
+
if (value === null)
|
|
40
|
+
return NULL;
|
|
41
|
+
if (typeof value === 'boolean')
|
|
42
|
+
return BOOLEAN;
|
|
43
|
+
if (typeof value === 'string')
|
|
44
|
+
return STRING;
|
|
45
|
+
if (typeof value === 'number')
|
|
46
|
+
return Number.isInteger(value) ? INTEGER : FRACTION;
|
|
47
|
+
return Array.isArray(value) ? ARRAY : OBJECT;
|
|
48
|
+
}
|
|
49
|
+
/** The finite value set of a kind mask made only of null and booleans. */
|
|
50
|
+
export function finiteKinds(kinds) {
|
|
51
|
+
if ((kinds & ~(NULL | BOOLEAN)) !== 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
return [
|
|
54
|
+
...((kinds & NULL) !== 0 ? [null] : []),
|
|
55
|
+
...((kinds & BOOLEAN) !== 0 ? [true, false] : []),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Value as JsonValue } from '@astrale-os/kernel-dsl/value';
|
|
2
|
+
import type { ValueProgram } from '../compile/model.js';
|
|
3
|
+
import type { JsonSchema } from '../model/schema.js';
|
|
4
|
+
/** One schema location inside one admitted program. */
|
|
5
|
+
export interface At {
|
|
6
|
+
readonly program: ValueProgram;
|
|
7
|
+
readonly index: number;
|
|
8
|
+
}
|
|
9
|
+
/** The location `index` of `program`. */
|
|
10
|
+
export declare function at(program: ValueProgram, index: number): At;
|
|
11
|
+
/** The schema written at a location. */
|
|
12
|
+
export declare function schemaOf(location: At): JsonSchema;
|
|
13
|
+
/** The subschema under `keyword`, or under its `member`, when the program compiled one. */
|
|
14
|
+
export declare function childOf(location: At, keyword: string, member?: string | number): At | undefined;
|
|
15
|
+
/** The subschemas of an array-valued `keyword`, in order. */
|
|
16
|
+
export declare function childrenOf(location: At, keyword: string): readonly At[];
|
|
17
|
+
/** The resolved `$ref` target of a location. */
|
|
18
|
+
export declare function referenceOf(location: At): At | undefined;
|
|
19
|
+
/** The location with its `$ref` targets and `allOf` branches; admission makes this acyclic. */
|
|
20
|
+
export declare function conjunctsOf(location: At): readonly At[];
|
|
21
|
+
/** Whether `name` matches `pattern` under the program's regular-expression dialect. */
|
|
22
|
+
export declare function matches(location: At, pattern: string, name: string): boolean;
|
|
23
|
+
/** Whether the V1 evaluator accepts `value` at a location. */
|
|
24
|
+
export declare function accepts(location: At, value: JsonValue): boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { loadValueProgram } from '../compile/load.js';
|
|
2
|
+
import { evaluateInstance } from '../evaluate/evaluator.js';
|
|
3
|
+
import { isValueObject as isJsonObject } from '../model/object.js';
|
|
4
|
+
import { appendPointer } from '../syntax/pointer.js';
|
|
5
|
+
/** The location `index` of `program`. */
|
|
6
|
+
export function at(program, index) {
|
|
7
|
+
return { program, index };
|
|
8
|
+
}
|
|
9
|
+
/** The schema written at a location. */
|
|
10
|
+
export function schemaOf(location) {
|
|
11
|
+
const node = location.program.nodes[location.index];
|
|
12
|
+
if (node === undefined)
|
|
13
|
+
throw new TypeError('Value program location is absent.');
|
|
14
|
+
return node.schema;
|
|
15
|
+
}
|
|
16
|
+
/** The subschema under `keyword`, or under its `member`, when the program compiled one. */
|
|
17
|
+
export function childOf(location, keyword, member) {
|
|
18
|
+
const base = appendPointer('', keyword);
|
|
19
|
+
const key = (member === undefined ? base : appendPointer(base, member)).slice(1);
|
|
20
|
+
const index = location.program.nodes[location.index]?.children[key];
|
|
21
|
+
return index === undefined ? undefined : at(location.program, index);
|
|
22
|
+
}
|
|
23
|
+
/** The subschemas of an array-valued `keyword`, in order. */
|
|
24
|
+
export function childrenOf(location, keyword) {
|
|
25
|
+
const schema = schemaOf(location);
|
|
26
|
+
if (!isJsonObject(schema) || !Array.isArray(schema[keyword]))
|
|
27
|
+
return [];
|
|
28
|
+
return schema[keyword].flatMap((_, index) => {
|
|
29
|
+
const child = childOf(location, keyword, index);
|
|
30
|
+
return child === undefined ? [] : [child];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** The resolved `$ref` target of a location. */
|
|
34
|
+
export function referenceOf(location) {
|
|
35
|
+
const index = location.program.nodes[location.index]?.reference;
|
|
36
|
+
return index === undefined ? undefined : at(location.program, index);
|
|
37
|
+
}
|
|
38
|
+
/** The location with its `$ref` targets and `allOf` branches; admission makes this acyclic. */
|
|
39
|
+
export function conjunctsOf(location) {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const conjuncts = [];
|
|
42
|
+
const visit = (current) => {
|
|
43
|
+
if (seen.has(current.index))
|
|
44
|
+
return;
|
|
45
|
+
seen.add(current.index);
|
|
46
|
+
conjuncts.push(current);
|
|
47
|
+
const reference = referenceOf(current);
|
|
48
|
+
if (reference !== undefined)
|
|
49
|
+
visit(reference);
|
|
50
|
+
for (const branch of childrenOf(current, 'allOf'))
|
|
51
|
+
visit(branch);
|
|
52
|
+
};
|
|
53
|
+
visit(location);
|
|
54
|
+
return conjuncts;
|
|
55
|
+
}
|
|
56
|
+
/** Whether `name` matches `pattern` under the program's regular-expression dialect. */
|
|
57
|
+
export function matches(location, pattern, name) {
|
|
58
|
+
return loadValueProgram(location.program).matcher(pattern).test(name);
|
|
59
|
+
}
|
|
60
|
+
/** Whether the V1 evaluator accepts `value` at a location. */
|
|
61
|
+
export function accepts(location, value) {
|
|
62
|
+
return evaluateInstance(loadValueProgram(location.program, location.index), value).valid;
|
|
63
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isMultipleOf } from '../evaluate/assertions.js';
|
|
2
|
+
import { has } from './evidence.js';
|
|
3
|
+
import { FRACTION, NUMBER } from './kinds.js';
|
|
4
|
+
/** Rules for the numeric bound and divisibility keywords. */
|
|
5
|
+
export const numberRules = new Map([
|
|
6
|
+
[
|
|
7
|
+
'maximum',
|
|
8
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
9
|
+
!has(evidence, NUMBER) ||
|
|
10
|
+
(evidence.upper !== undefined && evidence.upper.value <= value),
|
|
11
|
+
],
|
|
12
|
+
[
|
|
13
|
+
'exclusiveMaximum',
|
|
14
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
15
|
+
!has(evidence, NUMBER) ||
|
|
16
|
+
(evidence.upper !== undefined &&
|
|
17
|
+
(evidence.upper.value < value ||
|
|
18
|
+
(evidence.upper.exclusive && evidence.upper.value <= value))),
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
'minimum',
|
|
22
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
23
|
+
!has(evidence, NUMBER) ||
|
|
24
|
+
(evidence.lower !== undefined && evidence.lower.value >= value),
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
'exclusiveMinimum',
|
|
28
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
29
|
+
!has(evidence, NUMBER) ||
|
|
30
|
+
(evidence.lower !== undefined &&
|
|
31
|
+
(evidence.lower.value > value ||
|
|
32
|
+
(evidence.lower.exclusive && evidence.lower.value >= value))),
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
'multipleOf',
|
|
36
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
37
|
+
!has(evidence, NUMBER) ||
|
|
38
|
+
evidence.multipleOf.some((divisor) => isMultipleOf(divisor, value)) ||
|
|
39
|
+
(!has(evidence, FRACTION) && isMultipleOf(1, value)),
|
|
40
|
+
],
|
|
41
|
+
]);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { isValueObject as isJsonObject } from '../model/object.js';
|
|
2
|
+
import { has } from './evidence.js';
|
|
3
|
+
import { isTop } from './keywords.js';
|
|
4
|
+
import { OBJECT } from './kinds.js';
|
|
5
|
+
import { accepts, childOf, matches, schemaOf } from './location.js';
|
|
6
|
+
/** Rules for the object size, required-name and property-subschema keywords. */
|
|
7
|
+
export const objectRules = new Map([
|
|
8
|
+
[
|
|
9
|
+
'maxProperties',
|
|
10
|
+
({ inclusion, value, evidence }) => typeof value !== 'number' ||
|
|
11
|
+
!has(evidence, OBJECT) ||
|
|
12
|
+
(evidence.maxProperties !== undefined && evidence.maxProperties <= value) ||
|
|
13
|
+
evidence.conjuncts.some((conjunct) => {
|
|
14
|
+
const names = closedNames(inclusion, conjunct);
|
|
15
|
+
return names !== undefined && names.length <= value;
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
'minProperties',
|
|
20
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
21
|
+
!has(evidence, OBJECT) ||
|
|
22
|
+
value <= 0 ||
|
|
23
|
+
evidence.minProperties >= value ||
|
|
24
|
+
evidence.required.size >= value,
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
'required',
|
|
28
|
+
({ value, evidence }) => !Array.isArray(value) ||
|
|
29
|
+
!has(evidence, OBJECT) ||
|
|
30
|
+
value.every((name) => typeof name !== 'string' || evidence.required.has(name)),
|
|
31
|
+
],
|
|
32
|
+
['dependentRequired', dependentRequiredHolds],
|
|
33
|
+
['properties', propertiesHold],
|
|
34
|
+
['patternProperties', patternPropertiesHold],
|
|
35
|
+
['additionalProperties', additionalHolds],
|
|
36
|
+
['propertyNames', propertyNamesHold],
|
|
37
|
+
['dependentSchemas', dependentSchemasHold],
|
|
38
|
+
]);
|
|
39
|
+
function dependentRequiredHolds({ inclusion, value, evidence }) {
|
|
40
|
+
return (!isJsonObject(value) ||
|
|
41
|
+
!has(evidence, OBJECT) ||
|
|
42
|
+
Object.entries(value).every(([name, dependencies]) => !Array.isArray(dependencies) ||
|
|
43
|
+
forbids(inclusion, evidence, name) ||
|
|
44
|
+
dependencies.every((dependency) => typeof dependency !== 'string' ||
|
|
45
|
+
evidence.required.has(dependency) ||
|
|
46
|
+
evidence.dependentRequired.get(name)?.has(dependency) === true)));
|
|
47
|
+
}
|
|
48
|
+
function propertiesHold({ inclusion, value, outer, evidence }) {
|
|
49
|
+
return (!isJsonObject(value) ||
|
|
50
|
+
!has(evidence, OBJECT) ||
|
|
51
|
+
Object.keys(value).every((name) => {
|
|
52
|
+
const member = childOf(outer, 'properties', name);
|
|
53
|
+
return (member === undefined ||
|
|
54
|
+
isTop(schemaOf(member)) ||
|
|
55
|
+
memberConstraints(evidence, name).some((constraint) => inclusion.includes(member, constraint)));
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
function patternPropertiesHold({ inclusion, value, outer, evidence }) {
|
|
59
|
+
return (!isJsonObject(value) ||
|
|
60
|
+
!has(evidence, OBJECT) ||
|
|
61
|
+
Object.keys(value).every((pattern) => {
|
|
62
|
+
const member = childOf(outer, 'patternProperties', pattern);
|
|
63
|
+
if (member === undefined || isTop(schemaOf(member)))
|
|
64
|
+
return true;
|
|
65
|
+
return evidence.conjuncts.some((conjunct) => {
|
|
66
|
+
const same = childOf(conjunct, 'patternProperties', pattern);
|
|
67
|
+
if (same !== undefined && inclusion.includes(member, same))
|
|
68
|
+
return true;
|
|
69
|
+
return coversNames(inclusion, conjunct, member, (name) => matches(outer, pattern, name));
|
|
70
|
+
});
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
function additionalHolds({ inclusion, outer, schema, evidence }) {
|
|
74
|
+
const additional = childOf(outer, 'additionalProperties');
|
|
75
|
+
if (additional === undefined || isTop(schemaOf(additional)) || !has(evidence, OBJECT)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const declared = isJsonObject(schema.properties) ? schema.properties : {};
|
|
79
|
+
const patterns = isJsonObject(schema.patternProperties)
|
|
80
|
+
? Object.keys(schema.patternProperties)
|
|
81
|
+
: [];
|
|
82
|
+
const isAdditional = (name) => !Object.hasOwn(declared, name) && !patterns.some((pattern) => matches(outer, pattern, name));
|
|
83
|
+
return evidence.conjuncts.some((conjunct) => coversNames(inclusion, conjunct, additional, isAdditional, patterns));
|
|
84
|
+
}
|
|
85
|
+
function propertyNamesHold({ inclusion, outer, evidence }) {
|
|
86
|
+
const names = childOf(outer, 'propertyNames');
|
|
87
|
+
if (names === undefined || isTop(schemaOf(names)) || !has(evidence, OBJECT))
|
|
88
|
+
return true;
|
|
89
|
+
return evidence.conjuncts.some((conjunct) => {
|
|
90
|
+
const own = childOf(conjunct, 'propertyNames');
|
|
91
|
+
if (own !== undefined && inclusion.includes(names, own))
|
|
92
|
+
return true;
|
|
93
|
+
const closed = closedNames(inclusion, conjunct);
|
|
94
|
+
return closed !== undefined && closed.every((name) => accepts(names, name));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function dependentSchemasHold({ inclusion, value, outer, evidence }) {
|
|
98
|
+
return (!isJsonObject(value) ||
|
|
99
|
+
!has(evidence, OBJECT) ||
|
|
100
|
+
Object.keys(value).every((name) => {
|
|
101
|
+
const dependent = childOf(outer, 'dependentSchemas', name);
|
|
102
|
+
if (dependent === undefined || isTop(schemaOf(dependent)))
|
|
103
|
+
return true;
|
|
104
|
+
if (forbids(inclusion, evidence, name))
|
|
105
|
+
return true;
|
|
106
|
+
if (evidence.conjuncts.some((conjunct) => inclusion.includes(dependent, conjunct))) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return evidence.conjuncts.some((conjunct) => {
|
|
110
|
+
const own = childOf(conjunct, 'dependentSchemas', name);
|
|
111
|
+
return own !== undefined && inclusion.includes(dependent, own);
|
|
112
|
+
});
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether every property of a `conjunct`-admitted object whose name is `relevant` satisfies
|
|
117
|
+
* `required`. Names matching a pattern in `settled` are never relevant.
|
|
118
|
+
*/
|
|
119
|
+
function coversNames(inclusion, conjunct, required, relevant, settled = []) {
|
|
120
|
+
const schema = schemaOf(conjunct);
|
|
121
|
+
if (!isJsonObject(schema))
|
|
122
|
+
return false;
|
|
123
|
+
const rest = childOf(conjunct, 'additionalProperties');
|
|
124
|
+
if (rest === undefined || !inclusion.includes(required, rest))
|
|
125
|
+
return false;
|
|
126
|
+
const declared = isJsonObject(schema.properties) ? Object.keys(schema.properties) : [];
|
|
127
|
+
const patterns = isJsonObject(schema.patternProperties)
|
|
128
|
+
? Object.keys(schema.patternProperties)
|
|
129
|
+
: [];
|
|
130
|
+
return (declared.every((name) => {
|
|
131
|
+
if (!relevant(name))
|
|
132
|
+
return true;
|
|
133
|
+
const member = childOf(conjunct, 'properties', name);
|
|
134
|
+
return member !== undefined && inclusion.includes(required, member);
|
|
135
|
+
}) &&
|
|
136
|
+
patterns.every((pattern) => {
|
|
137
|
+
if (settled.includes(pattern))
|
|
138
|
+
return true;
|
|
139
|
+
const member = childOf(conjunct, 'patternProperties', pattern);
|
|
140
|
+
return member !== undefined && inclusion.includes(required, member);
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
/** Locations one conjunct applies to the value of property `name`. */
|
|
144
|
+
function memberConstraints(evidence, name) {
|
|
145
|
+
return evidence.conjuncts.flatMap((conjunct) => {
|
|
146
|
+
const schema = schemaOf(conjunct);
|
|
147
|
+
if (!isJsonObject(schema))
|
|
148
|
+
return [];
|
|
149
|
+
const constraints = [];
|
|
150
|
+
const declared = isJsonObject(schema.properties) && Object.hasOwn(schema.properties, name);
|
|
151
|
+
if (declared) {
|
|
152
|
+
const member = childOf(conjunct, 'properties', name);
|
|
153
|
+
if (member !== undefined)
|
|
154
|
+
constraints.push(member);
|
|
155
|
+
}
|
|
156
|
+
let matched = false;
|
|
157
|
+
if (isJsonObject(schema.patternProperties)) {
|
|
158
|
+
for (const pattern of Object.keys(schema.patternProperties)) {
|
|
159
|
+
if (!matches(conjunct, pattern, name))
|
|
160
|
+
continue;
|
|
161
|
+
matched = true;
|
|
162
|
+
const member = childOf(conjunct, 'patternProperties', pattern);
|
|
163
|
+
if (member !== undefined)
|
|
164
|
+
constraints.push(member);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!declared && !matched) {
|
|
168
|
+
const rest = childOf(conjunct, 'additionalProperties');
|
|
169
|
+
if (rest !== undefined)
|
|
170
|
+
constraints.push(rest);
|
|
171
|
+
}
|
|
172
|
+
return constraints;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** Whether an admitted object never has property `name`. */
|
|
176
|
+
function forbids(inclusion, evidence, name) {
|
|
177
|
+
return memberConstraints(evidence, name).some((constraint) => inclusion.evidence(constraint).kinds === 0);
|
|
178
|
+
}
|
|
179
|
+
/** The complete property-name set of a closed object conjunct, when it is one. */
|
|
180
|
+
function closedNames(inclusion, conjunct) {
|
|
181
|
+
const schema = schemaOf(conjunct);
|
|
182
|
+
if (!isJsonObject(schema))
|
|
183
|
+
return undefined;
|
|
184
|
+
if (isJsonObject(schema.patternProperties) && Object.keys(schema.patternProperties).length > 0)
|
|
185
|
+
return undefined;
|
|
186
|
+
const rest = childOf(conjunct, 'additionalProperties');
|
|
187
|
+
if (rest === undefined || inclusion.evidence(rest).kinds !== 0)
|
|
188
|
+
return undefined;
|
|
189
|
+
return isJsonObject(schema.properties) ? Object.keys(schema.properties) : [];
|
|
190
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { has } from './evidence.js';
|
|
2
|
+
import { STRING } from './kinds.js';
|
|
3
|
+
/** Rules for the string length and pattern keywords. */
|
|
4
|
+
export const stringRules = new Map([
|
|
5
|
+
[
|
|
6
|
+
'maxLength',
|
|
7
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
8
|
+
!has(evidence, STRING) ||
|
|
9
|
+
(evidence.maxLength !== undefined && evidence.maxLength <= value),
|
|
10
|
+
],
|
|
11
|
+
[
|
|
12
|
+
'minLength',
|
|
13
|
+
({ value, evidence }) => typeof value !== 'number' ||
|
|
14
|
+
!has(evidence, STRING) ||
|
|
15
|
+
value <= 0 ||
|
|
16
|
+
evidence.minLength >= value,
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
'pattern',
|
|
20
|
+
({ value, evidence }) => typeof value !== 'string' ||
|
|
21
|
+
!has(evidence, STRING) ||
|
|
22
|
+
evidence.patterns.has(value) ||
|
|
23
|
+
evidence.fullMatches.has(value),
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
'fullMatch',
|
|
27
|
+
({ value, evidence }) => typeof value !== 'string' || !has(evidence, STRING) || evidence.fullMatches.has(value),
|
|
28
|
+
],
|
|
29
|
+
]);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { typeMask } from './kinds.js';
|
|
2
|
+
/** Rules for the keywords that restrict the kind or the exact value. */
|
|
3
|
+
export const typeRules = new Map([
|
|
4
|
+
['type', ({ value, evidence }) => (evidence.kinds & ~typeMask(value)) === 0],
|
|
5
|
+
['const', () => false],
|
|
6
|
+
['enum', () => false],
|
|
7
|
+
[
|
|
8
|
+
'x-astrale-value-kind',
|
|
9
|
+
({ value, evidence }) => typeof value === 'string' && evidence.valueKinds.has(value),
|
|
10
|
+
],
|
|
11
|
+
]);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export type { JsonSchema as ValueSchema, JsonSchemaObject as ValueSchemaObject, PathAnnotation, PropertyObjectSchema, ValueSchemaContext, } from './model/schema.js';
|
|
3
3
|
export { JSON_SCHEMA_DIALECT, VALUE_SCHEMA_DIALECT, VALUE_SCHEMA_VOCABULARY, } from './model/schema.js';
|
|
4
4
|
export { instant, isInstantValue } from './model/instant.js';
|
|
5
|
-
export { classifyOrdering, type OrderingKind } from './ordering.js';
|
|
5
|
+
export { classifyOrdering, classifyValueKind, type OrderingKind } from './ordering.js';
|
|
6
6
|
export { canonicalizeValueSchema } from './profile/canonicalize.js';
|
|
7
7
|
export { validateValueSchema } from './profile/validate.js';
|
|
8
8
|
export { validateJsonInstance } from './evaluate/validate.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Complete V1 inline value language. Named Type Definitions do not exist. */
|
|
2
2
|
export { JSON_SCHEMA_DIALECT, VALUE_SCHEMA_DIALECT, VALUE_SCHEMA_VOCABULARY, } from './model/schema.js';
|
|
3
3
|
export { instant, isInstantValue } from './model/instant.js';
|
|
4
|
-
export { classifyOrdering } from './ordering.js';
|
|
4
|
+
export { classifyOrdering, classifyValueKind } from './ordering.js';
|
|
5
5
|
export { canonicalizeValueSchema } from './profile/canonicalize.js';
|
|
6
6
|
export { validateValueSchema } from './profile/validate.js';
|
|
7
7
|
export { validateJsonInstance } from './evaluate/validate.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ValueKind } from '@astrale-os/kernel-dsl/value';
|
|
1
2
|
import type { ValueProgram } from './compile/model.js';
|
|
2
3
|
import type { CompiledInstanceSchema } from './evaluate/types.js';
|
|
3
4
|
import type { JsonSchema } from './model/schema.js';
|
|
@@ -13,3 +14,11 @@ export declare function classifyOrdering(contract: JsonSchema, registry: ValueSc
|
|
|
13
14
|
/** Recompute the scalar ordering classification from one admitted portable value program. */
|
|
14
15
|
export declare function classifyValueProgramOrdering(program: ValueProgram): OrderingKind | undefined;
|
|
15
16
|
export declare function classifyPreparedOrdering(compiled: CompiledInstanceSchema): OrderingKind | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The value kind of a property schema (page "Fondations Query v7", § Atoms et value kinds, Y4):
|
|
19
|
+
* the one kind its non-null values admit, `null` being a point of every kind. An annotated
|
|
20
|
+
* instant is `instant`, a finite set of strings `enum`, an array of unique items `set`; an
|
|
21
|
+
* ambiguous union, an object, an array with repetitions or a domain without values is `json`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function classifyValueKind(contract: JsonSchema, registry: ValueSchemaRegistry): ValueKind;
|
|
24
|
+
export declare function classifyPreparedValueKind(compiled: CompiledInstanceSchema): ValueKind;
|
|
@@ -25,7 +25,8 @@ export function classifyValueProgramOrdering(program) {
|
|
|
25
25
|
return classifyPreparedOrdering(loadValueProgram(program));
|
|
26
26
|
}
|
|
27
27
|
export function classifyPreparedOrdering(compiled) {
|
|
28
|
-
|
|
28
|
+
// An enum orders as the strings it lists; a set is an array.
|
|
29
|
+
const candidates = new Set([...new OrderingClassifier(compiled).classify(compiled.root)].map((kind) => kind === 'enum' ? 'string' : kind === 'set' ? 'array' : kind));
|
|
29
30
|
if (candidates.size !== 1)
|
|
30
31
|
return undefined;
|
|
31
32
|
const [kind] = candidates;
|
|
@@ -33,6 +34,37 @@ export function classifyPreparedOrdering(compiled) {
|
|
|
33
34
|
? kind
|
|
34
35
|
: undefined;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The value kind of a property schema (page "Fondations Query v7", § Atoms et value kinds, Y4):
|
|
39
|
+
* the one kind its non-null values admit, `null` being a point of every kind. An annotated
|
|
40
|
+
* instant is `instant`, a finite set of strings `enum`, an array of unique items `set`; an
|
|
41
|
+
* ambiguous union, an object, an array with repetitions or a domain without values is `json`.
|
|
42
|
+
*/
|
|
43
|
+
export function classifyValueKind(contract, registry) {
|
|
44
|
+
const prepared = prepareValueSchema(contract, registry, '');
|
|
45
|
+
if (!prepared.compiled)
|
|
46
|
+
return 'json';
|
|
47
|
+
return classifyPreparedValueKind(prepared.value);
|
|
48
|
+
}
|
|
49
|
+
export function classifyPreparedValueKind(compiled) {
|
|
50
|
+
const candidates = new Set(new OrderingClassifier(compiled).classify(compiled.root));
|
|
51
|
+
candidates.delete('null');
|
|
52
|
+
if (candidates.size !== 1)
|
|
53
|
+
return 'json';
|
|
54
|
+
const [kind] = candidates;
|
|
55
|
+
switch (kind) {
|
|
56
|
+
case 'string':
|
|
57
|
+
return 'text';
|
|
58
|
+
case 'number':
|
|
59
|
+
case 'boolean':
|
|
60
|
+
case 'instant':
|
|
61
|
+
case 'enum':
|
|
62
|
+
case 'set':
|
|
63
|
+
return kind;
|
|
64
|
+
default:
|
|
65
|
+
return 'json';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
36
68
|
class OrderingClassifier {
|
|
37
69
|
compiled;
|
|
38
70
|
active = new Set();
|
|
@@ -72,6 +104,9 @@ class OrderingClassifier {
|
|
|
72
104
|
const declaredTypes = typesOf(schema.type);
|
|
73
105
|
if (declaredTypes !== undefined)
|
|
74
106
|
candidates = intersect(candidates, declaredTypes);
|
|
107
|
+
if (schema.uniqueItems === true) {
|
|
108
|
+
candidates = new Set([...candidates].map((kind) => (kind === 'array' ? 'set' : kind)));
|
|
109
|
+
}
|
|
75
110
|
if (Object.hasOwn(schema, 'const')) {
|
|
76
111
|
candidates = intersect(candidates, new Set([kindOfValue(schema.const)]));
|
|
77
112
|
}
|
|
@@ -130,6 +165,7 @@ function typeSet(types) {
|
|
|
130
165
|
}
|
|
131
166
|
return result;
|
|
132
167
|
}
|
|
168
|
+
/** The kind of one listed value: a listed string belongs to a finite set of strings. */
|
|
133
169
|
function kindOfValue(value) {
|
|
134
170
|
if (value === null)
|
|
135
171
|
return 'null';
|
|
@@ -141,18 +177,35 @@ function kindOfValue(value) {
|
|
|
141
177
|
return 'boolean';
|
|
142
178
|
if (typeof value === 'number')
|
|
143
179
|
return 'number';
|
|
144
|
-
return '
|
|
180
|
+
return 'enum';
|
|
145
181
|
}
|
|
182
|
+
/** The kind both admit: equal kinds, or the narrower of a kind and one of its subsets. */
|
|
183
|
+
function meet(left, right) {
|
|
184
|
+
if (left === right)
|
|
185
|
+
return left;
|
|
186
|
+
for (const [narrow, wide] of NARROWER) {
|
|
187
|
+
if ((left === narrow && right === wide) || (left === wide && right === narrow))
|
|
188
|
+
return narrow;
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The narrower kind of two that meet: a subset and its kind, and an enum of instants, which
|
|
194
|
+
* orders as instants.
|
|
195
|
+
*/
|
|
196
|
+
const NARROWER = [
|
|
197
|
+
['instant', 'string'],
|
|
198
|
+
['enum', 'string'],
|
|
199
|
+
['instant', 'enum'],
|
|
200
|
+
['set', 'array'],
|
|
201
|
+
];
|
|
146
202
|
function intersect(left, right) {
|
|
147
203
|
const result = new Set();
|
|
148
204
|
for (const leftKind of left) {
|
|
149
205
|
for (const rightKind of right) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
(leftKind === 'instant' && rightKind === 'string')) {
|
|
154
|
-
result.add('instant');
|
|
155
|
-
}
|
|
206
|
+
const kind = meet(leftKind, rightKind);
|
|
207
|
+
if (kind !== undefined)
|
|
208
|
+
result.add(kind);
|
|
156
209
|
}
|
|
157
210
|
}
|
|
158
211
|
return normalize(result);
|
|
@@ -160,9 +213,19 @@ function intersect(left, right) {
|
|
|
160
213
|
function union(left, right) {
|
|
161
214
|
return normalize(new Set([...left, ...right]));
|
|
162
215
|
}
|
|
216
|
+
/** A kind absorbs its subsets; strings of two subsets are strings. */
|
|
163
217
|
function normalize(input) {
|
|
164
|
-
if (input.has('
|
|
218
|
+
if (input.has('enum') && input.has('instant')) {
|
|
219
|
+
input.delete('enum');
|
|
220
|
+
input.delete('instant');
|
|
221
|
+
input.add('string');
|
|
222
|
+
}
|
|
223
|
+
if (input.has('string')) {
|
|
165
224
|
input.delete('instant');
|
|
225
|
+
input.delete('enum');
|
|
226
|
+
}
|
|
227
|
+
if (input.has('array'))
|
|
228
|
+
input.delete('set');
|
|
166
229
|
return input;
|
|
167
230
|
}
|
|
168
231
|
function schemaList(input) {
|