@maroonedog/luq 2.4.1 → 2.4.3

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.
@@ -1,10 +1,16 @@
1
1
  import type { ValidationPlan } from "../compile/validation-plan.types";
2
- import type { GlobalConfig } from "../types/global-config";
2
+ import type { GlobalConfig, ResolvedGlobalConfig } from "../types/global-config";
3
3
  import type { FieldEntry } from "./field-entry.types";
4
4
  import type { FieldDeclaredCalls } from "./field-declared-calls.types";
5
5
  /** What build() makes in one pass: the plan to run, and what was declared. */
6
6
  export interface CompiledDeclarations {
7
7
  readonly plan: ValidationPlan;
8
8
  readonly declaredCalls: readonly FieldDeclaredCalls[];
9
+ /**
10
+ * Resolved once here, and carried out because the root short-circuit needs
11
+ * it too. That rejection happens before any rule, so it cannot read the
12
+ * config the way a plugin does — through RuleBuildContext.
13
+ */
14
+ readonly config: ResolvedGlobalConfig;
9
15
  }
10
16
  export declare function compileDeclarations(entries: readonly FieldEntry[], configOverride: GlobalConfig | undefined): CompiledDeclarations;
@@ -46,5 +46,6 @@ function compileDeclarations(entries, configOverride) {
46
46
  return {
47
47
  plan: (0, compile_schema_1.compileSchema)(declarations, (0, run_branch_1.createBranchExecutor)()),
48
48
  declaredCalls,
49
+ config,
49
50
  };
50
51
  }
@@ -43,5 +43,6 @@ export function compileDeclarations(entries, configOverride) {
43
43
  return {
44
44
  plan: compileSchema(declarations, createBranchExecutor()),
45
45
  declaredCalls,
46
+ config,
46
47
  };
47
48
  }
@@ -27,7 +27,7 @@ const NO_ENTRIES = Object.freeze([]);
27
27
  */
28
28
  function buildValidator(entries, configOverride) {
29
29
  const compiled = (0, compile_declarations_1.compileDeclarations)(entries, configOverride);
30
- const validator = (0, create_plan_validator_1.createPlanBackedValidator)(compiled.plan);
30
+ const validator = (0, create_plan_validator_1.createPlanBackedValidator)(compiled.plan, compiled.config);
31
31
  (0, declared_calls_store_1.rememberDeclaredCalls)(validator, compiled.declaredCalls);
32
32
  return validator;
33
33
  }
@@ -24,7 +24,7 @@ const NO_ENTRIES = Object.freeze([]);
24
24
  */
25
25
  function buildValidator(entries, configOverride) {
26
26
  const compiled = compileDeclarations(entries, configOverride);
27
- const validator = createPlanBackedValidator(compiled.plan);
27
+ const validator = createPlanBackedValidator(compiled.plan, compiled.config);
28
28
  rememberDeclaredCalls(validator, compiled.declaredCalls);
29
29
  return validator;
30
30
  }
@@ -1,3 +1,4 @@
1
1
  import type { ValidationPlan } from "../compile/validation-plan.types";
2
+ import type { ResolvedGlobalConfig } from "../types/global-config";
2
3
  import type { PlanBackedValidator } from "./builder-surface.types";
3
- export declare function createPlanBackedValidator(plan: ValidationPlan): PlanBackedValidator;
4
+ export declare function createPlanBackedValidator(plan: ValidationPlan, config: ResolvedGlobalConfig): PlanBackedValidator;
@@ -4,8 +4,8 @@ exports.createPlanBackedValidator = createPlanBackedValidator;
4
4
  const create_field_validator_1 = require("../runtime/create-field-validator");
5
5
  const create_validator_1 = require("../runtime/create-validator");
6
6
  const create_subset_validator_1 = require("./create-subset-validator");
7
- function createPlanBackedValidator(plan) {
8
- const validator = (0, create_validator_1.createValidator)(plan);
7
+ function createPlanBackedValidator(plan, config) {
8
+ const validator = (0, create_validator_1.createValidator)(plan, config);
9
9
  return Object.freeze({
10
10
  validate: validator.validate,
11
11
  parse: validator.parse,
@@ -1,8 +1,8 @@
1
1
  import { createFieldValidator } from "../runtime/create-field-validator.mjs";
2
2
  import { createValidator } from "../runtime/create-validator.mjs";
3
3
  import { createSubsetValidator } from "./create-subset-validator.mjs";
4
- export function createPlanBackedValidator(plan) {
5
- const validator = createValidator(plan);
4
+ export function createPlanBackedValidator(plan, config) {
5
+ const validator = createValidator(plan, config);
6
6
  return Object.freeze({
7
7
  validate: validator.validate,
8
8
  parse: validator.parse,
@@ -1,14 +1,22 @@
1
1
  import type { ValidationIssue } from "../types";
2
2
  import type { ValidateOptions, ValidationResult } from "../types/validation-result.types";
3
3
  import type { ValidationPlan } from "../compile/validation-plan.types";
4
+ import { type ResolvedGlobalConfig } from "../types/global-config";
4
5
  /** The untyped pair. L6 puts the declared type back on top of it. */
5
6
  export interface PlanValidator {
6
7
  validate(value: unknown, options?: ValidateOptions): ValidationResult<unknown>;
7
8
  parse(value: unknown, options?: ValidateOptions): ValidationResult<unknown>;
8
9
  }
9
- /** The legacy root short-circuit, wording included (validator-factory.ts:498). */
10
- export declare const ROOT_MISSING_CODE = "REQUIRED";
11
- export declare const ROOT_MISSING_MESSAGE = "Value is required";
10
+ /**
11
+ * The code an absent SUBJECT reports.
12
+ *
13
+ * The same spelling a missing FIELD reports, deliberately. It was "REQUIRED"
14
+ * — 1.x's SCREAMING_SNAKE, carried over with the wording — which meant a
15
+ * caller switching on issue.code handled `required` for a missing field and
16
+ * missed the root, where the only difference is how much of the value was
17
+ * absent.
18
+ */
19
+ export declare const ROOT_MISSING_CODE = "required";
12
20
  /**
13
21
  * Severity decides validity, and nothing else does. An `info` or `warning`
14
22
  * issue is still an issue — the sink counted it and abortEarly saw it — but it
@@ -16,4 +24,4 @@ export declare const ROOT_MISSING_MESSAGE = "Value is required";
16
24
  * an `issues` list at all.
17
25
  */
18
26
  export declare function hasRejectingIssue(issues: readonly ValidationIssue[]): boolean;
19
- export declare function createValidator(plan: ValidationPlan): PlanValidator;
27
+ export declare function createValidator(plan: ValidationPlan, config?: ResolvedGlobalConfig): PlanValidator;
@@ -1,30 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ROOT_MISSING_MESSAGE = exports.ROOT_MISSING_CODE = void 0;
3
+ exports.ROOT_MISSING_CODE = void 0;
4
4
  exports.hasRejectingIssue = hasRejectingIssue;
5
5
  exports.createValidator = createValidator;
6
+ const global_config_1 = require("../types/global-config");
6
7
  const index_stack_1 = require("./index-stack");
7
8
  const issue_sink_1 = require("./issue-sink");
8
9
  const output_writer_1 = require("./output-writer");
9
10
  const run_recursion_1 = require("./run-recursion");
10
11
  const run_plan_1 = require("./run-plan");
11
- /** The legacy root short-circuit, wording included (validator-factory.ts:498). */
12
- exports.ROOT_MISSING_CODE = "REQUIRED";
13
- exports.ROOT_MISSING_MESSAGE = "Value is required";
12
+ /**
13
+ * The code an absent SUBJECT reports.
14
+ *
15
+ * The same spelling a missing FIELD reports, deliberately. It was "REQUIRED"
16
+ * — 1.x's SCREAMING_SNAKE, carried over with the wording — which meant a
17
+ * caller switching on issue.code handled `required` for a missing field and
18
+ * missed the root, where the only difference is how much of the value was
19
+ * absent.
20
+ */
21
+ exports.ROOT_MISSING_CODE = "required";
14
22
  /**
15
23
  * A null or undefined subject fails before the plan runs. Without it every
16
24
  * reader would answer `undefined`, OPEN_PRESENCE would permit every absence,
17
25
  * and `validate(null)` would report success for a schema that declares nothing
18
26
  * required.
19
27
  */
20
- function rejectMissingRoot() {
28
+ function rejectMissingRoot(config) {
21
29
  return {
22
30
  valid: false,
23
31
  issues: Object.freeze([
24
32
  Object.freeze({
25
33
  path: "",
26
34
  code: exports.ROOT_MISSING_CODE,
27
- message: exports.ROOT_MISSING_MESSAGE,
35
+ message: config.rootMissingMessage,
36
+ // Not config.defaultSeverity. A warning here would let validate(null)
37
+ // report success for a schema that declares nothing required, which is
38
+ // the outcome this rejection exists to prevent.
28
39
  severity: "error",
29
40
  }),
30
41
  ]),
@@ -66,9 +77,9 @@ function nodeCanRecurse(node) {
66
77
  return (node.elementFields.some((field) => field.recursion !== null) ||
67
78
  node.nested.some(nodeCanRecurse));
68
79
  }
69
- function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
80
+ function runRoot(plan, config, value, options, targets, shouldApplyTransforms, canRecurse) {
70
81
  if (value === null || value === undefined)
71
- return rejectMissingRoot();
82
+ return rejectMissingRoot(config);
72
83
  const sink = new issue_sink_1.IssueSink((0, issue_sink_1.resolveAbortPolicy)(options));
73
84
  const output = (0, run_plan_1.runPlan)(plan, value, {
74
85
  root: value,
@@ -89,12 +100,12 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurs
89
100
  return { valid: false, issues };
90
101
  return { valid: true, data: output, issues };
91
102
  }
92
- function createValidator(plan) {
103
+ function createValidator(plan, config = global_config_1.DEFAULT_GLOBAL_CONFIG) {
93
104
  const parseTargets = (0, output_writer_1.createPlanWriteTargets)(plan);
94
105
  const shouldWriteOutput = parseTargets !== null;
95
106
  const canRecurse = planCanRecurse(plan);
96
107
  return {
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),
108
+ validate: (value, options) => runRoot(plan, config, value, options, output_writer_1.NO_WRITE_TARGETS, false, canRecurse),
109
+ parse: (value, options) => runRoot(plan, config, value, options, parseTargets ?? output_writer_1.NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
99
110
  };
100
111
  }
@@ -1,25 +1,36 @@
1
+ import { DEFAULT_GLOBAL_CONFIG, } from "../types/global-config.mjs";
1
2
  import { IndexStack } from "./index-stack.mjs";
2
3
  import { IssueSink, resolveAbortPolicy } from "./issue-sink.mjs";
3
4
  import { NO_WRITE_TARGETS, createPlanWriteTargets } from "./output-writer.mjs";
4
5
  import { createRecursionRunner } from "./run-recursion.mjs";
5
6
  import { runPlan } from "./run-plan.mjs";
6
- /** The legacy root short-circuit, wording included (validator-factory.ts:498). */
7
- export const ROOT_MISSING_CODE = "REQUIRED";
8
- export const ROOT_MISSING_MESSAGE = "Value is required";
7
+ /**
8
+ * The code an absent SUBJECT reports.
9
+ *
10
+ * The same spelling a missing FIELD reports, deliberately. It was "REQUIRED"
11
+ * — 1.x's SCREAMING_SNAKE, carried over with the wording — which meant a
12
+ * caller switching on issue.code handled `required` for a missing field and
13
+ * missed the root, where the only difference is how much of the value was
14
+ * absent.
15
+ */
16
+ export const ROOT_MISSING_CODE = "required";
9
17
  /**
10
18
  * A null or undefined subject fails before the plan runs. Without it every
11
19
  * reader would answer `undefined`, OPEN_PRESENCE would permit every absence,
12
20
  * and `validate(null)` would report success for a schema that declares nothing
13
21
  * required.
14
22
  */
15
- function rejectMissingRoot() {
23
+ function rejectMissingRoot(config) {
16
24
  return {
17
25
  valid: false,
18
26
  issues: Object.freeze([
19
27
  Object.freeze({
20
28
  path: "",
21
29
  code: ROOT_MISSING_CODE,
22
- message: ROOT_MISSING_MESSAGE,
30
+ message: config.rootMissingMessage,
31
+ // Not config.defaultSeverity. A warning here would let validate(null)
32
+ // report success for a schema that declares nothing required, which is
33
+ // the outcome this rejection exists to prevent.
23
34
  severity: "error",
24
35
  }),
25
36
  ]),
@@ -61,9 +72,9 @@ function nodeCanRecurse(node) {
61
72
  return (node.elementFields.some((field) => field.recursion !== null) ||
62
73
  node.nested.some(nodeCanRecurse));
63
74
  }
64
- function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
75
+ function runRoot(plan, config, value, options, targets, shouldApplyTransforms, canRecurse) {
65
76
  if (value === null || value === undefined)
66
- return rejectMissingRoot();
77
+ return rejectMissingRoot(config);
67
78
  const sink = new IssueSink(resolveAbortPolicy(options));
68
79
  const output = runPlan(plan, value, {
69
80
  root: value,
@@ -84,12 +95,12 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurs
84
95
  return { valid: false, issues };
85
96
  return { valid: true, data: output, issues };
86
97
  }
87
- export function createValidator(plan) {
98
+ export function createValidator(plan, config = DEFAULT_GLOBAL_CONFIG) {
88
99
  const parseTargets = createPlanWriteTargets(plan);
89
100
  const shouldWriteOutput = parseTargets !== null;
90
101
  const canRecurse = planCanRecurse(plan);
91
102
  return {
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),
103
+ validate: (value, options) => runRoot(plan, config, value, options, NO_WRITE_TARGETS, false, canRecurse),
104
+ parse: (value, options) => runRoot(plan, config, value, options, parseTargets ?? NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
94
105
  };
95
106
  }
@@ -1,4 +1,5 @@
1
1
  export declare class IndexStack {
2
+ private readonly basePath;
2
3
  private readonly nodePaths;
3
4
  private readonly openIndices;
4
5
  /**
@@ -10,6 +11,11 @@ export declare class IndexStack {
10
11
  * raises no issue never renders its own prefix at all.
11
12
  */
12
13
  private currentPrefix;
14
+ /**
15
+ * Where this run sits inside the whole value: `""` for the run the caller
16
+ * started, and the entering rule's path for a branch or a recursive re-entry.
17
+ */
18
+ constructor(basePath?: string);
13
19
  /** How many array levels are currently open. */
14
20
  get depth(): number;
15
21
  /** The open indices, outermost first: `[0, 2]` inside `grid[0][2]`. */
@@ -25,7 +31,10 @@ export declare class IndexStack {
25
31
  * it took 12% of the array shape's self time.
26
32
  */
27
33
  private readTop;
28
- /** `""` at the root, `items[0]` inside the first element of `items`. */
34
+ /**
35
+ * `""` at the root of the run the caller started, `items[0]` inside the first
36
+ * element of `items`, and the base path when a nested run has no array open.
37
+ */
29
38
  get prefix(): string;
30
39
  /**
31
40
  * Enters element `index` of an array node. `nodePath` is the node's own path
@@ -25,12 +25,25 @@
25
25
  // stack lives for one validate() call, and 10k elements must not allocate 10k
26
26
  // index arrays. It is also why FieldRuleContext remembers the path it built —
27
27
  // this stack has moved on by the time a retained context could read it.
28
+ //
29
+ // A NESTED run starts from a BASE PATH rather than from the root. A branch plan
30
+ // and a recursive re-entry each declare their fields against their own subject,
31
+ // so a field renders `name` where the consumer must read `user.name`. The base
32
+ // is handed to the stack, so the path is right the first time it is built — and
33
+ // the message, which is rendered from that same path, names the field the issue
34
+ // reports. Rewriting the path afterwards cannot do that: by then the message has
35
+ // already been rendered against the short one.
28
36
  // ===========================================================================
29
37
  Object.defineProperty(exports, "__esModule", { value: true });
30
38
  exports.IndexStack = void 0;
31
39
  exports.joinIssuePath = joinIssuePath;
32
40
  class IndexStack {
33
- constructor() {
41
+ /**
42
+ * Where this run sits inside the whole value: `""` for the run the caller
43
+ * started, and the entering rule's path for a branch or a recursive re-entry.
44
+ */
45
+ constructor(basePath = "") {
46
+ this.basePath = basePath;
34
47
  this.nodePaths = [];
35
48
  this.openIndices = [];
36
49
  /**
@@ -65,7 +78,7 @@ class IndexStack {
65
78
  const already = this.currentPrefix;
66
79
  if (already !== null)
67
80
  return already;
68
- let built = "";
81
+ let built = this.basePath;
69
82
  for (let i = 0; i < this.nodePaths.length; i += 1) {
70
83
  const nodePath = this.nodePaths[i];
71
84
  const index = this.openIndices[i];
@@ -76,7 +89,10 @@ class IndexStack {
76
89
  this.currentPrefix = built;
77
90
  return built;
78
91
  }
79
- /** `""` at the root, `items[0]` inside the first element of `items`. */
92
+ /**
93
+ * `""` at the root of the run the caller started, `items[0]` inside the first
94
+ * element of `items`, and the base path when a nested run has no array open.
95
+ */
80
96
  get prefix() {
81
97
  return this.readTop();
82
98
  }
@@ -24,9 +24,22 @@
24
24
  // stack lives for one validate() call, and 10k elements must not allocate 10k
25
25
  // index arrays. It is also why FieldRuleContext remembers the path it built —
26
26
  // this stack has moved on by the time a retained context could read it.
27
+ //
28
+ // A NESTED run starts from a BASE PATH rather than from the root. A branch plan
29
+ // and a recursive re-entry each declare their fields against their own subject,
30
+ // so a field renders `name` where the consumer must read `user.name`. The base
31
+ // is handed to the stack, so the path is right the first time it is built — and
32
+ // the message, which is rendered from that same path, names the field the issue
33
+ // reports. Rewriting the path afterwards cannot do that: by then the message has
34
+ // already been rendered against the short one.
27
35
  // ===========================================================================
28
36
  export class IndexStack {
29
- constructor() {
37
+ /**
38
+ * Where this run sits inside the whole value: `""` for the run the caller
39
+ * started, and the entering rule's path for a branch or a recursive re-entry.
40
+ */
41
+ constructor(basePath = "") {
42
+ this.basePath = basePath;
30
43
  this.nodePaths = [];
31
44
  this.openIndices = [];
32
45
  /**
@@ -61,7 +74,7 @@ export class IndexStack {
61
74
  const already = this.currentPrefix;
62
75
  if (already !== null)
63
76
  return already;
64
- let built = "";
77
+ let built = this.basePath;
65
78
  for (let i = 0; i < this.nodePaths.length; i += 1) {
66
79
  const nodePath = this.nodePaths[i];
67
80
  const index = this.openIndices[i];
@@ -72,7 +85,10 @@ export class IndexStack {
72
85
  this.currentPrefix = built;
73
86
  return built;
74
87
  }
75
- /** `""` at the root, `items[0]` inside the first element of `items`. */
88
+ /**
89
+ * `""` at the root of the run the caller started, `items[0]` inside the first
90
+ * element of `items`, and the base path when a nested run has no array open.
91
+ */
76
92
  get prefix() {
77
93
  return this.readTop();
78
94
  }
@@ -9,11 +9,11 @@ export type { FieldRunContext, FieldRunOutcome, RecursionRunner, } from "./run-f
9
9
  export { NO_WRITE_TARGETS, createArrayWriteTargets, createPlanWriteTargets, replaceElement, writeFieldValue, } from "./output-writer";
10
10
  export type { ArrayWriteTarget } from "./output-writer";
11
11
  export { runArrayNodes } from "./run-array-node";
12
- export { prefixIssuePaths, runPlan } from "./run-plan";
12
+ export { runPlan } from "./run-plan";
13
13
  export { RECURSION_ABORT_POLICY, createRecursionRunner } from "./run-recursion";
14
14
  export type { RecursionHost } from "./run-recursion";
15
15
  export { BRANCH_ABORT_POLICY, createBranchExecutor } from "./run-branch";
16
- export { ROOT_MISSING_CODE, ROOT_MISSING_MESSAGE, createValidator, hasRejectingIssue, } from "./create-validator";
16
+ export { ROOT_MISSING_CODE, createValidator, hasRejectingIssue, } from "./create-validator";
17
17
  export type { PlanValidator } from "./create-validator";
18
18
  export { createFieldValidator } from "./create-field-validator";
19
19
  export type { PlanFieldValidator } from "./create-field-validator";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createFieldValidator = exports.hasRejectingIssue = exports.createValidator = exports.ROOT_MISSING_MESSAGE = exports.ROOT_MISSING_CODE = exports.createBranchExecutor = exports.BRANCH_ABORT_POLICY = exports.createRecursionRunner = exports.RECURSION_ABORT_POLICY = exports.runPlan = exports.prefixIssuePaths = exports.runArrayNodes = exports.writeFieldValue = exports.replaceElement = exports.createPlanWriteTargets = exports.createArrayWriteTargets = exports.NO_WRITE_TARGETS = exports.runField = exports.FIELD_VALUE_UNCHANGED = exports.decidePresence = exports.renderFallbackMessage = exports.createIssue = exports.joinIssuePath = exports.IndexStack = exports.resolveAbortPolicy = exports.IssueSink = exports.DEFAULT_ABORT_EARLY_ON_EACH_FIELD = exports.DEFAULT_ABORT_EARLY = exports.ARRAY_ELEMENTS_ABORT_ON_EACH_FIELD = void 0;
3
+ exports.createFieldValidator = exports.hasRejectingIssue = exports.createValidator = exports.ROOT_MISSING_CODE = exports.createBranchExecutor = exports.BRANCH_ABORT_POLICY = exports.createRecursionRunner = exports.RECURSION_ABORT_POLICY = exports.runPlan = exports.runArrayNodes = exports.writeFieldValue = exports.replaceElement = exports.createPlanWriteTargets = exports.createArrayWriteTargets = exports.NO_WRITE_TARGETS = exports.runField = exports.FIELD_VALUE_UNCHANGED = exports.decidePresence = exports.renderFallbackMessage = exports.createIssue = exports.joinIssuePath = exports.IndexStack = exports.resolveAbortPolicy = exports.IssueSink = exports.DEFAULT_ABORT_EARLY_ON_EACH_FIELD = exports.DEFAULT_ABORT_EARLY = exports.ARRAY_ELEMENTS_ABORT_ON_EACH_FIELD = void 0;
4
4
  // ===========================================================================
5
5
  // L5 src/runtime/index.ts — re-exports only. The engine: one plan in, issues
6
6
  // and (for parse) a copy-on-write output out. Nothing is defined in this file.
@@ -31,7 +31,6 @@ Object.defineProperty(exports, "writeFieldValue", { enumerable: true, get: funct
31
31
  var run_array_node_1 = require("./run-array-node");
32
32
  Object.defineProperty(exports, "runArrayNodes", { enumerable: true, get: function () { return run_array_node_1.runArrayNodes; } });
33
33
  var run_plan_1 = require("./run-plan");
34
- Object.defineProperty(exports, "prefixIssuePaths", { enumerable: true, get: function () { return run_plan_1.prefixIssuePaths; } });
35
34
  Object.defineProperty(exports, "runPlan", { enumerable: true, get: function () { return run_plan_1.runPlan; } });
36
35
  var run_recursion_1 = require("./run-recursion");
37
36
  Object.defineProperty(exports, "RECURSION_ABORT_POLICY", { enumerable: true, get: function () { return run_recursion_1.RECURSION_ABORT_POLICY; } });
@@ -41,7 +40,6 @@ Object.defineProperty(exports, "BRANCH_ABORT_POLICY", { enumerable: true, get: f
41
40
  Object.defineProperty(exports, "createBranchExecutor", { enumerable: true, get: function () { return run_branch_1.createBranchExecutor; } });
42
41
  var create_validator_1 = require("./create-validator");
43
42
  Object.defineProperty(exports, "ROOT_MISSING_CODE", { enumerable: true, get: function () { return create_validator_1.ROOT_MISSING_CODE; } });
44
- Object.defineProperty(exports, "ROOT_MISSING_MESSAGE", { enumerable: true, get: function () { return create_validator_1.ROOT_MISSING_MESSAGE; } });
45
43
  Object.defineProperty(exports, "createValidator", { enumerable: true, get: function () { return create_validator_1.createValidator; } });
46
44
  Object.defineProperty(exports, "hasRejectingIssue", { enumerable: true, get: function () { return create_validator_1.hasRejectingIssue; } });
47
45
  var create_field_validator_1 = require("./create-field-validator");
@@ -9,8 +9,8 @@ export { decidePresence } from "./decide-presence.mjs";
9
9
  export { FIELD_VALUE_UNCHANGED, runField } from "./run-field.mjs";
10
10
  export { NO_WRITE_TARGETS, createArrayWriteTargets, createPlanWriteTargets, replaceElement, writeFieldValue, } from "./output-writer.mjs";
11
11
  export { runArrayNodes } from "./run-array-node.mjs";
12
- export { prefixIssuePaths, runPlan } from "./run-plan.mjs";
12
+ export { runPlan } from "./run-plan.mjs";
13
13
  export { RECURSION_ABORT_POLICY, createRecursionRunner } from "./run-recursion.mjs";
14
14
  export { BRANCH_ABORT_POLICY, createBranchExecutor } from "./run-branch.mjs";
15
- export { ROOT_MISSING_CODE, ROOT_MISSING_MESSAGE, createValidator, hasRejectingIssue, } from "./create-validator.mjs";
15
+ export { ROOT_MISSING_CODE, createValidator, hasRejectingIssue, } from "./create-validator.mjs";
16
16
  export { createFieldValidator } from "./create-field-validator.mjs";
@@ -48,7 +48,13 @@ function createBranchExecutor() {
48
48
  (0, run_plan_1.runPlan)(plan, value, {
49
49
  root: ruleContext.root,
50
50
  sink,
51
- indices: new index_stack_1.IndexStack(),
51
+ // The branch plan's fields are declared against the branch's own
52
+ // subject, so a field of it renders `name` while the consumer must read
53
+ // `user.name`. Starting the stack at the composite's path is what makes
54
+ // the cause's message agree with the cause's path: both come from here,
55
+ // and a message rendered against the short path could not be corrected
56
+ // afterwards.
57
+ indices: new index_stack_1.IndexStack(ruleContext.path),
52
58
  shouldApplyTransforms: false,
53
59
  runRecursion: (0, run_recursion_1.createRecursionRunner)({
54
60
  root: ruleContext.root,
@@ -60,8 +66,7 @@ function createBranchExecutor() {
60
66
  });
61
67
  if (sink.count === 0)
62
68
  return types_1.PASS;
63
- const causes = (0, run_plan_1.prefixIssuePaths)(ruleContext.path, sink.issues);
64
- return (0, types_1.fail)({ causes: Object.freeze(causes) });
69
+ return (0, types_1.fail)({ causes: Object.freeze(sink.issues) });
65
70
  },
66
71
  };
67
72
  }
@@ -22,7 +22,7 @@ import { PASS, fail } from "../types/index.mjs";
22
22
  import { IndexStack } from "./index-stack.mjs";
23
23
  import { IssueSink } from "./issue-sink.mjs";
24
24
  import { createRecursionRunner } from "./run-recursion.mjs";
25
- import { prefixIssuePaths, runPlan } from "./run-plan.mjs";
25
+ import { runPlan } from "./run-plan.mjs";
26
26
  /**
27
27
  * A branch is a decision, not a report: the first failure already answers it,
28
28
  * so both aborts stay on. Collecting every issue of every alternative of a
@@ -44,7 +44,13 @@ export function createBranchExecutor() {
44
44
  runPlan(plan, value, {
45
45
  root: ruleContext.root,
46
46
  sink,
47
- indices: new IndexStack(),
47
+ // The branch plan's fields are declared against the branch's own
48
+ // subject, so a field of it renders `name` while the consumer must read
49
+ // `user.name`. Starting the stack at the composite's path is what makes
50
+ // the cause's message agree with the cause's path: both come from here,
51
+ // and a message rendered against the short path could not be corrected
52
+ // afterwards.
53
+ indices: new IndexStack(ruleContext.path),
48
54
  shouldApplyTransforms: false,
49
55
  runRecursion: createRecursionRunner({
50
56
  root: ruleContext.root,
@@ -56,8 +62,7 @@ export function createBranchExecutor() {
56
62
  });
57
63
  if (sink.count === 0)
58
64
  return PASS;
59
- const causes = prefixIssuePaths(ruleContext.path, sink.issues);
60
- return fail({ causes: Object.freeze(causes) });
65
+ return fail({ causes: Object.freeze(sink.issues) });
61
66
  },
62
67
  };
63
68
  }
@@ -1,4 +1,3 @@
1
- import type { ValidationIssue } from "../types";
2
1
  import type { ValidationPlan } from "../compile/validation-plan.types";
3
2
  import type { FieldRunContext } from "./run-field";
4
3
  import type { ArrayWriteTarget } from "./output-writer";
@@ -15,14 +14,9 @@ import type { ArrayWriteTarget } from "./output-writer";
15
14
  */
16
15
  export declare function runPlan(plan: ValidationPlan, subject: unknown, context: FieldRunContext, targets?: readonly ArrayWriteTarget[]): unknown;
17
16
  /**
18
- * Re-bases the issues of a NESTED plan onto the path of the rule that entered
19
- * it. A branch's fields and a recursive re-entry's fields are both declared
20
- * relative to their own subject, so their issues come back as `name`; the
21
- * enclosing rule knows the subject sits at `user`, and the consumer must read
22
- * `user.name`.
23
- *
24
- * It lives here because both callers already depend on this module and neither
25
- * may depend on the other — run-branch creates a recursion runner, so
26
- * run-recursion could not own it without a cycle.
17
+ * A nested plan does NOT come back to have its issue paths corrected. A branch
18
+ * and a recursive re-entry each start their IndexStack at the entering rule's
19
+ * path, so every issue is built at its full path and its message is rendered
20
+ * from that same path. Rewriting the path afterwards used to leave the message
21
+ * naming the field's short name, so one issue reported two different fields.
27
22
  */
28
- export declare function prefixIssuePaths(prefix: string, issues: readonly ValidationIssue[]): readonly ValidationIssue[];
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runPlan = runPlan;
4
- exports.prefixIssuePaths = prefixIssuePaths;
5
- const index_stack_1 = require("./index-stack");
6
4
  const run_array_node_1 = require("./run-array-node");
7
5
  const run_field_1 = require("./run-field");
8
6
  const output_writer_1 = require("./output-writer");
@@ -32,23 +30,9 @@ function runPlan(plan, subject, context, targets = output_writer_1.NO_WRITE_TARG
32
30
  return (0, run_array_node_1.runArrayNodes)(plan.arrays, current, context, targets);
33
31
  }
34
32
  /**
35
- * Re-bases the issues of a NESTED plan onto the path of the rule that entered
36
- * it. A branch's fields and a recursive re-entry's fields are both declared
37
- * relative to their own subject, so their issues come back as `name`; the
38
- * enclosing rule knows the subject sits at `user`, and the consumer must read
39
- * `user.name`.
40
- *
41
- * It lives here because both callers already depend on this module and neither
42
- * may depend on the other — run-branch creates a recursion runner, so
43
- * run-recursion could not own it without a cycle.
33
+ * A nested plan does NOT come back to have its issue paths corrected. A branch
34
+ * and a recursive re-entry each start their IndexStack at the entering rule's
35
+ * path, so every issue is built at its full path and its message is rendered
36
+ * from that same path. Rewriting the path afterwards used to leave the message
37
+ * naming the field's short name, so one issue reported two different fields.
44
38
  */
45
- function prefixIssuePaths(prefix, issues) {
46
- if (prefix === "")
47
- return issues;
48
- return issues.map((issue) => Object.freeze({
49
- path: (0, index_stack_1.joinIssuePath)(prefix, issue.path),
50
- code: issue.code,
51
- message: issue.message,
52
- severity: issue.severity,
53
- }));
54
- }
@@ -1,4 +1,3 @@
1
- import { joinIssuePath } from "./index-stack.mjs";
2
1
  import { runArrayNodes } from "./run-array-node.mjs";
3
2
  import { runField } from "./run-field.mjs";
4
3
  import { NO_WRITE_TARGETS, writeFieldValue } from "./output-writer.mjs";
@@ -28,23 +27,9 @@ export function runPlan(plan, subject, context, targets = NO_WRITE_TARGETS) {
28
27
  return runArrayNodes(plan.arrays, current, context, targets);
29
28
  }
30
29
  /**
31
- * Re-bases the issues of a NESTED plan onto the path of the rule that entered
32
- * it. A branch's fields and a recursive re-entry's fields are both declared
33
- * relative to their own subject, so their issues come back as `name`; the
34
- * enclosing rule knows the subject sits at `user`, and the consumer must read
35
- * `user.name`.
36
- *
37
- * It lives here because both callers already depend on this module and neither
38
- * may depend on the other — run-branch creates a recursion runner, so
39
- * run-recursion could not own it without a cycle.
30
+ * A nested plan does NOT come back to have its issue paths corrected. A branch
31
+ * and a recursive re-entry each start their IndexStack at the entering rule's
32
+ * path, so every issue is built at its full path and its message is rendered
33
+ * from that same path. Rewriting the path afterwards used to leave the message
34
+ * naming the field's short name, so one issue reported two different fields.
40
35
  */
41
- export function prefixIssuePaths(prefix, issues) {
42
- if (prefix === "")
43
- return issues;
44
- return issues.map((issue) => Object.freeze({
45
- path: joinIssuePath(prefix, issue.path),
46
- code: issue.code,
47
- message: issue.message,
48
- severity: issue.severity,
49
- }));
50
- }
@@ -15,11 +15,13 @@ exports.createRecursionRunner = createRecursionRunner;
15
15
  // Legacy truncated at maxDepth as a SUCCESS, which meant a structure deeper
16
16
  // than the limit silently skipped every rule below it.
17
17
  //
18
- // The nested plan runs into its OWN sink and its OWN index stack, and its
19
- // issues are re-based onto the entering field's path afterwards. That is what
20
- // makes `node.child.name` come out right: the plan's fields are declared
21
- // relative to the plan's subject and know nothing about where they were
22
- // re-entered from.
18
+ // The nested plan runs into its OWN sink, and its OWN index stack STARTED AT
19
+ // the entering field's path. That is what makes `node.child.name` come out
20
+ // right: the plan's fields are declared relative to the plan's subject and know
21
+ // nothing about where they were re-entered from, so the stack is what carries
22
+ // the descent. Giving it the path up front rather than rewriting the issues
23
+ // afterwards is also what makes a message agree with the path beside it — a
24
+ // message is rendered once, from that path, at the moment the issue is built.
23
25
  //
24
26
  // A re-entry never writes. RecursionRunner returns void by contract, so there
25
27
  // is nowhere for a transformed subtree to go; recursion is validation, and
@@ -88,12 +90,12 @@ function bindRecursionRunner(host, progress) {
88
90
  (0, run_plan_1.runPlan)(policy.plan.resolve(), value, {
89
91
  root: host.root,
90
92
  sink: nested,
91
- indices: new index_stack_1.IndexStack(),
93
+ indices: new index_stack_1.IndexStack(path),
92
94
  shouldApplyTransforms: false,
93
95
  runRecursion: bindRecursionRunner({ ...host, sink: nested }, progress),
94
96
  external: host.external,
95
97
  });
96
- for (const issue of (0, run_plan_1.prefixIssuePaths)(path, nested.issues)) {
98
+ for (const issue of nested.issues) {
97
99
  host.sink.add(issue);
98
100
  }
99
101
  }
@@ -11,11 +11,13 @@
11
11
  // Legacy truncated at maxDepth as a SUCCESS, which meant a structure deeper
12
12
  // than the limit silently skipped every rule below it.
13
13
  //
14
- // The nested plan runs into its OWN sink and its OWN index stack, and its
15
- // issues are re-based onto the entering field's path afterwards. That is what
16
- // makes `node.child.name` come out right: the plan's fields are declared
17
- // relative to the plan's subject and know nothing about where they were
18
- // re-entered from.
14
+ // The nested plan runs into its OWN sink, and its OWN index stack STARTED AT
15
+ // the entering field's path. That is what makes `node.child.name` come out
16
+ // right: the plan's fields are declared relative to the plan's subject and know
17
+ // nothing about where they were re-entered from, so the stack is what carries
18
+ // the descent. Giving it the path up front rather than rewriting the issues
19
+ // afterwards is also what makes a message agree with the path beside it — a
20
+ // message is rendered once, from that path, at the moment the issue is built.
19
21
  //
20
22
  // A re-entry never writes. RecursionRunner returns void by contract, so there
21
23
  // is nowhere for a transformed subtree to go; recursion is validation, and
@@ -25,7 +27,7 @@ import { isArray } from "../types/index.mjs";
25
27
  import { createIssue } from "./create-issue.mjs";
26
28
  import { IndexStack } from "./index-stack.mjs";
27
29
  import { IssueSink } from "./issue-sink.mjs";
28
- import { prefixIssuePaths, runPlan } from "./run-plan.mjs";
30
+ import { runPlan } from "./run-plan.mjs";
29
31
  /**
30
32
  * Legacy contract, kept: fields do not abort each other inside a recursive
31
33
  * re-entry. A descent that stopped at its first issue would report one leaf of
@@ -84,12 +86,12 @@ function bindRecursionRunner(host, progress) {
84
86
  runPlan(policy.plan.resolve(), value, {
85
87
  root: host.root,
86
88
  sink: nested,
87
- indices: new IndexStack(),
89
+ indices: new IndexStack(path),
88
90
  shouldApplyTransforms: false,
89
91
  runRecursion: bindRecursionRunner({ ...host, sink: nested }, progress),
90
92
  external: host.external,
91
93
  });
92
- for (const issue of prefixIssuePaths(path, nested.issues)) {
94
+ for (const issue of nested.issues) {
93
95
  host.sink.add(issue);
94
96
  }
95
97
  }
@@ -15,6 +15,17 @@ export interface GlobalConfig {
15
15
  readonly customTransforms?: Readonly<Record<string, ValueTransform>>;
16
16
  /** Severity a rule emits when neither the rule nor the call names one. */
17
17
  readonly defaultSeverity?: IssueSeverity;
18
+ /**
19
+ * What `validate(null)` and `validate(undefined)` say.
20
+ *
21
+ * A subject that is absent fails before the plan runs, so there is no rule
22
+ * to carry a messageFactory and no call site to name one. This is where it
23
+ * is said instead. The severity is NOT settable with it: a warning here
24
+ * would let `validate(null)` report success for a schema that declares
25
+ * nothing required, which is the outcome the short-circuit exists to
26
+ * prevent.
27
+ */
28
+ readonly rootMissingMessage?: string;
18
29
  }
19
30
  /** Every member present: this is what a plugin reads. */
20
31
  export interface ResolvedGlobalConfig {
@@ -26,6 +37,7 @@ export interface ResolvedGlobalConfig {
26
37
  readonly caseSensitive: boolean;
27
38
  readonly customTransforms: Readonly<Record<string, ValueTransform>>;
28
39
  readonly defaultSeverity: IssueSeverity;
40
+ readonly rootMissingMessage: string;
29
41
  }
30
42
  export declare const DEFAULT_GLOBAL_CONFIG: ResolvedGlobalConfig;
31
43
  export declare function resolveGlobalConfig(overrides: GlobalConfig | undefined, base?: ResolvedGlobalConfig): ResolvedGlobalConfig;
@@ -14,6 +14,7 @@ exports.DEFAULT_GLOBAL_CONFIG = Object.freeze({
14
14
  caseSensitive: true,
15
15
  customTransforms: Object.freeze({}),
16
16
  defaultSeverity: "error",
17
+ rootMissingMessage: "Value is required",
17
18
  });
18
19
  function resolveGlobalConfig(overrides, base = exports.DEFAULT_GLOBAL_CONFIG) {
19
20
  if (overrides === undefined)
@@ -32,5 +33,6 @@ function resolveGlobalConfig(overrides, base = exports.DEFAULT_GLOBAL_CONFIG) {
32
33
  caseSensitive: overrides.caseSensitive ?? base.caseSensitive,
33
34
  customTransforms: overrides.customTransforms ?? base.customTransforms,
34
35
  defaultSeverity: overrides.defaultSeverity ?? base.defaultSeverity,
36
+ rootMissingMessage: overrides.rootMissingMessage ?? base.rootMissingMessage,
35
37
  });
36
38
  }
@@ -10,6 +10,7 @@ export const DEFAULT_GLOBAL_CONFIG = Object.freeze({
10
10
  caseSensitive: true,
11
11
  customTransforms: Object.freeze({}),
12
12
  defaultSeverity: "error",
13
+ rootMissingMessage: "Value is required",
13
14
  });
14
15
  export function resolveGlobalConfig(overrides, base = DEFAULT_GLOBAL_CONFIG) {
15
16
  if (overrides === undefined)
@@ -28,5 +29,6 @@ export function resolveGlobalConfig(overrides, base = DEFAULT_GLOBAL_CONFIG) {
28
29
  caseSensitive: overrides.caseSensitive ?? base.caseSensitive,
29
30
  customTransforms: overrides.customTransforms ?? base.customTransforms,
30
31
  defaultSeverity: overrides.defaultSeverity ?? base.defaultSeverity,
32
+ rootMissingMessage: overrides.rootMissingMessage ?? base.rootMissingMessage,
31
33
  });
32
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedog/luq",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "Universal Model & API Definition Platform - TypeScript validation library evolving into cross-language code generation",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",