@zipbul/baker 3.3.1 → 3.4.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 (50) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/index.js +1 -10
  3. package/dist/src/collect.js +1 -26
  4. package/dist/src/configure.js +1 -43
  5. package/dist/src/create-rule.js +1 -41
  6. package/dist/src/decorators/field.js +1 -277
  7. package/dist/src/decorators/index.js +1 -2
  8. package/dist/src/decorators/recipe.js +1 -23
  9. package/dist/src/errors.js +1 -52
  10. package/dist/src/functions/check-call-options.js +1 -51
  11. package/dist/src/functions/deserialize.js +1 -57
  12. package/dist/src/functions/serialize.js +1 -52
  13. package/dist/src/functions/validate.js +1 -49
  14. package/dist/src/interfaces.js +0 -4
  15. package/dist/src/meta-access.js +1 -75
  16. package/dist/src/registry.js +1 -8
  17. package/dist/src/rule-metadata.js +1 -17
  18. package/dist/src/rule-plan.js +1 -117
  19. package/dist/src/rules/array.js +1 -96
  20. package/dist/src/rules/binary.d.ts +3 -0
  21. package/dist/src/rules/binary.js +3 -0
  22. package/dist/src/rules/combinators.js +1 -111
  23. package/dist/src/rules/common.js +1 -77
  24. package/dist/src/rules/date.js +1 -35
  25. package/dist/src/rules/index.d.ts +1 -0
  26. package/dist/src/rules/index.js +1 -9
  27. package/dist/src/rules/locales.js +1 -249
  28. package/dist/src/rules/number.js +1 -79
  29. package/dist/src/rules/object.js +1 -49
  30. package/dist/src/rules/string.js +10 -2033
  31. package/dist/src/rules/typechecker.js +5 -171
  32. package/dist/src/seal/circular-analyzer.js +1 -63
  33. package/dist/src/seal/codegen-utils.js +1 -18
  34. package/dist/src/seal/deserialize-builder.js +265 -1564
  35. package/dist/src/seal/expose-validator.js +1 -65
  36. package/dist/src/seal/seal-state.js +1 -18
  37. package/dist/src/seal/seal.js +1 -431
  38. package/dist/src/seal/serialize-builder.js +66 -370
  39. package/dist/src/seal/validate-meta.js +1 -61
  40. package/dist/src/symbols.js +1 -13
  41. package/dist/src/transformers/collection.transformer.js +1 -25
  42. package/dist/src/transformers/date.transformer.js +1 -18
  43. package/dist/src/transformers/index.js +1 -6
  44. package/dist/src/transformers/luxon.transformer.js +1 -34
  45. package/dist/src/transformers/moment.transformer.js +1 -32
  46. package/dist/src/transformers/number.transformer.js +1 -8
  47. package/dist/src/transformers/string.transformer.js +1 -12
  48. package/dist/src/types.js +0 -1
  49. package/dist/src/utils.js +1 -10
  50. package/package.json +2 -2
@@ -1,374 +1,70 @@
1
- import { BakerError } from '../errors.js';
2
- import { getSealed } from '../meta-access.js';
3
- import { sanitizeKey, buildGroupsHasExpr } from './codegen-utils.js';
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- // Generated variable name prefixes centralised to prevent typo-related bugs
6
- // ─────────────────────────────────────────────────────────────────────────────
7
- const GEN = {
8
- out: '__bk$out',
9
- fieldVal: '__bk$fv_',
10
- groups: '__bk$groups',
11
- group0: '__bk$group0',
12
- groupsSet: '__bk$groupsSet',
13
- setArr: '__bk$sa',
14
- setItem: '__bk$si',
15
- mapObj: '__bk$m',
16
- mapEntry: '__bk$me',
17
- serResult: '__bk$sr',
18
- outItem: '__bk$out_item',
19
- discArr: '__bk$da',
20
- discIdx: '__bk$di',
21
- nestedArr: '__bk$na',
22
- nestedIdx: '__bk$ni',
23
- nestedItem: '__bk$nitem',
24
- };
25
- // ─────────────────────────────────────────────────────────────────────────────
26
- // Helpers
27
- // ─────────────────────────────────────────────────────────────────────────────
28
- /** Determine the output key for serialize direction */
29
- function getSerializeOutputKey(fieldKey, exposeStack) {
30
- // serializeOnly @Expose with name → use that name
31
- const serDef = exposeStack.find(e => e.serializeOnly && e.name);
32
- if (serDef) {
33
- return serDef.name;
34
- }
35
- // Non-directional @Expose with name → use for both directions
36
- const biDef = exposeStack.find(e => !e.deserializeOnly && !e.serializeOnly && e.name);
37
- if (biDef) {
38
- return biDef.name;
39
- }
40
- return fieldKey;
1
+ import{BakerError as c}from"../errors.js";import{getSealed as A}from"../meta-access.js";import{sanitizeKey as p,buildGroupsHasExpr as E}from"./codegen-utils.js";const j={out:"__bk$out",fieldVal:"__bk$fv_",groups:"__bk$groups",group0:"__bk$group0",groupsSet:"__bk$groupsSet",setArr:"__bk$sa",setItem:"__bk$si",mapObj:"__bk$m",mapEntry:"__bk$me",serResult:"__bk$sr",outItem:"__bk$out_item",discArr:"__bk$da",discIdx:"__bk$di",nestedArr:"__bk$na",nestedIdx:"__bk$ni",nestedItem:"__bk$nitem"};function m(U,q){const L=q.find((R)=>R.serializeOnly&&R.name);if(L)return L.name;const M=q.find((R)=>!R.deserializeOnly&&!R.serializeOnly&&R.name);if(M)return M.name;return U}function y(U){let q=null;for(const L of U){if(L.deserializeOnly)continue;if(!L.groups||L.groups.length===0)return;if(q===null)q=new Set;for(const M of L.groups)q.add(M)}return q===null?void 0:[...q]}function x(U,q,L,M){if(L.length===0)return null;if(L.length===1){const Y=L[0],W=M.length;M.push(Y.fn);const Z=`refs[${W}]({value:${U},key:${JSON.stringify(q)},obj:instance})`;return Y.isAsync?`(await ${Z})`:Z}if(L.length===2){const Y=L[1],W=L[0],Z=M.length;M.push(Y.fn);const F=M.length;M.push(W.fn);const z=`refs[${Z}]({value:${U},key:${JSON.stringify(q)},obj:instance})`,Q=Y.isAsync?`(await ${z})`:z,_=`refs[${F}]({value:${Q},key:${JSON.stringify(q)},obj:instance})`;return W.isAsync?`(await ${_})`:_}let R=U;for(let Y=L.length-1;Y>=0;Y-=1){const W=L[Y],Z=M.length;M.push(W.fn);const F=`refs[${Z}]({value:${R},key:${JSON.stringify(q)},obj:instance})`;R=W.isAsync?`(await ${F})`:F}return R}function T(U,q,L,M){const R=x(U,q,L,M);return R?`
2
+ ${U} = ${R};`:""}function buildSerializeCode(U,q,L,M){const R=[],Y=[];let W=`'use strict';
3
+ `;W+=`var ${j.out} = {};
4
+ `;if(Object.values(q).some((_)=>{const v=y(_.expose);return v&&v.length>0})){W+=`var ${j.groups} = opts && opts.groups;
5
+ `;W+=`var ${j.group0} = ${j.groups} && ${j.groups}.length === 1 ? ${j.groups}[0] : null;
6
+ `;W+=`var ${j.groupsSet} = ${j.groups} && ${j.groups}.length > 1 ? new Set(${j.groups}) : null;
7
+ `}for(const[_,v]of Object.entries(q))W+=u(_,v,R,Y,M,L,U.name);W+=`return ${j.out};
8
+ `;const F=U.name.replace(/[^\w$.-]/g,"_");W+=`//# sourceURL=baker://${F}/serialize
9
+ `;return Function("refs","execs","BakerError",`return ${M?"async function":"function"}(instance, opts) { `+W+" }")(R,Y,c)}function u(U,q,L,M,R,Y,W=""){if(q.exclude){if(!q.exclude.deserializeOnly){if(Y?.debug){const X=q.exclude.serializeOnly?"serializeOnly":"bidirectional";return`// [baker] field ${JSON.stringify(U)} excluded (${X} @Exclude)
10
+ `}return""}}if(q.expose.length>0&&q.expose.every((X)=>X.deserializeOnly)){if(Y?.debug)return`// [baker] field ${JSON.stringify(U)} excluded (all @Expose entries are deserializeOnly)
11
+ `;return""}const Z=m(U,q.expose),F=y(q.expose),z=p(U),Q=`${j.fieldVal}${z}`;let _="";_+=`var ${Q} = instance[${JSON.stringify(U)}];
12
+ `;let v="",S="";if(F&&F.length>0){v=`if ((${j.group0} !== null || ${j.groupsSet}) && (${E(j.group0,j.groupsSet,F)})) {
13
+ `;S=`}
14
+ `}let H="";const b=q.flags.isOptional,g=q.transform.filter((X)=>!X.options?.deserializeOnly);if(q.type?.collection){const X=`${j.out}[${JSON.stringify(Z)}]`,$=q.type.collection;let J;if($==="Set")if(q.type.resolvedCollectionValue){const B=A(q.type.resolvedCollectionValue),D=M.length;M.push(B);if(R)J=`{ var __ser_ps = []; for (var __ser_item of ${Q}) { __ser_ps.push(__ser_item == null ? __ser_item : execs[${D}].serialize(__ser_item, opts)); } ${X} = await Promise.all(__ser_ps); }`;else{J=`var ${j.setArr} = [];
15
+ `;J+=` for (var ${j.setItem} of ${Q}) {
16
+ `;J+=` ${j.setArr}.push(${j.setItem} == null ? ${j.setItem} : execs[${D}].serialize(${j.setItem}, opts));
17
+ `;J+=` }
18
+ `;J+=` ${X} = ${j.setArr};`}}else J=`${X} = [...${Q}];`;else{const B=`if (typeof ${j.mapEntry}[0] !== 'string') { throw new BakerError(${JSON.stringify(W)} + ': Map field ' + ${JSON.stringify(U)} + ' has non-string key (' + typeof ${j.mapEntry}[0] + '). Map serialization requires string keys.'); }
19
+ `;if(q.type.resolvedCollectionValue){const D=A(q.type.resolvedCollectionValue),O=M.length;M.push(D);const w=R?"await ":"";J=`var ${j.mapObj} = Object.create(null);
20
+ `;J+=` for (var ${j.mapEntry} of ${Q}) {
21
+ `;J+=` ${B}`;J+=`${j.mapObj}[${j.mapEntry}[0]] = ${j.mapEntry}[1] == null ? ${j.mapEntry}[1] : ${w}execs[${O}].serialize(${j.mapEntry}[1], opts);
22
+ `;J+=` }
23
+ `;J+=` ${X} = ${j.mapObj};`}else{J=`var ${j.mapObj} = Object.create(null);
24
+ `;J+=` for (var ${j.mapEntry} of ${Q}) {
25
+ `;J+=` ${B}`;J+=`${j.mapObj}[${j.mapEntry}[0]] = ${j.mapEntry}[1];
26
+ `;J+=` }
27
+ `;J+=` ${X} = ${j.mapObj};`}}J+=T(X,U,g,L);if(b)H=`if (${Q} !== undefined && ${Q} !== null) {
28
+ ${J}
29
+ } else if (${Q} === null) {
30
+ ${X} = null;
41
31
  }
42
- /** Determine expose groups for serialize direction — returns undefined (no restriction) if any unconditional expose entry exists */
43
- function getSerializeExposeGroups(exposeStack) {
44
- // Single-pass: scan once, build set of groups; bail out as soon as we see an unconditional entry.
45
- let all = null;
46
- for (const e of exposeStack) {
47
- if (e.deserializeOnly) {
48
- continue;
49
- }
50
- if (!e.groups || e.groups.length === 0) {
51
- return undefined;
52
- }
53
- if (all === null) {
54
- all = new Set();
55
- }
56
- for (const g of e.groups) {
57
- all.add(g);
58
- }
59
- }
60
- return all === null ? undefined : [...all];
32
+ `;else H=`if (${Q} != null) {
33
+ ${J}
34
+ } else {
35
+ ${X} = ${Q};
61
36
  }
62
- /**
63
- * Build serialize-direction transform expression.
64
- * Serialize direction reverses declaration order (codec stack unwrapping).
65
- */
66
- function buildSerializeTransformExpr(inputExpr, fieldKey, serTransforms, refs) {
67
- if (serTransforms.length === 0) {
68
- return null;
69
- }
70
- if (serTransforms.length === 1) {
71
- const td = serTransforms[0];
72
- const refIdx = refs.length;
73
- refs.push(td.fn);
74
- const callExpr = `refs[${refIdx}]({value:${inputExpr},key:${JSON.stringify(fieldKey)},obj:instance})`;
75
- return td.isAsync ? `(await ${callExpr})` : callExpr;
76
- }
77
- if (serTransforms.length === 2) {
78
- const td1 = serTransforms[1];
79
- const td0 = serTransforms[0];
80
- const refIdx1 = refs.length;
81
- refs.push(td1.fn);
82
- const refIdx0 = refs.length;
83
- refs.push(td0.fn);
84
- const call1 = `refs[${refIdx1}]({value:${inputExpr},key:${JSON.stringify(fieldKey)},obj:instance})`;
85
- const expr1 = td1.isAsync ? `(await ${call1})` : call1;
86
- const call0 = `refs[${refIdx0}]({value:${expr1},key:${JSON.stringify(fieldKey)},obj:instance})`;
87
- return td0.isAsync ? `(await ${call0})` : call0;
88
- }
89
- // Walk serTransforms backwards in place — avoids [...arr].reverse() clone allocation
90
- let valueExpr = inputExpr;
91
- for (let k = serTransforms.length - 1; k >= 0; k -= 1) {
92
- const td = serTransforms[k];
93
- const refIdx = refs.length;
94
- refs.push(td.fn);
95
- const callExpr = `refs[${refIdx}]({value:${valueExpr},key:${JSON.stringify(fieldKey)},obj:instance})`;
96
- valueExpr = td.isAsync ? `(await ${callExpr})` : callExpr;
97
- }
98
- return valueExpr;
37
+ `;_+=v+H+S;return _}if(q.type?.resolvedClass||q.type?.discriminator||q.type?.fn&&q.flags.validateNested){const X=q.type?.isArray||q.flags.validateNestedEach||q.validation.some((B)=>B.each),$=`${j.out}[${JSON.stringify(Z)}]`;let J;if(q.type.discriminator){const{property:B,subTypes:D}=q.type.discriminator,O=q.type.keepDiscriminatorProperty!==!1,w=[...D].sort((P,I)=>{if(P.value.prototype instanceof I.value)return-1;if(I.value.prototype instanceof P.value)return 1;return 0}),K=(P,I)=>{let h="";for(let k=0;k<w.length;k++){const V=w[k],N=A(V.value),C=M.length;M.push(N);const G=L.length;L.push(V.value);h+=`${k===0?"if":"} else if"} (${P} instanceof refs[${G}]) {
38
+ `;h+=` var ${j.serResult} = ${I}execs[${C}].serialize(${P}, opts);
39
+ `;if(O)h+=` ${j.serResult}[${JSON.stringify(B)}] = ${JSON.stringify(V.name)};
40
+ `;h+=` ${j.outItem} = ${j.serResult};
41
+ `}h+=`} else { ${j.outItem} = `+P+`; }
42
+ `;return h};if(X){const P=R?"await ":"";if(R)J=`${$} = await Promise.all(${Q}.map(async function(__ser_item) {
43
+ `;else{J=`var ${j.discArr} = [];
44
+ `;J+=` for (var ${j.discIdx}=0; ${j.discIdx}<${Q}.length; ${j.discIdx}++) {
45
+ `;J+=` var __ser_item = ${Q}[${j.discIdx}];
46
+ `}J+=` var ${j.outItem};
47
+ `;J+=K("__ser_item",P);if(R){J+=` return ${j.outItem};
48
+ `;J+="}));"}else{J+=` ${j.discArr}.push(${j.outItem});
49
+ `;J+=` }
50
+ `;J+=` ${$} = ${j.discArr};`}}else{const P=R?"await ":"";J=`var ${j.outItem};
51
+ `;J+=K(Q,P);J+=`${$} = ${j.outItem};`}}else{const B=q.type.resolvedClass??q.type.fn(),D=A(B),O=M.length;M.push(D);if(X)if(R)J=`${$} = await Promise.all(${Q}.map(async function(__ser_item) { return __ser_item == null ? __ser_item : await execs[${O}].serialize(__ser_item, opts); }));`;else{J=`var ${j.nestedArr} = [];
52
+ `;J+=` for (var ${j.nestedIdx}=0; ${j.nestedIdx}<${Q}.length; ${j.nestedIdx}++) {
53
+ `;J+=` var ${j.nestedItem} = ${Q}[${j.nestedIdx}];
54
+ `;J+=` ${j.nestedArr}.push(${j.nestedItem} == null ? ${j.nestedItem} : execs[${O}].serialize(${j.nestedItem}, opts));
55
+ `;J+=` }
56
+ `;J+=` ${$} = ${j.nestedArr};`}else J=`${$} = ${R?"await ":""}execs[${O}].serialize(${Q}, opts);`}J+=T($,U,g,L);if(b)H=`if (${Q} !== undefined && ${Q} !== null) {
57
+ ${J}
58
+ } else if (${Q} === null) {
59
+ ${$} = null;
99
60
  }
100
- /**
101
- * Generate transform chain code to apply after nested/collection serialize.
102
- * Reads the current value from outputTarget, chains transforms, writes back.
103
- */
104
- function buildPostNestedTransformCode(outputTarget, fieldKey, serTransforms, refs) {
105
- const transformed = buildSerializeTransformExpr(outputTarget, fieldKey, serTransforms, refs);
106
- return transformed ? `\n${outputTarget} = ${transformed};` : '';
61
+ `;else H=`if (${Q} != null) {
62
+ ${J}
63
+ } else {
64
+ ${$} = ${Q};
107
65
  }
108
- // ─────────────────────────────────────────────────────────────────────────────
109
- // buildSerializeCode — new Function-based serialize executor generation (§4.3 serialize pipeline)
110
- // ─────────────────────────────────────────────────────────────────────────────
111
- /**
112
- * Generate serialize executor code.
113
- * Assumes no validation — always returns Record<string, unknown> (§4.3).
114
- */
115
- function buildSerializeCode(Class, merged, options, isAsync) {
116
- const refs = [];
117
- const execs = [];
118
- // ── Code generation ────────────────────────────────────────────────────────
119
- let body = "'use strict';\n";
120
- body += `var ${GEN.out} = {};\n`;
121
- // Groups variable — only when fields referencing groups exist
122
- const hasGroupsField = Object.values(merged).some(meta => {
123
- const groups = getSerializeExposeGroups(meta.expose);
124
- return groups && groups.length > 0;
125
- });
126
- if (hasGroupsField) {
127
- body += `var ${GEN.groups} = opts && opts.groups;\n`;
128
- body += `var ${GEN.group0} = ${GEN.groups} && ${GEN.groups}.length === 1 ? ${GEN.groups}[0] : null;\n`;
129
- body += `var ${GEN.groupsSet} = ${GEN.groups} && ${GEN.groups}.length > 1 ? new Set(${GEN.groups}) : null;\n`;
130
- }
131
- for (const [fieldKey, meta] of Object.entries(merged)) {
132
- body += generateSerializeFieldCode(fieldKey, meta, refs, execs, isAsync, options, Class.name);
133
- }
134
- body += `return ${GEN.out};\n`;
135
- // sourceURL (§4.9)
136
- // Sanitize class name so it cannot inject newlines / */ that would break out of the comment.
137
- const safeClsName = Class.name.replace(/[^\w$.-]/g, '_');
138
- body += `//# sourceURL=baker://${safeClsName}/serialize\n`;
139
- // ── Execute new Function ───────────────────────────────────────────────────
140
- const fnKeyword = isAsync ? 'async function' : 'function';
141
- const executor = new Function('refs', 'execs', 'BakerError', `return ${fnKeyword}(instance, opts) { ` + body + ' }')(refs, execs, BakerError);
142
- return executor;
143
- }
144
- // ─────────────────────────────────────────────────────────────────────────────
145
- // Per-field serialize code generation
146
- // ─────────────────────────────────────────────────────────────────────────────
147
- function generateSerializeFieldCode(fieldKey, meta, refs, execs, isAsync, options, className = '') {
148
- // ⓪ Exclude serializeOnly / bidirectional → skip
149
- if (meta.exclude) {
150
- if (!meta.exclude.deserializeOnly) {
151
- if (options?.debug) {
152
- const reason = meta.exclude.serializeOnly ? 'serializeOnly' : 'bidirectional';
153
- return `// [baker] field ${JSON.stringify(fieldKey)} excluded (${reason} @Exclude)\n`;
154
- }
155
- return '';
156
- }
157
- }
158
- // Expose: if all @Expose entries are deserializeOnly, skip for serialize
159
- if (meta.expose.length > 0 && meta.expose.every(e => e.deserializeOnly)) {
160
- if (options?.debug) {
161
- return `// [baker] field ${JSON.stringify(fieldKey)} excluded (all @Expose entries are deserializeOnly)\n`;
162
- }
163
- return '';
164
- }
165
- const outputKey = getSerializeOutputKey(fieldKey, meta.expose);
166
- const exposeGroups = getSerializeExposeGroups(meta.expose);
167
- const sk = sanitizeKey(fieldKey);
168
- const fieldVal = `${GEN.fieldVal}${sk}`;
169
- let fieldCode = '';
170
- fieldCode += `var ${fieldVal} = instance[${JSON.stringify(fieldKey)}];\n`;
171
- // groups check wrap (§4.5)
172
- let fieldStart = '';
173
- let fieldEnd = '';
174
- if (exposeGroups && exposeGroups.length > 0) {
175
- fieldStart = `if ((${GEN.group0} !== null || ${GEN.groupsSet}) && (${buildGroupsHasExpr(GEN.group0, GEN.groupsSet, exposeGroups)})) {\n`;
176
- fieldEnd = '}\n';
177
- }
178
- let innerCode = '';
179
- // ② @IsOptional → skip output if undefined (§4.3 serialize step 2)
180
- const useOptionalGuard = meta.flags.isOptional;
181
- // Collect serialize-direction transforms once
182
- const serTransforms = meta.transform.filter(td => !td.options?.deserializeOnly);
183
- // ③a Collection (Map/Set) serialize — Set → Array, Map → plain object
184
- if (meta.type?.collection) {
185
- const outputTarget = `${GEN.out}[${JSON.stringify(outputKey)}]`;
186
- const collection = meta.type.collection;
187
- let nestedCode;
188
- if (collection === 'Set') {
189
- if (meta.type.resolvedCollectionValue) {
190
- const nestedSealed = getSealed(meta.type.resolvedCollectionValue);
191
- const execIdx = execs.length;
192
- execs.push(nestedSealed);
193
- if (isAsync) {
194
- nestedCode = `{ var __ser_ps = []; for (var __ser_item of ${fieldVal}) { __ser_ps.push(__ser_item == null ? __ser_item : execs[${execIdx}].serialize(__ser_item, opts)); } ${outputTarget} = await Promise.all(__ser_ps); }`;
195
- }
196
- else {
197
- nestedCode = `var ${GEN.setArr} = [];\n`;
198
- nestedCode += ` for (var ${GEN.setItem} of ${fieldVal}) {\n`;
199
- nestedCode += ` ${GEN.setArr}.push(${GEN.setItem} == null ? ${GEN.setItem} : execs[${execIdx}].serialize(${GEN.setItem}, opts));\n`;
200
- nestedCode += ` }\n`;
201
- nestedCode += ` ${outputTarget} = ${GEN.setArr};`;
202
- }
203
- }
204
- else {
205
- nestedCode = `${outputTarget} = [...${fieldVal}];`;
206
- }
207
- }
208
- else {
209
- // Map → plain object (W8: keys must be strings — throw otherwise)
210
- const keyCheck = `if (typeof ${GEN.mapEntry}[0] !== 'string') { throw new BakerError(${JSON.stringify(className)} + ': Map field ' + ${JSON.stringify(fieldKey)} + ' has non-string key (' + typeof ${GEN.mapEntry}[0] + '). Map serialization requires string keys.'); }\n `;
211
- if (meta.type.resolvedCollectionValue) {
212
- const nestedSealed = getSealed(meta.type.resolvedCollectionValue);
213
- const execIdx = execs.length;
214
- execs.push(nestedSealed);
215
- const awaitKw = isAsync ? 'await ' : '';
216
- nestedCode = `var ${GEN.mapObj} = Object.create(null);\n`;
217
- nestedCode += ` for (var ${GEN.mapEntry} of ${fieldVal}) {\n`;
218
- nestedCode += ` ${keyCheck}`;
219
- nestedCode += `${GEN.mapObj}[${GEN.mapEntry}[0]] = ${GEN.mapEntry}[1] == null ? ${GEN.mapEntry}[1] : ${awaitKw}execs[${execIdx}].serialize(${GEN.mapEntry}[1], opts);\n`;
220
- nestedCode += ` }\n`;
221
- nestedCode += ` ${outputTarget} = ${GEN.mapObj};`;
222
- }
223
- else {
224
- nestedCode = `var ${GEN.mapObj} = Object.create(null);\n`;
225
- nestedCode += ` for (var ${GEN.mapEntry} of ${fieldVal}) {\n`;
226
- nestedCode += ` ${keyCheck}`;
227
- nestedCode += `${GEN.mapObj}[${GEN.mapEntry}[0]] = ${GEN.mapEntry}[1];\n`;
228
- nestedCode += ` }\n`;
229
- nestedCode += ` ${outputTarget} = ${GEN.mapObj};`;
230
- }
231
- }
232
- // Apply serialize transforms after collection serialize (nested → transform)
233
- nestedCode += buildPostNestedTransformCode(outputTarget, fieldKey, serTransforms, refs);
234
- if (useOptionalGuard) {
235
- innerCode = `if (${fieldVal} !== undefined && ${fieldVal} !== null) {\n ${nestedCode}\n} else if (${fieldVal} === null) {\n ${outputTarget} = null;\n}\n`;
236
- }
237
- else {
238
- innerCode = `if (${fieldVal} != null) {\n ${nestedCode}\n} else {\n ${outputTarget} = ${fieldVal};\n}\n`;
239
- }
240
- fieldCode += fieldStart + innerCode + fieldEnd;
241
- return fieldCode;
242
- }
243
- // ③b nested @Type handling (H4) — supports type + transform combination (nested serialize → transform)
244
- if (meta.type?.resolvedClass || meta.type?.discriminator || (meta.type?.fn && meta.flags.validateNested)) {
245
- // Determine array/each mode
246
- const hasEach = meta.type?.isArray || meta.flags.validateNestedEach || meta.validation.some(rd => rd.each);
247
- const outputTarget = `${GEN.out}[${JSON.stringify(outputKey)}]`;
248
- let nestedCode;
249
- if (meta.type.discriminator) {
250
- // §C-8 discriminator serialize — instanceof dispatch
251
- const { property, subTypes } = meta.type.discriminator;
252
- const keepDisc = meta.type.keepDiscriminatorProperty !== false; // default true for round-trip
253
- // Sort most-specific-first (subclasses take priority in inheritance relationships)
254
- const sorted = [...subTypes].sort((a, b) => {
255
- if (a.value.prototype instanceof b.value) {
256
- return -1;
257
- }
258
- if (b.value.prototype instanceof a.value) {
259
- return 1;
260
- }
261
- return 0;
262
- });
263
- // Helper for generating instanceof branch code
264
- const buildInstanceofChain = (itemVar, awaitKw) => {
265
- let code = '';
266
- for (let i = 0; i < sorted.length; i++) {
267
- const sub = sorted[i];
268
- const nestedSealed = getSealed(sub.value);
269
- const execIdx = execs.length;
270
- execs.push(nestedSealed);
271
- const refIdx = refs.length;
272
- refs.push(sub.value);
273
- const prefix = i === 0 ? 'if' : '} else if';
274
- code += `${prefix} (${itemVar} instanceof refs[${refIdx}]) {\n`;
275
- code += ` var ${GEN.serResult} = ${awaitKw}execs[${execIdx}].serialize(${itemVar}, opts);\n`;
276
- if (keepDisc) {
277
- code += ` ${GEN.serResult}[${JSON.stringify(property)}] = ${JSON.stringify(sub.name)};\n`;
278
- }
279
- code += ` ${GEN.outItem} = ${GEN.serResult};\n`;
280
- }
281
- code += `} else { ${GEN.outItem} = ` + itemVar + '; }\n';
282
- return code;
283
- };
284
- if (hasEach) {
285
- const awaitKw = isAsync ? 'await ' : '';
286
- if (isAsync) {
287
- nestedCode = `${outputTarget} = await Promise.all(${fieldVal}.map(async function(__ser_item) {\n`;
288
- }
289
- else {
290
- nestedCode = `var ${GEN.discArr} = [];\n`;
291
- nestedCode += ` for (var ${GEN.discIdx}=0; ${GEN.discIdx}<${fieldVal}.length; ${GEN.discIdx}++) {\n`;
292
- nestedCode += ` var __ser_item = ${fieldVal}[${GEN.discIdx}];\n`;
293
- }
294
- nestedCode += ` var ${GEN.outItem};\n`;
295
- nestedCode += buildInstanceofChain('__ser_item', awaitKw);
296
- if (isAsync) {
297
- nestedCode += ` return ${GEN.outItem};\n`;
298
- nestedCode += `}));`;
299
- }
300
- else {
301
- nestedCode += ` ${GEN.discArr}.push(${GEN.outItem});\n`;
302
- nestedCode += ` }\n`;
303
- nestedCode += ` ${outputTarget} = ${GEN.discArr};`;
304
- }
305
- }
306
- else {
307
- const awaitKw = isAsync ? 'await ' : '';
308
- nestedCode = `var ${GEN.outItem};\n`;
309
- nestedCode += buildInstanceofChain(fieldVal, awaitKw);
310
- nestedCode += `${outputTarget} = ${GEN.outItem};`;
311
- }
312
- }
313
- else {
314
- // Existing simple nested logic
315
- const nestedCls = meta.type.resolvedClass ?? meta.type.fn();
316
- const nestedSealed = getSealed(nestedCls);
317
- const execIdx = execs.length;
318
- execs.push(nestedSealed);
319
- if (hasEach) {
320
- if (isAsync) {
321
- nestedCode = `${outputTarget} = await Promise.all(${fieldVal}.map(async function(__ser_item) { return __ser_item == null ? __ser_item : await execs[${execIdx}].serialize(__ser_item, opts); }));`;
322
- }
323
- else {
324
- nestedCode = `var ${GEN.nestedArr} = [];\n`;
325
- nestedCode += ` for (var ${GEN.nestedIdx}=0; ${GEN.nestedIdx}<${fieldVal}.length; ${GEN.nestedIdx}++) {\n`;
326
- nestedCode += ` var ${GEN.nestedItem} = ${fieldVal}[${GEN.nestedIdx}];\n`;
327
- nestedCode += ` ${GEN.nestedArr}.push(${GEN.nestedItem} == null ? ${GEN.nestedItem} : execs[${execIdx}].serialize(${GEN.nestedItem}, opts));\n`;
328
- nestedCode += ` }\n`;
329
- nestedCode += ` ${outputTarget} = ${GEN.nestedArr};`;
330
- }
331
- }
332
- else {
333
- const awaitKw = isAsync ? 'await ' : '';
334
- nestedCode = `${outputTarget} = ${awaitKw}execs[${execIdx}].serialize(${fieldVal}, opts);`;
335
- }
336
- }
337
- // Apply serialize transforms after nested serialize (nested serialize → transform)
338
- nestedCode += buildPostNestedTransformCode(outputTarget, fieldKey, serTransforms, refs);
339
- if (useOptionalGuard) {
340
- innerCode = `if (${fieldVal} !== undefined && ${fieldVal} !== null) {\n ${nestedCode}\n} else if (${fieldVal} === null) {\n ${outputTarget} = null;\n}\n`;
341
- }
342
- else {
343
- innerCode = `if (${fieldVal} != null) {\n ${nestedCode}\n} else {\n ${outputTarget} = ${fieldVal};\n}\n`;
344
- }
345
- }
346
- else {
347
- // Existing @Transform or direct assign handling
348
- const outputExpr = buildSerializeOutputExpr(fieldKey, outputKey, fieldVal, meta, refs);
349
- if (useOptionalGuard) {
350
- innerCode += `if (${fieldVal} !== undefined) {\n`;
351
- innerCode += ' ' + outputExpr + '\n';
352
- innerCode += '}\n';
353
- }
354
- else {
355
- innerCode += outputExpr + '\n';
356
- }
357
- }
358
- fieldCode += fieldStart + innerCode + fieldEnd;
359
- return fieldCode;
360
- }
361
- /**
362
- * Build field output expression.
363
- * If @Transform exists, call refs[i](params); otherwise, direct assignment.
364
- */
365
- function buildSerializeOutputExpr(fieldKey, outputKey, fieldValueExpr, meta, refs) {
366
- const outputTarget = `${GEN.out}[${JSON.stringify(outputKey)}]`;
367
- const serTransforms = meta.transform.filter(td => !td.options?.deserializeOnly);
368
- if (serTransforms.length > 0) {
369
- const transformed = buildSerializeTransformExpr(fieldValueExpr, fieldKey, serTransforms, refs);
370
- return `${outputTarget} = ${transformed};`;
371
- }
372
- return `${outputTarget} = ${fieldValueExpr};`;
373
- }
374
- export { buildSerializeCode };
66
+ `}else{const X=f(U,Z,Q,q,L);if(b){H+=`if (${Q} !== undefined) {
67
+ `;H+=" "+X+`
68
+ `;H+=`}
69
+ `}else H+=X+`
70
+ `}_+=v+H+S;return _}function f(U,q,L,M,R){const Y=`${j.out}[${JSON.stringify(q)}]`,W=M.transform.filter((Z)=>!Z.options?.deserializeOnly);if(W.length>0){const Z=x(L,U,W,R);return`${Y} = ${Z};`}return`${Y} = ${L};`}export{buildSerializeCode};
@@ -1,61 +1 @@
1
- import { BakerError } from '../errors.js';
2
- import { hasRawOwn } from '../meta-access.js';
3
- /**
4
- * @internal — seal-time invariant checks invoked from sealOne after merge + type normalization,
5
- * before codegen. Throws BakerError on the first violation.
6
- *
7
- * Covers W2 (D7 + D9):
8
- * - Discriminator shape: empty subTypes / invalid subType entry / name collision / missing property
9
- * - Set/Map pairing: Set without setValue, Map without mapValue, setValue/mapValue target missing @Field metadata
10
- * - async-in-sync: a DTO that mixes async rules/transforms with sync rules/transforms in such a way
11
- * that the caller cannot easily tell — this throws BakerError so the user makes the intent explicit.
12
- * (Per W2 decision: throw, not warn.)
13
- */
14
- export function validateMeta(Class, merged) {
15
- const className = Class.name;
16
- for (const [key, meta] of Object.entries(merged)) {
17
- // ─── Discriminator shape ─────────────────────────────────────────────
18
- if (meta.type?.discriminator) {
19
- const disc = meta.type.discriminator;
20
- if (typeof disc.property !== 'string' || disc.property.length === 0) {
21
- throw new BakerError(`${className}.${key}: discriminator.property must be a non-empty string.`);
22
- }
23
- if (!Array.isArray(disc.subTypes) || disc.subTypes.length === 0) {
24
- throw new BakerError(`${className}.${key}: discriminator.subTypes must be a non-empty array of { value, name } entries.`);
25
- }
26
- const seenNames = new Set();
27
- for (let i = 0; i < disc.subTypes.length; i++) {
28
- const sub = disc.subTypes[i];
29
- if (typeof sub.name !== 'string' || sub.name.length === 0) {
30
- throw new BakerError(`${className}.${key}: discriminator.subTypes[${i}].name must be a non-empty string.`);
31
- }
32
- if (typeof sub.value !== 'function') {
33
- throw new BakerError(`${className}.${key}: discriminator.subTypes[${i}].value must be a class constructor (got ${typeof sub.value}).`);
34
- }
35
- if (seenNames.has(sub.name)) {
36
- throw new BakerError(`${className}.${key}: discriminator.subTypes has duplicate name '${sub.name}'. Each subType must have a unique name.`);
37
- }
38
- seenNames.add(sub.name);
39
- // subType class must have @Field metadata (RAW) — otherwise codegen will fail with a less clear error
40
- if (!hasRawOwn(sub.value)) {
41
- throw new BakerError(`${className}.${key}: discriminator.subTypes[${i}].value (${sub.value.name}) has no @Field decorators.`);
42
- }
43
- }
44
- }
45
- // ─── Set/Map collection pairing — unified single-pass check ──────────
46
- const collection = meta.type?.collection;
47
- if (collection !== undefined && meta.type?.resolvedCollectionValue) {
48
- const target = meta.type.resolvedCollectionValue;
49
- if (!hasRawOwn(target)) {
50
- const accessor = collection === 'Set' ? 'setValue' : 'mapValue';
51
- throw new BakerError(`${className}.${key}: ${accessor} target (${target.name}) has no @Field decorators.`);
52
- }
53
- }
54
- }
55
- // ─── async-in-sync: D9 ────────────────────────────────────────────────
56
- // Seal-time strict check for "mixed sync/async rules" was attempted but produces too many
57
- // false positives — sync rules + async transform is a common, valid baker pattern. The
58
- // remediation for D9 lives in W14's strict API: `validateSync(AsyncDto, x)` and the other
59
- // `*Sync` variants throw BakerError at the call site after consulting `isAsync`/`isSerializeAsync`.
60
- // No seal-time invariant added here.
61
- }
1
+ import{BakerError as x}from"../errors.js";import{hasRawOwn as G}from"../meta-access.js";export function validateMeta(H,I){const p=H.name;for(const[q,A]of Object.entries(I)){if(A.type?.discriminator){const f=A.type.discriminator;if(typeof f.property!=="string"||f.property.length===0)throw new x(`${p}.${q}: discriminator.property must be a non-empty string.`);if(!Array.isArray(f.subTypes)||f.subTypes.length===0)throw new x(`${p}.${q}: discriminator.subTypes must be a non-empty array of { value, name } entries.`);const D=new Set;for(let z=0;z<f.subTypes.length;z++){const j=f.subTypes[z];if(typeof j.name!=="string"||j.name.length===0)throw new x(`${p}.${q}: discriminator.subTypes[${z}].name must be a non-empty string.`);if(typeof j.value!=="function")throw new x(`${p}.${q}: discriminator.subTypes[${z}].value must be a class constructor (got ${typeof j.value}).`);if(D.has(j.name))throw new x(`${p}.${q}: discriminator.subTypes has duplicate name '${j.name}'. Each subType must have a unique name.`);D.add(j.name);if(!G(j.value))throw new x(`${p}.${q}: discriminator.subTypes[${z}].value (${j.value.name}) has no @Field decorators.`)}}const F=A.type?.collection;if(F!==void 0&&A.type?.resolvedCollectionValue){const f=A.type.resolvedCollectionValue;if(!G(f))throw new x(`${p}.${q}: ${F==="Set"?"setValue":"mapValue"} target (${f.name}) has no @Field decorators.`)}}}
@@ -1,13 +1 @@
1
- /**
2
- * 2 Symbols — zero external storage, zero global pollution
3
- * Uses Symbol.for: allows AOT code and runtime code to share the same Symbol via the global registry
4
- */
5
- // TC39 decorator metadata polyfill. Bun 1.3.13 does not yet expose Symbol.metadata natively,
6
- // so this defines it; the `??=` guard yields to a native value if a future runtime provides one.
7
- // Must run before any decorated class is evaluated — symbols.ts sits at the root of the
8
- // metadata import graph, so it always does.
9
- Symbol.metadata ??= Symbol.for('Symbol.metadata');
10
- /** Tier 1 collection metadata (stored on Class[Symbol.metadata] by decorators) */
11
- export const RAW = Symbol.for('baker:raw');
12
- /** Tier 2 seal result (dual executor stored on Class by seal()) */
13
- export const SEALED = Symbol.for('baker:sealed');
1
+ Symbol.metadata??=Symbol.for("Symbol.metadata");export const RAW=Symbol.for("baker:raw");export const SEALED=Symbol.for("baker:sealed");
@@ -1,25 +1 @@
1
- export function csvTransformer(separator = ',') {
2
- return {
3
- deserialize: ({ value }) => (typeof value === 'string' ? value.split(separator) : value),
4
- serialize: ({ value }) => (Array.isArray(value) ? value.join(separator) : value),
5
- };
6
- }
7
- export const jsonTransformer = {
8
- deserialize: ({ value }) => {
9
- if (typeof value !== 'string') {
10
- return value;
11
- }
12
- try {
13
- return JSON.parse(value);
14
- }
15
- catch {
16
- return value;
17
- }
18
- },
19
- serialize: ({ value }) => {
20
- if (value != null && typeof value === 'object') {
21
- return JSON.stringify(value);
22
- }
23
- return value;
24
- },
25
- };
1
+ export function csvTransformer(r=","){return{deserialize:({value:t})=>typeof t==="string"?t.split(r):t,serialize:({value:t})=>Array.isArray(t)?t.join(r):t}}export const jsonTransformer={deserialize:({value:r})=>{if(typeof r!=="string")return r;try{return JSON.parse(r)}catch{return r}},serialize:({value:r})=>{if(r!=null&&typeof r==="object")return JSON.stringify(r);return r}};
@@ -1,18 +1 @@
1
- export const unixSecondsTransformer = {
2
- deserialize: ({ value }) => (typeof value === 'number' ? new Date(value * 1000) : value),
3
- serialize: ({ value }) => (value instanceof Date ? Math.floor(value.getTime() / 1000) : value),
4
- };
5
- export const unixMillisTransformer = {
6
- deserialize: ({ value }) => (typeof value === 'number' ? new Date(value) : value),
7
- serialize: ({ value }) => (value instanceof Date ? value.getTime() : value),
8
- };
9
- export const isoStringTransformer = {
10
- deserialize: ({ value }) => {
11
- if (typeof value !== 'string') {
12
- return value;
13
- }
14
- const d = new Date(value);
15
- return Number.isNaN(d.getTime()) ? value : d;
16
- },
17
- serialize: ({ value }) => (value instanceof Date ? value.toISOString() : value),
18
- };
1
+ export const unixSecondsTransformer={deserialize:({value:r})=>typeof r==="number"?new Date(r*1000):r,serialize:({value:r})=>r instanceof Date?Math.floor(r.getTime()/1000):r};export const unixMillisTransformer={deserialize:({value:r})=>typeof r==="number"?new Date(r):r,serialize:({value:r})=>r instanceof Date?r.getTime():r};export const isoStringTransformer={deserialize:({value:r})=>{if(typeof r!=="string")return r;const e=new Date(r);return Number.isNaN(e.getTime())?r:e},serialize:({value:r})=>r instanceof Date?r.toISOString():r};
@@ -1,6 +1 @@
1
- export { trimTransformer, toLowerCaseTransformer, toUpperCaseTransformer } from './string.transformer.js';
2
- export { roundTransformer } from './number.transformer.js';
3
- export { unixSecondsTransformer, unixMillisTransformer, isoStringTransformer } from './date.transformer.js';
4
- export { csvTransformer, jsonTransformer } from './collection.transformer.js';
5
- export { luxonTransformer } from './luxon.transformer.js';
6
- export { momentTransformer } from './moment.transformer.js';
1
+ export{trimTransformer,toLowerCaseTransformer,toUpperCaseTransformer}from"./string.transformer.js";export{roundTransformer}from"./number.transformer.js";export{unixSecondsTransformer,unixMillisTransformer,isoStringTransformer}from"./date.transformer.js";export{csvTransformer,jsonTransformer}from"./collection.transformer.js";export{luxonTransformer}from"./luxon.transformer.js";export{momentTransformer}from"./moment.transformer.js";
@@ -1,34 +1 @@
1
- import { BakerError } from '../errors.js';
2
- const LUXON_MISSING = "luxonTransformer requires the optional peer dependency 'luxon'. Install it with: bun add luxon";
3
- async function luxonTransformer(opts) {
4
- let luxon;
5
- try {
6
- luxon = await import('luxon');
7
- }
8
- catch (e) {
9
- throw new BakerError(LUXON_MISSING, { cause: e });
10
- }
11
- const { DateTime } = luxon;
12
- const zone = opts?.zone ?? 'utc';
13
- // Hoist format option once so the serialize closure doesn't re-read opts per call
14
- const format = opts?.format;
15
- return {
16
- deserialize: ({ value }) => {
17
- if (typeof value === 'string') {
18
- return DateTime.fromISO(value, { zone });
19
- }
20
- if (value instanceof Date) {
21
- return DateTime.fromJSDate(value, { zone });
22
- }
23
- return value;
24
- },
25
- serialize: ({ value }) => {
26
- if (value && typeof value === 'object' && typeof value.toISO === 'function') {
27
- const v = value;
28
- return format ? v.toFormat(format) : v.toISO();
29
- }
30
- return value;
31
- },
32
- };
33
- }
34
- export { luxonTransformer };
1
+ import{BakerError as C}from"../errors.js";const F="luxonTransformer requires the optional peer dependency 'luxon'. Install it with: bun add luxon";async function luxonTransformer(g){let j;try{j=await import("luxon")}catch(b){throw new C(F,{cause:b})}const{DateTime:q}=j,w=g?.zone??"utc",y=g?.format;return{deserialize:({value:b})=>{if(typeof b==="string")return q.fromISO(b,{zone:w});if(b instanceof Date)return q.fromJSDate(b,{zone:w});return b},serialize:({value:b})=>{if(b&&typeof b==="object"&&typeof b.toISO==="function"){const A=b;return y?A.toFormat(y):A.toISO()}return b}}}export{luxonTransformer};