@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
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import type { Draft07Schema } from "./draft07.types";
|
|
2
|
-
|
|
3
|
-
readonly ref: string;
|
|
4
|
-
constructor(ref: string, reason: string);
|
|
5
|
-
}
|
|
2
|
+
import type { RefScope } from "./ref-scope";
|
|
6
3
|
/**
|
|
7
|
-
* Resolves `ref`
|
|
8
|
-
*
|
|
9
|
-
* THIS chain, so `a -> b -> a` throws instead of looping forever
|
|
4
|
+
* Resolves `ref` in `scope`, then keeps following `$ref` until it reaches a
|
|
5
|
+
* schema that is not one. `visited` holds the ABSOLUTE URIs already entered on
|
|
6
|
+
* THIS chain, so `a -> b -> a` throws instead of looping forever — absolute,
|
|
7
|
+
* because two different bases can spell the same target differently and a
|
|
8
|
+
* relative comparison would miss the cycle.
|
|
10
9
|
*/
|
|
10
|
+
export declare function resolveRefInScope(ref: string, scope: RefScope): {
|
|
11
|
+
schema: Draft07Schema;
|
|
12
|
+
scope: RefScope;
|
|
13
|
+
};
|
|
14
|
+
/** The local-only door, kept for callers that have nothing but a root. */
|
|
11
15
|
export declare function resolveRef(ref: string, root: Draft07Schema): Draft07Schema;
|
|
12
16
|
/**
|
|
13
|
-
* True when `ref` resolves without leaving the
|
|
14
|
-
* cycle. It answers the question without making the caller
|
|
17
|
+
* True when `ref` resolves without leaving what the caller supplied and
|
|
18
|
+
* without entering a cycle. It answers the question without making the caller
|
|
19
|
+
* catch to find out.
|
|
15
20
|
*/
|
|
16
21
|
export declare function isResolvableRef(ref: string, root: Draft07Schema): boolean;
|
|
@@ -1,68 +1,93 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.resolveRefInScope = resolveRefInScope;
|
|
4
4
|
exports.resolveRef = resolveRef;
|
|
5
5
|
exports.isResolvableRef = isResolvableRef;
|
|
6
|
+
const ref_resolution_error_1 = require("./ref-resolution-error");
|
|
6
7
|
const draft07_types_1 = require("./draft07.types");
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.name = "RefResolutionError";
|
|
12
|
-
this.ref = ref;
|
|
13
|
-
// Without this, `instanceof` fails when the package is compiled to ES5.
|
|
14
|
-
Object.setPrototypeOf(this, RefResolutionError.prototype);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
exports.RefResolutionError = RefResolutionError;
|
|
8
|
+
const collect_definitions_1 = require("./collect-definitions");
|
|
9
|
+
const follow_json_pointer_1 = require("./follow-json-pointer");
|
|
10
|
+
const ref_scope_1 = require("./ref-scope");
|
|
11
|
+
const uri_reference_1 = require("./uri-reference");
|
|
18
12
|
/** No legitimate document chains this many `$ref`s. See resolveRef. */
|
|
19
13
|
const MAX_REF_HOPS = 1000;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
function followPointer(ref, fragment, scope) {
|
|
15
|
+
const walked = (0, follow_json_pointer_1.walkPointer)(fragment, scope.document, scope, (at, node) => (0, draft07_types_1.isDraft07Schema)(node) && (0, draft07_types_1.isSchemaObject)(node)
|
|
16
|
+
? (0, collect_definitions_1.advanceBase)(at, node)
|
|
17
|
+
: at, (token) => {
|
|
18
|
+
throw new ref_resolution_error_1.RefResolutionError(ref, `no schema at segment "${token}"`);
|
|
19
|
+
});
|
|
20
|
+
if (!(0, draft07_types_1.isDraft07Schema)(walked.node)) {
|
|
21
|
+
throw new ref_resolution_error_1.RefResolutionError(ref, "the target is not a schema");
|
|
22
|
+
}
|
|
23
|
+
return { schema: walked.node, scope: walked.scope };
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return [];
|
|
28
|
-
return pointer.split("/").slice(1).map(decodePointerToken);
|
|
25
|
+
/** True for `#name`: a plain-name fragment, which names rather than locates. */
|
|
26
|
+
function isPlainNameFragment(fragment) {
|
|
27
|
+
return fragment !== "" && !fragment.startsWith("/");
|
|
29
28
|
}
|
|
30
29
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* One hop. The returned scope's `document` is the RESOURCE the target was
|
|
31
|
+
* found in, never the target itself — otherwise `a -> b` would resolve `b`
|
|
32
|
+
* against the node `a` names, and a two-link chain of local pointers stops
|
|
33
|
+
* finding anything after the first hop.
|
|
35
34
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
function stepRef(ref, scope) {
|
|
36
|
+
const absolute = (0, uri_reference_1.resolveUriReference)(scope.baseUri, ref);
|
|
37
|
+
const { resource, fragment } = (0, uri_reference_1.splitUri)(absolute);
|
|
38
|
+
if (isPlainNameFragment(fragment)) {
|
|
39
|
+
const named = scope.registry.findIdentified(absolute);
|
|
40
|
+
if (named === undefined) {
|
|
41
|
+
throw new ref_resolution_error_1.RefResolutionError(ref, `no schema is named "${absolute}"`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
schema: named.schema,
|
|
45
|
+
scope: {
|
|
46
|
+
registry: scope.registry,
|
|
47
|
+
document: named.document,
|
|
48
|
+
baseUri: named.baseUri,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
43
51
|
}
|
|
44
|
-
|
|
52
|
+
const target = resolveResource(ref, resource, scope);
|
|
53
|
+
// The RESOURCE's retrieval URI wins over any `$id` the document carries —
|
|
54
|
+
// "retrieved nested refs resolve relative to their URI not $id" in the
|
|
55
|
+
// suite is exactly that. Every `$id` INSIDE it still counts, and
|
|
56
|
+
// followPointer collects them as it walks.
|
|
57
|
+
const landed = {
|
|
58
|
+
registry: scope.registry,
|
|
59
|
+
document: target.document,
|
|
60
|
+
baseUri: target.baseUri,
|
|
61
|
+
};
|
|
62
|
+
return followPointer(ref, fragment, landed);
|
|
45
63
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
64
|
+
/**
|
|
65
|
+
* The DOCUMENT a resource URI names. An empty resource is "the document this
|
|
66
|
+
* reference was written in", which is what makes `#/definitions/x` local.
|
|
67
|
+
*/
|
|
68
|
+
function resolveResource(ref, resource, scope) {
|
|
69
|
+
if (resource === "" || resource === scope.baseUri) {
|
|
70
|
+
return { document: scope.document, baseUri: scope.baseUri };
|
|
53
71
|
}
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
const identified = scope.registry.findIdentified(resource);
|
|
73
|
+
if (identified === undefined || !(0, draft07_types_1.isSchemaObject)(identified.document)) {
|
|
74
|
+
// The one place an external reference is refused, and it is refused for
|
|
75
|
+
// ONE reason: nobody handed Luq that document. Luq does not go and get it.
|
|
76
|
+
throw new ref_resolution_error_1.RefResolutionError(ref, `"${resource}" was not supplied. Luq never fetches a schema; pass it ` +
|
|
77
|
+
"in externalDocuments.");
|
|
56
78
|
}
|
|
57
|
-
return
|
|
79
|
+
return { document: identified.document, baseUri: identified.baseUri };
|
|
58
80
|
}
|
|
59
81
|
/**
|
|
60
|
-
* Resolves `ref`
|
|
61
|
-
*
|
|
62
|
-
* THIS chain, so `a -> b -> a` throws instead of looping forever
|
|
82
|
+
* Resolves `ref` in `scope`, then keeps following `$ref` until it reaches a
|
|
83
|
+
* schema that is not one. `visited` holds the ABSOLUTE URIs already entered on
|
|
84
|
+
* THIS chain, so `a -> b -> a` throws instead of looping forever — absolute,
|
|
85
|
+
* because two different bases can spell the same target differently and a
|
|
86
|
+
* relative comparison would miss the cycle.
|
|
63
87
|
*/
|
|
64
|
-
function
|
|
88
|
+
function resolveRefInScope(ref, scope) {
|
|
65
89
|
const visited = new Set();
|
|
90
|
+
let current = scope;
|
|
66
91
|
let currentRef = ref;
|
|
67
92
|
for (let hop = 0;; hop += 1) {
|
|
68
93
|
// The visited set is the real cycle detector and gives the good message.
|
|
@@ -71,24 +96,29 @@ function resolveRef(ref, root) {
|
|
|
71
96
|
// still terminate. Measured: removing the visited check without this made
|
|
72
97
|
// the resolver hang the whole test process instead of failing it.
|
|
73
98
|
if (hop > MAX_REF_HOPS) {
|
|
74
|
-
throw new RefResolutionError(ref, `more than ${MAX_REF_HOPS} $ref hops`);
|
|
99
|
+
throw new ref_resolution_error_1.RefResolutionError(ref, `more than ${MAX_REF_HOPS} $ref hops`);
|
|
75
100
|
}
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
const absolute = (0, uri_reference_1.resolveUriReference)(current.baseUri, currentRef);
|
|
102
|
+
if (visited.has(absolute)) {
|
|
103
|
+
throw new ref_resolution_error_1.RefResolutionError(ref, `circular reference: ${[...visited, absolute].join(" -> ")}`);
|
|
78
104
|
}
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
visited.add(absolute);
|
|
106
|
+
const step = stepRef(currentRef, current);
|
|
107
|
+
current = step.scope;
|
|
108
|
+
if (!(0, draft07_types_1.isSchemaObject)(step.schema) || step.schema.$ref === undefined) {
|
|
109
|
+
return { schema: step.schema, scope: current };
|
|
81
110
|
}
|
|
82
|
-
|
|
83
|
-
const target = followPointer(currentRef, root);
|
|
84
|
-
if (!(0, draft07_types_1.isSchemaObject)(target) || target.$ref === undefined)
|
|
85
|
-
return target;
|
|
86
|
-
currentRef = target.$ref;
|
|
111
|
+
currentRef = step.schema.$ref;
|
|
87
112
|
}
|
|
88
113
|
}
|
|
114
|
+
/** The local-only door, kept for callers that have nothing but a root. */
|
|
115
|
+
function resolveRef(ref, root) {
|
|
116
|
+
return resolveRefInScope(ref, (0, ref_scope_1.createLocalScope)(root)).schema;
|
|
117
|
+
}
|
|
89
118
|
/**
|
|
90
|
-
* True when `ref` resolves without leaving the
|
|
91
|
-
* cycle. It answers the question without making the caller
|
|
119
|
+
* True when `ref` resolves without leaving what the caller supplied and
|
|
120
|
+
* without entering a cycle. It answers the question without making the caller
|
|
121
|
+
* catch to find out.
|
|
92
122
|
*/
|
|
93
123
|
function isResolvableRef(ref, root) {
|
|
94
124
|
try {
|
|
@@ -96,7 +126,7 @@ function isResolvableRef(ref, root) {
|
|
|
96
126
|
return true;
|
|
97
127
|
}
|
|
98
128
|
catch (error) {
|
|
99
|
-
if (error instanceof RefResolutionError)
|
|
129
|
+
if (error instanceof ref_resolution_error_1.RefResolutionError)
|
|
100
130
|
return false;
|
|
101
131
|
throw error;
|
|
102
132
|
}
|
|
@@ -1,62 +1,88 @@
|
|
|
1
|
+
import { RefResolutionError } from "./ref-resolution-error.mjs";
|
|
1
2
|
import { isDraft07Schema, isSchemaObject } from "./draft07.types.mjs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
this.name = "RefResolutionError";
|
|
7
|
-
this.ref = ref;
|
|
8
|
-
// Without this, `instanceof` fails when the package is compiled to ES5.
|
|
9
|
-
Object.setPrototypeOf(this, RefResolutionError.prototype);
|
|
10
|
-
}
|
|
11
|
-
}
|
|
3
|
+
import { advanceBase } from "./collect-definitions.mjs";
|
|
4
|
+
import { walkPointer } from "./follow-json-pointer.mjs";
|
|
5
|
+
import { createLocalScope } from "./ref-scope.mjs";
|
|
6
|
+
import { resolveUriReference, splitUri } from "./uri-reference.mjs";
|
|
12
7
|
/** No legitimate document chains this many `$ref`s. See resolveRef. */
|
|
13
8
|
const MAX_REF_HOPS = 1000;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
function followPointer(ref, fragment, scope) {
|
|
10
|
+
const walked = walkPointer(fragment, scope.document, scope, (at, node) => isDraft07Schema(node) && isSchemaObject(node)
|
|
11
|
+
? advanceBase(at, node)
|
|
12
|
+
: at, (token) => {
|
|
13
|
+
throw new RefResolutionError(ref, `no schema at segment "${token}"`);
|
|
14
|
+
});
|
|
15
|
+
if (!isDraft07Schema(walked.node)) {
|
|
16
|
+
throw new RefResolutionError(ref, "the target is not a schema");
|
|
17
|
+
}
|
|
18
|
+
return { schema: walked.node, scope: walked.scope };
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return [];
|
|
22
|
-
return pointer.split("/").slice(1).map(decodePointerToken);
|
|
20
|
+
/** True for `#name`: a plain-name fragment, which names rather than locates. */
|
|
21
|
+
function isPlainNameFragment(fragment) {
|
|
22
|
+
return fragment !== "" && !fragment.startsWith("/");
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* One hop. The returned scope's `document` is the RESOURCE the target was
|
|
26
|
+
* found in, never the target itself — otherwise `a -> b` would resolve `b`
|
|
27
|
+
* against the node `a` names, and a two-link chain of local pointers stops
|
|
28
|
+
* finding anything after the first hop.
|
|
29
29
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
function stepRef(ref, scope) {
|
|
31
|
+
const absolute = resolveUriReference(scope.baseUri, ref);
|
|
32
|
+
const { resource, fragment } = splitUri(absolute);
|
|
33
|
+
if (isPlainNameFragment(fragment)) {
|
|
34
|
+
const named = scope.registry.findIdentified(absolute);
|
|
35
|
+
if (named === undefined) {
|
|
36
|
+
throw new RefResolutionError(ref, `no schema is named "${absolute}"`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
schema: named.schema,
|
|
40
|
+
scope: {
|
|
41
|
+
registry: scope.registry,
|
|
42
|
+
document: named.document,
|
|
43
|
+
baseUri: named.baseUri,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
37
46
|
}
|
|
38
|
-
|
|
47
|
+
const target = resolveResource(ref, resource, scope);
|
|
48
|
+
// The RESOURCE's retrieval URI wins over any `$id` the document carries —
|
|
49
|
+
// "retrieved nested refs resolve relative to their URI not $id" in the
|
|
50
|
+
// suite is exactly that. Every `$id` INSIDE it still counts, and
|
|
51
|
+
// followPointer collects them as it walks.
|
|
52
|
+
const landed = {
|
|
53
|
+
registry: scope.registry,
|
|
54
|
+
document: target.document,
|
|
55
|
+
baseUri: target.baseUri,
|
|
56
|
+
};
|
|
57
|
+
return followPointer(ref, fragment, landed);
|
|
39
58
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
59
|
+
/**
|
|
60
|
+
* The DOCUMENT a resource URI names. An empty resource is "the document this
|
|
61
|
+
* reference was written in", which is what makes `#/definitions/x` local.
|
|
62
|
+
*/
|
|
63
|
+
function resolveResource(ref, resource, scope) {
|
|
64
|
+
if (resource === "" || resource === scope.baseUri) {
|
|
65
|
+
return { document: scope.document, baseUri: scope.baseUri };
|
|
47
66
|
}
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
const identified = scope.registry.findIdentified(resource);
|
|
68
|
+
if (identified === undefined || !isSchemaObject(identified.document)) {
|
|
69
|
+
// The one place an external reference is refused, and it is refused for
|
|
70
|
+
// ONE reason: nobody handed Luq that document. Luq does not go and get it.
|
|
71
|
+
throw new RefResolutionError(ref, `"${resource}" was not supplied. Luq never fetches a schema; pass it ` +
|
|
72
|
+
"in externalDocuments.");
|
|
50
73
|
}
|
|
51
|
-
return
|
|
74
|
+
return { document: identified.document, baseUri: identified.baseUri };
|
|
52
75
|
}
|
|
53
76
|
/**
|
|
54
|
-
* Resolves `ref`
|
|
55
|
-
*
|
|
56
|
-
* THIS chain, so `a -> b -> a` throws instead of looping forever
|
|
77
|
+
* Resolves `ref` in `scope`, then keeps following `$ref` until it reaches a
|
|
78
|
+
* schema that is not one. `visited` holds the ABSOLUTE URIs already entered on
|
|
79
|
+
* THIS chain, so `a -> b -> a` throws instead of looping forever — absolute,
|
|
80
|
+
* because two different bases can spell the same target differently and a
|
|
81
|
+
* relative comparison would miss the cycle.
|
|
57
82
|
*/
|
|
58
|
-
export function
|
|
83
|
+
export function resolveRefInScope(ref, scope) {
|
|
59
84
|
const visited = new Set();
|
|
85
|
+
let current = scope;
|
|
60
86
|
let currentRef = ref;
|
|
61
87
|
for (let hop = 0;; hop += 1) {
|
|
62
88
|
// The visited set is the real cycle detector and gives the good message.
|
|
@@ -67,22 +93,27 @@ export function resolveRef(ref, root) {
|
|
|
67
93
|
if (hop > MAX_REF_HOPS) {
|
|
68
94
|
throw new RefResolutionError(ref, `more than ${MAX_REF_HOPS} $ref hops`);
|
|
69
95
|
}
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
const absolute = resolveUriReference(current.baseUri, currentRef);
|
|
97
|
+
if (visited.has(absolute)) {
|
|
98
|
+
throw new RefResolutionError(ref, `circular reference: ${[...visited, absolute].join(" -> ")}`);
|
|
72
99
|
}
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
visited.add(absolute);
|
|
101
|
+
const step = stepRef(currentRef, current);
|
|
102
|
+
current = step.scope;
|
|
103
|
+
if (!isSchemaObject(step.schema) || step.schema.$ref === undefined) {
|
|
104
|
+
return { schema: step.schema, scope: current };
|
|
75
105
|
}
|
|
76
|
-
|
|
77
|
-
const target = followPointer(currentRef, root);
|
|
78
|
-
if (!isSchemaObject(target) || target.$ref === undefined)
|
|
79
|
-
return target;
|
|
80
|
-
currentRef = target.$ref;
|
|
106
|
+
currentRef = step.schema.$ref;
|
|
81
107
|
}
|
|
82
108
|
}
|
|
109
|
+
/** The local-only door, kept for callers that have nothing but a root. */
|
|
110
|
+
export function resolveRef(ref, root) {
|
|
111
|
+
return resolveRefInScope(ref, createLocalScope(root)).schema;
|
|
112
|
+
}
|
|
83
113
|
/**
|
|
84
|
-
* True when `ref` resolves without leaving the
|
|
85
|
-
* cycle. It answers the question without making the caller
|
|
114
|
+
* True when `ref` resolves without leaving what the caller supplied and
|
|
115
|
+
* without entering a cycle. It answers the question without making the caller
|
|
116
|
+
* catch to find out.
|
|
86
117
|
*/
|
|
87
118
|
export function isResolvableRef(ref, root) {
|
|
88
119
|
try {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Draft07Schema } from "./draft07.types";
|
|
2
|
+
/**
|
|
3
|
+
* A node the index names, with the base URI in force inside it and the
|
|
4
|
+
* DOCUMENT that base belongs to.
|
|
5
|
+
*
|
|
6
|
+
* The document is not always the node. `$id: "#name"` names a sub-schema
|
|
7
|
+
* without starting a new resource, so a `#/definitions/x` written next to it
|
|
8
|
+
* still has to resolve against the enclosing document — resolving it against
|
|
9
|
+
* the anchored sub-schema instead is how a chain of refs silently stops
|
|
10
|
+
* finding anything after the first hop.
|
|
11
|
+
*/
|
|
12
|
+
export interface RegisteredTarget {
|
|
13
|
+
readonly schema: Draft07Schema;
|
|
14
|
+
readonly baseUri: string;
|
|
15
|
+
readonly document: Draft07Schema;
|
|
16
|
+
}
|
|
17
|
+
export interface SchemaRegistry {
|
|
18
|
+
/** The node this absolute URI NAMES — a document `$id`, or an anchor. */
|
|
19
|
+
findIdentified(uri: string): RegisteredTarget | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Every `$id` in `root` and in each supplied external document.
|
|
23
|
+
*
|
|
24
|
+
* The external map's KEY is a URI in its own right: the suite (and real
|
|
25
|
+
* callers) fetch `http://host/a.json` into a document that does not repeat its
|
|
26
|
+
* own `$id`, so the key is what a `$ref` will name. A document that DOES carry
|
|
27
|
+
* `$id` registers that too, and the `$id` wins where they disagree — it is the
|
|
28
|
+
* document's own statement about its identity.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createSchemaRegistry(root: Draft07Schema, externalDocuments?: Readonly<Record<string, unknown>>): SchemaRegistry;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSchemaRegistry = createSchemaRegistry;
|
|
4
|
+
// ===========================================================================
|
|
5
|
+
// L8 src/json-schema/schema-registry.ts — どの URI がどのスキーマを指すか。
|
|
6
|
+
//
|
|
7
|
+
// Draft-07 §8.2 では `$id` が二つの働きをする: 場所を示す URI はベースを
|
|
8
|
+
// 立て直し、`#name` の形は**位置に依らない名前**を付ける。どちらも
|
|
9
|
+
// 「この URI はこのノード」という索引で、それがこのファイルの責務である。
|
|
10
|
+
// ポインタ (`#/definitions/x`) を辿るのは場所の話なので resolve-ref.ts が持つ。
|
|
11
|
+
//
|
|
12
|
+
// 外部文書は**呼び出し側が渡したものだけ**である。Luq は取りに行かない:
|
|
13
|
+
// 関数ではなく地図 (`externalDocuments`) を受けるのは、そうすれば
|
|
14
|
+
// * スキーマに書かれた URI でプロセスがソケットを開くことがない (SSRF)、
|
|
15
|
+
// * 変換が同期のままで、CSP でも動く、
|
|
16
|
+
// * 何が読まれうるかが呼び出し側のコードに全部書いてある、
|
|
17
|
+
// の三つが同時に成り立つからで、非同期ローダーではどれも失われる。
|
|
18
|
+
// ===========================================================================
|
|
19
|
+
const draft07_types_1 = require("./draft07.types");
|
|
20
|
+
const types_1 = require("../types");
|
|
21
|
+
const uri_reference_1 = require("./uri-reference");
|
|
22
|
+
/** Keywords whose value is ONE schema. */
|
|
23
|
+
const SCHEMA_VALUED = Object.freeze([
|
|
24
|
+
"additionalProperties",
|
|
25
|
+
"additionalItems",
|
|
26
|
+
"contains",
|
|
27
|
+
"propertyNames",
|
|
28
|
+
"not",
|
|
29
|
+
"if",
|
|
30
|
+
"then",
|
|
31
|
+
"else",
|
|
32
|
+
]);
|
|
33
|
+
/** Keywords whose value is an ARRAY of schemas. `items` is in both lists. */
|
|
34
|
+
const SCHEMA_LIST_VALUED = Object.freeze([
|
|
35
|
+
"allOf",
|
|
36
|
+
"anyOf",
|
|
37
|
+
"oneOf",
|
|
38
|
+
]);
|
|
39
|
+
/** Keywords whose value is a MAP of name to schema. */
|
|
40
|
+
const SCHEMA_MAP_VALUED = Object.freeze([
|
|
41
|
+
"properties",
|
|
42
|
+
"patternProperties",
|
|
43
|
+
"definitions",
|
|
44
|
+
"$defs",
|
|
45
|
+
"dependencies",
|
|
46
|
+
]);
|
|
47
|
+
function walkSchema(schema, base, document, register) {
|
|
48
|
+
if (!(0, draft07_types_1.isSchemaObject)(schema))
|
|
49
|
+
return;
|
|
50
|
+
const id = typeof schema.$id === "string" ? schema.$id : undefined;
|
|
51
|
+
const here = (0, uri_reference_1.nextBaseUri)(base, id);
|
|
52
|
+
// A base-setting `$id` starts a new resource, so the node becomes the
|
|
53
|
+
// document every relative pointer under it resolves against.
|
|
54
|
+
const startsResource = id !== undefined && here !== base;
|
|
55
|
+
const inner = startsResource ? schema : document;
|
|
56
|
+
if (startsResource) {
|
|
57
|
+
register((0, uri_reference_1.normalizeUri)(here), { schema, baseUri: here, document: schema });
|
|
58
|
+
}
|
|
59
|
+
const anchor = (0, uri_reference_1.readAnchor)(id);
|
|
60
|
+
if (anchor !== undefined) {
|
|
61
|
+
register((0, uri_reference_1.normalizeUri)(`${here}#${anchor}`), {
|
|
62
|
+
schema,
|
|
63
|
+
baseUri: here,
|
|
64
|
+
document: inner,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
walkMembers(schema, here, inner, register);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read by ENTRY rather than by indexing the typed node: Draft07SchemaObject
|
|
71
|
+
* declares its keywords and carries no index signature, and reaching a keyword
|
|
72
|
+
* by name would need an assertion — src/core/type-erasure.ts is the only place
|
|
73
|
+
* permitted to write one, and this is not it.
|
|
74
|
+
*/
|
|
75
|
+
function walkMembers(schema, base, document, register) {
|
|
76
|
+
for (const [keyword, value] of Object.entries(schema)) {
|
|
77
|
+
if (keyword === "items") {
|
|
78
|
+
// The one keyword with both shapes, so it is walked as both.
|
|
79
|
+
walkIfSchema(value, base, document, register);
|
|
80
|
+
walkList(value, base, document, register);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (SCHEMA_VALUED.includes(keyword)) {
|
|
84
|
+
walkIfSchema(value, base, document, register);
|
|
85
|
+
}
|
|
86
|
+
else if (SCHEMA_LIST_VALUED.includes(keyword)) {
|
|
87
|
+
walkList(value, base, document, register);
|
|
88
|
+
}
|
|
89
|
+
else if (SCHEMA_MAP_VALUED.includes(keyword)) {
|
|
90
|
+
walkMap(value, base, document, register);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function walkIfSchema(value, base, document, register) {
|
|
95
|
+
if (!(0, draft07_types_1.isDraft07Schema)(value))
|
|
96
|
+
return;
|
|
97
|
+
walkSchema(value, base, document, register);
|
|
98
|
+
}
|
|
99
|
+
function walkList(value, base, document, register) {
|
|
100
|
+
if (!(0, types_1.isArray)(value))
|
|
101
|
+
return;
|
|
102
|
+
for (const member of value)
|
|
103
|
+
walkIfSchema(member, base, document, register);
|
|
104
|
+
}
|
|
105
|
+
function walkMap(value, base, document, register) {
|
|
106
|
+
if (!(0, types_1.isPlainObject)(value))
|
|
107
|
+
return;
|
|
108
|
+
for (const member of Object.values(value)) {
|
|
109
|
+
walkIfSchema(member, base, document, register);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Every `$id` in `root` and in each supplied external document.
|
|
114
|
+
*
|
|
115
|
+
* The external map's KEY is a URI in its own right: the suite (and real
|
|
116
|
+
* callers) fetch `http://host/a.json` into a document that does not repeat its
|
|
117
|
+
* own `$id`, so the key is what a `$ref` will name. A document that DOES carry
|
|
118
|
+
* `$id` registers that too, and the `$id` wins where they disagree — it is the
|
|
119
|
+
* document's own statement about its identity.
|
|
120
|
+
*/
|
|
121
|
+
function createSchemaRegistry(root, externalDocuments = {}) {
|
|
122
|
+
const byUri = new Map();
|
|
123
|
+
const register = (uri, target) => {
|
|
124
|
+
if (!byUri.has(uri))
|
|
125
|
+
byUri.set(uri, target);
|
|
126
|
+
};
|
|
127
|
+
for (const [uri, document] of Object.entries(externalDocuments)) {
|
|
128
|
+
if (!(0, draft07_types_1.isDraft07Schema)(document))
|
|
129
|
+
continue;
|
|
130
|
+
const key = (0, uri_reference_1.normalizeUri)(uri);
|
|
131
|
+
register(key, { schema: document, baseUri: key, document });
|
|
132
|
+
}
|
|
133
|
+
for (const [uri, document] of Object.entries(externalDocuments)) {
|
|
134
|
+
if (!(0, draft07_types_1.isDraft07Schema)(document))
|
|
135
|
+
continue;
|
|
136
|
+
walkSchema(document, (0, uri_reference_1.normalizeUri)(uri), document, register);
|
|
137
|
+
}
|
|
138
|
+
walkSchema(root, "", root, register);
|
|
139
|
+
return {
|
|
140
|
+
findIdentified: (uri) => byUri.get((0, uri_reference_1.normalizeUri)(uri)),
|
|
141
|
+
};
|
|
142
|
+
}
|