@zipbul/baker 3.2.0 → 3.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.
- package/CHANGELOG.md +28 -0
- package/dist/src/decorators/field.js +9 -0
- package/dist/src/rules/index.d.ts +1 -1
- package/dist/src/rules/index.js +1 -1
- package/dist/src/rules/typechecker.d.ts +1 -0
- package/dist/src/rules/typechecker.js +10 -0
- package/dist/src/seal/deserialize-builder.js +39 -21
- package/dist/src/types.d.ts +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @zipbul/baker
|
|
2
2
|
|
|
3
|
+
## 3.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8b15524: Fix `@Field({ context, message })` being dropped on non-rule-body failures. Field-level
|
|
8
|
+
context/message are now first-class and attached to EVERY field-own-path failure — the type
|
|
9
|
+
gate (e.g. `isInt` rejecting `NaN`/non-number), required-missing (`isDefined`), implicit
|
|
10
|
+
conversion (`conversionFailed`), and structural array/object gates — plus per-element
|
|
11
|
+
validation of `Set`/`Map` collections, matching the array-element behavior. Previously only
|
|
12
|
+
rule-body failures (e.g. `isInt` rejecting `Infinity`) carried them, so the same field could
|
|
13
|
+
emit an issue with or without `context` depending on which code path failed.
|
|
14
|
+
|
|
15
|
+
Descendant failures (nested child fields, array/collection elements with their own rules) keep
|
|
16
|
+
their own context, and `invalidDiscriminator` keeps its structural context — field context is
|
|
17
|
+
not leaked across paths.
|
|
18
|
+
|
|
19
|
+
## 3.3.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 317e536: Add the `isStatelessRegExp` type-checker rule: a value is valid if it is a `RegExp`
|
|
24
|
+
without the `g` (global) or `y` (sticky) flag. Those two flags make
|
|
25
|
+
`RegExp.prototype.test`/`exec` mutate `lastIndex` across calls, so a regex carrying them
|
|
26
|
+
produces order-dependent results when reused as a single-shot matcher. `isStatelessRegExp`
|
|
27
|
+
rejects them at validation time (all other flags — `d`, `i`, `m`, `s`, `u`, `v` — are
|
|
28
|
+
stateless and pass). It is the safe-form sibling of `isRegExp` (which is unchanged).
|
|
29
|
+
Exported from `@zipbul/baker/rules`.
|
|
30
|
+
|
|
3
31
|
## 3.2.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
|
@@ -224,6 +224,15 @@ function Field(...args) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
applyValidation(meta, rules, options);
|
|
227
|
+
// Field-level message/context — stored regardless of rules so non-rule failures
|
|
228
|
+
// (type gate, required-missing, conversion, structural gates) and type-only fields
|
|
229
|
+
// can carry them, not just rule-body failures.
|
|
230
|
+
if (options.context !== undefined) {
|
|
231
|
+
meta.context = options.context;
|
|
232
|
+
}
|
|
233
|
+
if (options.message !== undefined) {
|
|
234
|
+
meta.message = options.message;
|
|
235
|
+
}
|
|
227
236
|
// ── flags ──
|
|
228
237
|
if (options.optional) {
|
|
229
238
|
meta.flags.isOptional = true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { isString, isNumber, isBoolean, isDate, isEnum, isInt, isArray, isObject, isRegExp, isFunction } from './typechecker';
|
|
1
|
+
export { isString, isNumber, isBoolean, isDate, isEnum, isInt, isArray, isObject, isRegExp, isFunction, isStatelessRegExp, } from './typechecker';
|
|
2
2
|
export type { IsNumberOptions } from './typechecker';
|
|
3
3
|
export { oneOf, arrayEvery } from './combinators';
|
|
4
4
|
export { min, max, isPositive, isNegative, isDivisibleBy } from './number';
|
package/dist/src/rules/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { isString, isNumber, isBoolean, isDate, isEnum, isInt, isArray, isObject, isRegExp, isFunction } from './typechecker.js';
|
|
1
|
+
export { isString, isNumber, isBoolean, isDate, isEnum, isInt, isArray, isObject, isRegExp, isFunction, isStatelessRegExp, } from './typechecker.js';
|
|
2
2
|
export { oneOf, arrayEvery } from './combinators.js';
|
|
3
3
|
export { min, max, isPositive, isNegative, isDivisibleBy } from './number.js';
|
|
4
4
|
export { minDate, maxDate } from './date.js';
|
|
@@ -14,3 +14,4 @@ export declare const isArray: import("../types").InternalRule;
|
|
|
14
14
|
export declare const isObject: import("../types").InternalRule;
|
|
15
15
|
export declare const isRegExp: import("../types").InternalRule;
|
|
16
16
|
export declare const isFunction: import("../types").InternalRule;
|
|
17
|
+
export declare const isStatelessRegExp: import("../types").InternalRule;
|
|
@@ -159,3 +159,13 @@ export const isFunction = makeRule({
|
|
|
159
159
|
validate: value => typeof value === 'function',
|
|
160
160
|
emit: (varName, ctx) => `if (typeof ${varName} !== 'function') ${ctx.fail('isFunction')};`,
|
|
161
161
|
});
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
// isStatelessRegExp — RegExp without g/y (the only flags that mutate lastIndex
|
|
164
|
+
// across repeated test()/exec()). Safe for reuse as a single-shot matcher.
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
export const isStatelessRegExp = makeRule({
|
|
167
|
+
name: 'isStatelessRegExp',
|
|
168
|
+
constraints: {},
|
|
169
|
+
validate: value => value instanceof RegExp && !value.global && !value.sticky,
|
|
170
|
+
emit: (varName, ctx) => `if (!(${varName} instanceof RegExp) || ${varName}.global || ${varName}.sticky) ${ctx.fail('isStatelessRegExp')};`,
|
|
171
|
+
});
|
|
@@ -295,8 +295,10 @@ function generateFieldCode(fieldKey, meta, ctx) {
|
|
|
295
295
|
const extractKey = getDeserializeExtractKey(fieldKey, meta.expose);
|
|
296
296
|
const exposeGroups = getDeserializeExposeGroups(meta.expose);
|
|
297
297
|
const inputObj = ctx.inputExpr || 'input';
|
|
298
|
-
// Create EmitContext
|
|
299
|
-
|
|
298
|
+
// Create EmitContext — bake field-level message/context so EVERY field-own-path failure
|
|
299
|
+
// (gate, required-missing, conversion, structural gates) carries them, not just rule bodies.
|
|
300
|
+
const fieldExtras = computeFieldExtras(meta, fieldKey, varName, ctx);
|
|
301
|
+
const emitCtx = makeEmitCtx(fieldKey, ctx, fieldExtras);
|
|
300
302
|
let fieldCode = '';
|
|
301
303
|
// ① @ValidateIf guard
|
|
302
304
|
let validateIfIdx = null;
|
|
@@ -413,28 +415,43 @@ function generateValidationCode(fieldKey, varName, meta, ctx, emitCtx, fieldGrou
|
|
|
413
415
|
return code;
|
|
414
416
|
}
|
|
415
417
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
416
|
-
//
|
|
418
|
+
// Helpers for computing message/context extra fields in generated issue objects
|
|
417
419
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
+
/** Build the `,message:...,context:...` extras string for a generated issue object.
|
|
421
|
+
* `getConstraintsArg` produces the JS expression for a message function's `constraints`
|
|
422
|
+
* field; it runs AFTER the message ref is pushed, preserving ref-array order. */
|
|
423
|
+
function buildIssueExtras(message, context, getConstraintsArg, fieldKey, varName, ctx) {
|
|
420
424
|
let extra = '';
|
|
421
|
-
if (typeof
|
|
422
|
-
extra += `,message:${JSON.stringify(
|
|
425
|
+
if (typeof message === 'string') {
|
|
426
|
+
extra += `,message:${JSON.stringify(message)}`;
|
|
423
427
|
}
|
|
424
|
-
else if (typeof
|
|
428
|
+
else if (typeof message === 'function') {
|
|
425
429
|
const msgIdx = ctx.refs.length;
|
|
426
|
-
ctx.refs.push(
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
extra += `,message:refs[${msgIdx}]({property:${JSON.stringify(fieldKey)},value:${varName},constraints:refs[${constraintsIdx}]})`;
|
|
430
|
+
ctx.refs.push(message);
|
|
431
|
+
const constraintsArg = getConstraintsArg();
|
|
432
|
+
extra += `,message:refs[${msgIdx}]({property:${JSON.stringify(fieldKey)},value:${varName},constraints:${constraintsArg}})`;
|
|
430
433
|
}
|
|
431
|
-
if (
|
|
434
|
+
if (context !== undefined) {
|
|
432
435
|
const ctxIdx = ctx.refs.length;
|
|
433
|
-
ctx.refs.push(
|
|
436
|
+
ctx.refs.push(context);
|
|
434
437
|
extra += `,context:refs[${ctxIdx}]`;
|
|
435
438
|
}
|
|
436
439
|
return extra;
|
|
437
440
|
}
|
|
441
|
+
/** Per-rule extras — a message function receives the failing rule's `constraints`. */
|
|
442
|
+
function computeRuleExtras(rd, fieldKey, varName, ctx) {
|
|
443
|
+
return buildIssueExtras(rd.message, rd.context, () => {
|
|
444
|
+
const constraintsIdx = ctx.refs.length;
|
|
445
|
+
ctx.refs.push(rd.rule.constraints ?? {});
|
|
446
|
+
return `refs[${constraintsIdx}]`;
|
|
447
|
+
}, fieldKey, varName, ctx);
|
|
448
|
+
}
|
|
449
|
+
/** Field-level extras appended to EVERY failure of a field — including non-rule failures
|
|
450
|
+
* (type gate, required-missing, conversion, structural gates) and type-only fields. No
|
|
451
|
+
* specific rule applies, so a message function gets `constraints:{}`. */
|
|
452
|
+
function computeFieldExtras(meta, fieldKey, varName, ctx) {
|
|
453
|
+
return buildIssueExtras(meta.message, meta.context, () => '{}', fieldKey, varName, ctx);
|
|
454
|
+
}
|
|
438
455
|
/** Create per-rule EmitContext (with message/context overrides) */
|
|
439
456
|
function makeRuleEmitCtx(baseEmitCtx, fieldKey, varName, rd, ctx) {
|
|
440
457
|
const extra = computeRuleExtras(rd, fieldKey, varName, ctx);
|
|
@@ -862,7 +879,7 @@ function emitEachRules(fieldKey, varName, eachRules, collectErrors, emitCtx, ctx
|
|
|
862
879
|
code += emitCollectionBlock(collections[1]);
|
|
863
880
|
code += `} else if (${kindVar} === 3) {\n`;
|
|
864
881
|
code += emitCollectionBlock(collections[2]);
|
|
865
|
-
code += `} else { ${
|
|
882
|
+
code += `} else { ${emitCtx.fail('isArray')}; }\n`;
|
|
866
883
|
}
|
|
867
884
|
else {
|
|
868
885
|
code += `if (${kindVar} === 0) ${emitCtx.fail('isArray')};\n`;
|
|
@@ -998,9 +1015,10 @@ function generateCollectionCode(fieldKey, varName, meta, ctx, emitCtx) {
|
|
|
998
1015
|
code += ` var ${siVar} = 0;\n`;
|
|
999
1016
|
code += ` for (var ${svVar} of ${GEN.out}[${JSON.stringify(fieldKey)}]) {\n`;
|
|
1000
1017
|
for (const rd of eachRules) {
|
|
1018
|
+
const extra = computeRuleExtras(rd, fieldKey, varName, ctx);
|
|
1001
1019
|
const failFn = (c) => collectErrors
|
|
1002
|
-
? `${GEN.errList}.push({path:${JSON.stringify(fieldKey)}+'['+${siVar}+']',code:${JSON.stringify(c)}})`
|
|
1003
|
-
: `return err([{path:${JSON.stringify(fieldKey)}+'['+${siVar}+']',code:${JSON.stringify(c)}}])`;
|
|
1020
|
+
? `${GEN.errList}.push({path:${JSON.stringify(fieldKey)}+'['+${siVar}+']',code:${JSON.stringify(c)}${extra}})`
|
|
1021
|
+
: `return err([{path:${JSON.stringify(fieldKey)}+'['+${siVar}+']',code:${JSON.stringify(c)}${extra}}])`;
|
|
1004
1022
|
const colEmitCtx = { ...emitCtx, fail: failFn };
|
|
1005
1023
|
code += ` ${rd.rule.emit(svVar, colEmitCtx)}\n`;
|
|
1006
1024
|
}
|
|
@@ -1514,7 +1532,7 @@ function generateCollectionCodeValidateOnly(fieldKey, varName, meta, ctx, emitCt
|
|
|
1514
1532
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1515
1533
|
// makeEmitCtx — create per-field EmitContext
|
|
1516
1534
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1517
|
-
function makeEmitCtx(fieldKey, ctx) {
|
|
1535
|
+
function makeEmitCtx(fieldKey, ctx, fieldExtras = '') {
|
|
1518
1536
|
const { collectErrors, regexes, refs, execs, validateOnly, pathPrefix } = ctx;
|
|
1519
1537
|
const pathExpr = pathPrefix ? `${pathPrefix}+${JSON.stringify(fieldKey)}` : JSON.stringify(fieldKey);
|
|
1520
1538
|
return {
|
|
@@ -1532,12 +1550,12 @@ function makeEmitCtx(fieldKey, ctx) {
|
|
|
1532
1550
|
},
|
|
1533
1551
|
fail(code) {
|
|
1534
1552
|
if (collectErrors) {
|
|
1535
|
-
return `${GEN.errList}.push({path:${pathExpr},code:${JSON.stringify(code)}})`;
|
|
1553
|
+
return `${GEN.errList}.push({path:${pathExpr},code:${JSON.stringify(code)}${fieldExtras}})`;
|
|
1536
1554
|
}
|
|
1537
1555
|
else if (validateOnly) {
|
|
1538
|
-
return `return [{path:${pathExpr},code:${JSON.stringify(code)}}]`;
|
|
1556
|
+
return `return [{path:${pathExpr},code:${JSON.stringify(code)}${fieldExtras}}]`;
|
|
1539
1557
|
}
|
|
1540
|
-
return `return err([{path:${pathExpr},code:${JSON.stringify(code)}}])`;
|
|
1558
|
+
return `return err([{path:${pathExpr},code:${JSON.stringify(code)}${fieldExtras}}])`;
|
|
1541
1559
|
},
|
|
1542
1560
|
collectErrors,
|
|
1543
1561
|
pathExpr: pathExpr,
|
package/dist/src/types.d.ts
CHANGED
|
@@ -152,6 +152,10 @@ export interface RawPropertyMeta {
|
|
|
152
152
|
exclude: ExcludeDef | null;
|
|
153
153
|
type: TypeDef | null;
|
|
154
154
|
flags: PropertyFlags;
|
|
155
|
+
/** Field-level message applied to ALL failures of this field (gate/structural/required/conversion/rule) */
|
|
156
|
+
message?: string | ((args: MessageArgs) => string);
|
|
157
|
+
/** Field-level context attached to ALL failures of this field */
|
|
158
|
+
context?: unknown;
|
|
155
159
|
}
|
|
156
160
|
export interface RawClassMeta {
|
|
157
161
|
[propertyKey: string]: RawPropertyMeta;
|
package/package.json
CHANGED