@zipbul/baker 3.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @zipbul/baker
2
2
 
3
+ ## 3.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ea40a75: Docs/metadata accuracy: every README example now includes the required `@Recipe` decorator
8
+ (without it `seal()` does not register the class and `deserialize` throws), drop the
9
+ unnecessary `() => Set as any` / `Map as any`, state the Bun-only requirement (Bun ≥ 1.3.13),
10
+ and replace unsubstantiated speed multipliers with honest qualitative claims. Refresh the
11
+ package description and remove the now-redundant MIGRATION-3.0.md (its content lives in the
12
+ CHANGELOG 3.0.0 entry).
13
+
3
14
  ## 3.0.0
4
15
 
5
16
  ### Major Changes
@@ -21,35 +32,31 @@
21
32
  mode was removed — call a rule directly instead). `configure()` rejects unknown keys and
22
33
  post-`seal()` calls, and seal-time options can no longer be passed per-call.
23
34
 
24
- See `MIGRATION-3.0.md` for the full upgrade guide.
25
-
26
35
  ### Minor Changes
27
36
 
28
37
  - 421fd54: Add the `isHttpToken` rule — validates the RFC 9110 §5.6.2 HTTP `token` production
29
38
  (`1*tchar`), used for HTTP method names and header field-names. Usable as a predicate
30
39
  (`isHttpToken(value)`) or as `@Field(isHttpToken)`, and exported from `@zipbul/baker/rules`.
31
40
 
32
- ## 3.0.0
33
-
34
41
  ### DX reform — breaking changes
35
42
 
36
- - **Auto-seal removed.** Call `seal()` once at app startup, after every DTO module is loaded. Without it, the first `deserialize` / `serialize` / `validate` call throws `SealError`.
43
+ - **Auto-seal removed.** Call `seal()` once at app startup, after every DTO module is loaded. Without it, the first `deserialize` / `serialize` / `validate` call throws `BakerError`.
37
44
  - Migration: import `seal` and call `seal()` once before any deserialize/serialize/validate call. For tests, call `seal()` after each `unseal()` / `configure(...)` reconfiguration.
38
- - **Per-call options are validated.** Only `groups` is a valid per-call option. Passing any other key (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`, …) throws `SealError`. Move those keys into `configure({...})` before `seal()`.
39
- - **`@Field` argument validation.** Passing a non-rule value (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) now throws `SealError` immediately with the four valid forms listed in the message.
40
- - **Map non-string keys.** Serializing a `Map<K, V>` whose key is not a `string` throws `TypeError` — previously the key was silently coerced via `[object Object]` and collided.
45
+ - **Per-call options are validated.** Only `groups` is a valid per-call option. Passing any other key (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`, …) throws `BakerError`. Move those keys into `configure({...})` before `seal()`.
46
+ - **`@Field` argument validation.** Passing a non-rule value (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) now throws `BakerError` immediately with the four valid forms listed in the message.
47
+ - **Map non-string keys.** Serializing a `Map<K, V>` whose key is not a `string` throws `BakerError` — previously the key was silently coerced via `[object Object]` and collided.
41
48
 
42
49
  ### API additions
43
50
 
44
51
  - `seal(...classes?)` — explicit AOT seal trigger.
45
- - `deserializeSync<T>` / `deserializeAsync<T>` / `serializeSync<T>` / `serializeAsync<T>` / `validateSync` / `validateAsync` — strict variants. `*Sync` throws `SealError` when the DTO is async on the relevant direction; `*Async` always returns `Promise`.
52
+ - `deserializeSync<T>` / `deserializeAsync<T>` / `serializeSync<T>` / `serializeAsync<T>` / `validateSync` / `validateAsync` — strict variants. `*Sync` throws `BakerError` when the DTO is async on the relevant direction; `*Async` always returns `Promise`.
46
53
 
47
54
  ### Defect fixes
48
55
 
49
56
  - **F-1** `circular-analyzer.walk()` now walks `meta.type.collectionValue` — Set/Map nested DTO cycles are caught at seal time, no more `stack overflow` at runtime.
50
- - **F-2** Discriminator / Set·Map / inheritance invariants now run before codegen via the new `validate-meta` pass — invalid metadata throws `SealError` with a precise message instead of producing invalid generated JS.
57
+ - **F-2** Discriminator / Set·Map / inheritance invariants now run before codegen via the new `validate-meta` pass — invalid metadata throws `BakerError` with a precise message instead of producing invalid generated JS.
51
58
  - **F-3** Discriminator default branch now reports `context: { received, validSubTypes: [...] }` so callers can show the user the allowed values.
52
- - **F-4** Per-call options other than `groups` are rejected with `SealError` instead of being silently dropped.
59
+ - **F-4** Per-call options other than `groups` are rejected with `BakerError` instead of being silently dropped.
53
60
  - **F-8** FR passport regex now anchors both ends (`/^[A-Z0-9]{9}$/i`).
54
61
  - **F-9** `MAGNET_URI_RE` is anchored on the trailing end.
55
62
  - **N-3** Circular-detection `WeakSet` is now allocated per call via `Symbol.for('baker:circular-seen')` threaded through `_opts` — concurrent async calls no longer false-circular on shared input objects.
package/README.md CHANGED
@@ -8,12 +8,15 @@ bun add @zipbul/baker
8
8
 
9
9
  Zero `reflect-metadata`. Sealed codegen. 99%+ line coverage.
10
10
 
11
+ > **Requires Bun ≥ 1.3.13.** baker relies on TC39 decorator metadata (`Symbol.metadata`), which Node does not populate — it is Bun-only.
12
+
11
13
  ## Quick Start
12
14
 
13
15
  ```typescript
14
- import { deserialize, isBakerIssueSet, Field, seal } from '@zipbul/baker';
16
+ import { deserialize, isBakerIssueSet, Field, Recipe, seal } from '@zipbul/baker';
15
17
  import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
16
18
 
19
+ @Recipe
17
20
  class UserDto {
18
21
  @Field(isString, minLength(2)) name!: string;
19
22
  @Field(isNumber(), min(0)) age!: number;
@@ -106,9 +109,13 @@ configure({
106
109
  });
107
110
  ```
108
111
 
109
- ### `createRule(name, validate)`
112
+ ### `createRule(name, validate)` / `createRule(options)`
110
113
 
111
- Custom validation rule.
114
+ Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
115
+
116
+ ```typescript
117
+ const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
118
+ ```
112
119
 
113
120
  ```typescript
114
121
  const isEven = createRule({
@@ -139,6 +146,7 @@ Each rule must be an emittable rule object created via `createRule()` or one of
139
146
  | ----------------- | ------------------------------------------------- | ------------------------------ |
140
147
  | `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
141
148
  | `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
149
+ | `keepDiscriminatorProperty` | `boolean` | Keep the discriminator key in the result |
142
150
  | `optional` | `boolean` | Allow undefined |
143
151
  | `nullable` | `boolean` | Allow null |
144
152
  | `name` | `string` | Bidirectional key mapping |
@@ -215,6 +223,7 @@ email!: string;
215
223
  import { luxonTransformer } from '@zipbul/baker/transformers';
216
224
  const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
217
225
 
226
+ @Recipe
218
227
  class EventDto {
219
228
  @Field({ transform: luxon }) startAt!: DateTime;
220
229
  }
@@ -269,10 +278,12 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
269
278
  ## Nested DTOs
270
279
 
271
280
  ```typescript
281
+ @Recipe
272
282
  class AddressDto {
273
283
  @Field(isString) city!: string;
274
284
  }
275
285
 
286
+ @Recipe
276
287
  class UserDto {
277
288
  @Field({ type: () => AddressDto }) address!: AddressDto;
278
289
  @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
@@ -282,15 +293,19 @@ class UserDto {
282
293
  ## Collections
283
294
 
284
295
  ```typescript
296
+ @Recipe
285
297
  class UserDto {
286
- @Field({ type: () => Set as any, setValue: () => TagDto }) tags!: Set<TagDto>;
287
- @Field({ type: () => Map as any, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
298
+ @Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
299
+ @Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
288
300
  }
289
301
  ```
290
302
 
303
+ > Deserialize input shape: a `Set` field accepts a JSON **array**, a `Map` field accepts a plain **object** keyed by string. Serialize emits the same shapes.
304
+
291
305
  ## Discriminator
292
306
 
293
307
  ```typescript
308
+ @Recipe
294
309
  class PetOwner {
295
310
  @Field({
296
311
  type: () => CatDto,
@@ -309,10 +324,12 @@ class PetOwner {
309
324
  ## Inheritance
310
325
 
311
326
  ```typescript
327
+ @Recipe
312
328
  class BaseDto {
313
329
  @Field(isString) id!: string;
314
330
  }
315
331
 
332
+ @Recipe
316
333
  class UserDto extends BaseDto {
317
334
  @Field(isString) name!: string;
318
335
  // inherits 'id' field with isString rule
@@ -323,11 +340,11 @@ class UserDto extends BaseDto {
323
340
 
324
341
  ### When should I use baker instead of class-validator?
325
342
 
326
- When performance matters. baker is 163x faster on valid input and 109x faster on invalid input, while providing the same decorator-based DX. baker also eliminates the `reflect-metadata` dependency.
343
+ When performance matters. baker generates optimized validation/serialization code at seal time instead of interpreting rules on every call, so it is substantially faster than class-validator on both valid and invalid input while providing the same decorator-based DX. baker also eliminates the `reflect-metadata` dependency. Run [`bench/`](./bench) to measure the exact difference on your machine.
327
344
 
328
345
  ### How does baker compare to Zod?
329
346
 
330
- Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker is 16x faster on valid input because it generates optimized code at definition time instead of interpreting schemas at runtime. Choose Zod if you need schema-first design; choose baker if you need class-based DTOs with maximum performance.
347
+ Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker generates optimized code at definition time instead of interpreting schemas at runtime. Choose Zod if you need schema-first design or Node support; choose baker if you need class-based DTOs on Bun with maximum performance.
331
348
 
332
349
  ### Does baker support async validation?
333
350
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zipbul/baker",
3
- "version": "3.0.0",
4
- "description": "Fastest decorator-based DTO validation for TypeScript. AOT code generation, 42ns per validation, 163x faster than class-validator. Zero reflect-metadata.",
3
+ "version": "3.0.1",
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",
7
7
  "bun",
@@ -37,7 +37,6 @@
37
37
  "dist/**/*.d.ts",
38
38
  "README.md",
39
39
  "CHANGELOG.md",
40
- "MIGRATION-3.0.md",
41
40
  "LICENSE"
42
41
  ],
43
42
  "type": "module",
package/MIGRATION-3.0.md DELETED
@@ -1,104 +0,0 @@
1
- # Baker 2.x → 3.x Migration
2
-
3
- This release replaces the implicit "auto-seal on first call" model with explicit, user-triggered `seal()`. It also tightens per-call options validation and adds strict sync/async variants.
4
-
5
- ## Required changes
6
-
7
- ### 1. Call `seal()` once at app startup
8
-
9
- **Before**
10
-
11
- ```ts
12
- // Module load registers DTOs; first deserialize implicitly seals everything.
13
- const r = await deserialize(UserDto, payload);
14
- ```
15
-
16
- **After**
17
-
18
- ```ts
19
- import { seal, deserialize } from '@zipbul/baker';
20
- // Call after every DTO module has been imported (before HTTP server / job runner starts).
21
- seal();
22
- const r = await deserialize(UserDto, payload);
23
- ```
24
-
25
- `deserialize` / `serialize` / `validate` throw `BakerError` if the DTO is not sealed.
26
-
27
- ### 2. Move per-call options into `configure(...)`
28
-
29
- Only `groups` survives as a per-call option.
30
-
31
- **Before**
32
-
33
- ```ts
34
- await deserialize(UserDto, payload, { stopAtFirstError: true });
35
- ```
36
-
37
- **After**
38
-
39
- ```ts
40
- import { configure, seal } from '@zipbul/baker';
41
- configure({ stopAtFirstError: true });
42
- seal();
43
- await deserialize(UserDto, payload);
44
- ```
45
-
46
- All other keys (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`) and their legacy `SealOptions` aliases (`enableImplicitConversion`, `exposeDefaultValues`, `whitelist`) now throw `BakerError` when passed per-call.
47
-
48
- ### 3. `configure()` must run before `seal()`
49
-
50
- After `seal()`, `configure(...)` throws `BakerError`. Tests that need to reconfigure must call the test-only `unseal()` helper, change config, then `seal()` again.
51
-
52
- ### 4. `@Field` argument validation is strict
53
-
54
- Passing a non-rule value (factory not invoked, primitive, plain function without `.emit` / `.ruleName`) throws `BakerError` at decorator-evaluation time with the four valid forms listed.
55
-
56
- ```ts
57
- @Field(isNumber) // ✗ factory not invoked → BakerError
58
- @Field(isNumber()) // ✓
59
- @Field(isString) // ✓ constant rule
60
- @Field() // ✓ marker only
61
- @Field(isString, { optional: true }) // ✓
62
- @Field({ type: () => NestedDto }) // ✓
63
- ```
64
-
65
- ### 5. `Map<K, V>` requires string keys at serialize
66
-
67
- Serializing a `Map` with non-string keys throws `TypeError`. Previously the key was silently coerced via `String(key)`, producing collisions like `'[object Object]'`.
68
-
69
- ### 6. New strict sync/async variants
70
-
71
- Six new entry points enforce the call-direction asymmetry at the type level:
72
-
73
- | Integrated | Strict sync | Strict async |
74
- | ------------- | ----------------- | ------------------ |
75
- | `deserialize` | `deserializeSync` | `deserializeAsync` |
76
- | `serialize` | `serializeSync` | `serializeAsync` |
77
- | `validate` | `validateSync` | `validateAsync` |
78
-
79
- `*Sync` throws `BakerError` if the DTO is async on that direction (e.g. async transform on deserialize side for `deserializeSync`). `*Async` always returns `Promise` (sync DTOs are wrapped via `Promise.resolve`). The integrated `deserialize` / `serialize` / `validate` remain available for ergonomic use.
80
-
81
- ## Defect fixes (no migration needed)
82
-
83
- The following bugs in 2.x are silently fixed in 3.x:
84
-
85
- - **Set/Map nested DTO cycles** no longer cause stack overflow (`circular-analyzer` now walks `collectionValue`).
86
- - **Set/Map value DTOs marked async** now correctly propagate `_isAsync` / `_isSerializeAsync` to the parent.
87
- - **Discriminator with empty `subTypes`** throws `BakerError` at seal time instead of producing invalid generated JS.
88
- - **Concurrent async deserialize on the same input** no longer reports a false `circular` error (per-call `WeakSet` via `Symbol.for('baker:circular-seen')`).
89
- - **`Object.hasOwn` checks** prevent prototype-chain values from leaking into DTO results.
90
- - **Discriminator default branch** error now reports `context: { received, validSubTypes: [...] }`.
91
- - **FR passport regex** now anchors both ends.
92
- - **MAGNET URI regex** now anchors the trailing end.
93
- - **Inheritance dedup** now compares by `ruleName` — a child re-declaring the same rule replaces the parent's rule.
94
- - **`seal(Class)` failure** is now transactional: a failed seal removes the placeholder so retry can succeed.
95
- - **`collectionValue` thunk** errors are wrapped in `BakerError` with the field name.
96
-
97
- ## Removed APIs
98
-
99
- - `_runSealed` (was internal, but some test code depended on it). Use the public functions instead.
100
-
101
- ## Notes
102
-
103
- - The decorator-side registry (`globalRegistry`) is still used internally as an index of decorated classes so `seal()` (no args) can seal everything. This is not auto-seal — `seal()` must be called explicitly.
104
- - `unseal()` is exported by `test/integration/helpers/unseal.ts` for testing only. It is not part of the public API.