@zipbul/baker 3.3.0 → 3.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @zipbul/baker
2
2
 
3
+ ## 3.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 92028cf: Add two binary-value validation rules to `@zipbul/baker/rules`:
8
+
9
+ - `isUint8Array` — a bare type guard (`value instanceof Uint8Array`), mirroring `isRegExp` /
10
+ `isArray`. Accepts `Uint8Array` and its subclasses (e.g. `Buffer`); rejects `Uint8ClampedArray`,
11
+ `DataView`, and plain arrays.
12
+ - `isByteSize(min, max?)` — the binary analogue of `isByteLength`. Validates the `.byteLength` of
13
+ any `ArrayBuffer.isView(v)` value (all typed arrays + `DataView`), measuring the view window
14
+ rather than the backing buffer. The generated code guards `ArrayBuffer.isView` before any
15
+ `.byteLength` read, so non-views fail cleanly instead of throwing.
16
+
17
+ Both are exported from `@zipbul/baker/rules` as imported identifiers (AOT-safe), are synchronous,
18
+ and compose inside a single `@Field(...)` — e.g. `@Field(isUint8Array, isByteSize(16), { optional: true })`
19
+ for raw key material such as an HKDF salt.
20
+
21
+ ## 3.3.1
22
+
23
+ ### Patch Changes
24
+
25
+ - 8b15524: Fix `@Field({ context, message })` being dropped on non-rule-body failures. Field-level
26
+ context/message are now first-class and attached to EVERY field-own-path failure — the type
27
+ gate (e.g. `isInt` rejecting `NaN`/non-number), required-missing (`isDefined`), implicit
28
+ conversion (`conversionFailed`), and structural array/object gates — plus per-element
29
+ validation of `Set`/`Map` collections, matching the array-element behavior. Previously only
30
+ rule-body failures (e.g. `isInt` rejecting `Infinity`) carried them, so the same field could
31
+ emit an issue with or without `context` depending on which code path failed.
32
+
33
+ Descendant failures (nested child fields, array/collection elements with their own rules) keep
34
+ their own context, and `invalidDiscriminator` keeps its structural context — field context is
35
+ not leaked across paths.
36
+
3
37
  ## 3.3.0
4
38
 
5
39
  ### 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;
@@ -0,0 +1,3 @@
1
+ import type { EmittableRule } from '../types';
2
+ export declare const isUint8Array: import("../types").InternalRule;
3
+ export declare function isByteSize(min: number, max?: number): EmittableRule;
@@ -0,0 +1,51 @@
1
+ import { makeRule } from '../rule-plan.js';
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // isUint8Array — instanceof guard (self-narrowing, no typeof gate; mirrors isRegExp)
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ export const isUint8Array = makeRule({
6
+ name: 'isUint8Array',
7
+ constraints: {},
8
+ validate: value => value instanceof Uint8Array,
9
+ emit: (varName, ctx) => `if (!(${varName} instanceof Uint8Array)) ${ctx.fail('isUint8Array')};`,
10
+ });
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ // isByteSize(min, max?) — byte length of any ArrayBufferView (binary analogue of isByteLength)
13
+ //
14
+ // The ArrayBuffer.isView guard MUST short-circuit before any .byteLength read: reading .byteLength
15
+ // on a non-view yields undefined (and undefined < min is false → would wrongly pass), and on
16
+ // null/undefined it throws. The guard-first else-if chain prevents both. .byteLength is inlined
17
+ // (not aliased to a local like isByteLength) — it is a trivial getter, not an expensive call.
18
+ //
19
+ // min/max are dev-supplied constants; per "trust TS for dev inputs" they are not runtime-guarded,
20
+ // consistent with isByteLength.
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ export function isByteSize(min, max) {
23
+ return makeRule({
24
+ name: 'isByteSize',
25
+ constraints: { min, max },
26
+ // Fail-form mirrors emit exactly (same as isByteLength), so validate() and the generated code
27
+ // agree for ALL inputs — including degenerate NaN bounds, where pass-form (>= NaN) would reject
28
+ // but the emitted (< NaN) accepts, breaking validate/emit parity.
29
+ validate: value => {
30
+ if (!ArrayBuffer.isView(value)) {
31
+ return false;
32
+ }
33
+ const byteLen = value.byteLength;
34
+ if (byteLen < min) {
35
+ return false;
36
+ }
37
+ if (max !== undefined && byteLen > max) {
38
+ return false;
39
+ }
40
+ return true;
41
+ },
42
+ emit: (varName, ctx) => {
43
+ let code = `if (!ArrayBuffer.isView(${varName})) ${ctx.fail('isByteSize')};`;
44
+ code += `\nelse if (${varName}.byteLength < ${min}) ${ctx.fail('isByteSize')};`;
45
+ if (max !== undefined) {
46
+ code += `\nelse if (${varName}.byteLength > ${max}) ${ctx.fail('isByteSize')};`;
47
+ }
48
+ return code;
49
+ },
50
+ });
51
+ }
@@ -10,3 +10,4 @@ export { arrayContains, arrayNotContains, arrayMinSize, arrayMaxSize, arrayUniqu
10
10
  export { isNotEmptyObject, isInstance } from './object';
11
11
  export type { IsNotEmptyObjectOptions } from './object';
12
12
  export { isMobilePhone, isPostalCode, isIdentityCard, isPassportNumber } from './locales';
13
+ export { isUint8Array, isByteSize } from './binary';
@@ -7,3 +7,4 @@ export { minLength, maxLength, length, contains, notContains, matches, isLowerca
7
7
  export { arrayContains, arrayNotContains, arrayMinSize, arrayMaxSize, arrayUnique, arrayNotEmpty } from './array.js';
8
8
  export { isNotEmptyObject, isInstance } from './object.js';
9
9
  export { isMobilePhone, isPostalCode, isIdentityCard, isPassportNumber } from './locales.js';
10
+ export { isUint8Array, isByteSize } from './binary.js';
@@ -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.3.0",
3
+ "version": "3.4.0",
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",