@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 +12 -1
- package/dist/json-schema/declare-value-keywords.d.ts +0 -2
- package/dist/json-schema/declare-value-keywords.js +7 -10
- package/dist/json-schema/declare-value-keywords.mjs +7 -9
- package/dist/json-schema/extensions/json-schema-full-feature/bundle-coverage.js +6 -1
- package/dist/json-schema/extensions/json-schema-full-feature/bundle-coverage.mjs +6 -1
- package/dist/json-schema/schema-to-declarations.js +1 -4
- package/dist/json-schema/schema-to-declarations.mjs +2 -5
- package/dist/runtime/create-issue.d.ts +9 -0
- package/dist/runtime/create-issue.js +9 -2
- package/dist/runtime/create-issue.mjs +9 -2
- package/dist/runtime/run-field.js +1 -0
- package/dist/runtime/run-field.mjs +1 -0
- package/dist/types/index.d.ts +12 -0
- package/package.json +15 -6
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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"
|