@maroonedog/luq 2.0.0 → 2.1.0
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/README.md +100 -10
- package/dist/chain/create-chain-node.js +22 -1
- package/dist/chain/create-chain-node.mjs +22 -1
- package/dist/compile/resolve-conditional-presence.js +1 -0
- package/dist/compile/resolve-conditional-presence.mjs +1 -0
- package/dist/compile/resolve-presence.js +4 -0
- package/dist/compile/resolve-presence.mjs +4 -0
- package/dist/compile/validation-plan.types.d.ts +10 -0
- package/dist/json-schema/build-from-schema.js +2 -3
- package/dist/json-schema/build-from-schema.mjs +2 -3
- package/dist/json-schema/collect-definitions.d.ts +30 -0
- package/dist/json-schema/collect-definitions.js +47 -3
- package/dist/json-schema/collect-definitions.mjs +46 -4
- package/dist/json-schema/collect-sub-schema-rules.d.ts +12 -1
- package/dist/json-schema/collect-sub-schema-rules.js +37 -7
- package/dist/json-schema/collect-sub-schema-rules.mjs +38 -8
- package/dist/json-schema/create-structural-context.d.ts +13 -3
- package/dist/json-schema/create-structural-context.js +72 -6
- package/dist/json-schema/create-structural-context.mjs +72 -7
- package/dist/json-schema/declare-additional-properties.d.ts +19 -0
- package/dist/json-schema/declare-additional-properties.js +42 -0
- package/dist/json-schema/declare-additional-properties.mjs +37 -0
- package/dist/json-schema/declare-object-keywords.d.ts +0 -2
- package/dist/json-schema/declare-object-keywords.js +6 -13
- package/dist/json-schema/declare-object-keywords.mjs +7 -13
- package/dist/json-schema/declare-presence.d.ts +9 -3
- package/dist/json-schema/declare-presence.js +12 -6
- package/dist/json-schema/declare-presence.mjs +12 -6
- package/dist/json-schema/extensions/json-schema/index.d.ts +1 -0
- package/dist/json-schema/extensions/json-schema/json-schema.d.ts +17 -2
- package/dist/json-schema/extensions/json-schema/json-schema.js +15 -7
- package/dist/json-schema/extensions/json-schema/json-schema.mjs +16 -8
- package/dist/json-schema/extensions/json-schema-full-feature/index.d.ts +1 -0
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.d.ts +2 -1
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.js +3 -1
- package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.mjs +3 -1
- package/dist/json-schema/follow-json-pointer.d.ts +23 -0
- package/dist/json-schema/follow-json-pointer.js +87 -0
- package/dist/json-schema/follow-json-pointer.mjs +82 -0
- package/dist/json-schema/index.d.ts +7 -2
- package/dist/json-schema/index.js +10 -2
- package/dist/json-schema/index.mjs +5 -2
- package/dist/json-schema/keyword-map-object.d.ts +1 -1
- package/dist/json-schema/ref-resolution-error.d.ts +4 -0
- package/dist/json-schema/ref-resolution-error.js +20 -0
- package/dist/json-schema/ref-resolution-error.mjs +16 -0
- package/dist/json-schema/ref-scope.d.ts +18 -0
- package/dist/json-schema/ref-scope.js +37 -0
- package/dist/json-schema/ref-scope.mjs +33 -0
- package/dist/json-schema/resolve-ref.d.ts +14 -9
- package/dist/json-schema/resolve-ref.js +89 -59
- package/dist/json-schema/resolve-ref.mjs +86 -55
- package/dist/json-schema/schema-registry.d.ts +30 -0
- package/dist/json-schema/schema-registry.js +142 -0
- package/dist/json-schema/schema-registry.mjs +139 -0
- package/dist/json-schema/schema-to-declarations.js +2 -1
- package/dist/json-schema/schema-to-declarations.mjs +2 -1
- package/dist/json-schema/structural-expansion.types.d.ts +8 -1
- package/dist/json-schema/uri-reference.d.ts +29 -0
- package/dist/json-schema/uri-reference.js +118 -0
- package/dist/json-schema/uri-reference.mjs +111 -0
- package/dist/path/create-value-writer.js +27 -2
- package/dist/path/create-value-writer.mjs +27 -2
- package/dist/path/reserved-segment.d.ts +22 -4
- package/dist/path/reserved-segment.js +27 -7
- package/dist/path/reserved-segment.mjs +27 -7
- package/dist/plugin-kit/compiled-rule.d.ts +13 -0
- package/dist/plugin-kit/create-rule.d.ts +11 -0
- package/dist/plugin-kit/create-rule.js +1 -0
- package/dist/plugin-kit/create-rule.mjs +1 -0
- package/dist/plugin-kit/plugin-definition.d.ts +15 -0
- package/dist/plugin-kit/plugin-definition.js +1 -0
- package/dist/plugin-kit/plugin-definition.mjs +1 -0
- package/dist/plugins/object-additional-properties/object-additional-properties-schema.d.ts +1 -1
- package/dist/plugins/object-additional-properties/object-additional-properties-schema.js +4 -2
- package/dist/plugins/object-additional-properties/object-additional-properties-schema.mjs +4 -2
- package/dist/plugins/object-additional-properties/object-additional-properties.d.ts +1 -1
- package/dist/plugins/object-additional-properties/object-additional-properties.js +4 -2
- package/dist/plugins/object-additional-properties/object-additional-properties.mjs +4 -2
- package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +15 -0
- package/dist/plugins/object-additional-properties/select-additional-keys.js +51 -0
- package/dist/plugins/object-additional-properties/select-additional-keys.mjs +47 -0
- package/dist/plugins/string-exact-length/string-exact-length.js +2 -2
- package/dist/plugins/string-exact-length/string-exact-length.mjs +3 -3
- package/dist/plugins/string-max/string-max.js +2 -2
- package/dist/plugins/string-max/string-max.mjs +3 -3
- package/dist/plugins/string-min/string-min.js +2 -2
- package/dist/plugins/string-min/string-min.mjs +3 -3
- package/dist/runtime/decide-presence.js +2 -1
- package/dist/runtime/decide-presence.mjs +2 -1
- package/dist/standard-schema/index.d.ts +6 -0
- package/dist/standard-schema/index.js +7 -0
- package/dist/standard-schema/index.mjs +2 -0
- package/dist/standard-schema/split-issue-path.d.ts +9 -0
- package/dist/standard-schema/split-issue-path.js +66 -0
- package/dist/standard-schema/split-issue-path.mjs +63 -0
- package/dist/standard-schema/standard-schema.types.d.ts +46 -0
- package/dist/standard-schema/standard-schema.types.js +13 -0
- package/dist/standard-schema/standard-schema.types.mjs +12 -0
- package/dist/standard-schema/to-standard-schema.d.ts +41 -0
- package/dist/standard-schema/to-standard-schema.js +60 -0
- package/dist/standard-schema/to-standard-schema.mjs +57 -0
- package/dist/standard-schema.d.ts +1 -0
- package/dist/standard-schema.js +2 -0
- package/dist/standard-schema.mjs +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.mjs +17 -0
- package/package.json +10 -3
|
@@ -18,20 +18,17 @@
|
|
|
18
18
|
// ===========================================================================
|
|
19
19
|
import { composite, fieldsBranch } from "../plugin-kit/create-rule.mjs";
|
|
20
20
|
import { PASS } from "../types/index.mjs";
|
|
21
|
-
import {
|
|
21
|
+
import { resolveSchemaNodeInScope } from "./collect-definitions.mjs";
|
|
22
22
|
import { declarePresenceRules } from "./declare-presence.mjs";
|
|
23
|
-
import { permitsNull } from "./declare-value-keywords.mjs";
|
|
24
23
|
import { EACH_STEP } from "./flatten-array-schema.mjs";
|
|
25
24
|
import { expandSchemaRules, readChildSchemas } from "./schema-to-declarations.mjs";
|
|
26
25
|
const NO_BRANCH_FIELDS = Object.freeze([]);
|
|
27
26
|
function toBranchField(child, context) {
|
|
28
|
-
const node = resolveSchemaNode(child.schema, context.root);
|
|
29
27
|
return {
|
|
30
28
|
path: child.step,
|
|
31
29
|
rules: Object.freeze([
|
|
32
30
|
...declarePresenceRules({
|
|
33
31
|
isRequired: child.isRequired,
|
|
34
|
-
allowsNull: permitsNull(node),
|
|
35
32
|
severity: context.build.config.defaultSeverity,
|
|
36
33
|
}),
|
|
37
34
|
...context.collectSubSchemaRules(child.schema),
|
|
@@ -52,9 +49,31 @@ function composeProperties(children, context) {
|
|
|
52
49
|
}),
|
|
53
50
|
];
|
|
54
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Every sub-schema subject gets the same one-line statement: null is a VALUE
|
|
54
|
+
* here, so the checks run on it.
|
|
55
|
+
*
|
|
56
|
+
* src/runtime/run-field.ts settles presence BEFORE any check, and a subject
|
|
57
|
+
* with no presence rule carries OPEN_PRESENCE, which ends the field on null.
|
|
58
|
+
* A branch subject and an array element both arrive without one, so every
|
|
59
|
+
* check the sub-schema declared was skipped for null: `[null]` passed
|
|
60
|
+
* `{"items":{"type":"boolean"}}`, and `additionalItems: false` accepted a
|
|
61
|
+
* trailing null. Deciding it from `type` alone is not enough either —
|
|
62
|
+
* `false`, `{"not": {}}` and an `enum` without null forbid null while
|
|
63
|
+
* saying nothing about `type`.
|
|
64
|
+
*/
|
|
65
|
+
function declareOwnNullPolicy(context) {
|
|
66
|
+
return declarePresenceRules({
|
|
67
|
+
isRequired: false,
|
|
68
|
+
severity: context.build.config.defaultSeverity,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
55
71
|
export function collectSubSchemaRules(schema, context) {
|
|
56
|
-
const node =
|
|
57
|
-
const rules = [
|
|
72
|
+
const node = resolveSchemaNodeInScope(schema, context.scope).node;
|
|
73
|
+
const rules = [
|
|
74
|
+
...declareOwnNullPolicy(context),
|
|
75
|
+
...expandSchemaRules(node, context),
|
|
76
|
+
];
|
|
58
77
|
const children = readChildSchemas(node);
|
|
59
78
|
const properties = children.filter((child) => child.step !== EACH_STEP);
|
|
60
79
|
if (properties.length > 0) {
|
|
@@ -68,11 +87,22 @@ export function collectSubSchemaRules(schema, context) {
|
|
|
68
87
|
}
|
|
69
88
|
return Object.freeze(rules);
|
|
70
89
|
}
|
|
71
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Every branch of every composite is built here, so a branch is one shape.
|
|
92
|
+
*
|
|
93
|
+
* The call is to the function above and NOT to `context.collectSubSchemaRules`,
|
|
94
|
+
* and the difference is load-bearing. `context` is already the child context
|
|
95
|
+
* that createStructuralContext produced by DESCENDING through this schema, so
|
|
96
|
+
* a `$ref` here is already recorded in `visitedRefs`. Going through the
|
|
97
|
+
* context would descend the same `$ref` a second time, the recursion guard
|
|
98
|
+
* would see it as a cycle, and the branch would come back with no rules at all
|
|
99
|
+
* — `{"items":[{"$ref":"#/definitions/x"}]}` constrained nothing while the
|
|
100
|
+
* inline form `{"items":[{"type":"integer"}]}` worked.
|
|
101
|
+
*/
|
|
72
102
|
export function toSchemaBranch(label, schema, context) {
|
|
73
103
|
return {
|
|
74
104
|
label,
|
|
75
|
-
rules:
|
|
105
|
+
rules: collectSubSchemaRules(schema, context),
|
|
76
106
|
fields: NO_BRANCH_FIELDS,
|
|
77
107
|
};
|
|
78
108
|
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { ChainBuildContext } from "../chain/create-chain-node";
|
|
2
|
-
import type {
|
|
2
|
+
import type { RefScope } from "./ref-scope";
|
|
3
|
+
import type { Draft07SchemaObject } from "./draft07.types";
|
|
3
4
|
import type { JsonSchemaBag } from "./json-schema-bag.types";
|
|
4
5
|
import type { StructuralContext } from "./structural-expansion.types";
|
|
6
|
+
/** Shared by every context of ONE conversion, so the budget is the whole. */
|
|
7
|
+
interface ExpansionBudget {
|
|
8
|
+
remaining: number;
|
|
9
|
+
}
|
|
5
10
|
export interface ConversionSeed {
|
|
6
11
|
readonly bag: JsonSchemaBag;
|
|
7
|
-
readonly
|
|
12
|
+
readonly scope: RefScope;
|
|
8
13
|
readonly chain: ChainBuildContext;
|
|
14
|
+
/** Created by the entry point; every nested context shares this one. */
|
|
15
|
+
readonly budget?: ExpansionBudget;
|
|
9
16
|
}
|
|
10
|
-
|
|
17
|
+
/** The budget a conversion starts with. One per document, not one per node. */
|
|
18
|
+
export declare function createExpansionBudget(): ExpansionBudget;
|
|
19
|
+
export declare function createStructuralContext(seed: ConversionSeed, node: Draft07SchemaObject, visitedRefs: readonly string[], scope?: RefScope): StructuralContext;
|
|
20
|
+
export {};
|
|
@@ -1,25 +1,91 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createExpansionBudget = createExpansionBudget;
|
|
3
4
|
exports.createStructuralContext = createStructuralContext;
|
|
4
5
|
const collect_definitions_1 = require("./collect-definitions");
|
|
5
6
|
const collect_sub_schema_rules_1 = require("./collect-sub-schema-rules");
|
|
6
7
|
const NO_RULES = Object.freeze([]);
|
|
8
|
+
/**
|
|
9
|
+
* How many times one `$ref` may be entered on a single branch of the
|
|
10
|
+
* conversion, and how much expansion the whole conversion may do.
|
|
11
|
+
*
|
|
12
|
+
* A recursive definition has infinitely many declared paths and Luq declares
|
|
13
|
+
* finite ones, so SOMETHING has to stop. It used to stop at the FIRST repeat,
|
|
14
|
+
* which is why a mutually recursive schema (tree -> node -> tree) checked only
|
|
15
|
+
* two levels and silently accepted anything deeper.
|
|
16
|
+
*
|
|
17
|
+
* Unrolling further is exponential in the number of mutually recursive
|
|
18
|
+
* definitions, and the measurement says so plainly. A three-way cycle
|
|
19
|
+
* (a -> b -> c -> a, each also naming the others) builds in:
|
|
20
|
+
*
|
|
21
|
+
* 1 unroll 17 ms 2 unrolls 55 ms
|
|
22
|
+
* 3 unrolls 538 ms 4 unrolls 9257 ms
|
|
23
|
+
*
|
|
24
|
+
* Three is what the corpus needs (the mutually recursive tree in ref.json
|
|
25
|
+
* puts its invalid value three levels down) and is where the cost is still
|
|
26
|
+
* a fraction of a second. It is a BUILD-time cost, paid once per document.
|
|
27
|
+
*
|
|
28
|
+
* The per-ref cap bounds the depth and leaves the WIDTH to the document, so
|
|
29
|
+
* it is not a bound on the work by itself. EXPANSION_BUDGET is: it caps the
|
|
30
|
+
* total number of `$ref` expansions one conversion may do, whatever shape
|
|
31
|
+
* the document has. It is deliberately far above what a real schema reaches
|
|
32
|
+
* — the whole official corpus stays under it — so it changes nothing for
|
|
33
|
+
* ordinary documents and only stops a pathological one from running away.
|
|
34
|
+
*
|
|
35
|
+
* Running out is not an error. The conversion stops adding rules, exactly as
|
|
36
|
+
* it did at the first repeat before, and the document is checked to the depth
|
|
37
|
+
* that was reached — the same silence as before, moved further out.
|
|
38
|
+
*/
|
|
39
|
+
const MAX_REF_UNROLL = 3;
|
|
40
|
+
const EXPANSION_BUDGET = 100000;
|
|
41
|
+
function countOf(pointers, pointer) {
|
|
42
|
+
let count = 0;
|
|
43
|
+
for (const one of pointers)
|
|
44
|
+
if (one === pointer)
|
|
45
|
+
count += 1;
|
|
46
|
+
return count;
|
|
47
|
+
}
|
|
48
|
+
/** The budget a conversion starts with. One per document, not one per node. */
|
|
49
|
+
function createExpansionBudget() {
|
|
50
|
+
return { remaining: EXPANSION_BUDGET };
|
|
51
|
+
}
|
|
7
52
|
function propertyKeysOf(schema) {
|
|
8
53
|
return Object.freeze(Object.keys(schema.properties ?? {}));
|
|
9
54
|
}
|
|
10
|
-
function createStructuralContext(seed, node, visitedRefs) {
|
|
55
|
+
function createStructuralContext(seed, node, visitedRefs, scope = seed.scope) {
|
|
56
|
+
const budget = seed.budget ?? createExpansionBudget();
|
|
11
57
|
const build = {
|
|
12
58
|
fieldPath: seed.chain.fieldPath,
|
|
13
59
|
declaredSiblingKeys: propertyKeysOf(node),
|
|
14
60
|
config: seed.chain.config,
|
|
15
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* One descent: the node this schema really is, and the context inside it.
|
|
64
|
+
*
|
|
65
|
+
* The RESOLVED node is handed back, and callers must use it rather than the
|
|
66
|
+
* schema they passed in. Resolving the same `$ref` a second time — which
|
|
67
|
+
* is what happens if the original is passed on — moves the base twice, so
|
|
68
|
+
* a relative reference under a relative `$id` doubles its own folder.
|
|
69
|
+
*/
|
|
16
70
|
const descend = (schema) => {
|
|
17
71
|
const pointer = (0, collect_definitions_1.readRefPointer)(schema);
|
|
18
|
-
if (pointer !== undefined &&
|
|
72
|
+
if (pointer !== undefined &&
|
|
73
|
+
countOf(visitedRefs, pointer) >= MAX_REF_UNROLL) {
|
|
19
74
|
return undefined;
|
|
20
75
|
}
|
|
76
|
+
if (pointer !== undefined) {
|
|
77
|
+
if (budget.remaining <= 0)
|
|
78
|
+
return undefined;
|
|
79
|
+
budget.remaining -= 1;
|
|
80
|
+
}
|
|
21
81
|
const seen = pointer === undefined ? visitedRefs : [...visitedRefs, pointer];
|
|
22
|
-
|
|
82
|
+
// The scope moves with the node: following a `$ref` into another
|
|
83
|
+
// document makes THAT document's base the one its own refs resolve in.
|
|
84
|
+
const resolved = (0, collect_definitions_1.resolveSchemaNodeInScope)(schema, scope);
|
|
85
|
+
return {
|
|
86
|
+
node: resolved.node,
|
|
87
|
+
context: createStructuralContext({ ...seed, budget }, resolved.node, seen, resolved.scope),
|
|
88
|
+
};
|
|
23
89
|
};
|
|
24
90
|
const ruleContextFor = (pluginName, code) => ({
|
|
25
91
|
pluginName,
|
|
@@ -32,19 +98,19 @@ function createStructuralContext(seed, node, visitedRefs) {
|
|
|
32
98
|
return {
|
|
33
99
|
bag: seed.bag,
|
|
34
100
|
build,
|
|
35
|
-
|
|
101
|
+
scope,
|
|
36
102
|
visitedRefs,
|
|
37
103
|
collectSubSchemaRules: (schema) => {
|
|
38
104
|
const child = descend(schema);
|
|
39
105
|
return child === undefined
|
|
40
106
|
? NO_RULES
|
|
41
|
-
: (0, collect_sub_schema_rules_1.collectSubSchemaRules)(
|
|
107
|
+
: (0, collect_sub_schema_rules_1.collectSubSchemaRules)(child.node, child.context);
|
|
42
108
|
},
|
|
43
109
|
toBranch: (label, schema) => {
|
|
44
110
|
const child = descend(schema);
|
|
45
111
|
return child === undefined
|
|
46
112
|
? { label, rules: NO_RULES, fields: [] }
|
|
47
|
-
: (0, collect_sub_schema_rules_1.toSchemaBranch)(label,
|
|
113
|
+
: (0, collect_sub_schema_rules_1.toSchemaBranch)(label, child.node, child.context);
|
|
48
114
|
},
|
|
49
115
|
ruleContextFor,
|
|
50
116
|
};
|
|
@@ -1,22 +1,87 @@
|
|
|
1
|
-
import { readRefPointer,
|
|
1
|
+
import { readRefPointer, resolveSchemaNodeInScope, } from "./collect-definitions.mjs";
|
|
2
2
|
import { collectSubSchemaRules, toSchemaBranch, } from "./collect-sub-schema-rules.mjs";
|
|
3
3
|
const NO_RULES = Object.freeze([]);
|
|
4
|
+
/**
|
|
5
|
+
* How many times one `$ref` may be entered on a single branch of the
|
|
6
|
+
* conversion, and how much expansion the whole conversion may do.
|
|
7
|
+
*
|
|
8
|
+
* A recursive definition has infinitely many declared paths and Luq declares
|
|
9
|
+
* finite ones, so SOMETHING has to stop. It used to stop at the FIRST repeat,
|
|
10
|
+
* which is why a mutually recursive schema (tree -> node -> tree) checked only
|
|
11
|
+
* two levels and silently accepted anything deeper.
|
|
12
|
+
*
|
|
13
|
+
* Unrolling further is exponential in the number of mutually recursive
|
|
14
|
+
* definitions, and the measurement says so plainly. A three-way cycle
|
|
15
|
+
* (a -> b -> c -> a, each also naming the others) builds in:
|
|
16
|
+
*
|
|
17
|
+
* 1 unroll 17 ms 2 unrolls 55 ms
|
|
18
|
+
* 3 unrolls 538 ms 4 unrolls 9257 ms
|
|
19
|
+
*
|
|
20
|
+
* Three is what the corpus needs (the mutually recursive tree in ref.json
|
|
21
|
+
* puts its invalid value three levels down) and is where the cost is still
|
|
22
|
+
* a fraction of a second. It is a BUILD-time cost, paid once per document.
|
|
23
|
+
*
|
|
24
|
+
* The per-ref cap bounds the depth and leaves the WIDTH to the document, so
|
|
25
|
+
* it is not a bound on the work by itself. EXPANSION_BUDGET is: it caps the
|
|
26
|
+
* total number of `$ref` expansions one conversion may do, whatever shape
|
|
27
|
+
* the document has. It is deliberately far above what a real schema reaches
|
|
28
|
+
* — the whole official corpus stays under it — so it changes nothing for
|
|
29
|
+
* ordinary documents and only stops a pathological one from running away.
|
|
30
|
+
*
|
|
31
|
+
* Running out is not an error. The conversion stops adding rules, exactly as
|
|
32
|
+
* it did at the first repeat before, and the document is checked to the depth
|
|
33
|
+
* that was reached — the same silence as before, moved further out.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_REF_UNROLL = 3;
|
|
36
|
+
const EXPANSION_BUDGET = 100000;
|
|
37
|
+
function countOf(pointers, pointer) {
|
|
38
|
+
let count = 0;
|
|
39
|
+
for (const one of pointers)
|
|
40
|
+
if (one === pointer)
|
|
41
|
+
count += 1;
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
/** The budget a conversion starts with. One per document, not one per node. */
|
|
45
|
+
export function createExpansionBudget() {
|
|
46
|
+
return { remaining: EXPANSION_BUDGET };
|
|
47
|
+
}
|
|
4
48
|
function propertyKeysOf(schema) {
|
|
5
49
|
return Object.freeze(Object.keys(schema.properties ?? {}));
|
|
6
50
|
}
|
|
7
|
-
export function createStructuralContext(seed, node, visitedRefs) {
|
|
51
|
+
export function createStructuralContext(seed, node, visitedRefs, scope = seed.scope) {
|
|
52
|
+
const budget = seed.budget ?? createExpansionBudget();
|
|
8
53
|
const build = {
|
|
9
54
|
fieldPath: seed.chain.fieldPath,
|
|
10
55
|
declaredSiblingKeys: propertyKeysOf(node),
|
|
11
56
|
config: seed.chain.config,
|
|
12
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* One descent: the node this schema really is, and the context inside it.
|
|
60
|
+
*
|
|
61
|
+
* The RESOLVED node is handed back, and callers must use it rather than the
|
|
62
|
+
* schema they passed in. Resolving the same `$ref` a second time — which
|
|
63
|
+
* is what happens if the original is passed on — moves the base twice, so
|
|
64
|
+
* a relative reference under a relative `$id` doubles its own folder.
|
|
65
|
+
*/
|
|
13
66
|
const descend = (schema) => {
|
|
14
67
|
const pointer = readRefPointer(schema);
|
|
15
|
-
if (pointer !== undefined &&
|
|
68
|
+
if (pointer !== undefined &&
|
|
69
|
+
countOf(visitedRefs, pointer) >= MAX_REF_UNROLL) {
|
|
16
70
|
return undefined;
|
|
17
71
|
}
|
|
72
|
+
if (pointer !== undefined) {
|
|
73
|
+
if (budget.remaining <= 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
budget.remaining -= 1;
|
|
76
|
+
}
|
|
18
77
|
const seen = pointer === undefined ? visitedRefs : [...visitedRefs, pointer];
|
|
19
|
-
|
|
78
|
+
// The scope moves with the node: following a `$ref` into another
|
|
79
|
+
// document makes THAT document's base the one its own refs resolve in.
|
|
80
|
+
const resolved = resolveSchemaNodeInScope(schema, scope);
|
|
81
|
+
return {
|
|
82
|
+
node: resolved.node,
|
|
83
|
+
context: createStructuralContext({ ...seed, budget }, resolved.node, seen, resolved.scope),
|
|
84
|
+
};
|
|
20
85
|
};
|
|
21
86
|
const ruleContextFor = (pluginName, code) => ({
|
|
22
87
|
pluginName,
|
|
@@ -29,19 +94,19 @@ export function createStructuralContext(seed, node, visitedRefs) {
|
|
|
29
94
|
return {
|
|
30
95
|
bag: seed.bag,
|
|
31
96
|
build,
|
|
32
|
-
|
|
97
|
+
scope,
|
|
33
98
|
visitedRefs,
|
|
34
99
|
collectSubSchemaRules: (schema) => {
|
|
35
100
|
const child = descend(schema);
|
|
36
101
|
return child === undefined
|
|
37
102
|
? NO_RULES
|
|
38
|
-
: collectSubSchemaRules(
|
|
103
|
+
: collectSubSchemaRules(child.node, child.context);
|
|
39
104
|
},
|
|
40
105
|
toBranch: (label, schema) => {
|
|
41
106
|
const child = descend(schema);
|
|
42
107
|
return child === undefined
|
|
43
108
|
? { label, rules: NO_RULES, fields: [] }
|
|
44
|
-
: toSchemaBranch(label,
|
|
109
|
+
: toSchemaBranch(label, child.node, child.context);
|
|
45
110
|
},
|
|
46
111
|
ruleContextFor,
|
|
47
112
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Rule } from "../plugin-kit/compiled-rule";
|
|
2
|
+
import type { ConverterChain } from "./apply-keyword-binding";
|
|
3
|
+
import type { Draft07SchemaObject } from "./draft07.types";
|
|
4
|
+
import type { StructuralContext } from "./structural-expansion.types";
|
|
5
|
+
/**
|
|
6
|
+
* `patternProperties` のキー、つまり正規表現の文字列。
|
|
7
|
+
* additionalProperties が「該当しないキー」を選ぶのに要る。
|
|
8
|
+
*/
|
|
9
|
+
export declare function patternPropertyKeys(schema: Draft07SchemaObject): readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* BOOLEAN 形。
|
|
12
|
+
*
|
|
13
|
+
* キーワード束縛は自分の値 (boolean) しか運べないので、パターンがあるときだけ
|
|
14
|
+
* プラグインを直接呼ぶ。束縛のほうは残す: キーワード表と「そのメソッドが
|
|
15
|
+
* 実在する」というコンパイル時のゲートはそちらが持っている。
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyAdditionalPropertiesBoolean(chain: ConverterChain<"object">, schema: Draft07SchemaObject, allowed: boolean): ConverterChain<"object">;
|
|
18
|
+
/** SCHEMA 形: 宣言にもパターンにも該当しない値が、そのスキーマに従う。 */
|
|
19
|
+
export declare function declareAdditionalPropertiesSchema(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.patternPropertyKeys = patternPropertyKeys;
|
|
4
|
+
exports.applyAdditionalPropertiesBoolean = applyAdditionalPropertiesBoolean;
|
|
5
|
+
exports.declareAdditionalPropertiesSchema = declareAdditionalPropertiesSchema;
|
|
6
|
+
const apply_keyword_binding_1 = require("./apply-keyword-binding");
|
|
7
|
+
const keyword_map_object_1 = require("./keyword-map-object");
|
|
8
|
+
const NO_RULES = Object.freeze([]);
|
|
9
|
+
/**
|
|
10
|
+
* `patternProperties` のキー、つまり正規表現の文字列。
|
|
11
|
+
* additionalProperties が「該当しないキー」を選ぶのに要る。
|
|
12
|
+
*/
|
|
13
|
+
function patternPropertyKeys(schema) {
|
|
14
|
+
const patterns = schema.patternProperties;
|
|
15
|
+
if (patterns === undefined || patterns === null)
|
|
16
|
+
return Object.freeze([]);
|
|
17
|
+
return Object.freeze(Object.keys(patterns));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* BOOLEAN 形。
|
|
21
|
+
*
|
|
22
|
+
* キーワード束縛は自分の値 (boolean) しか運べないので、パターンがあるときだけ
|
|
23
|
+
* プラグインを直接呼ぶ。束縛のほうは残す: キーワード表と「そのメソッドが
|
|
24
|
+
* 実在する」というコンパイル時のゲートはそちらが持っている。
|
|
25
|
+
*/
|
|
26
|
+
function applyAdditionalPropertiesBoolean(chain, schema, allowed) {
|
|
27
|
+
const patterns = patternPropertyKeys(schema);
|
|
28
|
+
return patterns.length === 0
|
|
29
|
+
? (0, apply_keyword_binding_1.applyKeywordBinding)(chain, keyword_map_object_1.additionalPropertiesBinding, allowed)
|
|
30
|
+
: chain.additionalProperties(allowed, undefined, patterns);
|
|
31
|
+
}
|
|
32
|
+
/** SCHEMA 形: 宣言にもパターンにも該当しない値が、そのスキーマに従う。 */
|
|
33
|
+
function declareAdditionalPropertiesSchema(schema, context) {
|
|
34
|
+
const additional = schema.additionalProperties;
|
|
35
|
+
if (additional === undefined || typeof additional === "boolean") {
|
|
36
|
+
return NO_RULES;
|
|
37
|
+
}
|
|
38
|
+
const plugin = context.bag.objectAdditionalPropertiesSchema;
|
|
39
|
+
return Object.freeze([
|
|
40
|
+
plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional), undefined, patternPropertyKeys(schema)),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { applyKeywordBinding } from "./apply-keyword-binding.mjs";
|
|
2
|
+
import { additionalPropertiesBinding } from "./keyword-map-object.mjs";
|
|
3
|
+
const NO_RULES = Object.freeze([]);
|
|
4
|
+
/**
|
|
5
|
+
* `patternProperties` のキー、つまり正規表現の文字列。
|
|
6
|
+
* additionalProperties が「該当しないキー」を選ぶのに要る。
|
|
7
|
+
*/
|
|
8
|
+
export function patternPropertyKeys(schema) {
|
|
9
|
+
const patterns = schema.patternProperties;
|
|
10
|
+
if (patterns === undefined || patterns === null)
|
|
11
|
+
return Object.freeze([]);
|
|
12
|
+
return Object.freeze(Object.keys(patterns));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* BOOLEAN 形。
|
|
16
|
+
*
|
|
17
|
+
* キーワード束縛は自分の値 (boolean) しか運べないので、パターンがあるときだけ
|
|
18
|
+
* プラグインを直接呼ぶ。束縛のほうは残す: キーワード表と「そのメソッドが
|
|
19
|
+
* 実在する」というコンパイル時のゲートはそちらが持っている。
|
|
20
|
+
*/
|
|
21
|
+
export function applyAdditionalPropertiesBoolean(chain, schema, allowed) {
|
|
22
|
+
const patterns = patternPropertyKeys(schema);
|
|
23
|
+
return patterns.length === 0
|
|
24
|
+
? applyKeywordBinding(chain, additionalPropertiesBinding, allowed)
|
|
25
|
+
: chain.additionalProperties(allowed, undefined, patterns);
|
|
26
|
+
}
|
|
27
|
+
/** SCHEMA 形: 宣言にもパターンにも該当しない値が、そのスキーマに従う。 */
|
|
28
|
+
export function declareAdditionalPropertiesSchema(schema, context) {
|
|
29
|
+
const additional = schema.additionalProperties;
|
|
30
|
+
if (additional === undefined || typeof additional === "boolean") {
|
|
31
|
+
return NO_RULES;
|
|
32
|
+
}
|
|
33
|
+
const plugin = context.bag.objectAdditionalPropertiesSchema;
|
|
34
|
+
return Object.freeze([
|
|
35
|
+
plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional), undefined, patternPropertyKeys(schema)),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
@@ -2,8 +2,6 @@ import type { Rule } from "../plugin-kit/compiled-rule";
|
|
|
2
2
|
import type { Draft07Schema, Draft07SchemaObject } from "./draft07.types";
|
|
3
3
|
import type { StructuralContext } from "./structural-expansion.types";
|
|
4
4
|
export declare function declareObjectRules(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
|
|
5
|
-
/** The SCHEMA form of `additionalProperties`: every undeclared value obeys it. */
|
|
6
|
-
export declare function declareAdditionalPropertiesSchema(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
|
|
7
5
|
/** EVERY matching pattern applies; 1.x broke after the first match. */
|
|
8
6
|
export declare function declarePatternProperties(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
|
|
9
7
|
/** A sub-chain over the property NAMES, which are always strings. */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.declareObjectRules = declareObjectRules;
|
|
4
|
-
exports.declareAdditionalPropertiesSchema = declareAdditionalPropertiesSchema;
|
|
5
4
|
exports.declarePatternProperties = declarePatternProperties;
|
|
6
5
|
exports.declarePropertyNames = declarePropertyNames;
|
|
7
6
|
exports.declareDependencies = declareDependencies;
|
|
@@ -23,6 +22,7 @@ exports.readPropertyChildren = readPropertyChildren;
|
|
|
23
22
|
const create_chain_node_1 = require("../chain/create-chain-node");
|
|
24
23
|
const create_field_slots_1 = require("../chain/create-field-slots");
|
|
25
24
|
const types_1 = require("../types");
|
|
25
|
+
const declare_additional_properties_1 = require("./declare-additional-properties");
|
|
26
26
|
const apply_keyword_binding_1 = require("./apply-keyword-binding");
|
|
27
27
|
const collect_definitions_1 = require("./collect-definitions");
|
|
28
28
|
const keyword_map_object_1 = require("./keyword-map-object");
|
|
@@ -40,21 +40,14 @@ function declareObjectRules(schema, context) {
|
|
|
40
40
|
}
|
|
41
41
|
const additional = schema.additionalProperties;
|
|
42
42
|
if (typeof additional === "boolean") {
|
|
43
|
-
|
|
43
|
+
// §6.5.4 の対象は「properties にも patternProperties にも該当しない」キー。
|
|
44
|
+
// キーワード束縛は自分の値 (boolean) しか運べないので、パターンがある
|
|
45
|
+
// ときだけプラグインを直接呼ぶ。束縛のほうは残す: キーワード表と
|
|
46
|
+
// 「そのメソッドが実在する」というコンパイル時のゲートはそちらが持つ。
|
|
47
|
+
chain = (0, declare_additional_properties_1.applyAdditionalPropertiesBoolean)(chain, schema, additional);
|
|
44
48
|
}
|
|
45
49
|
return readRules(chain);
|
|
46
50
|
}
|
|
47
|
-
/** The SCHEMA form of `additionalProperties`: every undeclared value obeys it. */
|
|
48
|
-
function declareAdditionalPropertiesSchema(schema, context) {
|
|
49
|
-
const additional = schema.additionalProperties;
|
|
50
|
-
if (additional === undefined || typeof additional === "boolean") {
|
|
51
|
-
return NO_RULES;
|
|
52
|
-
}
|
|
53
|
-
const plugin = context.bag.objectAdditionalPropertiesSchema;
|
|
54
|
-
return Object.freeze([
|
|
55
|
-
plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional)),
|
|
56
|
-
]);
|
|
57
|
-
}
|
|
58
51
|
/** EVERY matching pattern applies; 1.x broke after the first match. */
|
|
59
52
|
function declarePatternProperties(schema, context) {
|
|
60
53
|
const patterns = schema.patternProperties;
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
import { readChainRules } from "../chain/create-chain-node.mjs";
|
|
16
16
|
import { createFieldSlots } from "../chain/create-field-slots.mjs";
|
|
17
17
|
import { isStringArray } from "../types/index.mjs";
|
|
18
|
+
import { applyAdditionalPropertiesBoolean } from "./declare-additional-properties.mjs";
|
|
18
19
|
import { applyKeywordBinding } from "./apply-keyword-binding.mjs";
|
|
19
20
|
import { toSchemaObject } from "./collect-definitions.mjs";
|
|
20
|
-
import {
|
|
21
|
+
import { maxPropertiesBinding, minPropertiesBinding, } from "./keyword-map-object.mjs";
|
|
21
22
|
const NO_RULES = Object.freeze([]);
|
|
22
23
|
function readRules(chain) {
|
|
23
24
|
return readChainRules(chain) ?? NO_RULES;
|
|
@@ -32,21 +33,14 @@ export function declareObjectRules(schema, context) {
|
|
|
32
33
|
}
|
|
33
34
|
const additional = schema.additionalProperties;
|
|
34
35
|
if (typeof additional === "boolean") {
|
|
35
|
-
|
|
36
|
+
// §6.5.4 の対象は「properties にも patternProperties にも該当しない」キー。
|
|
37
|
+
// キーワード束縛は自分の値 (boolean) しか運べないので、パターンがある
|
|
38
|
+
// ときだけプラグインを直接呼ぶ。束縛のほうは残す: キーワード表と
|
|
39
|
+
// 「そのメソッドが実在する」というコンパイル時のゲートはそちらが持つ。
|
|
40
|
+
chain = applyAdditionalPropertiesBoolean(chain, schema, additional);
|
|
36
41
|
}
|
|
37
42
|
return readRules(chain);
|
|
38
43
|
}
|
|
39
|
-
/** The SCHEMA form of `additionalProperties`: every undeclared value obeys it. */
|
|
40
|
-
export function declareAdditionalPropertiesSchema(schema, context) {
|
|
41
|
-
const additional = schema.additionalProperties;
|
|
42
|
-
if (additional === undefined || typeof additional === "boolean") {
|
|
43
|
-
return NO_RULES;
|
|
44
|
-
}
|
|
45
|
-
const plugin = context.bag.objectAdditionalPropertiesSchema;
|
|
46
|
-
return Object.freeze([
|
|
47
|
-
plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional)),
|
|
48
|
-
]);
|
|
49
|
-
}
|
|
50
44
|
/** EVERY matching pattern applies; 1.x broke after the first match. */
|
|
51
45
|
export function declarePatternProperties(schema, context) {
|
|
52
46
|
const patterns = schema.patternProperties;
|
|
@@ -2,11 +2,17 @@ import type { Rule } from "../plugin-kit/compiled-rule";
|
|
|
2
2
|
import type { IssueSeverity } from "../types";
|
|
3
3
|
export interface PresenceDeclaration {
|
|
4
4
|
readonly isRequired: boolean;
|
|
5
|
-
readonly allowsNull: boolean;
|
|
6
5
|
readonly severity: IssueSeverity;
|
|
7
6
|
}
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The rule is emitted even when it forbids nothing, because it carries
|
|
9
|
+
* `nullIsValue` and that is not a permission — it is the statement that null
|
|
10
|
+
* has to be JUDGED here rather than settled by presence. A subject with no
|
|
11
|
+
* presence rule carries OPEN_PRESENCE, which ends the field on null before a
|
|
12
|
+
* single check runs; under Draft-07 that is wrong, because `false`,
|
|
13
|
+
* `{"not": {}}` and an `enum` without null all forbid null while saying
|
|
14
|
+
* nothing about `type`. Null therefore goes to the checks, and `type`
|
|
15
|
+
* (declare-value-keywords.ts) is what rejects it when the document says so —
|
|
16
|
+
* one answer instead of two that have to agree.
|
|
11
17
|
*/
|
|
12
18
|
export declare function declarePresenceRules(declaration: PresenceDeclaration): readonly Rule[];
|
|
@@ -23,20 +23,26 @@ exports.declarePresenceRules = declarePresenceRules;
|
|
|
23
23
|
// ===========================================================================
|
|
24
24
|
const create_rule_1 = require("../plugin-kit/create-rule");
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* The rule is emitted even when it forbids nothing, because it carries
|
|
27
|
+
* `nullIsValue` and that is not a permission — it is the statement that null
|
|
28
|
+
* has to be JUDGED here rather than settled by presence. A subject with no
|
|
29
|
+
* presence rule carries OPEN_PRESENCE, which ends the field on null before a
|
|
30
|
+
* single check runs; under Draft-07 that is wrong, because `false`,
|
|
31
|
+
* `{"not": {}}` and an `enum` without null all forbid null while saying
|
|
32
|
+
* nothing about `type`. Null therefore goes to the checks, and `type`
|
|
33
|
+
* (declare-value-keywords.ts) is what rejects it when the document says so —
|
|
34
|
+
* one answer instead of two that have to agree.
|
|
28
35
|
*/
|
|
29
36
|
function declarePresenceRules(declaration) {
|
|
30
|
-
if (!declaration.isRequired && declaration.allowsNull) {
|
|
31
|
-
return Object.freeze([]);
|
|
32
|
-
}
|
|
33
37
|
return Object.freeze([
|
|
34
38
|
(0, create_rule_1.presence)({
|
|
35
39
|
code: declaration.isRequired ? "required" : "type",
|
|
36
40
|
severity: declaration.severity,
|
|
37
41
|
allowUndefined: !declaration.isRequired,
|
|
38
|
-
|
|
42
|
+
// Unreachable for null while nullIsValue is true; `type` decides.
|
|
43
|
+
allowNull: true,
|
|
39
44
|
emptyStringIsMissing: false,
|
|
45
|
+
nullIsValue: true,
|
|
40
46
|
describe: (messageContext) => declaration.isRequired
|
|
41
47
|
? `${messageContext.path} is required`
|
|
42
48
|
: `${messageContext.path} must not be null`,
|
|
@@ -20,20 +20,26 @@
|
|
|
20
20
|
// ===========================================================================
|
|
21
21
|
import { presence } from "../plugin-kit/create-rule.mjs";
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* The rule is emitted even when it forbids nothing, because it carries
|
|
24
|
+
* `nullIsValue` and that is not a permission — it is the statement that null
|
|
25
|
+
* has to be JUDGED here rather than settled by presence. A subject with no
|
|
26
|
+
* presence rule carries OPEN_PRESENCE, which ends the field on null before a
|
|
27
|
+
* single check runs; under Draft-07 that is wrong, because `false`,
|
|
28
|
+
* `{"not": {}}` and an `enum` without null all forbid null while saying
|
|
29
|
+
* nothing about `type`. Null therefore goes to the checks, and `type`
|
|
30
|
+
* (declare-value-keywords.ts) is what rejects it when the document says so —
|
|
31
|
+
* one answer instead of two that have to agree.
|
|
25
32
|
*/
|
|
26
33
|
export function declarePresenceRules(declaration) {
|
|
27
|
-
if (!declaration.isRequired && declaration.allowsNull) {
|
|
28
|
-
return Object.freeze([]);
|
|
29
|
-
}
|
|
30
34
|
return Object.freeze([
|
|
31
35
|
presence({
|
|
32
36
|
code: declaration.isRequired ? "required" : "type",
|
|
33
37
|
severity: declaration.severity,
|
|
34
38
|
allowUndefined: !declaration.isRequired,
|
|
35
|
-
|
|
39
|
+
// Unreachable for null while nullIsValue is true; `type` decides.
|
|
40
|
+
allowNull: true,
|
|
36
41
|
emptyStringIsMissing: false,
|
|
42
|
+
nullIsValue: true,
|
|
37
43
|
describe: (messageContext) => declaration.isRequired
|
|
38
44
|
? `${messageContext.path} is required`
|
|
39
45
|
: `${messageContext.path} must not be null`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { SCHEMA_BRANCH_LABEL, collectDocumentRules, jsonSchemaPlugin, } from "./json-schema";
|
|
2
|
+
export type { JsonSchemaOptions } from "./json-schema";
|
|
2
3
|
export { NotASchemaError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index";
|
|
3
4
|
export type { Draft07Schema, Draft07SchemaObject, JsonSchemaBag, } from "../../index";
|