@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 +34 -0
- package/dist/src/decorators/field.js +9 -0
- package/dist/src/rules/binary.d.ts +3 -0
- package/dist/src/rules/binary.js +51 -0
- package/dist/src/rules/index.d.ts +1 -0
- package/dist/src/rules/index.js +1 -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,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,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';
|
package/dist/src/rules/index.js
CHANGED
|
@@ -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
|
-
|
|
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