@maroonedog/luq 2.6.0 → 2.7.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 CHANGED
@@ -140,6 +140,7 @@ if (!result.valid) {
140
140
  // issue.code "stringMin" — which rule; the vocabulary is pinned
141
141
  // issue.message — the text, overridable per call
142
142
  // issue.severity "error" — only "error" makes the value invalid
143
+ // issue.causes — present only on a composite: why it failed
143
144
  }
144
145
  }
145
146
  ```
@@ -155,6 +156,12 @@ missing field both report `required`. Codes a gate carries (`skip`,
155
156
  `validateIf`) sit in a separate list: they are accepted from a caller but no
156
157
  issue can ever carry one.
157
158
 
159
+ A composite reports which applicator failed — `allOf`, `anyOf`, `oneOf` — and
160
+ that code cannot say why. `issue.causes` carries the branch failures behind it,
161
+ each a full issue with its own code, so `allOf` can be read down to the
162
+ `stringMin` underneath it. The key is **absent** on an ordinary issue rather
163
+ than present and undefined, so `"causes" in issue` is a question worth asking.
164
+
158
165
  `.v(path, chain)` declares rules for one field. A path you do not declare is
159
166
  not validated, not required and not read, so covering a type partly is a normal
160
167
  state rather than a half-finished one.
@@ -215,7 +222,11 @@ accounts.pickAll(["email"]); // several fields, same plan
215
222
 
216
223
  `validate` and `parse` return the same discriminated union: `{ valid: true,
217
224
  data, issues }` or `{ valid: false, issues }`. `validate` hands back the object
218
- you passed, by identity, when nothing was written.
225
+ you passed, by identity, when nothing was written. Only `parse` runs your
226
+ transform functions, so only `parse` can throw what one of them throws: a throw
227
+ from a transform is a programmer error, not a validation issue, and zod, valibot
228
+ and yup all propagate it the same way. To reject a value instead, use a check
229
+ such as [`custom`](https://luq.dev/docs/api/validator#transform-throws).
219
230
 
220
231
  ### Slots
221
232
 
@@ -20,8 +20,6 @@ export declare function permitsNull(schema: Draft07SchemaObject): boolean;
20
20
  * that fails all of them.
21
21
  */
22
22
  export declare function declareTypeRules(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
23
- /** `type: "integer"` is the only `type` member that adds a chain method. */
24
- export declare function declareIntegerRules(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
25
23
  export declare function declareConstRules(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
26
24
  /**
27
25
  * `enum` drives the `.oneOf()` METHOD, which is a different thing from the
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readDeclaredTypes = readDeclaredTypes;
4
4
  exports.permitsNull = permitsNull;
5
5
  exports.declareTypeRules = declareTypeRules;
6
- exports.declareIntegerRules = declareIntegerRules;
7
6
  exports.declareConstRules = declareConstRules;
8
7
  exports.declareEnumRules = declareEnumRules;
9
8
  // ===========================================================================
@@ -41,6 +40,13 @@ const NO_RULES = Object.freeze([]);
41
40
  const JSON_TYPE_TESTS = {
42
41
  string: types_1.isString,
43
42
  number: (value) => typeof value === "number",
43
+ // The whole of `type: "integer"`. A second rule built from the numberInteger
44
+ // plugin used to run beside this one with its code overridden to `type`,
45
+ // which put two issues coded `type` at one path for one value — and (path,
46
+ // code) is the pair a machine consumer de-duplicates on, so one of them
47
+ // disappeared without trace. This test is the same predicate the plugin
48
+ // applies, so removing the second rule changed no verdict: the conformance
49
+ // corpus stayed at 929 / 929.
44
50
  integer: (value) => typeof value === "number" && Number.isInteger(value),
45
51
  boolean: (value) => typeof value === "boolean",
46
52
  array: types_1.isArray,
@@ -126,15 +132,6 @@ function declareTypeRules(schema, context) {
126
132
  }),
127
133
  ]);
128
134
  }
129
- /** `type: "integer"` is the only `type` member that adds a chain method. */
130
- function declareIntegerRules(schema, context) {
131
- if (!readDeclaredTypes(schema).includes("integer"))
132
- return NO_RULES;
133
- const plugin = context.bag.numberInteger;
134
- return Object.freeze([
135
- plugin.build(context.ruleContextFor(plugin.name, "type")),
136
- ]);
137
- }
138
135
  function declareConstRules(schema, context) {
139
136
  if (!Object.prototype.hasOwnProperty.call(schema, "const"))
140
137
  return NO_RULES;
@@ -33,6 +33,13 @@ const NO_RULES = Object.freeze([]);
33
33
  const JSON_TYPE_TESTS = {
34
34
  string: isString,
35
35
  number: (value) => typeof value === "number",
36
+ // The whole of `type: "integer"`. A second rule built from the numberInteger
37
+ // plugin used to run beside this one with its code overridden to `type`,
38
+ // which put two issues coded `type` at one path for one value — and (path,
39
+ // code) is the pair a machine consumer de-duplicates on, so one of them
40
+ // disappeared without trace. This test is the same predicate the plugin
41
+ // applies, so removing the second rule changed no verdict: the conformance
42
+ // corpus stayed at 929 / 929.
36
43
  integer: (value) => typeof value === "number" && Number.isInteger(value),
37
44
  boolean: (value) => typeof value === "boolean",
38
45
  array: isArray,
@@ -118,15 +125,6 @@ export function declareTypeRules(schema, context) {
118
125
  }),
119
126
  ]);
120
127
  }
121
- /** `type: "integer"` is the only `type` member that adds a chain method. */
122
- export function declareIntegerRules(schema, context) {
123
- if (!readDeclaredTypes(schema).includes("integer"))
124
- return NO_RULES;
125
- const plugin = context.bag.numberInteger;
126
- return Object.freeze([
127
- plugin.build(context.ruleContextFor(plugin.name, "type")),
128
- ]);
129
- }
130
128
  export function declareConstRules(schema, context) {
131
129
  if (!Object.prototype.hasOwnProperty.call(schema, "const"))
132
130
  return NO_RULES;
@@ -44,7 +44,6 @@ exports.UNBOUND_BUNDLED_PLUGIN_NAMES = Object.freeze([
44
44
  "arrayContains",
45
45
  "arrayEach",
46
46
  "conditionalSchema",
47
- "numberInteger",
48
47
  "objectAdditionalPropertiesSchema",
49
48
  "objectDependentRequired",
50
49
  "objectDependentSchemas",
@@ -53,6 +52,12 @@ exports.UNBOUND_BUNDLED_PLUGIN_NAMES = Object.freeze([
53
52
  "oneOf",
54
53
  // Reachable only from a hand-written chain; the converter never calls them.
55
54
  "compareField",
55
+ // numberInteger moved into this group when the converter stopped building a
56
+ // second rule from it for `type: "integer"`: the type table's own predicate
57
+ // is `Number.isInteger`, so the plugin's rule only ever duplicated it. It
58
+ // stays in the bag because `.integer()` is still a method a hand-written
59
+ // chain can call.
60
+ "numberInteger",
56
61
  "nullable",
57
62
  "optional",
58
63
  "tupleBuilder",
@@ -37,7 +37,6 @@ export const UNBOUND_BUNDLED_PLUGIN_NAMES = Object.freeze([
37
37
  "arrayContains",
38
38
  "arrayEach",
39
39
  "conditionalSchema",
40
- "numberInteger",
41
40
  "objectAdditionalPropertiesSchema",
42
41
  "objectDependentRequired",
43
42
  "objectDependentSchemas",
@@ -46,6 +45,12 @@ export const UNBOUND_BUNDLED_PLUGIN_NAMES = Object.freeze([
46
45
  "oneOf",
47
46
  // Reachable only from a hand-written chain; the converter never calls them.
48
47
  "compareField",
48
+ // numberInteger moved into this group when the converter stopped building a
49
+ // second rule from it for `type: "integer"`: the type table's own predicate
50
+ // is `Number.isInteger`, so the plugin's rule only ever duplicated it. It
51
+ // stays in the bag because `.integer()` is still a method a hand-written
52
+ // chain can call.
53
+ "numberInteger",
49
54
  "nullable",
50
55
  "optional",
51
56
  "tupleBuilder",
@@ -30,10 +30,7 @@ exports.STRUCTURAL_EXPANSIONS = {
30
30
  },
31
31
  type: {
32
32
  expandsTo: "rules",
33
- toRules: (schema, context) => [
34
- ...(0, declare_value_keywords_1.declareTypeRules)(schema, context),
35
- ...(0, declare_value_keywords_1.declareIntegerRules)(schema, context),
36
- ],
33
+ toRules: declare_value_keywords_1.declareTypeRules,
37
34
  },
38
35
  enum: { expandsTo: "rules", toRules: declare_value_keywords_1.declareEnumRules },
39
36
  allOf: { expandsTo: "rules", toRules: compose_keyword_1.composeAllOf },
@@ -5,7 +5,7 @@ import { declareAdditionalPropertiesSchema } from "./declare-additional-properti
5
5
  import { declareDependencies, declareObjectRules, declarePatternProperties, declarePropertyNames, readPropertyChildren, } from "./declare-object-keywords.mjs";
6
6
  import { declareRequiredProperties } from "./declare-required-properties.mjs";
7
7
  import { declareFormatRules, declareNumberRules, declareStringRules, } from "./declare-scalar-keywords.mjs";
8
- import { declareConstRules, declareEnumRules, declareIntegerRules, declareTypeRules, } from "./declare-value-keywords.mjs";
8
+ import { declareConstRules, declareEnumRules, declareTypeRules, } from "./declare-value-keywords.mjs";
9
9
  import { composeContains, composeTupleItems, declareArrayRules, readItemChildren, } from "./flatten-array-schema.mjs";
10
10
  import { NON_DRAFT07_KEYWORDS, findKeywordHandling } from "./keyword-map.mjs";
11
11
  import { readChildExpansion, readRuleExpansion, } from "./structural-expansion.types.mjs";
@@ -24,10 +24,7 @@ export const STRUCTURAL_EXPANSIONS = {
24
24
  },
25
25
  type: {
26
26
  expandsTo: "rules",
27
- toRules: (schema, context) => [
28
- ...declareTypeRules(schema, context),
29
- ...declareIntegerRules(schema, context),
30
- ],
27
+ toRules: declareTypeRules,
31
28
  },
32
29
  enum: { expandsTo: "rules", toRules: declareEnumRules },
33
30
  allOf: { expandsTo: "rules", toRules: composeAllOf },
@@ -13,6 +13,15 @@ export interface IssueRequest {
13
13
  * sees one call shape and needs no rule kind to dispatch on.
14
14
  */
15
15
  render(context: MessageContext): string;
16
+ /**
17
+ * The branch failures behind a composite, when the rule that failed is one.
18
+ *
19
+ * Passed rather than read off the detail because this layer never sees a
20
+ * rule kind: a presence policy closes over nothing and a check closes over
21
+ * its own IssueDetail, and keeping that asymmetry out of here is what lets
22
+ * one call shape serve both.
23
+ */
24
+ readonly causes?: readonly ValidationIssue[] | undefined;
16
25
  }
17
26
  /**
18
27
  * A plugin whose message factory throws must not take the validation down
@@ -17,12 +17,19 @@ function createIssue(request) {
17
17
  value: request.value,
18
18
  code: request.code,
19
19
  };
20
- return Object.freeze({
20
+ const issue = {
21
21
  path: request.path,
22
22
  code: request.code,
23
23
  message: renderMessageOrFallback(request, context),
24
24
  severity: request.severity,
25
- });
25
+ };
26
+ // Spread CONDITIONALLY. Writing `causes: request.causes` unconditionally
27
+ // would put the key on every issue the library reports, with the value
28
+ // undefined — and `"causes" in issue` would then answer true for a plain
29
+ // stringMin failure, which is exactly the question a caller asks it.
30
+ return Object.freeze(request.causes === undefined || request.causes.length === 0
31
+ ? issue
32
+ : { ...issue, causes: request.causes });
26
33
  }
27
34
  function renderMessageOrFallback(request, context) {
28
35
  try {
@@ -13,12 +13,19 @@ export function createIssue(request) {
13
13
  value: request.value,
14
14
  code: request.code,
15
15
  };
16
- return Object.freeze({
16
+ const issue = {
17
17
  path: request.path,
18
18
  code: request.code,
19
19
  message: renderMessageOrFallback(request, context),
20
20
  severity: request.severity,
21
- });
21
+ };
22
+ // Spread CONDITIONALLY. Writing `causes: request.causes` unconditionally
23
+ // would put the key on every issue the library reports, with the value
24
+ // undefined — and `"causes" in issue` would then answer true for a plain
25
+ // stringMin failure, which is exactly the question a caller asks it.
26
+ return Object.freeze(request.causes === undefined || request.causes.length === 0
27
+ ? issue
28
+ : { ...issue, causes: request.causes });
22
29
  }
23
30
  function renderMessageOrFallback(request, context) {
24
31
  try {
@@ -78,6 +78,7 @@ function reportCheckFailure(check, detail, value, ruleContext, context) {
78
78
  severity: check.severity,
79
79
  value,
80
80
  render: (ctx) => check.describe(detail, ctx),
81
+ causes: detail.causes,
81
82
  }));
82
83
  }
83
84
  function runChecks(field, value, ruleContext, context, mark) {
@@ -74,6 +74,7 @@ function reportCheckFailure(check, detail, value, ruleContext, context) {
74
74
  severity: check.severity,
75
75
  value,
76
76
  render: (ctx) => check.describe(detail, ctx),
77
+ causes: detail.causes,
77
78
  }));
78
79
  }
79
80
  function runChecks(field, value, ruleContext, context, mark) {
@@ -26,6 +26,18 @@ export interface ValidationIssue {
26
26
  /** Never optional: the chain resolves it once at build time, so a reader
27
27
  * never has to re-implement the "absent means error" fallback. */
28
28
  readonly severity: IssueSeverity;
29
+ /**
30
+ * Why a composite failed, when the failing rule is one.
31
+ *
32
+ * `allOf`, `anyOf` and `oneOf` report their own code, which says WHICH
33
+ * applicator failed and never why. The branch failures behind it land here,
34
+ * so a caller can reach "minLength" without re-running the sub-schemas.
35
+ *
36
+ * ABSENT, not undefined, on an ordinary issue. A key present everywhere
37
+ * would make `"causes" in issue` answer true for every issue the library
38
+ * reports, which is the opposite of what it is for.
39
+ */
40
+ readonly causes?: readonly ValidationIssue[];
29
41
  }
30
42
  /** branch / index / causes let a composite failure explain itself. */
31
43
  export interface IssueDetail {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maroonedog/luq",
3
- "version": "2.6.0",
4
- "description": "Universal Model & API Definition Platform - TypeScript validation library evolving into cross-language code generation",
3
+ "version": "2.7.0",
4
+ "description": "Validate the TypeScript types you already have. Rules are declared against your own type's field paths, so a rule that does not apply to its field is a compile error.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -510,13 +510,22 @@
510
510
  "url": "https://github.com/maroonedog/luq/issues"
511
511
  },
512
512
  "keywords": [
513
- "form",
514
- "validation",
515
513
  "typescript",
514
+ "validation",
516
515
  "validator",
517
- "library"
516
+ "schema",
517
+ "json-schema",
518
+ "draft-07",
519
+ "openapi",
520
+ "standard-schema",
521
+ "standardschema",
522
+ "type-safe",
523
+ "form-validation",
524
+ "runtime-validation",
525
+ "content-security-policy",
526
+ "zero-dependencies"
518
527
  ],
519
- "author": "",
528
+ "author": "maroonedog (https://github.com/maroonedog)",
520
529
  "license": "MIT",
521
530
  "files": [
522
531
  "dist"