@maroonedog/luq 2.4.0 → 2.4.2

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
  }
@@ -13,7 +13,7 @@ export { prefixIssuePaths, 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.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;
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.
@@ -41,7 +41,6 @@ Object.defineProperty(exports, "BRANCH_ABORT_POLICY", { enumerable: true, get: f
41
41
  Object.defineProperty(exports, "createBranchExecutor", { enumerable: true, get: function () { return run_branch_1.createBranchExecutor; } });
42
42
  var create_validator_1 = require("./create-validator");
43
43
  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
44
  Object.defineProperty(exports, "createValidator", { enumerable: true, get: function () { return create_validator_1.createValidator; } });
46
45
  Object.defineProperty(exports, "hasRejectingIssue", { enumerable: true, get: function () { return create_validator_1.hasRejectingIssue; } });
47
46
  var create_field_validator_1 = require("./create-field-validator");
@@ -12,5 +12,5 @@ export { runArrayNodes } from "./run-array-node.mjs";
12
12
  export { prefixIssuePaths, 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";
@@ -55,6 +55,16 @@ function emitFieldSchema(fieldPath, calls, policy) {
55
55
  continue;
56
56
  }
57
57
  const keywords = toKeywords(call.args);
58
+ // undefined is the table saying it cannot express THESE arguments, which
59
+ // is the same answer as having no entry at all: refuse, rather than emit a
60
+ // schema missing a constraint the validator enforces. null is different —
61
+ // it means type or presence already carries the declaration.
62
+ if (keywords === undefined) {
63
+ if (policy === "throw") {
64
+ throw new unrepresentable_rule_error_1.UnrepresentableRuleError(fieldPath, call.pluginName, "no JSON Schema keyword expresses the arguments it was given");
65
+ }
66
+ continue;
67
+ }
58
68
  if (keywords === null)
59
69
  continue;
60
70
  Object.assign(schema, keywords);
@@ -52,6 +52,16 @@ export function emitFieldSchema(fieldPath, calls, policy) {
52
52
  continue;
53
53
  }
54
54
  const keywords = toKeywords(call.args);
55
+ // undefined is the table saying it cannot express THESE arguments, which
56
+ // is the same answer as having no entry at all: refuse, rather than emit a
57
+ // schema missing a constraint the validator enforces. null is different —
58
+ // it means type or presence already carries the declaration.
59
+ if (keywords === undefined) {
60
+ if (policy === "throw") {
61
+ throw new UnrepresentableRuleError(fieldPath, call.pluginName, "no JSON Schema keyword expresses the arguments it was given");
62
+ }
63
+ continue;
64
+ }
55
65
  if (keywords === null)
56
66
  continue;
57
67
  Object.assign(schema, keywords);
@@ -1,3 +1,16 @@
1
- /** A keyword fragment made from the arguments; null means type or presence handles it. */
2
- export type ToKeywords = (args: readonly unknown[]) => Record<string, unknown> | null;
1
+ /**
2
+ * A keyword fragment made from the arguments. Three answers, and the
3
+ * difference between the last two is the point:
4
+ *
5
+ * an object the keywords this declaration means
6
+ * null it adds no keyword, and that is correct — type or presence
7
+ * already carries it
8
+ * undefined it cannot be expressed with these arguments, so the caller
9
+ * must refuse rather than emit a schema that quietly says
10
+ * something else
11
+ *
12
+ * Collapsing the last two into `null` is how a constraint disappears without
13
+ * a trace, which is the failure UnrepresentableRuleError exists to prevent.
14
+ */
15
+ export type ToKeywords = (args: readonly unknown[]) => Record<string, unknown> | null | undefined;
3
16
  export declare const PLUGIN_KEYWORDS: Readonly<Record<string, ToKeywords>>;
@@ -37,9 +37,14 @@ exports.PLUGIN_KEYWORDS = Object.freeze({
37
37
  maxLength: numberAt(args, 0),
38
38
  }),
39
39
  // Draft-07's `pattern` is an ECMA-262 SOURCE string. Flags cannot be
40
- // spelled there, and dropping them changes the meaning, so a flagged
41
- // RegExp counts as unwritable rather than being silently narrowed.
42
- stringPattern: (args) => args[0] instanceof RegExp ? { pattern: args[0].source } : null,
40
+ // spelled there, and dropping them changes the meaning `/^a.c$/i`
41
+ // accepts "ABC" and `{"pattern":"^a.c$"}` does not so a flagged RegExp
42
+ // is unwritable rather than something to narrow quietly. Anything that is
43
+ // not a RegExp at all is unwritable for the same reason: refusing is the
44
+ // only answer that does not lose the constraint in silence.
45
+ stringPattern: (args) => args[0] instanceof RegExp && args[0].flags === ""
46
+ ? { pattern: args[0].source }
47
+ : undefined,
43
48
  stringContentEncoding: (args) => ({ contentEncoding: args[0] }),
44
49
  stringContentMediaType: (args) => ({ contentMediaType: args[0] }),
45
50
  // --- formats (names taken verbatim from the format map) ---------------
@@ -81,7 +86,9 @@ exports.PLUGIN_KEYWORDS = Object.freeze({
81
86
  arrayUnique: () => ({ uniqueItems: true }),
82
87
  // --- values -----------------------------------------------------------
83
88
  literal: (args) => ({ const: args[0] }),
84
- oneOf: (args) => (Array.isArray(args[0]) ? { enum: args[0] } : null),
89
+ // `enum` needs the list itself. Given something else there is no list to
90
+ // write, and answering "no keyword" would drop the constraint.
91
+ oneOf: (args) => (Array.isArray(args[0]) ? { enum: args[0] } : undefined),
85
92
  // --- handled by type or presence, adding no keyword -------------------
86
93
  // numberInteger becomes `type: "integer"`, so it feeds the type decision.
87
94
  numberInteger: () => null,
@@ -34,9 +34,14 @@ export const PLUGIN_KEYWORDS = Object.freeze({
34
34
  maxLength: numberAt(args, 0),
35
35
  }),
36
36
  // Draft-07's `pattern` is an ECMA-262 SOURCE string. Flags cannot be
37
- // spelled there, and dropping them changes the meaning, so a flagged
38
- // RegExp counts as unwritable rather than being silently narrowed.
39
- stringPattern: (args) => args[0] instanceof RegExp ? { pattern: args[0].source } : null,
37
+ // spelled there, and dropping them changes the meaning `/^a.c$/i`
38
+ // accepts "ABC" and `{"pattern":"^a.c$"}` does not so a flagged RegExp
39
+ // is unwritable rather than something to narrow quietly. Anything that is
40
+ // not a RegExp at all is unwritable for the same reason: refusing is the
41
+ // only answer that does not lose the constraint in silence.
42
+ stringPattern: (args) => args[0] instanceof RegExp && args[0].flags === ""
43
+ ? { pattern: args[0].source }
44
+ : undefined,
40
45
  stringContentEncoding: (args) => ({ contentEncoding: args[0] }),
41
46
  stringContentMediaType: (args) => ({ contentMediaType: args[0] }),
42
47
  // --- formats (names taken verbatim from the format map) ---------------
@@ -78,7 +83,9 @@ export const PLUGIN_KEYWORDS = Object.freeze({
78
83
  arrayUnique: () => ({ uniqueItems: true }),
79
84
  // --- values -----------------------------------------------------------
80
85
  literal: (args) => ({ const: args[0] }),
81
- oneOf: (args) => (Array.isArray(args[0]) ? { enum: args[0] } : null),
86
+ // `enum` needs the list itself. Given something else there is no list to
87
+ // write, and answering "no keyword" would drop the constraint.
88
+ oneOf: (args) => (Array.isArray(args[0]) ? { enum: args[0] } : undefined),
82
89
  // --- handled by type or presence, adding no keyword -------------------
83
90
  // numberInteger becomes `type: "integer"`, so it feeds the type decision.
84
91
  numberInteger: () => null,
@@ -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.0",
3
+ "version": "2.4.2",
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",