@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 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';
@@ -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
- const emitCtx = makeEmitCtx(fieldKey, ctx);
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
- // Helper for computing per-rule extra fields (message/context) code strings
418
+ // Helpers for computing message/context extra fields in generated issue objects
417
419
  // ─────────────────────────────────────────────────────────────────────────────
418
- /** Convert rule's message/context options into extra field strings within generated code */
419
- function computeRuleExtras(rd, fieldKey, varName, ctx) {
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 rd.message === 'string') {
422
- extra += `,message:${JSON.stringify(rd.message)}`;
425
+ if (typeof message === 'string') {
426
+ extra += `,message:${JSON.stringify(message)}`;
423
427
  }
424
- else if (typeof rd.message === 'function') {
428
+ else if (typeof message === 'function') {
425
429
  const msgIdx = ctx.refs.length;
426
- ctx.refs.push(rd.message);
427
- const constraintsIdx = ctx.refs.length;
428
- ctx.refs.push(rd.rule.constraints ?? {});
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 (rd.context !== undefined) {
434
+ if (context !== undefined) {
432
435
  const ctxIdx = ctx.refs.length;
433
- ctx.refs.push(rd.context);
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 { ${GEN.errList}.push({path:${pathKey},code:'isArray'}); }\n`;
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,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zipbul/baker",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Bun-only AOT decorator-based DTO validation & serialization. class-validator DX, sealed code generation, zero reflect-metadata.",
5
5
  "keywords": [
6
6
  "aot",