@maroonedog/luq 2.3.0 → 2.3.1

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.
@@ -9,6 +9,16 @@ export interface ChainBuildContext {
9
9
  readonly fieldPath: string;
10
10
  readonly declaredSiblingKeys: readonly string[];
11
11
  readonly config: ResolvedGlobalConfig;
12
+ /**
13
+ * True when something other than the slot already decides the runtime type.
14
+ *
15
+ * Set by the JSON Schema converter and nowhere else: there the DOCUMENT owns
16
+ * the type, its `type` keyword reports under the code `type`, and its
17
+ * vocabulary is wider than a slot's — `integer`, a list of types, and `null`
18
+ * as a type in its own right. A slot guard on that path would report a
19
+ * second issue for one bad value, under a code no schema asked for.
20
+ */
21
+ readonly typeDecidedElsewhere?: boolean;
12
22
  }
13
23
  /**
14
24
  * The parts a node cannot make for itself. `resolveArguments` is injected so
@@ -5,6 +5,7 @@ exports.createFieldSlots = createFieldSlots;
5
5
  const type_erasure_1 = require("../core/type-erasure");
6
6
  const create_chain_node_1 = require("./create-chain-node");
7
7
  const collect_branch_rules_1 = require("./collect-branch-rules");
8
+ const slot_type_guard_1 = require("./slot-type-guard");
8
9
  const SLOT_NAMES = Object.freeze([
9
10
  "string",
10
11
  "number",
@@ -25,9 +26,16 @@ function buildSlotSurface(bag, context) {
25
26
  resolveArguments: (plugin, declared) => (0, collect_branch_rules_1.resolvePluginArguments)(bag, context, buildSlotSurface, plugin, declared),
26
27
  };
27
28
  for (const slot of SLOT_NAMES) {
29
+ // The slot's own type check leads the chain, so it runs before any value
30
+ // rule. Those rules pass a wrong-typed value through on purpose; this is
31
+ // what reports it. See slot-type-guard.ts.
32
+ const guard = context.typeDecidedElsewhere === true
33
+ ? null
34
+ : (0, slot_type_guard_1.slotTypeGuard)(slot, context.config.defaultSeverity);
35
+ const seed = guard === null ? create_chain_node_1.EMPTY_RULES : Object.freeze([guard]);
28
36
  Object.defineProperty(surface, slot, {
29
37
  enumerable: true,
30
- get: () => (0, create_chain_node_1.createChainNode)(wiring, slot, create_chain_node_1.EMPTY_RULES),
38
+ get: () => (0, create_chain_node_1.createChainNode)(wiring, slot, seed),
31
39
  });
32
40
  }
33
41
  return surface;
@@ -1,6 +1,7 @@
1
1
  import { eraseChainSurface } from "../core/type-erasure.mjs";
2
2
  import { createChainNode, EMPTY_RULES, } from "./create-chain-node.mjs";
3
3
  import { resolvePluginArguments } from "./collect-branch-rules.mjs";
4
+ import { slotTypeGuard } from "./slot-type-guard.mjs";
4
5
  const SLOT_NAMES = Object.freeze([
5
6
  "string",
6
7
  "number",
@@ -21,9 +22,16 @@ export function buildSlotSurface(bag, context) {
21
22
  resolveArguments: (plugin, declared) => resolvePluginArguments(bag, context, buildSlotSurface, plugin, declared),
22
23
  };
23
24
  for (const slot of SLOT_NAMES) {
25
+ // The slot's own type check leads the chain, so it runs before any value
26
+ // rule. Those rules pass a wrong-typed value through on purpose; this is
27
+ // what reports it. See slot-type-guard.ts.
28
+ const guard = context.typeDecidedElsewhere === true
29
+ ? null
30
+ : slotTypeGuard(slot, context.config.defaultSeverity);
31
+ const seed = guard === null ? EMPTY_RULES : Object.freeze([guard]);
24
32
  Object.defineProperty(surface, slot, {
25
33
  enumerable: true,
26
- get: () => createChainNode(wiring, slot, EMPTY_RULES),
34
+ get: () => createChainNode(wiring, slot, seed),
27
35
  });
28
36
  }
29
37
  return surface;
@@ -0,0 +1,10 @@
1
+ import type { IssueSeverity, TypeName } from "../types";
2
+ import type { Rule } from "../plugin-kit/compiled-rule";
3
+ /**
4
+ * The rule `b.<slot>` starts its chain with, or null for a slot that claims
5
+ * nothing about the runtime type.
6
+ *
7
+ * The code is `<slot>Type` — `stringType`, `numberType` — which reads as the
8
+ * plugin name it would have had, matching every other code in the library.
9
+ */
10
+ export declare function slotTypeGuard(slot: TypeName, severity: IssueSeverity): Rule | null;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.slotTypeGuard = slotTypeGuard;
4
+ const types_1 = require("../types");
5
+ const create_rule_1 = require("../plugin-kit/create-rule");
6
+ const SLOT_TYPES = Object.freeze({
7
+ string: { accepts: types_1.isString, noun: "a string" },
8
+ number: { accepts: types_1.isNumber, noun: "a number" },
9
+ boolean: {
10
+ accepts: (value) => typeof value === "boolean",
11
+ noun: "a boolean",
12
+ },
13
+ date: { accepts: (value) => value instanceof Date, noun: "a Date" },
14
+ array: { accepts: types_1.isArray, noun: "an array" },
15
+ object: { accepts: types_1.isPlainObject, noun: "an object" },
16
+ });
17
+ /**
18
+ * The rule `b.<slot>` starts its chain with, or null for a slot that claims
19
+ * nothing about the runtime type.
20
+ *
21
+ * The code is `<slot>Type` — `stringType`, `numberType` — which reads as the
22
+ * plugin name it would have had, matching every other code in the library.
23
+ */
24
+ function slotTypeGuard(slot, severity) {
25
+ const slotType = SLOT_TYPES[slot];
26
+ if (slotType === undefined)
27
+ return null;
28
+ return (0, create_rule_1.check)({
29
+ code: `${slot}Type`,
30
+ severity,
31
+ run: (value) => value === undefined || value === null || slotType.accepts(value)
32
+ ? types_1.PASS
33
+ : (0, types_1.fail)({ expected: slotType.noun, actual: value }),
34
+ describe: () => `Value must be ${slotType.noun}`,
35
+ buildMessageContext: () => ({}),
36
+ });
37
+ }
@@ -0,0 +1,34 @@
1
+ import { PASS, fail, isArray, isNumber, isPlainObject, isString, } from "../types/index.mjs";
2
+ import { check } from "../plugin-kit/create-rule.mjs";
3
+ const SLOT_TYPES = Object.freeze({
4
+ string: { accepts: isString, noun: "a string" },
5
+ number: { accepts: isNumber, noun: "a number" },
6
+ boolean: {
7
+ accepts: (value) => typeof value === "boolean",
8
+ noun: "a boolean",
9
+ },
10
+ date: { accepts: (value) => value instanceof Date, noun: "a Date" },
11
+ array: { accepts: isArray, noun: "an array" },
12
+ object: { accepts: isPlainObject, noun: "an object" },
13
+ });
14
+ /**
15
+ * The rule `b.<slot>` starts its chain with, or null for a slot that claims
16
+ * nothing about the runtime type.
17
+ *
18
+ * The code is `<slot>Type` — `stringType`, `numberType` — which reads as the
19
+ * plugin name it would have had, matching every other code in the library.
20
+ */
21
+ export function slotTypeGuard(slot, severity) {
22
+ const slotType = SLOT_TYPES[slot];
23
+ if (slotType === undefined)
24
+ return null;
25
+ return check({
26
+ code: `${slot}Type`,
27
+ severity,
28
+ run: (value) => value === undefined || value === null || slotType.accepts(value)
29
+ ? PASS
30
+ : fail({ expected: slotType.noun, actual: value }),
31
+ describe: () => `Value must be ${slotType.noun}`,
32
+ buildMessageContext: () => ({}),
33
+ });
34
+ }
@@ -58,6 +58,9 @@ function createStructuralContext(seed, node, visitedRefs, scope = seed.scope) {
58
58
  fieldPath: seed.chain.fieldPath,
59
59
  declaredSiblingKeys: propertyKeysOf(node),
60
60
  config: seed.chain.config,
61
+ // The document owns the type here: `type` is a keyword with its own code
62
+ // and a wider vocabulary than a slot has. The slot must not add a second.
63
+ typeDecidedElsewhere: true,
61
64
  };
62
65
  /**
63
66
  * One descent: the node this schema really is, and the context inside it.
@@ -54,6 +54,9 @@ export function createStructuralContext(seed, node, visitedRefs, scope = seed.sc
54
54
  fieldPath: seed.chain.fieldPath,
55
55
  declaredSiblingKeys: propertyKeysOf(node),
56
56
  config: seed.chain.config,
57
+ // The document owns the type here: `type` is a keyword with its own code
58
+ // and a wider vocabulary than a slot has. The slot must not add a second.
59
+ typeDecidedElsewhere: true,
57
60
  };
58
61
  /**
59
62
  * One descent: the node this schema really is, and the context inside it.
@@ -2,6 +2,12 @@ import type { Unchanged } from "../../plugin-kit/marker.types";
2
2
  export interface ObjectTypeContext {
3
3
  readonly actual: string;
4
4
  }
5
+ /**
6
+ * @deprecated Entering `b.object` already checks this, under the code
7
+ * `objectType`. Drop the `.use(objectPlugin)` and the `.object()` call: the
8
+ * field goes on rejecting arrays, `null` and primitives, and only the issue's
9
+ * code and message change.
10
+ */
5
11
  export declare const objectPlugin: import("../../plugin-kit/plugin-definition").PluginDefinition<"object", "object", readonly ["object"], {
6
12
  args: readonly [];
7
13
  out: Unchanged;
@@ -4,14 +4,12 @@ exports.objectPlugin = void 0;
4
4
  const types_1 = require("../../types");
5
5
  const create_rule_1 = require("../../plugin-kit/create-rule");
6
6
  const plugin_definition_1 = require("../../plugin-kit/plugin-definition");
7
- /** "array" is worth naming: it is the mistake this plugin exists to catch. */
8
- function describeType(value) {
9
- if ((0, types_1.isArray)(value))
10
- return "array";
11
- if (value === null)
12
- return "null";
13
- return typeof value;
14
- }
7
+ /**
8
+ * @deprecated Entering `b.object` already checks this, under the code
9
+ * `objectType`. Drop the `.use(objectPlugin)` and the `.object()` call: the
10
+ * field goes on rejecting arrays, `null` and primitives, and only the issue's
11
+ * code and message change.
12
+ */
15
13
  exports.objectPlugin = (0, plugin_definition_1.definePlugin)()({
16
14
  name: "object",
17
15
  method: "object",
@@ -20,9 +18,9 @@ exports.objectPlugin = (0, plugin_definition_1.definePlugin)()({
20
18
  code: ctx.code,
21
19
  messageFactory: ctx.messageFactory,
22
20
  severity: ctx.severity,
23
- run: (value) => (0, types_1.isPlainObject)(value)
24
- ? types_1.PASS
25
- : (0, types_1.fail)({ expected: "object", actual: describeType(value) }),
21
+ run: () => types_1.PASS,
22
+ // Unreachable while run() never fails, and kept so the shape of the rule
23
+ // stays a rule rather than becoming a special case for the engine.
26
24
  describe: (detail) => `Value must be an object, but got ${String(detail.actual)}`,
27
25
  buildMessageContext: (detail) => ({ actual: String(detail.actual) }),
28
26
  }),
@@ -1,14 +1,12 @@
1
- import { PASS, fail, isArray, isPlainObject } from "../../types/index.mjs";
1
+ import { PASS } from "../../types/index.mjs";
2
2
  import { check } from "../../plugin-kit/create-rule.mjs";
3
3
  import { definePlugin } from "../../plugin-kit/plugin-definition.mjs";
4
- /** "array" is worth naming: it is the mistake this plugin exists to catch. */
5
- function describeType(value) {
6
- if (isArray(value))
7
- return "array";
8
- if (value === null)
9
- return "null";
10
- return typeof value;
11
- }
4
+ /**
5
+ * @deprecated Entering `b.object` already checks this, under the code
6
+ * `objectType`. Drop the `.use(objectPlugin)` and the `.object()` call: the
7
+ * field goes on rejecting arrays, `null` and primitives, and only the issue's
8
+ * code and message change.
9
+ */
12
10
  export const objectPlugin = /*#__PURE__*/ definePlugin()({
13
11
  name: "object",
14
12
  method: "object",
@@ -17,9 +15,9 @@ export const objectPlugin = /*#__PURE__*/ definePlugin()({
17
15
  code: ctx.code,
18
16
  messageFactory: ctx.messageFactory,
19
17
  severity: ctx.severity,
20
- run: (value) => isPlainObject(value)
21
- ? PASS
22
- : fail({ expected: "object", actual: describeType(value) }),
18
+ run: () => PASS,
19
+ // Unreachable while run() never fails, and kept so the shape of the rule
20
+ // stays a rule rather than becoming a special case for the engine.
23
21
  describe: (detail) => `Value must be an object, but got ${String(detail.actual)}`,
24
22
  buildMessageContext: (detail) => ({ actual: String(detail.actual) }),
25
23
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedog/luq",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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",