@zipbul/baker 2.2.0 → 3.0.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +132 -69
  3. package/dist/index.d.ts +7 -6
  4. package/dist/index.js +10 -321
  5. package/dist/src/collect.d.ts +13 -10
  6. package/dist/src/collect.js +26 -0
  7. package/dist/src/configure.d.ts +8 -6
  8. package/dist/src/configure.js +43 -0
  9. package/dist/src/create-rule.js +41 -0
  10. package/dist/src/decorators/field.d.ts +22 -18
  11. package/dist/src/decorators/field.js +268 -0
  12. package/dist/src/decorators/index.d.ts +1 -0
  13. package/dist/src/decorators/index.js +2 -2
  14. package/dist/src/decorators/recipe.d.ts +17 -0
  15. package/dist/src/decorators/recipe.js +23 -0
  16. package/dist/src/errors.d.ts +27 -17
  17. package/dist/src/errors.js +52 -0
  18. package/dist/src/functions/check-call-options.d.ts +8 -0
  19. package/dist/src/functions/check-call-options.js +51 -0
  20. package/dist/src/functions/deserialize.d.ts +13 -6
  21. package/dist/src/functions/deserialize.js +57 -0
  22. package/dist/src/functions/serialize.d.ts +10 -4
  23. package/dist/src/functions/serialize.js +52 -0
  24. package/dist/src/functions/validate.d.ts +13 -10
  25. package/dist/src/functions/validate.js +49 -0
  26. package/dist/src/interfaces.d.ts +1 -1
  27. package/dist/src/interfaces.js +4 -0
  28. package/dist/src/meta-access.d.ts +19 -0
  29. package/dist/src/meta-access.js +75 -0
  30. package/dist/src/registry.js +8 -0
  31. package/dist/src/rule-metadata.d.ts +11 -0
  32. package/dist/src/rule-metadata.js +17 -0
  33. package/dist/src/rule-plan.d.ts +10 -11
  34. package/dist/src/rule-plan.js +117 -0
  35. package/dist/src/rules/array.d.ts +7 -6
  36. package/dist/src/rules/array.js +96 -0
  37. package/dist/src/rules/common.js +77 -0
  38. package/dist/src/rules/date.js +35 -0
  39. package/dist/src/rules/index.d.ts +2 -4
  40. package/dist/src/rules/index.js +8 -21
  41. package/dist/src/rules/locales.d.ts +5 -4
  42. package/dist/src/rules/locales.js +249 -0
  43. package/dist/src/rules/number.js +79 -0
  44. package/dist/src/rules/object.d.ts +1 -1
  45. package/dist/src/rules/object.js +49 -0
  46. package/dist/src/rules/string.d.ts +83 -80
  47. package/dist/src/rules/string.js +1998 -0
  48. package/dist/src/rules/typechecker.js +143 -0
  49. package/dist/src/seal/circular-analyzer.js +63 -0
  50. package/dist/src/seal/codegen-utils.js +18 -0
  51. package/dist/src/seal/deserialize-builder.d.ts +8 -4
  52. package/dist/src/seal/deserialize-builder.js +1546 -0
  53. package/dist/src/seal/expose-validator.d.ts +3 -2
  54. package/dist/src/seal/expose-validator.js +65 -0
  55. package/dist/src/seal/seal-state.d.ts +10 -0
  56. package/dist/src/seal/seal-state.js +18 -0
  57. package/dist/src/seal/seal.d.ts +22 -21
  58. package/dist/src/seal/seal.js +431 -0
  59. package/dist/src/seal/serialize-builder.d.ts +3 -2
  60. package/dist/src/seal/serialize-builder.js +374 -0
  61. package/dist/src/seal/validate-meta.d.ts +13 -0
  62. package/dist/src/seal/validate-meta.js +61 -0
  63. package/dist/src/symbols.d.ts +1 -1
  64. package/dist/src/symbols.js +13 -2
  65. package/dist/src/transformers/collection.transformer.js +25 -0
  66. package/dist/src/transformers/date.transformer.js +18 -0
  67. package/dist/src/transformers/index.js +6 -2
  68. package/dist/src/transformers/luxon.transformer.d.ts +4 -2
  69. package/dist/src/transformers/luxon.transformer.js +34 -0
  70. package/dist/src/transformers/moment.transformer.d.ts +4 -2
  71. package/dist/src/transformers/moment.transformer.js +32 -0
  72. package/dist/src/transformers/number.transformer.js +8 -0
  73. package/dist/src/transformers/string.transformer.js +12 -0
  74. package/dist/src/types.d.ts +27 -25
  75. package/dist/src/types.js +1 -0
  76. package/dist/src/utils.d.ts +2 -2
  77. package/dist/src/utils.js +10 -0
  78. package/package.json +80 -68
  79. package/dist/index-03cysbck.js +0 -3
  80. package/dist/index-dcbd798a.js +0 -3
  81. package/dist/index-jp2yjd6g.js +0 -3
  82. package/dist/index-mw7met6r.js +0 -3
  83. package/dist/index-xdn55cz3.js +0 -1
  84. package/dist/src/functions/_run-sealed.d.ts +0 -7
  85. package/dist/src/functions/index.d.ts +0 -3
  86. package/dist/src/seal/index.d.ts +0 -5
@@ -1,5 +1,5 @@
1
- import type { EmittableRule, Transformer } from '../types';
2
- export interface ArrayOfMarker {
1
+ import type { ClassCtor, EmittableRule, Transformer } from '../types';
2
+ interface ArrayOfMarker {
3
3
  readonly [key: symbol]: true;
4
4
  readonly rules: EmittableRule[];
5
5
  }
@@ -7,13 +7,15 @@ export interface ArrayOfMarker {
7
7
  * Apply rules to each element of an array.
8
8
  *
9
9
  * @example
10
- * @Field(arrayOf(isString(), minLength(1)))
10
+ * ```ts
11
+ * \@Field(arrayOf(isString(), minLength(1)))
11
12
  * tags!: string[];
13
+ * ```
12
14
  */
13
- export declare function arrayOf(...rules: EmittableRule[]): ArrayOfMarker;
14
- export interface FieldOptions {
15
+ declare function arrayOf(...rules: EmittableRule[]): ArrayOfMarker;
16
+ interface FieldOptions {
15
17
  /** Nested DTO type. Thunk — supports circular references. [Dto] for arrays. */
16
- type?: () => (new (...args: any[]) => any) | (new (...args: any[]) => any)[];
18
+ type?: () => ClassCtor | ClassCtor[] | MapConstructor | SetConstructor;
17
19
  /** Polymorphic discriminator configuration — used with type */
18
20
  discriminator?: {
19
21
  property: string;
@@ -41,7 +43,7 @@ export interface FieldOptions {
41
43
  /** Groups — field visibility control + conditional validation rule application */
42
44
  groups?: string[];
43
45
  /** Conditional validation — skip all field validation when false */
44
- when?: (obj: Record<string, any>) => boolean;
46
+ when?: (obj: Record<string, unknown>) => boolean;
45
47
  /** Transformer or array of transformers (serialize direction applies in reverse order) */
46
48
  transform?: Transformer | Transformer[];
47
49
  /** Error message on validation failure — applied to all rules of the field (rule's own message takes precedence) */
@@ -53,17 +55,19 @@ export interface FieldOptions {
53
55
  /** Error context on validation failure — applied to all rules of the field (rule's own context takes precedence) */
54
56
  context?: unknown;
55
57
  /** Nested DTO class thunk for Map values — used with type: () => Map */
56
- mapValue?: () => new (...args: any[]) => any;
58
+ mapValue?: () => ClassCtor;
57
59
  /** Nested DTO class thunk for Set elements — used with type: () => Set */
58
- setValue?: () => new (...args: any[]) => any;
60
+ setValue?: () => ClassCtor;
59
61
  }
60
62
  type RuleArg = EmittableRule | ArrayOfMarker;
61
- /** @Field() empty field registration */
62
- export declare function Field(): PropertyDecorator;
63
- /** @Field(isString(), email()) — variadic rules */
64
- export declare function Field(...rules: RuleArg[]): PropertyDecorator;
65
- /** @Field({ type: () => Dto }) — options object */
66
- export declare function Field(options: FieldOptions): PropertyDecorator;
67
- /** @Field(isString(), { optional: true }) — rules + options mixed */
68
- export declare function Field(...rulesAndOptions: [...RuleArg[], FieldOptions]): PropertyDecorator;
69
- export {};
63
+ type FieldDecorator = (value: undefined, context: ClassFieldDecoratorContext) => void;
64
+ /** `@Field`() — empty field registration */
65
+ declare function Field(): FieldDecorator;
66
+ /** `@Field`(isString(), email()) — variadic rules */
67
+ declare function Field(...rules: RuleArg[]): FieldDecorator;
68
+ /** `@Field`({ type: () => Dto }) — options object */
69
+ declare function Field(options: FieldOptions): FieldDecorator;
70
+ /** `@Field`(isString(), { optional: true }) — rules + options mixed */
71
+ declare function Field(...rulesAndOptions: [...RuleArg[], FieldOptions]): FieldDecorator;
72
+ export { arrayOf, Field };
73
+ export type { ArrayOfMarker, FieldOptions };
@@ -0,0 +1,268 @@
1
+ import { ensureMeta } from '../collect.js';
2
+ import { BakerError } from '../errors.js';
3
+ import { isAsyncFunction, isPromiseLike } from '../utils.js';
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // arrayOf — Array element validation marker (replaces each: true)
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ const ARRAY_OF = Symbol.for('baker:arrayOf');
8
+ /**
9
+ * Apply rules to each element of an array.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * \@Field(arrayOf(isString(), minLength(1)))
14
+ * tags!: string[];
15
+ * ```
16
+ */
17
+ function arrayOf(...rules) {
18
+ const marker = { rules, [ARRAY_OF]: true };
19
+ return marker;
20
+ }
21
+ function isArrayOfMarker(arg) {
22
+ return typeof arg === 'object' && arg !== null && arg[ARRAY_OF] === true;
23
+ }
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // FieldOptions detection — distinguish from EmittableRule/ArrayOfMarker
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ const FIELD_OPTION_KEYS = new Set([
28
+ 'type',
29
+ 'discriminator',
30
+ 'keepDiscriminatorProperty',
31
+ 'rules',
32
+ 'optional',
33
+ 'nullable',
34
+ 'name',
35
+ 'deserializeName',
36
+ 'serializeName',
37
+ 'exclude',
38
+ 'groups',
39
+ 'when',
40
+ 'transform',
41
+ 'message',
42
+ 'context',
43
+ 'mapValue',
44
+ 'setValue',
45
+ ]);
46
+ function isFieldOptions(arg) {
47
+ if (typeof arg === 'function') {
48
+ return false;
49
+ }
50
+ if (typeof arg !== 'object' || arg === null) {
51
+ return false;
52
+ }
53
+ if (isArrayOfMarker(arg)) {
54
+ return false;
55
+ }
56
+ // Treat as FieldOptions if at least one known key exists
57
+ const keys = Object.keys(arg);
58
+ if (keys.length === 0) {
59
+ return true;
60
+ } // @Field({})
61
+ return keys.some(k => FIELD_OPTION_KEYS.has(k));
62
+ }
63
+ /** W5: assert that a value is a valid baker rule (has `.emit` fn + `.ruleName` string). */
64
+ function assertRule(value, fieldKey, slot) {
65
+ const loc = slot ? `${fieldKey} ${slot}` : fieldKey;
66
+ const validForms = ` Valid @Field forms: @Field(), @Field(rule, ...), @Field(options), @Field(rule, ..., options).`;
67
+ if (typeof value === 'function') {
68
+ const fn = value;
69
+ if (typeof fn.emit !== 'function' || typeof fn.ruleName !== 'string') {
70
+ const hint = fn.name
71
+ ? ` Did you forget to call '${fn.name}()'? Factories must be invoked (e.g., '${fn.name}()'). Rule constants are passed directly (e.g., 'isString' without parentheses).`
72
+ : ` Use createRule() or import a rule from @zipbul/baker/rules.`;
73
+ throw new BakerError(`@Field on ${loc}: argument is not a baker rule.${hint}${validForms}`);
74
+ }
75
+ return;
76
+ }
77
+ throw new BakerError(`@Field on ${loc}: expected a baker rule (function with .emit and .ruleName), got ${value === null ? 'null' : typeof value}. Use createRule() or import a rule from @zipbul/baker/rules.${validForms}`);
78
+ }
79
+ /** Normalize 4 overload signatures into `{ rules, options }` */
80
+ function parseFieldArgs(args) {
81
+ if (args.length === 0) {
82
+ // Form 1: @Field()
83
+ return { rules: [], options: {} };
84
+ }
85
+ if (args.length === 1 && isFieldOptions(args[0])) {
86
+ // Form 3: @Field({ type: () => Dto })
87
+ const options = args[0];
88
+ return { rules: options.rules ?? [], options };
89
+ }
90
+ // Form 2 or 4
91
+ const lastArg = args[args.length - 1];
92
+ if (isFieldOptions(lastArg)) {
93
+ // Form 4: @Field(isString(), { optional: true })
94
+ const options = lastArg;
95
+ let rules = args.slice(0, -1);
96
+ if (options.rules) {
97
+ rules = [...rules, ...options.rules];
98
+ }
99
+ return { rules, options };
100
+ }
101
+ // Form 2: @Field(isString(), email())
102
+ return { rules: args, options: {} };
103
+ }
104
+ /** Register validation rules + handle arrayOf */
105
+ function applyValidation(meta, rules, options) {
106
+ for (const rule of rules) {
107
+ if (isArrayOfMarker(rule)) {
108
+ for (const innerRule of rule.rules) {
109
+ const rd = { rule: innerRule, each: true };
110
+ if (options.groups !== undefined) {
111
+ rd.groups = options.groups;
112
+ }
113
+ if (options.message !== undefined) {
114
+ rd.message = options.message;
115
+ }
116
+ if (options.context !== undefined) {
117
+ rd.context = options.context;
118
+ }
119
+ meta.validation.push(rd);
120
+ }
121
+ }
122
+ else {
123
+ const rd = { rule: rule };
124
+ if (options.groups !== undefined) {
125
+ rd.groups = options.groups;
126
+ }
127
+ if (options.message !== undefined) {
128
+ rd.message = options.message;
129
+ }
130
+ if (options.context !== undefined) {
131
+ rd.context = options.context;
132
+ }
133
+ meta.validation.push(rd);
134
+ }
135
+ }
136
+ }
137
+ /** Handle expose 5-branch logic */
138
+ function applyExpose(meta, options) {
139
+ if (options.name) {
140
+ const ed = { name: options.name };
141
+ if (options.groups !== undefined) {
142
+ ed.groups = options.groups;
143
+ }
144
+ meta.expose.push(ed);
145
+ }
146
+ else if (options.deserializeName || options.serializeName) {
147
+ if (options.deserializeName) {
148
+ const ed = { name: options.deserializeName, deserializeOnly: true };
149
+ if (options.groups !== undefined) {
150
+ ed.groups = options.groups;
151
+ }
152
+ meta.expose.push(ed);
153
+ }
154
+ if (options.serializeName) {
155
+ const ed = { name: options.serializeName, serializeOnly: true };
156
+ if (options.groups !== undefined) {
157
+ ed.groups = options.groups;
158
+ }
159
+ meta.expose.push(ed);
160
+ }
161
+ }
162
+ else if (options.groups) {
163
+ meta.expose.push({ groups: options.groups });
164
+ }
165
+ else {
166
+ meta.expose.push({});
167
+ }
168
+ }
169
+ /** Register Transformer — split into direction-specific TransformDefs */
170
+ function wrapTransform(propertyKey, direction, fn) {
171
+ const isAsync = isAsyncFunction(fn);
172
+ const wrapped = (params => {
173
+ const result = fn(params);
174
+ if (!isAsync && isPromiseLike(result)) {
175
+ throw new BakerError(`@Field(${propertyKey}) ${direction} transform returned Promise. Declare the transform with async if it is asynchronous.`);
176
+ }
177
+ return result;
178
+ });
179
+ return { fn: wrapped, isAsync };
180
+ }
181
+ /** Register Transformer — split into direction-specific TransformDefs */
182
+ function applyTransform(meta, propertyKey, options) {
183
+ if (!options.transform) {
184
+ return;
185
+ }
186
+ const transformers = Array.isArray(options.transform) ? options.transform : [options.transform];
187
+ for (const t of transformers) {
188
+ const deserialize = wrapTransform(propertyKey, 'deserialize', t.deserialize);
189
+ const serialize = wrapTransform(propertyKey, 'serialize', t.serialize);
190
+ meta.transform.push({ fn: deserialize.fn, isAsync: deserialize.isAsync, options: { deserializeOnly: true } }, { fn: serialize.fn, isAsync: serialize.isAsync, options: { serializeOnly: true } });
191
+ }
192
+ }
193
+ function Field(...args) {
194
+ return (_value, context) => {
195
+ if (context.static) {
196
+ throw new BakerError(`@Field cannot decorate static fields.`);
197
+ }
198
+ if (context.private) {
199
+ throw new BakerError(`@Field cannot decorate private fields.`);
200
+ }
201
+ if (typeof context.name === 'symbol') {
202
+ throw new BakerError(`@Field: symbol property keys are not supported. Use a string property name.`);
203
+ }
204
+ const propertyKey = context.name;
205
+ const meta = ensureMeta(context.metadata, propertyKey);
206
+ const { rules, options } = parseFieldArgs(args);
207
+ // `name` is bidirectional; `deserializeName`/`serializeName` are per-direction. Combining them
208
+ // is contradictory — reject it instead of silently dropping the per-direction names. Truthiness
209
+ // matches applyExpose: an empty-string name is treated as "no name" consistently throughout.
210
+ if (options.name && (options.deserializeName || options.serializeName)) {
211
+ throw new BakerError(`@Field on ${propertyKey}: 'name' cannot be combined with 'deserializeName'/'serializeName'. Use one or the other.`);
212
+ }
213
+ // W5: validate each rule shape — `.emit` function + `.ruleName` string required.
214
+ // Catches D2/D4: `@Field(isString())` (boolean), `@Field(isNumber)` (factory unstamped), `@Field(() => true)`.
215
+ for (let i = 0; i < rules.length; i++) {
216
+ const r = rules[i];
217
+ if (isArrayOfMarker(r)) {
218
+ for (let j = 0; j < r.rules.length; j++) {
219
+ assertRule(r.rules[j], propertyKey, `arrayOf[${j}]`);
220
+ }
221
+ }
222
+ else {
223
+ assertRule(r, propertyKey);
224
+ }
225
+ }
226
+ applyValidation(meta, rules, options);
227
+ // ── flags ──
228
+ if (options.optional) {
229
+ meta.flags.isOptional = true;
230
+ }
231
+ if (options.nullable) {
232
+ meta.flags.isNullable = true;
233
+ }
234
+ if (options.when) {
235
+ meta.flags.validateIf = options.when;
236
+ }
237
+ // ── type (nested DTO + discriminator + collection) ──
238
+ if (options.type) {
239
+ const td = { fn: options.type };
240
+ if (options.discriminator !== undefined) {
241
+ td.discriminator = options.discriminator;
242
+ }
243
+ if (options.keepDiscriminatorProperty !== undefined) {
244
+ td.keepDiscriminatorProperty = options.keepDiscriminatorProperty;
245
+ }
246
+ const cv = options.mapValue ?? options.setValue;
247
+ if (cv !== undefined) {
248
+ td.collectionValue = cv;
249
+ }
250
+ meta.type = td;
251
+ }
252
+ applyExpose(meta, options);
253
+ // ── exclude ──
254
+ if (options.exclude) {
255
+ if (options.exclude === true) {
256
+ meta.exclude = {};
257
+ }
258
+ else if (options.exclude === 'deserializeOnly') {
259
+ meta.exclude = { deserializeOnly: true };
260
+ }
261
+ else if (options.exclude === 'serializeOnly') {
262
+ meta.exclude = { serializeOnly: true };
263
+ }
264
+ }
265
+ applyTransform(meta, propertyKey, options);
266
+ };
267
+ }
268
+ export { arrayOf, Field };
@@ -1,2 +1,3 @@
1
1
  export { Field, arrayOf } from './field';
2
2
  export type { FieldOptions, ArrayOfMarker } from './field';
3
+ export { Recipe } from './recipe';
@@ -1,2 +1,2 @@
1
- // @bun
2
- import"../../index-xdn55cz3.js";import{m as a,n as b}from"../../index-03cysbck.js";import"../../index-jp2yjd6g.js";import"../../index-mw7met6r.js";export{a as arrayOf,b as Field};
1
+ export { Field, arrayOf } from './field.js';
2
+ export { Recipe } from './recipe.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Marks a class as a baker DTO so `seal()` (called with no arguments) discovers and seals it.
3
+ *
4
+ * Modern (TC39) field decorators receive no class reference, so `@Field` alone cannot register
5
+ * the owning class. `@Recipe` runs after the field decorators and registers the class itself.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * \@Recipe
10
+ * class UserDto {
11
+ * \@Field(isString()) name!: string;
12
+ * }
13
+ * seal();
14
+ * ```
15
+ */
16
+ declare function Recipe<T extends Function>(value: T, _context: ClassDecoratorContext): void;
17
+ export { Recipe };
@@ -0,0 +1,23 @@
1
+ import { globalRegistry } from '../registry.js';
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // @Recipe — class decorator that registers a DTO for argless seal()
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ /**
6
+ * Marks a class as a baker DTO so `seal()` (called with no arguments) discovers and seals it.
7
+ *
8
+ * Modern (TC39) field decorators receive no class reference, so `@Field` alone cannot register
9
+ * the owning class. `@Recipe` runs after the field decorators and registers the class itself.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * \@Recipe
14
+ * class UserDto {
15
+ * \@Field(isString()) name!: string;
16
+ * }
17
+ * seal();
18
+ * ```
19
+ */
20
+ function Recipe(value, _context) {
21
+ globalRegistry.add(value);
22
+ }
23
+ export { Recipe };
@@ -9,9 +9,9 @@
9
9
  * - 'conversionFailed': when type conversion fails in enableImplicitConversion
10
10
  * - 'whitelistViolation': when undeclared fields exist in input with whitelist: true
11
11
  *
12
- * Future extension fields (message, expected, actual, etc.) must be added as Optional.
12
+ * Future extension fields (expected, actual, etc.) must be added as Optional.
13
13
  */
14
- export interface BakerError {
14
+ export interface BakerIssue {
15
15
  readonly path: string;
16
16
  readonly code: string;
17
17
  /** User-defined error message — included only when the decorator message option is set */
@@ -19,33 +19,43 @@ export interface BakerError {
19
19
  /** User-defined context — included only when the decorator context option is set */
20
20
  readonly context?: unknown;
21
21
  }
22
- /** Symbol tag for isBakerError() type guard — collision-proof discriminator */
22
+ /** Symbol tag for isBakerIssueSet() type guard — collision-proof discriminator */
23
23
  export declare const BAKER_ERROR: unique symbol;
24
24
  /** Validation failure — returned by deserialize()/validate() on invalid input */
25
- export interface BakerErrors {
25
+ export interface BakerIssueSet {
26
26
  readonly [BAKER_ERROR]: true;
27
- readonly errors: readonly BakerError[];
27
+ readonly errors: readonly BakerIssue[];
28
28
  }
29
29
  /**
30
- * Type guard — narrows deserialize()/validate() result to BakerErrors.
30
+ * Type guard — narrows deserialize()/validate() result to BakerIssueSet.
31
31
  *
32
32
  * @example
33
33
  * const result = await deserialize(UserDto, input);
34
- * if (isBakerError(result)) {
35
- * result.errors // readonly BakerError[]
34
+ * if (isBakerIssueSet(result)) {
35
+ * result.errors // readonly BakerIssue[]
36
36
  * } else {
37
37
  * result // UserDto
38
38
  * }
39
39
  */
40
- export declare function isBakerError(value: unknown): value is BakerErrors;
41
- /** @internal — create BakerErrors object */
42
- export declare function _toBakerErrors(errors: BakerError[]): BakerErrors;
40
+ export declare function isBakerIssueSet(value: unknown): value is BakerIssueSet;
41
+ /** @internal — create BakerIssueSet object */
42
+ export declare function toBakerIssueSet(errors: BakerIssue[]): BakerIssueSet;
43
43
  /**
44
- * Seal-related error:
45
- * - When seal() is called more than once
46
- * - When deserialize()/serialize()/validate() is called on an unsealed class
47
- * - When configure() is called after auto-seal
44
+ * The single error thrown by baker for any developer/config/schema misuse — i.e. anything
45
+ * discoverable without external input. End-user input-data failures are NOT thrown; they are
46
+ * returned as a {@link BakerIssueSet}.
47
+ *
48
+ * Thrown when, e.g.:
49
+ * - deserialize()/serialize()/validate() is called on an unsealed class
50
+ * - configure() is called after seal(), or with an unknown key
51
+ * - seal-time metadata invariants fail (discriminator, Map keys, banned names, …)
52
+ * - per-call options contain unsupported keys
53
+ * - @Field receives a non-rule value, or a rule/transformer factory is misused
54
+ * - a user @Type/collectionValue thunk throws (wrapped, with the original error as `cause`)
55
+ * - an optional peer dependency (luxon/moment) is missing
48
56
  */
49
- export declare class SealError extends Error {
50
- constructor(message: string);
57
+ export declare class BakerError extends Error {
58
+ constructor(message: string, options?: {
59
+ cause?: unknown;
60
+ });
51
61
  }
@@ -0,0 +1,52 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // BakerIssue — Individual field error (§12.2)
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // BakerIssueSet — Validation failure return (§12.2)
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ /** Symbol tag for isBakerIssueSet() type guard — collision-proof discriminator */
8
+ export const BAKER_ERROR = Symbol.for('baker:error');
9
+ /**
10
+ * Type guard — narrows deserialize()/validate() result to BakerIssueSet.
11
+ *
12
+ * @example
13
+ * const result = await deserialize(UserDto, input);
14
+ * if (isBakerIssueSet(result)) {
15
+ * result.errors // readonly BakerIssue[]
16
+ * } else {
17
+ * result // UserDto
18
+ * }
19
+ */
20
+ export function isBakerIssueSet(value) {
21
+ return (value != null &&
22
+ typeof value === 'object' &&
23
+ !Array.isArray(value) &&
24
+ value[BAKER_ERROR] === true);
25
+ }
26
+ /** @internal — create BakerIssueSet object */
27
+ export function toBakerIssueSet(errors) {
28
+ return { [BAKER_ERROR]: true, errors };
29
+ }
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // BakerError — the single throw channel (§12.2)
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ /**
34
+ * The single error thrown by baker for any developer/config/schema misuse — i.e. anything
35
+ * discoverable without external input. End-user input-data failures are NOT thrown; they are
36
+ * returned as a {@link BakerIssueSet}.
37
+ *
38
+ * Thrown when, e.g.:
39
+ * - deserialize()/serialize()/validate() is called on an unsealed class
40
+ * - configure() is called after seal(), or with an unknown key
41
+ * - seal-time metadata invariants fail (discriminator, Map keys, banned names, …)
42
+ * - per-call options contain unsupported keys
43
+ * - @Field receives a non-rule value, or a rule/transformer factory is misused
44
+ * - a user @Type/collectionValue thunk throws (wrapped, with the original error as `cause`)
45
+ * - an optional peer dependency (luxon/moment) is missing
46
+ */
47
+ export class BakerError extends Error {
48
+ constructor(message, options) {
49
+ super(message, options);
50
+ this.name = 'BakerError';
51
+ }
52
+ }
@@ -0,0 +1,8 @@
1
+ import type { RuntimeOptions } from '../interfaces';
2
+ /**
3
+ * @internal — validate per-call options object at public-API entry.
4
+ * `groups` is the only valid per-call key; everything else is rejected:
5
+ * - seal-time keys (BakerConfig / SealOptions) → "move to configure({...})"
6
+ * - any other key → "unknown call option"
7
+ */
8
+ export declare function checkCallOptions(opts: unknown): RuntimeOptions | undefined;
@@ -0,0 +1,51 @@
1
+ import { BakerError } from '../errors.js';
2
+ const CALL_OPTION_KEYS = new Set(['groups']);
3
+ const SEAL_TIME_KEYS = new Set([
4
+ // BakerConfig (public, configure-time)
5
+ 'autoConvert',
6
+ 'allowClassDefaults',
7
+ 'stopAtFirstError',
8
+ 'forbidUnknown',
9
+ 'debug',
10
+ // SealOptions (internal, legacy aliases — same set covered by public names)
11
+ 'enableImplicitConversion',
12
+ 'exposeDefaultValues',
13
+ 'whitelist',
14
+ ]);
15
+ /**
16
+ * @internal — validate per-call options object at public-API entry.
17
+ * `groups` is the only valid per-call key; everything else is rejected:
18
+ * - seal-time keys (BakerConfig / SealOptions) → "move to configure({...})"
19
+ * - any other key → "unknown call option"
20
+ */
21
+ export function checkCallOptions(opts) {
22
+ if (opts === undefined || opts === null) {
23
+ return undefined;
24
+ }
25
+ if (typeof opts !== 'object' || Array.isArray(opts)) {
26
+ throw new BakerError(`Call options must be a plain object. Received: ${Array.isArray(opts) ? 'array' : typeof opts}.`);
27
+ }
28
+ // Strict same-realm plain-object check.
29
+ // Accept: `{}` (proto === Object.prototype) and `Object.create(null)` (proto === null).
30
+ // Reject: every other prototype, including class instances whose `constructor.name` is
31
+ // renamed to 'Object' (trojan), built-ins (Date/Map/Set), cross-realm objects.
32
+ // Cross-realm consumers can normalize with `Object.assign({}, opts)` before calling.
33
+ const proto = Object.getPrototypeOf(opts);
34
+ if (proto !== null && proto !== Object.prototype) {
35
+ const ctorName = opts.constructor?.name ?? 'unknown';
36
+ throw new BakerError(`Call options must be a plain object literal. Received instance of ${ctorName}.`);
37
+ }
38
+ for (const key of Object.keys(opts)) {
39
+ if (CALL_OPTION_KEYS.has(key)) {
40
+ continue;
41
+ }
42
+ if (SEAL_TIME_KEYS.has(key)) {
43
+ throw new BakerError(`Option '${key}' is a seal-time setting and cannot be passed per-call. ` +
44
+ `Move it to configure({ ${key}: ... }) at app startup. ` +
45
+ `Per-call options: ${[...CALL_OPTION_KEYS].join(', ')}.`);
46
+ }
47
+ throw new BakerError(`Unknown per-call option '${key}'. Valid per-call options: ${[...CALL_OPTION_KEYS].join(', ')}. ` +
48
+ `Seal-time options go to configure({...}).`);
49
+ }
50
+ return opts;
51
+ }
@@ -1,12 +1,19 @@
1
- import { type BakerErrors } from '../errors';
2
1
  import type { RuntimeOptions } from '../interfaces';
2
+ import { type BakerIssueSet } from '../errors';
3
3
  /**
4
4
  * Converts input to a Class instance + validates.
5
- * - Auto-seals on first call (batches entire globalRegistry)
5
+ * - Requires `seal()` to be called beforehand; throws `BakerError` if not sealed
6
6
  * - Sync DTOs return directly; async DTOs return Promise
7
7
  * - Success: T
8
- * - Validation failure: BakerErrors (use isBakerError() to narrow)
9
- * - Class without decorators: throws SealError
8
+ * - Validation failure: BakerIssueSet (use isBakerIssueSet() to narrow)
10
9
  */
11
- export declare function deserialize<T>(Class: new (...args: any[]) => T, input: unknown, options?: RuntimeOptions): T | BakerErrors;
12
- export declare function deserialize<T>(Class: new (...args: any[]) => T, input: unknown, options?: RuntimeOptions): Promise<T | BakerErrors>;
10
+ export declare function deserialize<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): T | BakerIssueSet | Promise<T | BakerIssueSet>;
11
+ /**
12
+ * Sync-asserted deserialize. Throws `BakerError` if Class has any async rule/transform
13
+ * on the deserialize side.
14
+ */
15
+ export declare function deserializeSync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): T | BakerIssueSet;
16
+ /**
17
+ * Async-asserted deserialize. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
18
+ */
19
+ export declare function deserializeAsync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): Promise<T | BakerIssueSet>;
@@ -0,0 +1,57 @@
1
+ import { isErr } from '@zipbul/result';
2
+ import { toBakerIssueSet, BakerError } from '../errors.js';
3
+ import { ensureSealed } from '../seal/seal.js';
4
+ import { checkCallOptions } from './check-call-options.js';
5
+ export function deserialize(Class, input, options) {
6
+ const checkedOpts = checkCallOptions(options);
7
+ const sealed = ensureSealed(Class);
8
+ if (sealed.isAsync) {
9
+ return sealed.deserialize(input, checkedOpts).then((result) => {
10
+ if (isErr(result)) {
11
+ return toBakerIssueSet(result.data);
12
+ }
13
+ return result;
14
+ });
15
+ }
16
+ const result = sealed.deserialize(input, checkedOpts);
17
+ if (isErr(result)) {
18
+ return toBakerIssueSet(result.data);
19
+ }
20
+ return result;
21
+ }
22
+ /**
23
+ * Sync-asserted deserialize. Throws `BakerError` if Class has any async rule/transform
24
+ * on the deserialize side.
25
+ */
26
+ export function deserializeSync(Class, input, options) {
27
+ const checkedOpts = checkCallOptions(options);
28
+ const sealed = ensureSealed(Class);
29
+ if (sealed.isAsync) {
30
+ throw new BakerError(`deserializeSync(${Class.name}): DTO has async rules/transforms. Use deserializeAsync() instead.`);
31
+ }
32
+ const result = sealed.deserialize(input, checkedOpts);
33
+ if (isErr(result)) {
34
+ return toBakerIssueSet(result.data);
35
+ }
36
+ return result;
37
+ }
38
+ /**
39
+ * Async-asserted deserialize. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
40
+ */
41
+ export function deserializeAsync(Class, input, options) {
42
+ const checkedOpts = checkCallOptions(options);
43
+ const sealed = ensureSealed(Class);
44
+ if (sealed.isAsync) {
45
+ return sealed.deserialize(input, checkedOpts).then((result) => {
46
+ if (isErr(result)) {
47
+ return toBakerIssueSet(result.data);
48
+ }
49
+ return result;
50
+ });
51
+ }
52
+ const result = sealed.deserialize(input, checkedOpts);
53
+ if (isErr(result)) {
54
+ return Promise.resolve(toBakerIssueSet(result.data));
55
+ }
56
+ return Promise.resolve(result);
57
+ }