@maroonedog/luq 2.1.0 → 2.3.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 +92 -430
- package/dist/builder/builder-surface.types.d.ts +1 -0
- package/dist/builder/compile-declarations.d.ts +7 -1
- package/dist/builder/compile-declarations.js +18 -8
- package/dist/builder/compile-declarations.mjs +18 -8
- package/dist/builder/create-builder.js +8 -0
- package/dist/builder/create-builder.mjs +8 -0
- package/dist/builder/create-field-builder.js +13 -1
- package/dist/builder/create-field-builder.mjs +13 -1
- package/dist/builder/declared-calls-store.d.ts +9 -0
- package/dist/builder/declared-calls-store.js +19 -0
- package/dist/builder/declared-calls-store.mjs +15 -0
- package/dist/builder/field-builder.types.d.ts +13 -0
- package/dist/builder/field-declared-calls.types.d.ts +6 -0
- package/dist/builder/field-declared-calls.types.js +2 -0
- package/dist/builder/field-declared-calls.types.mjs +1 -0
- package/dist/builder/field-entry.types.d.ts +9 -3
- package/dist/builder/field-options.types.d.ts +26 -0
- package/dist/chain/bundle-paths.types.d.ts +23 -0
- package/dist/chain/bundle-paths.types.js +2 -0
- package/dist/chain/bundle-paths.types.mjs +1 -0
- package/dist/chain/chain-method.types.d.ts +8 -3
- package/dist/chain/chain-node-store.d.ts +5 -0
- package/dist/chain/chain-node-store.js +15 -0
- package/dist/chain/chain-node-store.mjs +11 -0
- package/dist/chain/collect-field-rules.d.ts +13 -2
- package/dist/chain/collect-field-rules.js +10 -3
- package/dist/chain/collect-field-rules.mjs +10 -3
- package/dist/chain/create-chain-node.js +19 -9
- package/dist/chain/create-chain-node.mjs +19 -9
- package/dist/chain/declaration-recorder.port.d.ts +31 -0
- package/dist/chain/declaration-recorder.port.js +17 -0
- package/dist/chain/declaration-recorder.port.mjs +13 -0
- package/dist/chain/declared-call.types.d.ts +15 -0
- package/dist/chain/declared-call.types.js +2 -0
- package/dist/chain/declared-call.types.mjs +1 -0
- package/dist/chain/index.d.ts +3 -1
- package/dist/chain/resolve-args.types.d.ts +4 -2
- package/dist/compile/compile-array-node.d.ts +0 -7
- package/dist/compile/compile-array-node.js +5 -0
- package/dist/compile/compile-array-node.mjs +5 -0
- package/dist/compile/compile-field.d.ts +1 -0
- package/dist/compile/compile-field.js +13 -2
- package/dist/compile/compile-field.mjs +13 -2
- package/dist/compile/compile-schema.js +4 -0
- package/dist/compile/compile-schema.mjs +4 -0
- package/dist/compile/group-array-fields.d.ts +1 -0
- package/dist/compile/split-rules-by-kind.js +27 -7
- package/dist/compile/split-rules-by-kind.mjs +27 -7
- package/dist/compile/validation-plan.types.d.ts +36 -0
- package/dist/core/type-erasure.d.ts +36 -30
- package/dist/core/type-erasure.js +36 -30
- package/dist/core/type-erasure.mjs +36 -30
- package/dist/json-schema/build-from-schema.js +8 -1
- package/dist/json-schema/build-from-schema.mjs +8 -1
- package/dist/json-schema/declare-additional-properties.d.ts +7 -7
- package/dist/json-schema/declare-additional-properties.js +7 -7
- package/dist/json-schema/declare-additional-properties.mjs +7 -7
- package/dist/json-schema/declare-object-keywords.js +4 -4
- package/dist/json-schema/declare-object-keywords.mjs +4 -4
- package/dist/json-schema/flatten-schema.js +1 -1
- package/dist/json-schema/flatten-schema.mjs +1 -1
- package/dist/json-schema/follow-json-pointer.d.ts +7 -6
- package/dist/json-schema/follow-json-pointer.js +24 -24
- package/dist/json-schema/follow-json-pointer.mjs +24 -24
- package/dist/json-schema/ref-resolution-error.js +3 -3
- package/dist/json-schema/ref-resolution-error.mjs +3 -3
- package/dist/json-schema/schema-registry.js +12 -11
- package/dist/json-schema/schema-registry.mjs +12 -11
- package/dist/json-schema/uri-reference.js +12 -12
- package/dist/json-schema/uri-reference.mjs +12 -12
- package/dist/path/create-value-writer.js +12 -12
- package/dist/path/create-value-writer.mjs +12 -12
- package/dist/path/reserved-segment.d.ts +16 -16
- package/dist/path/reserved-segment.js +17 -21
- package/dist/path/reserved-segment.mjs +17 -21
- package/dist/plugin-kit/marker.types.d.ts +18 -0
- package/dist/plugins/index.generated.d.ts +1 -0
- package/dist/plugins/index.generated.js +6 -4
- package/dist/plugins/index.generated.mjs +3 -2
- package/dist/plugins/manifest.generated.js +3 -2
- package/dist/plugins/manifest.generated.mjs +3 -2
- package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +8 -8
- package/dist/plugins/object-additional-properties/select-additional-keys.js +16 -16
- package/dist/plugins/object-additional-properties/select-additional-keys.mjs +16 -16
- package/dist/plugins/stitch/stitch.d.ts +24 -8
- package/dist/plugins/stitch-with/index.d.ts +2 -0
- package/dist/plugins/stitch-with/index.js +5 -0
- package/dist/plugins/stitch-with/index.mjs +1 -0
- package/dist/plugins/stitch-with/stitch-with.d.ts +12 -0
- package/dist/plugins/stitch-with/stitch-with.js +92 -0
- package/dist/plugins/stitch-with/stitch-with.mjs +89 -0
- package/dist/plugins/stitchWith.d.ts +1 -0
- package/dist/plugins/stitchWith.js +2 -0
- package/dist/plugins/stitchWith.mjs +1 -0
- package/dist/plugins/string-min/string-min.js +19 -3
- package/dist/plugins/string-min/string-min.mjs +20 -4
- package/dist/presets/index.d.ts +1 -0
- package/dist/presets/index.js +9 -0
- package/dist/presets/index.mjs +1 -0
- package/dist/presets/presets.d.ts +164 -0
- package/dist/presets/presets.js +79 -0
- package/dist/presets/presets.mjs +76 -0
- package/dist/runtime/create-field-validator.js +22 -8
- package/dist/runtime/create-field-validator.mjs +22 -8
- package/dist/runtime/create-validator.js +38 -8
- package/dist/runtime/create-validator.mjs +38 -8
- package/dist/runtime/field-rule-context.d.ts +32 -0
- package/dist/runtime/field-rule-context.js +48 -0
- package/dist/runtime/field-rule-context.mjs +44 -0
- package/dist/runtime/index-stack.d.ts +29 -4
- package/dist/runtime/index-stack.js +76 -15
- package/dist/runtime/index-stack.mjs +76 -15
- package/dist/runtime/output-writer.js +5 -1
- package/dist/runtime/output-writer.mjs +5 -1
- package/dist/runtime/run-array-node.js +22 -10
- package/dist/runtime/run-array-node.mjs +22 -10
- package/dist/runtime/run-field.js +47 -18
- package/dist/runtime/run-field.mjs +47 -18
- package/dist/runtime/run-plan.js +5 -1
- package/dist/runtime/run-plan.mjs +5 -1
- package/dist/standard-schema/assemble-json-schema.d.ts +4 -0
- package/dist/standard-schema/assemble-json-schema.js +95 -0
- package/dist/standard-schema/assemble-json-schema.mjs +92 -0
- package/dist/standard-schema/declaration-recorder.d.ts +6 -0
- package/dist/standard-schema/declaration-recorder.js +30 -0
- package/dist/standard-schema/declaration-recorder.mjs +27 -0
- package/dist/standard-schema/declarations-unavailable-error.d.ts +4 -0
- package/dist/standard-schema/declarations-unavailable-error.js +32 -0
- package/dist/standard-schema/declarations-unavailable-error.mjs +28 -0
- package/dist/standard-schema/emit-field-schema.d.ts +9 -0
- package/dist/standard-schema/emit-field-schema.js +68 -0
- package/dist/standard-schema/emit-field-schema.mjs +65 -0
- package/dist/standard-schema/index.d.ts +5 -0
- package/dist/standard-schema/index.js +9 -1
- package/dist/standard-schema/index.mjs +4 -0
- package/dist/standard-schema/json-schema-target.d.ts +6 -0
- package/dist/standard-schema/json-schema-target.js +44 -0
- package/dist/standard-schema/json-schema-target.mjs +39 -0
- package/dist/standard-schema/plugin-keyword-map.d.ts +3 -0
- package/dist/standard-schema/plugin-keyword-map.js +93 -0
- package/dist/standard-schema/plugin-keyword-map.mjs +90 -0
- package/dist/standard-schema/split-issue-path.d.ts +6 -4
- package/dist/standard-schema/split-issue-path.js +15 -13
- package/dist/standard-schema/split-issue-path.mjs +15 -13
- package/dist/standard-schema/standard-schema.types.d.ts +8 -7
- package/dist/standard-schema/standard-schema.types.js +6 -6
- package/dist/standard-schema/standard-schema.types.mjs +6 -6
- package/dist/standard-schema/to-standard-json-schema.d.ts +19 -0
- package/dist/standard-schema/to-standard-json-schema.js +36 -0
- package/dist/standard-schema/to-standard-json-schema.mjs +33 -0
- package/dist/standard-schema/to-standard-schema.d.ts +16 -15
- package/dist/standard-schema/to-standard-schema.js +15 -22
- package/dist/standard-schema/to-standard-schema.mjs +15 -22
- package/dist/standard-schema/unrepresentable-rule-error.d.ts +15 -0
- package/dist/standard-schema/unrepresentable-rule-error.js +43 -0
- package/dist/standard-schema/unrepresentable-rule-error.mjs +38 -0
- package/dist/types/index.d.ts +19 -7
- package/dist/types/index.js +7 -7
- package/dist/types/index.mjs +7 -7
- package/package.json +35 -19
|
@@ -53,15 +53,29 @@ function createSubjectComposer(template) {
|
|
|
53
53
|
const write = createValueWriter(template);
|
|
54
54
|
return (siblings, value) => write(siblings ?? {}, value);
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const nest = compileNester(template);
|
|
57
|
+
return (siblings, value) => mergeOntoSiblings(siblings, nest(value));
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Walks the template **once** and folds it into a wrapping function.
|
|
61
|
+
*
|
|
62
|
+
* Walking it per validation re-read each segment's kind every time, which is
|
|
63
|
+
* exactly the thing validation time must not do: what something IS was settled
|
|
64
|
+
* at compile time. The non-wildcard route was always shaped this way.
|
|
65
|
+
*/
|
|
66
|
+
function compileNester(template) {
|
|
67
|
+
let nest = (value) => value;
|
|
68
|
+
for (let index = template.length - 1; index >= 0; index -= 1) {
|
|
69
|
+
const segment = template[index];
|
|
70
|
+
if (segment === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
const inner = nest;
|
|
73
|
+
nest =
|
|
74
|
+
segment.kind === "each"
|
|
75
|
+
? (value) => [inner(value)]
|
|
76
|
+
: ((key) => (value) => ({ [key]: inner(value) }))(segment.key);
|
|
77
|
+
}
|
|
78
|
+
return nest;
|
|
65
79
|
}
|
|
66
80
|
/** The wrapper wins on the key it names; every other sibling survives. */
|
|
67
81
|
function mergeOntoSiblings(siblings, fragment) {
|
|
@@ -39,7 +39,34 @@ function rejectMissingRoot() {
|
|
|
39
39
|
function hasRejectingIssue(issues) {
|
|
40
40
|
return issues.some((issue) => issue.severity === "error");
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The shared do-nothing runner for a plan that cannot recurse.
|
|
44
|
+
*
|
|
45
|
+
* Building a real runner allocates a WeakSet and several closures. Doing that
|
|
46
|
+
* on every validate() of a plan with no recursive rule in it was most of the
|
|
47
|
+
* fixed per-call cost. Whether a plan can recurse is known at build time and
|
|
48
|
+
* is not a question to re-ask at validation time.
|
|
49
|
+
*
|
|
50
|
+
* Never actually called: a field only reaches its runner when it declares
|
|
51
|
+
* recursion, and a plan containing one gets the real runner instead.
|
|
52
|
+
*/
|
|
53
|
+
const NO_RECURSION = () => { };
|
|
54
|
+
/**
|
|
55
|
+
* Whether the root needs a real runner.
|
|
56
|
+
*
|
|
57
|
+
* Only the fields directly under the root and the elements of array nodes
|
|
58
|
+
* matter. A composite branch makes its own runner for the nested plan, so it
|
|
59
|
+
* never uses the one passed from here.
|
|
60
|
+
*/
|
|
61
|
+
function planCanRecurse(plan) {
|
|
62
|
+
return (plan.fields.some((field) => field.recursion !== null) ||
|
|
63
|
+
plan.arrays.some(nodeCanRecurse));
|
|
64
|
+
}
|
|
65
|
+
function nodeCanRecurse(node) {
|
|
66
|
+
return (node.elementFields.some((field) => field.recursion !== null) ||
|
|
67
|
+
node.nested.some(nodeCanRecurse));
|
|
68
|
+
}
|
|
69
|
+
function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
|
|
43
70
|
if (value === null || value === undefined)
|
|
44
71
|
return rejectMissingRoot();
|
|
45
72
|
const sink = new issue_sink_1.IssueSink((0, issue_sink_1.resolveAbortPolicy)(options));
|
|
@@ -48,11 +75,13 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
|
|
|
48
75
|
sink,
|
|
49
76
|
indices: new index_stack_1.IndexStack(),
|
|
50
77
|
shouldApplyTransforms,
|
|
51
|
-
runRecursion:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
runRecursion: canRecurse
|
|
79
|
+
? (0, run_recursion_1.createRecursionRunner)({
|
|
80
|
+
root: value,
|
|
81
|
+
sink,
|
|
82
|
+
external: options?.external,
|
|
83
|
+
})
|
|
84
|
+
: NO_RECURSION,
|
|
56
85
|
external: options?.external,
|
|
57
86
|
}, targets);
|
|
58
87
|
const issues = Object.freeze(sink.issues);
|
|
@@ -63,8 +92,9 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
|
|
|
63
92
|
function createValidator(plan) {
|
|
64
93
|
const parseTargets = (0, output_writer_1.createPlanWriteTargets)(plan);
|
|
65
94
|
const shouldWriteOutput = parseTargets !== null;
|
|
95
|
+
const canRecurse = planCanRecurse(plan);
|
|
66
96
|
return {
|
|
67
|
-
validate: (value, options) => runRoot(plan, value, options, output_writer_1.NO_WRITE_TARGETS, false),
|
|
68
|
-
parse: (value, options) => runRoot(plan, value, options, parseTargets ?? output_writer_1.NO_WRITE_TARGETS, shouldWriteOutput),
|
|
97
|
+
validate: (value, options) => runRoot(plan, value, options, output_writer_1.NO_WRITE_TARGETS, false, canRecurse),
|
|
98
|
+
parse: (value, options) => runRoot(plan, value, options, parseTargets ?? output_writer_1.NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
|
|
69
99
|
};
|
|
70
100
|
}
|
|
@@ -34,7 +34,34 @@ function rejectMissingRoot() {
|
|
|
34
34
|
export function hasRejectingIssue(issues) {
|
|
35
35
|
return issues.some((issue) => issue.severity === "error");
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The shared do-nothing runner for a plan that cannot recurse.
|
|
39
|
+
*
|
|
40
|
+
* Building a real runner allocates a WeakSet and several closures. Doing that
|
|
41
|
+
* on every validate() of a plan with no recursive rule in it was most of the
|
|
42
|
+
* fixed per-call cost. Whether a plan can recurse is known at build time and
|
|
43
|
+
* is not a question to re-ask at validation time.
|
|
44
|
+
*
|
|
45
|
+
* Never actually called: a field only reaches its runner when it declares
|
|
46
|
+
* recursion, and a plan containing one gets the real runner instead.
|
|
47
|
+
*/
|
|
48
|
+
const NO_RECURSION = () => { };
|
|
49
|
+
/**
|
|
50
|
+
* Whether the root needs a real runner.
|
|
51
|
+
*
|
|
52
|
+
* Only the fields directly under the root and the elements of array nodes
|
|
53
|
+
* matter. A composite branch makes its own runner for the nested plan, so it
|
|
54
|
+
* never uses the one passed from here.
|
|
55
|
+
*/
|
|
56
|
+
function planCanRecurse(plan) {
|
|
57
|
+
return (plan.fields.some((field) => field.recursion !== null) ||
|
|
58
|
+
plan.arrays.some(nodeCanRecurse));
|
|
59
|
+
}
|
|
60
|
+
function nodeCanRecurse(node) {
|
|
61
|
+
return (node.elementFields.some((field) => field.recursion !== null) ||
|
|
62
|
+
node.nested.some(nodeCanRecurse));
|
|
63
|
+
}
|
|
64
|
+
function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
|
|
38
65
|
if (value === null || value === undefined)
|
|
39
66
|
return rejectMissingRoot();
|
|
40
67
|
const sink = new IssueSink(resolveAbortPolicy(options));
|
|
@@ -43,11 +70,13 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
|
|
|
43
70
|
sink,
|
|
44
71
|
indices: new IndexStack(),
|
|
45
72
|
shouldApplyTransforms,
|
|
46
|
-
runRecursion:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
runRecursion: canRecurse
|
|
74
|
+
? createRecursionRunner({
|
|
75
|
+
root: value,
|
|
76
|
+
sink,
|
|
77
|
+
external: options?.external,
|
|
78
|
+
})
|
|
79
|
+
: NO_RECURSION,
|
|
51
80
|
external: options?.external,
|
|
52
81
|
}, targets);
|
|
53
82
|
const issues = Object.freeze(sink.issues);
|
|
@@ -58,8 +87,9 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
|
|
|
58
87
|
export function createValidator(plan) {
|
|
59
88
|
const parseTargets = createPlanWriteTargets(plan);
|
|
60
89
|
const shouldWriteOutput = parseTargets !== null;
|
|
90
|
+
const canRecurse = planCanRecurse(plan);
|
|
61
91
|
return {
|
|
62
|
-
validate: (value, options) => runRoot(plan, value, options, NO_WRITE_TARGETS, false),
|
|
63
|
-
parse: (value, options) => runRoot(plan, value, options, parseTargets ?? NO_WRITE_TARGETS, shouldWriteOutput),
|
|
92
|
+
validate: (value, options) => runRoot(plan, value, options, NO_WRITE_TARGETS, false, canRecurse),
|
|
93
|
+
parse: (value, options) => runRoot(plan, value, options, parseTargets ?? NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
|
|
64
94
|
};
|
|
65
95
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ArrayItemContext, RuleContext } from "../types";
|
|
2
|
+
import type { IndexStack } from "./index-stack";
|
|
3
|
+
export declare class FieldRuleContext implements RuleContext {
|
|
4
|
+
readonly root: unknown;
|
|
5
|
+
private readonly indices;
|
|
6
|
+
/** Built at compile time, relative to the subject this field reads from. */
|
|
7
|
+
private readonly ownPath;
|
|
8
|
+
readonly item: ArrayItemContext | undefined;
|
|
9
|
+
readonly external: Readonly<Record<string, unknown>> | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The path once something has asked for it. `null` rather than `""` because
|
|
12
|
+
* the empty string is a real path — the root.
|
|
13
|
+
*
|
|
14
|
+
* It is remembered because the index stack underneath is MUTABLE and is
|
|
15
|
+
* popped when the element ends. A rule that kept its context and read `path`
|
|
16
|
+
* afterwards would otherwise see wherever the runner had moved on to.
|
|
17
|
+
* Reading it during the rule's own call — which is what every rule does —
|
|
18
|
+
* pins it.
|
|
19
|
+
*/
|
|
20
|
+
private rendered;
|
|
21
|
+
constructor(root: unknown, indices: IndexStack,
|
|
22
|
+
/** Built at compile time, relative to the subject this field reads from. */
|
|
23
|
+
ownPath: string, item: ArrayItemContext | undefined, external: Readonly<Record<string, unknown>> | undefined);
|
|
24
|
+
get path(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Serialising a context is a thing people do when they log a failure, and a
|
|
27
|
+
* prototype getter would silently drop `path` from the output. This pins the
|
|
28
|
+
* JSON shape to the four DECLARED members and, as a side effect, stops the
|
|
29
|
+
* internals above from reaching a log line.
|
|
30
|
+
*/
|
|
31
|
+
toJSON(): Record<string, unknown>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FieldRuleContext = void 0;
|
|
4
|
+
class FieldRuleContext {
|
|
5
|
+
constructor(root, indices,
|
|
6
|
+
/** Built at compile time, relative to the subject this field reads from. */
|
|
7
|
+
ownPath, item, external) {
|
|
8
|
+
this.root = root;
|
|
9
|
+
this.indices = indices;
|
|
10
|
+
this.ownPath = ownPath;
|
|
11
|
+
this.item = item;
|
|
12
|
+
this.external = external;
|
|
13
|
+
/**
|
|
14
|
+
* The path once something has asked for it. `null` rather than `""` because
|
|
15
|
+
* the empty string is a real path — the root.
|
|
16
|
+
*
|
|
17
|
+
* It is remembered because the index stack underneath is MUTABLE and is
|
|
18
|
+
* popped when the element ends. A rule that kept its context and read `path`
|
|
19
|
+
* afterwards would otherwise see wherever the runner had moved on to.
|
|
20
|
+
* Reading it during the rule's own call — which is what every rule does —
|
|
21
|
+
* pins it.
|
|
22
|
+
*/
|
|
23
|
+
this.rendered = null;
|
|
24
|
+
}
|
|
25
|
+
get path() {
|
|
26
|
+
const already = this.rendered;
|
|
27
|
+
if (already !== null)
|
|
28
|
+
return already;
|
|
29
|
+
const built = this.indices.renderFieldPath(this.ownPath);
|
|
30
|
+
this.rendered = built;
|
|
31
|
+
return built;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Serialising a context is a thing people do when they log a failure, and a
|
|
35
|
+
* prototype getter would silently drop `path` from the output. This pins the
|
|
36
|
+
* JSON shape to the four DECLARED members and, as a side effect, stops the
|
|
37
|
+
* internals above from reaching a log line.
|
|
38
|
+
*/
|
|
39
|
+
toJSON() {
|
|
40
|
+
return {
|
|
41
|
+
root: this.root,
|
|
42
|
+
path: this.path,
|
|
43
|
+
item: this.item,
|
|
44
|
+
external: this.external,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.FieldRuleContext = FieldRuleContext;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class FieldRuleContext {
|
|
2
|
+
constructor(root, indices,
|
|
3
|
+
/** Built at compile time, relative to the subject this field reads from. */
|
|
4
|
+
ownPath, item, external) {
|
|
5
|
+
this.root = root;
|
|
6
|
+
this.indices = indices;
|
|
7
|
+
this.ownPath = ownPath;
|
|
8
|
+
this.item = item;
|
|
9
|
+
this.external = external;
|
|
10
|
+
/**
|
|
11
|
+
* The path once something has asked for it. `null` rather than `""` because
|
|
12
|
+
* the empty string is a real path — the root.
|
|
13
|
+
*
|
|
14
|
+
* It is remembered because the index stack underneath is MUTABLE and is
|
|
15
|
+
* popped when the element ends. A rule that kept its context and read `path`
|
|
16
|
+
* afterwards would otherwise see wherever the runner had moved on to.
|
|
17
|
+
* Reading it during the rule's own call — which is what every rule does —
|
|
18
|
+
* pins it.
|
|
19
|
+
*/
|
|
20
|
+
this.rendered = null;
|
|
21
|
+
}
|
|
22
|
+
get path() {
|
|
23
|
+
const already = this.rendered;
|
|
24
|
+
if (already !== null)
|
|
25
|
+
return already;
|
|
26
|
+
const built = this.indices.renderFieldPath(this.ownPath);
|
|
27
|
+
this.rendered = built;
|
|
28
|
+
return built;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Serialising a context is a thing people do when they log a failure, and a
|
|
32
|
+
* prototype getter would silently drop `path` from the output. This pins the
|
|
33
|
+
* JSON shape to the four DECLARED members and, as a side effect, stops the
|
|
34
|
+
* internals above from reaching a log line.
|
|
35
|
+
*/
|
|
36
|
+
toJSON() {
|
|
37
|
+
return {
|
|
38
|
+
root: this.root,
|
|
39
|
+
path: this.path,
|
|
40
|
+
item: this.item,
|
|
41
|
+
external: this.external,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
import type { PathSegment } from "../path/path-segment.types";
|
|
2
1
|
export declare class IndexStack {
|
|
3
|
-
private readonly
|
|
2
|
+
private readonly nodePaths;
|
|
4
3
|
private readonly openIndices;
|
|
4
|
+
/**
|
|
5
|
+
* The rendered prefix, or `null` when it has not been asked for since the
|
|
6
|
+
* last push or pop. `null` and not `""` because the empty string is a real
|
|
7
|
+
* answer — the root.
|
|
8
|
+
*
|
|
9
|
+
* Invalidating rather than recomputing on push is the point: an element that
|
|
10
|
+
* raises no issue never renders its own prefix at all.
|
|
11
|
+
*/
|
|
12
|
+
private currentPrefix;
|
|
5
13
|
/** How many array levels are currently open. */
|
|
6
14
|
get depth(): number;
|
|
7
15
|
/** The open indices, outermost first: `[0, 2]` inside `grid[0][2]`. */
|
|
8
16
|
get indices(): readonly number[];
|
|
17
|
+
/**
|
|
18
|
+
* The prefix for the currently open frames — `""` when none are open,
|
|
19
|
+
* `items[0]` inside one, `grid[0][2]` inside two.
|
|
20
|
+
*
|
|
21
|
+
* Indexed, never `nodePaths[nodePaths.length - 1]`. That expression at
|
|
22
|
+
* depth 0 is `nodePaths[-1]`, which is not an element read at all: -1 is
|
|
23
|
+
* outside the array, so V8 falls back to a named-property lookup and walks
|
|
24
|
+
* the prototype chain. The `prefix` getter used to be written that way and
|
|
25
|
+
* it took 12% of the array shape's self time.
|
|
26
|
+
*/
|
|
27
|
+
private readTop;
|
|
9
28
|
/** `""` at the root, `items[0]` inside the first element of `items`. */
|
|
10
29
|
get prefix(): string;
|
|
11
30
|
/**
|
|
@@ -21,8 +40,14 @@ export declare class IndexStack {
|
|
|
21
40
|
push(nodePath: string, index: number): void;
|
|
22
41
|
/** An unbalanced pop means a runner lost track of its own nesting. */
|
|
23
42
|
pop(): void;
|
|
24
|
-
/**
|
|
25
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The issue path of a field read from the subject at the current level.
|
|
45
|
+
*
|
|
46
|
+
* `renderedPath` was built once, at compile time. This used to walk the
|
|
47
|
+
* template on every call, which for an array meant rebuilding the same
|
|
48
|
+
* string once per element.
|
|
49
|
+
*/
|
|
50
|
+
renderFieldPath(renderedPath: string): string;
|
|
26
51
|
}
|
|
27
52
|
/** `""` is the root path, so it never contributes a separator dot. */
|
|
28
53
|
export declare function joinIssuePath(prefix: string, own: string): string;
|
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// ===========================================================================
|
|
3
|
+
// L5 src/runtime/index-stack.ts
|
|
4
|
+
// Where the runtime currently is, expressed as a concrete issue path.
|
|
5
|
+
//
|
|
6
|
+
// An issue path must read `items[0].name`, never the declaration pattern
|
|
7
|
+
// `items[*].name`. The legacy tree carried indices two different ways — the
|
|
8
|
+
// nested-array processor threaded them through its own recursion while
|
|
9
|
+
// validator-factory fell back to `error.path || elementPath` — which is how
|
|
10
|
+
// the pattern string leaked into user-visible errors. There is one stack here
|
|
11
|
+
// and every path a runner reports comes out of it.
|
|
12
|
+
//
|
|
13
|
+
// A CompiledField's template is RELATIVE to the subject it reads from, so an
|
|
14
|
+
// element field of `items[*]` has the template `name` and could never render
|
|
15
|
+
// `items[0].name` on its own. The array runner pushes the enclosing node and
|
|
16
|
+
// the index; the stack renders the prefix.
|
|
17
|
+
//
|
|
18
|
+
// The prefix is rendered ON DEMAND and remembered until the next push or pop.
|
|
19
|
+
// Entering used to build `items[0]` eagerly, which is one string per element
|
|
20
|
+
// that nothing reads unless a rule fails — and now nothing reads it even then
|
|
21
|
+
// until FieldRuleContext is asked for its path. Entering therefore costs two
|
|
22
|
+
// pushes and one assignment, and the first field that asks pays the render.
|
|
23
|
+
//
|
|
24
|
+
// It is deliberately MUTABLE and pushed/popped around an element loop: one
|
|
25
|
+
// stack lives for one validate() call, and 10k elements must not allocate 10k
|
|
26
|
+
// index arrays. It is also why FieldRuleContext remembers the path it built —
|
|
27
|
+
// this stack has moved on by the time a retained context could read it.
|
|
28
|
+
// ===========================================================================
|
|
2
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
30
|
exports.IndexStack = void 0;
|
|
4
31
|
exports.joinIssuePath = joinIssuePath;
|
|
5
|
-
const format_issue_path_1 = require("../path/format-issue-path");
|
|
6
|
-
/**
|
|
7
|
-
* A compiled template never contains a wildcard (createValueReader rejects
|
|
8
|
-
* one), so rendering it needs no index. Passing none is what makes a stray
|
|
9
|
-
* wildcard template throw here instead of silently rendering a second copy of
|
|
10
|
-
* an index this stack has already written into the prefix.
|
|
11
|
-
*/
|
|
12
|
-
const NO_INDICES = Object.freeze([]);
|
|
13
32
|
class IndexStack {
|
|
14
33
|
constructor() {
|
|
15
|
-
this.
|
|
34
|
+
this.nodePaths = [];
|
|
16
35
|
this.openIndices = [];
|
|
36
|
+
/**
|
|
37
|
+
* The rendered prefix, or `null` when it has not been asked for since the
|
|
38
|
+
* last push or pop. `null` and not `""` because the empty string is a real
|
|
39
|
+
* answer — the root.
|
|
40
|
+
*
|
|
41
|
+
* Invalidating rather than recomputing on push is the point: an element that
|
|
42
|
+
* raises no issue never renders its own prefix at all.
|
|
43
|
+
*/
|
|
44
|
+
this.currentPrefix = null;
|
|
17
45
|
}
|
|
18
46
|
/** How many array levels are currently open. */
|
|
19
47
|
get depth() {
|
|
@@ -23,9 +51,34 @@ class IndexStack {
|
|
|
23
51
|
get indices() {
|
|
24
52
|
return this.openIndices;
|
|
25
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The prefix for the currently open frames — `""` when none are open,
|
|
56
|
+
* `items[0]` inside one, `grid[0][2]` inside two.
|
|
57
|
+
*
|
|
58
|
+
* Indexed, never `nodePaths[nodePaths.length - 1]`. That expression at
|
|
59
|
+
* depth 0 is `nodePaths[-1]`, which is not an element read at all: -1 is
|
|
60
|
+
* outside the array, so V8 falls back to a named-property lookup and walks
|
|
61
|
+
* the prototype chain. The `prefix` getter used to be written that way and
|
|
62
|
+
* it took 12% of the array shape's self time.
|
|
63
|
+
*/
|
|
64
|
+
readTop() {
|
|
65
|
+
const already = this.currentPrefix;
|
|
66
|
+
if (already !== null)
|
|
67
|
+
return already;
|
|
68
|
+
let built = "";
|
|
69
|
+
for (let i = 0; i < this.nodePaths.length; i += 1) {
|
|
70
|
+
const nodePath = this.nodePaths[i];
|
|
71
|
+
const index = this.openIndices[i];
|
|
72
|
+
if (nodePath === undefined || index === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
built = `${joinIssuePath(built, nodePath)}[${index}]`;
|
|
75
|
+
}
|
|
76
|
+
this.currentPrefix = built;
|
|
77
|
+
return built;
|
|
78
|
+
}
|
|
26
79
|
/** `""` at the root, `items[0]` inside the first element of `items`. */
|
|
27
80
|
get prefix() {
|
|
28
|
-
return this.
|
|
81
|
+
return this.readTop();
|
|
29
82
|
}
|
|
30
83
|
/**
|
|
31
84
|
* Enters element `index` of an array node. `nodePath` is the node's own path
|
|
@@ -41,20 +94,28 @@ class IndexStack {
|
|
|
41
94
|
if (!Number.isInteger(index) || index < 0) {
|
|
42
95
|
throw new RangeError(`an array index must be a non-negative integer, received ${String(index)}`);
|
|
43
96
|
}
|
|
44
|
-
this.
|
|
97
|
+
this.nodePaths.push(nodePath);
|
|
45
98
|
this.openIndices.push(index);
|
|
99
|
+
this.currentPrefix = null;
|
|
46
100
|
}
|
|
47
101
|
/** An unbalanced pop means a runner lost track of its own nesting. */
|
|
48
102
|
pop() {
|
|
49
103
|
if (this.openIndices.length === 0) {
|
|
50
104
|
throw new RangeError("popped an array index that was never pushed");
|
|
51
105
|
}
|
|
52
|
-
this.
|
|
106
|
+
this.nodePaths.pop();
|
|
53
107
|
this.openIndices.pop();
|
|
108
|
+
this.currentPrefix = null;
|
|
54
109
|
}
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The issue path of a field read from the subject at the current level.
|
|
112
|
+
*
|
|
113
|
+
* `renderedPath` was built once, at compile time. This used to walk the
|
|
114
|
+
* template on every call, which for an array meant rebuilding the same
|
|
115
|
+
* string once per element.
|
|
116
|
+
*/
|
|
117
|
+
renderFieldPath(renderedPath) {
|
|
118
|
+
return joinIssuePath(this.readTop(), renderedPath);
|
|
58
119
|
}
|
|
59
120
|
}
|
|
60
121
|
exports.IndexStack = IndexStack;
|
|
@@ -1,15 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// L5 src/runtime/index-stack.ts
|
|
3
|
+
// Where the runtime currently is, expressed as a concrete issue path.
|
|
4
|
+
//
|
|
5
|
+
// An issue path must read `items[0].name`, never the declaration pattern
|
|
6
|
+
// `items[*].name`. The legacy tree carried indices two different ways — the
|
|
7
|
+
// nested-array processor threaded them through its own recursion while
|
|
8
|
+
// validator-factory fell back to `error.path || elementPath` — which is how
|
|
9
|
+
// the pattern string leaked into user-visible errors. There is one stack here
|
|
10
|
+
// and every path a runner reports comes out of it.
|
|
11
|
+
//
|
|
12
|
+
// A CompiledField's template is RELATIVE to the subject it reads from, so an
|
|
13
|
+
// element field of `items[*]` has the template `name` and could never render
|
|
14
|
+
// `items[0].name` on its own. The array runner pushes the enclosing node and
|
|
15
|
+
// the index; the stack renders the prefix.
|
|
16
|
+
//
|
|
17
|
+
// The prefix is rendered ON DEMAND and remembered until the next push or pop.
|
|
18
|
+
// Entering used to build `items[0]` eagerly, which is one string per element
|
|
19
|
+
// that nothing reads unless a rule fails — and now nothing reads it even then
|
|
20
|
+
// until FieldRuleContext is asked for its path. Entering therefore costs two
|
|
21
|
+
// pushes and one assignment, and the first field that asks pays the render.
|
|
22
|
+
//
|
|
23
|
+
// It is deliberately MUTABLE and pushed/popped around an element loop: one
|
|
24
|
+
// stack lives for one validate() call, and 10k elements must not allocate 10k
|
|
25
|
+
// index arrays. It is also why FieldRuleContext remembers the path it built —
|
|
26
|
+
// this stack has moved on by the time a retained context could read it.
|
|
27
|
+
// ===========================================================================
|
|
9
28
|
export class IndexStack {
|
|
10
29
|
constructor() {
|
|
11
|
-
this.
|
|
30
|
+
this.nodePaths = [];
|
|
12
31
|
this.openIndices = [];
|
|
32
|
+
/**
|
|
33
|
+
* The rendered prefix, or `null` when it has not been asked for since the
|
|
34
|
+
* last push or pop. `null` and not `""` because the empty string is a real
|
|
35
|
+
* answer — the root.
|
|
36
|
+
*
|
|
37
|
+
* Invalidating rather than recomputing on push is the point: an element that
|
|
38
|
+
* raises no issue never renders its own prefix at all.
|
|
39
|
+
*/
|
|
40
|
+
this.currentPrefix = null;
|
|
13
41
|
}
|
|
14
42
|
/** How many array levels are currently open. */
|
|
15
43
|
get depth() {
|
|
@@ -19,9 +47,34 @@ export class IndexStack {
|
|
|
19
47
|
get indices() {
|
|
20
48
|
return this.openIndices;
|
|
21
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The prefix for the currently open frames — `""` when none are open,
|
|
52
|
+
* `items[0]` inside one, `grid[0][2]` inside two.
|
|
53
|
+
*
|
|
54
|
+
* Indexed, never `nodePaths[nodePaths.length - 1]`. That expression at
|
|
55
|
+
* depth 0 is `nodePaths[-1]`, which is not an element read at all: -1 is
|
|
56
|
+
* outside the array, so V8 falls back to a named-property lookup and walks
|
|
57
|
+
* the prototype chain. The `prefix` getter used to be written that way and
|
|
58
|
+
* it took 12% of the array shape's self time.
|
|
59
|
+
*/
|
|
60
|
+
readTop() {
|
|
61
|
+
const already = this.currentPrefix;
|
|
62
|
+
if (already !== null)
|
|
63
|
+
return already;
|
|
64
|
+
let built = "";
|
|
65
|
+
for (let i = 0; i < this.nodePaths.length; i += 1) {
|
|
66
|
+
const nodePath = this.nodePaths[i];
|
|
67
|
+
const index = this.openIndices[i];
|
|
68
|
+
if (nodePath === undefined || index === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
built = `${joinIssuePath(built, nodePath)}[${index}]`;
|
|
71
|
+
}
|
|
72
|
+
this.currentPrefix = built;
|
|
73
|
+
return built;
|
|
74
|
+
}
|
|
22
75
|
/** `""` at the root, `items[0]` inside the first element of `items`. */
|
|
23
76
|
get prefix() {
|
|
24
|
-
return this.
|
|
77
|
+
return this.readTop();
|
|
25
78
|
}
|
|
26
79
|
/**
|
|
27
80
|
* Enters element `index` of an array node. `nodePath` is the node's own path
|
|
@@ -37,20 +90,28 @@ export class IndexStack {
|
|
|
37
90
|
if (!Number.isInteger(index) || index < 0) {
|
|
38
91
|
throw new RangeError(`an array index must be a non-negative integer, received ${String(index)}`);
|
|
39
92
|
}
|
|
40
|
-
this.
|
|
93
|
+
this.nodePaths.push(nodePath);
|
|
41
94
|
this.openIndices.push(index);
|
|
95
|
+
this.currentPrefix = null;
|
|
42
96
|
}
|
|
43
97
|
/** An unbalanced pop means a runner lost track of its own nesting. */
|
|
44
98
|
pop() {
|
|
45
99
|
if (this.openIndices.length === 0) {
|
|
46
100
|
throw new RangeError("popped an array index that was never pushed");
|
|
47
101
|
}
|
|
48
|
-
this.
|
|
102
|
+
this.nodePaths.pop();
|
|
49
103
|
this.openIndices.pop();
|
|
104
|
+
this.currentPrefix = null;
|
|
50
105
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The issue path of a field read from the subject at the current level.
|
|
108
|
+
*
|
|
109
|
+
* `renderedPath` was built once, at compile time. This used to walk the
|
|
110
|
+
* template on every call, which for an array meant rebuilding the same
|
|
111
|
+
* string once per element.
|
|
112
|
+
*/
|
|
113
|
+
renderFieldPath(renderedPath) {
|
|
114
|
+
return joinIssuePath(this.readTop(), renderedPath);
|
|
54
115
|
}
|
|
55
116
|
}
|
|
56
117
|
/** `""` is the root path, so it never contributes a separator dot. */
|
|
@@ -24,8 +24,12 @@ function createArrayWriteTargets(nodes) {
|
|
|
24
24
|
* hands back the very object it was given.
|
|
25
25
|
*/
|
|
26
26
|
function createPlanWriteTargets(plan) {
|
|
27
|
-
|
|
27
|
+
// A field declaring only a normalizer is still written back by parse().
|
|
28
|
+
// Leave it out of this condition and no writer is made, so the value read
|
|
29
|
+
// comes back unchanged and in silence.
|
|
30
|
+
if (!plan.hasTransforms && !plan.hasDefaults && !plan.hasNormalizers) {
|
|
28
31
|
return null;
|
|
32
|
+
}
|
|
29
33
|
return createArrayWriteTargets(plan.arrays);
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
@@ -17,8 +17,12 @@ export function createArrayWriteTargets(nodes) {
|
|
|
17
17
|
* hands back the very object it was given.
|
|
18
18
|
*/
|
|
19
19
|
export function createPlanWriteTargets(plan) {
|
|
20
|
-
|
|
20
|
+
// A field declaring only a normalizer is still written back by parse().
|
|
21
|
+
// Leave it out of this condition and no writer is made, so the value read
|
|
22
|
+
// comes back unchanged and in silence.
|
|
23
|
+
if (!plan.hasTransforms && !plan.hasDefaults && !plan.hasNormalizers) {
|
|
21
24
|
return null;
|
|
25
|
+
}
|
|
22
26
|
return createArrayWriteTargets(plan.arrays);
|
|
23
27
|
}
|
|
24
28
|
/**
|