@zipbul/baker 2.2.0 → 3.0.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/MIGRATION-3.0.md +104 -0
  3. package/README.md +109 -63
  4. package/dist/index.d.ts +7 -6
  5. package/dist/index.js +10 -321
  6. package/dist/src/collect.d.ts +13 -10
  7. package/dist/src/collect.js +26 -0
  8. package/dist/src/configure.d.ts +8 -6
  9. package/dist/src/configure.js +43 -0
  10. package/dist/src/create-rule.js +41 -0
  11. package/dist/src/decorators/field.d.ts +22 -18
  12. package/dist/src/decorators/field.js +268 -0
  13. package/dist/src/decorators/index.d.ts +1 -0
  14. package/dist/src/decorators/index.js +2 -2
  15. package/dist/src/decorators/recipe.d.ts +17 -0
  16. package/dist/src/decorators/recipe.js +23 -0
  17. package/dist/src/errors.d.ts +27 -17
  18. package/dist/src/errors.js +52 -0
  19. package/dist/src/functions/check-call-options.d.ts +8 -0
  20. package/dist/src/functions/check-call-options.js +51 -0
  21. package/dist/src/functions/deserialize.d.ts +13 -6
  22. package/dist/src/functions/deserialize.js +57 -0
  23. package/dist/src/functions/serialize.d.ts +10 -4
  24. package/dist/src/functions/serialize.js +52 -0
  25. package/dist/src/functions/validate.d.ts +13 -10
  26. package/dist/src/functions/validate.js +49 -0
  27. package/dist/src/interfaces.d.ts +1 -1
  28. package/dist/src/interfaces.js +4 -0
  29. package/dist/src/meta-access.d.ts +19 -0
  30. package/dist/src/meta-access.js +75 -0
  31. package/dist/src/registry.js +8 -0
  32. package/dist/src/rule-metadata.d.ts +11 -0
  33. package/dist/src/rule-metadata.js +17 -0
  34. package/dist/src/rule-plan.d.ts +10 -11
  35. package/dist/src/rule-plan.js +117 -0
  36. package/dist/src/rules/array.d.ts +7 -6
  37. package/dist/src/rules/array.js +96 -0
  38. package/dist/src/rules/common.js +77 -0
  39. package/dist/src/rules/date.js +35 -0
  40. package/dist/src/rules/index.d.ts +2 -4
  41. package/dist/src/rules/index.js +8 -21
  42. package/dist/src/rules/locales.d.ts +5 -4
  43. package/dist/src/rules/locales.js +249 -0
  44. package/dist/src/rules/number.js +79 -0
  45. package/dist/src/rules/object.d.ts +1 -1
  46. package/dist/src/rules/object.js +49 -0
  47. package/dist/src/rules/string.d.ts +83 -80
  48. package/dist/src/rules/string.js +1998 -0
  49. package/dist/src/rules/typechecker.js +143 -0
  50. package/dist/src/seal/circular-analyzer.js +63 -0
  51. package/dist/src/seal/codegen-utils.js +18 -0
  52. package/dist/src/seal/deserialize-builder.d.ts +8 -4
  53. package/dist/src/seal/deserialize-builder.js +1546 -0
  54. package/dist/src/seal/expose-validator.d.ts +3 -2
  55. package/dist/src/seal/expose-validator.js +65 -0
  56. package/dist/src/seal/seal-state.d.ts +10 -0
  57. package/dist/src/seal/seal-state.js +18 -0
  58. package/dist/src/seal/seal.d.ts +22 -21
  59. package/dist/src/seal/seal.js +431 -0
  60. package/dist/src/seal/serialize-builder.d.ts +3 -2
  61. package/dist/src/seal/serialize-builder.js +374 -0
  62. package/dist/src/seal/validate-meta.d.ts +13 -0
  63. package/dist/src/seal/validate-meta.js +61 -0
  64. package/dist/src/symbols.d.ts +1 -1
  65. package/dist/src/symbols.js +13 -2
  66. package/dist/src/transformers/collection.transformer.js +25 -0
  67. package/dist/src/transformers/date.transformer.js +18 -0
  68. package/dist/src/transformers/index.js +6 -2
  69. package/dist/src/transformers/luxon.transformer.d.ts +4 -2
  70. package/dist/src/transformers/luxon.transformer.js +34 -0
  71. package/dist/src/transformers/moment.transformer.d.ts +4 -2
  72. package/dist/src/transformers/moment.transformer.js +32 -0
  73. package/dist/src/transformers/number.transformer.js +8 -0
  74. package/dist/src/transformers/string.transformer.js +12 -0
  75. package/dist/src/types.d.ts +27 -25
  76. package/dist/src/types.js +1 -0
  77. package/dist/src/utils.d.ts +2 -2
  78. package/dist/src/utils.js +10 -0
  79. package/package.json +80 -67
  80. package/dist/index-03cysbck.js +0 -3
  81. package/dist/index-dcbd798a.js +0 -3
  82. package/dist/index-jp2yjd6g.js +0 -3
  83. package/dist/index-mw7met6r.js +0 -3
  84. package/dist/index-xdn55cz3.js +0 -1
  85. package/dist/src/functions/_run-sealed.d.ts +0 -7
  86. package/dist/src/functions/index.d.ts +0 -3
  87. package/dist/src/seal/index.d.ts +0 -5
@@ -0,0 +1,1546 @@
1
+ import { err as resultErr, isErr as resultIsErr } from '@zipbul/result';
2
+ import { BakerError } from '../errors.js';
3
+ import { getSealed } from '../meta-access.js';
4
+ import { emitRulePlan } from '../rule-plan.js';
5
+ import { sanitizeKey, buildGroupsHasExpr } from './codegen-utils.js';
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ // Generated variable name prefixes — centralised to prevent typo-related bugs
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ const GEN = {
10
+ field: '__bk$f_',
11
+ index: '__bk$i_',
12
+ setIdx: '__bk$si_',
13
+ setVal: '__bk$sv_',
14
+ mapIdx: '__bk$mi_',
15
+ mapVal: '__bk$mv_',
16
+ mark: '__bk$mark_',
17
+ skip: '__bk$skip_',
18
+ result: '__bk$r_',
19
+ errors: '__bk$re_',
20
+ arr: '__bk$arr_',
21
+ disc: '__bk$dt_',
22
+ nestedIdx: '__bk$j_',
23
+ out: '__bk$out',
24
+ errList: '__bk$errors',
25
+ groups: '__bk$groups',
26
+ group0: '__bk$group0',
27
+ groupsSet: '__bk$groupsSet',
28
+ key: '__bk$k',
29
+ };
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // Helpers — code generation utilities
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ /** Generate nested error push code that propagates message/context fields */
34
+ function nestedErrPush(errList, pathExpr, errItemExpr, tmpVar) {
35
+ // Cache errItemExpr once — avoids repeated property reads in the generated body
36
+ const eVar = `${tmpVar}_e`;
37
+ return (`var ${eVar}=${errItemExpr};\n` +
38
+ ` if(${eVar}.message===undefined&&${eVar}.context===undefined){${errList}.push({path:${pathExpr},code:${eVar}.code});}\n` +
39
+ ` else{var ${tmpVar}={path:${pathExpr},code:${eVar}.code};\n` +
40
+ ` if(${eVar}.message!==undefined)${tmpVar}.message=${eVar}.message;\n` +
41
+ ` if(${eVar}.context!==undefined)${tmpVar}.context=${eVar}.context;\n` +
42
+ ` ${errList}.push(${tmpVar});}\n`);
43
+ }
44
+ /** Generate nested error return code that propagates message/context fields */
45
+ function nestedErrReturn(pathExpr, errItemExpr, tmpVar, validateOnly) {
46
+ const ret = (arr) => (validateOnly ? `return ${arr};\n` : `return err(${arr});\n`);
47
+ return (`if(${errItemExpr}.message===undefined&&${errItemExpr}.context===undefined)${ret(`[{path:${pathExpr},code:${errItemExpr}.code}]`)}` +
48
+ ` var ${tmpVar}={path:${pathExpr},code:${errItemExpr}.code};\n` +
49
+ ` if(${errItemExpr}.message!==undefined)${tmpVar}.message=${errItemExpr}.message;\n` +
50
+ ` if(${errItemExpr}.context!==undefined)${tmpVar}.context=${errItemExpr}.context;\n` +
51
+ ` ${ret(`[${tmpVar}]`)}`);
52
+ }
53
+ /** Convert field name to a safe JS variable name (includes prefix to prevent internal variable collisions) */
54
+ function toVarName(key, prefix) {
55
+ return GEN.field + (prefix || '') + sanitizeKey(key);
56
+ }
57
+ /** Determine the extraction key for deserialization (§4.3 step 3) */
58
+ function getDeserializeExtractKey(fieldKey, exposeStack) {
59
+ // deserializeOnly @Expose with name → use that name
60
+ const desDef = exposeStack.find(e => e.deserializeOnly && e.name);
61
+ if (desDef) {
62
+ return desDef.name;
63
+ }
64
+ // Non-directional @Expose with name → use for both directions
65
+ const biDef = exposeStack.find(e => !e.deserializeOnly && !e.serializeOnly && e.name);
66
+ if (biDef) {
67
+ return biDef.name;
68
+ }
69
+ return fieldKey;
70
+ }
71
+ /** Determine field expose groups — returns undefined (no restriction) if any unconditional expose entry exists */
72
+ function getDeserializeExposeGroups(exposeStack) {
73
+ // Single-pass: scan once, bail out as soon as we see an unconditional entry,
74
+ // lazily allocate the result Set.
75
+ let all = null;
76
+ for (const e of exposeStack) {
77
+ if (e.serializeOnly) {
78
+ continue;
79
+ }
80
+ if (!e.groups || e.groups.length === 0) {
81
+ return undefined;
82
+ }
83
+ if (all === null) {
84
+ all = new Set();
85
+ }
86
+ for (const g of e.groups) {
87
+ all.add(g);
88
+ }
89
+ }
90
+ return all === null ? undefined : [...all];
91
+ }
92
+ function buildDeserializeCode(Class, merged, options, needsCircularCheck, isAsync, validateOnly = false) {
93
+ const stopAtFirstError = options?.stopAtFirstError ?? false;
94
+ const collectErrors = !stopAtFirstError;
95
+ const exposeDefaultValues = options?.exposeDefaultValues ?? false;
96
+ // Reference arrays — injected into new Function closure
97
+ const regexes = [];
98
+ const refs = [];
99
+ const execs = [];
100
+ // ── Code generation ────────────────────────────────────────────────────────
101
+ // Helper: wrap error array return — validate mode returns raw array, deserialize mode wraps in Result.err
102
+ const wrapErr = validateOnly ? (inner) => inner : (inner) => `err(${inner})`;
103
+ let body = "'use strict';\n";
104
+ // Create instance — skip in validate mode (no object creation needed)
105
+ if (validateOnly) {
106
+ if (exposeDefaultValues) {
107
+ body += 'var __bk$defs = new _Cls();\n';
108
+ }
109
+ }
110
+ else {
111
+ body += exposeDefaultValues ? `var ${GEN.out} = new _Cls();\n` : `var ${GEN.out} = Object.create(_Cls.prototype);\n`;
112
+ }
113
+ // Error array (collectErrors mode)
114
+ if (collectErrors) {
115
+ body += `var ${GEN.errList} = [];\n`;
116
+ }
117
+ // preamble: input type guard (§4.9)
118
+ body += `if (input == null || typeof input !== 'object' || Array.isArray(input)) return ${wrapErr("[{path:'',code:'invalidInput'}]")};\n`;
119
+ // WeakSet guard (circular references) — N-3 fix: WeakSet lives per-call, threaded through
120
+ // `opts` via a Symbol-keyed slot so nested DTOs in the same call share it. Symbol keys are
121
+ // invisible to `Object.keys`/checkCallOptions, so this doesn't pollute the user's opts shape.
122
+ // The previous shared-ref WeakSet caused concurrent async deserialize() to false-positive.
123
+ if (needsCircularCheck) {
124
+ // __SEEN_KEY is hoisted out of the per-call body and captured via the closure
125
+ // arguments of `new Function(...)` below — eliminates Symbol.for() lookup on every call.
126
+ // Object literal spread is replaced with branched alloc — Bun/JSC optimizes literal-spread
127
+ // better than Object.assign({}, ...) (audit H4/H5).
128
+ body += `var __seen = (opts && opts[__SEEN_KEY]) || null;\n`;
129
+ body += `if (__seen === null) { __seen = new WeakSet(); opts = opts ? { ...opts, [__SEEN_KEY]: __seen } : { [__SEEN_KEY]: __seen }; }\n`;
130
+ body += `if (__seen.has(input)) return ${wrapErr("[{path:'',code:'circular'}]")};\n`;
131
+ body += `__seen.add(input);\n`;
132
+ body += `try {\n`;
133
+ }
134
+ // Whitelist check (§7.2) — reject undeclared fields
135
+ if (options?.whitelist) {
136
+ const allowedKeys = new Set();
137
+ for (const [fieldKey, meta] of Object.entries(merged)) {
138
+ const extractKey = getDeserializeExtractKey(fieldKey, meta.expose);
139
+ allowedKeys.add(extractKey);
140
+ }
141
+ const allowedIdx = refs.length;
142
+ refs.push(allowedKeys);
143
+ // Indexed Object.keys loop — empirically 2–30× faster than for-in + Object.hasOwn on
144
+ // Bun/JSC. The keys array allocation is dominated by the per-iteration cost of for-in's
145
+ // prototype walk + hasOwn function call.
146
+ if (collectErrors) {
147
+ body += `{var __wlk=Object.keys(input);for(var __wli=0;__wli<__wlk.length;__wli++){var ${GEN.key}=__wlk[__wli];if(!refs[${allowedIdx}].has(${GEN.key}))${GEN.errList}.push({path:${GEN.key},code:'whitelistViolation'});}}\n`;
148
+ }
149
+ else {
150
+ body += `{var __wlk=Object.keys(input);for(var __wli=0;__wli<__wlk.length;__wli++){var ${GEN.key}=__wlk[__wli];if(!refs[${allowedIdx}].has(${GEN.key}))return ${wrapErr(`[{path:${GEN.key},code:'whitelistViolation'}]`)};}}\n`;
151
+ }
152
+ }
153
+ // Groups variable — only when expose groups or validation rule groups exist (§4.9, §M4).
154
+ // Single for-of with early break avoids Object.values alloc + closure allocations.
155
+ let hasGroupsField = false;
156
+ for (const fk in merged) {
157
+ const meta = merged[fk];
158
+ const exposeGroups = getDeserializeExposeGroups(meta.expose);
159
+ if (exposeGroups && exposeGroups.length > 0) {
160
+ hasGroupsField = true;
161
+ break;
162
+ }
163
+ let ruleHasGroups = false;
164
+ for (const rd of meta.validation) {
165
+ if (rd.groups && rd.groups.length > 0) {
166
+ ruleHasGroups = true;
167
+ break;
168
+ }
169
+ }
170
+ if (ruleHasGroups) {
171
+ hasGroupsField = true;
172
+ break;
173
+ }
174
+ }
175
+ if (hasGroupsField) {
176
+ body += `var ${GEN.groups} = opts && opts.groups;\n`;
177
+ body += `var ${GEN.group0} = ${GEN.groups} && ${GEN.groups}.length === 1 ? ${GEN.groups}[0] : null;\n`;
178
+ body += `var ${GEN.groupsSet} = ${GEN.groups} && ${GEN.groups}.length > 1 ? new Set(${GEN.groups}) : null;\n`;
179
+ }
180
+ // ── Per-field code generation ──────────────────────────────────────────────
181
+ for (const [fieldKey, meta] of Object.entries(merged)) {
182
+ const fieldCode = generateFieldCode(fieldKey, meta, {
183
+ stopAtFirstError,
184
+ collectErrors,
185
+ exposeDefaultValues,
186
+ isAsync,
187
+ regexes,
188
+ refs,
189
+ execs,
190
+ options,
191
+ validateOnly,
192
+ });
193
+ body += fieldCode;
194
+ }
195
+ // ── epilogue ──────────────────────────────────────────────────────────────
196
+ if (collectErrors) {
197
+ body += `if (${GEN.errList}.length) return ${validateOnly ? GEN.errList : `err(${GEN.errList})`};\n`;
198
+ }
199
+ body += `return ${validateOnly ? 'null' : GEN.out};\n`;
200
+ // Close try/finally for circular reference WeakSet cleanup
201
+ if (needsCircularCheck) {
202
+ body += `} finally { __seen.delete(input); }\n`;
203
+ }
204
+ // sourceURL (§4.9)
205
+ // Sanitize class name so it cannot inject newlines / */ that would break out of the comment.
206
+ const safeClsName = Class.name.replace(/[^\w$.-]/g, '_');
207
+ body += `//# sourceURL=baker://${safeClsName}/${validateOnly ? 'validate' : 'deserialize'}\n`;
208
+ // ── Execute new Function ───────────────────────────────────────────────────
209
+ const fnKeyword = isAsync ? 'async function' : 'function';
210
+ const seenKey = Symbol.for('baker:circular-seen');
211
+ const executor = new Function('_Cls', 're', 'refs', 'execs', 'err', 'isErr', '__SEEN_KEY', `return ${fnKeyword}(input, opts) { ` + body + ' }')(Class, regexes, refs, execs, resultErr, resultIsErr, seenKey);
212
+ return executor;
213
+ }
214
+ // ─────────────────────────────────────────────────────────────────────────────
215
+ // buildValidateCode — validate-only executor (no Object.create, no assignments)
216
+ // ─────────────────────────────────────────────────────────────────────────────
217
+ function buildValidateCode(Class, merged, options, needsCircularCheck, isAsync) {
218
+ return buildDeserializeCode(Class, merged, options, needsCircularCheck, isAsync, true);
219
+ }
220
+ function resolveGuardKey(isNullable, useOptionalGuard, isDefined) {
221
+ if (isNullable && useOptionalGuard) {
222
+ return 'nullable+optional';
223
+ }
224
+ if (isNullable) {
225
+ return 'nullable';
226
+ }
227
+ if (isDefined) {
228
+ return 'defined';
229
+ }
230
+ if (useOptionalGuard) {
231
+ return 'optional';
232
+ }
233
+ return 'default';
234
+ }
235
+ const GUARD_STRATEGIES = {
236
+ // Case 4: @IsNullable + @IsOptional — assign null, skip undefined
237
+ 'nullable+optional'({ varName, assignNull, validationCode }) {
238
+ let code = `if (${varName} === null) { ${assignNull}}\n`;
239
+ code += `else if (${varName} !== undefined) {\n`;
240
+ code += validationCode;
241
+ code += '}\n';
242
+ return code;
243
+ },
244
+ // Case 3: @IsNullable (+ optional @IsDefined — same behavior)
245
+ nullable({ varName, emitCtx, assignNull, validationCode }) {
246
+ let code = `if (${varName} === undefined) ${emitCtx.fail('isDefined')};\n`;
247
+ code += `else if (${varName} !== null) {\n`;
248
+ code += validationCode;
249
+ code += `} else { ${assignNull}}\n`;
250
+ return code;
251
+ },
252
+ // @IsDefined — reject only undefined, null/""/0 etc. pass through to subsequent validation
253
+ defined({ varName, emitCtx, validationCode }) {
254
+ let code = `if (${varName} === undefined) ${emitCtx.fail('isDefined')};\n`;
255
+ code += validationCode;
256
+ return code;
257
+ },
258
+ // Case 2: @IsOptional — skip entirely on undefined/null
259
+ optional({ varName, validationCode }) {
260
+ let code = `if (${varName} !== undefined && ${varName} !== null) {\n`;
261
+ code += validationCode;
262
+ code += '}\n';
263
+ return code;
264
+ },
265
+ // Case 1: No flags (default) — reject undefined/null
266
+ default({ varName, emitCtx, validationCode }) {
267
+ let code = `if (${varName} === undefined || ${varName} === null) ${emitCtx.fail('isDefined')};\n`;
268
+ code += `else {\n`;
269
+ code += validationCode;
270
+ code += '}\n';
271
+ return code;
272
+ },
273
+ };
274
+ function generateFieldCode(fieldKey, meta, ctx) {
275
+ const { exposeDefaultValues } = ctx;
276
+ // ⓪ Exclude deserializeOnly / bidirectional → skip
277
+ if (meta.exclude) {
278
+ if (!meta.exclude.serializeOnly) {
279
+ if (ctx.options?.debug) {
280
+ const reason = meta.exclude.deserializeOnly ? 'deserializeOnly' : 'bidirectional';
281
+ return `// [baker] field ${JSON.stringify(fieldKey)} excluded (${reason} @Exclude)\n`;
282
+ }
283
+ return '';
284
+ }
285
+ }
286
+ // Expose: check if this field is exposed to deserialize
287
+ // If all @Expose entries are serializeOnly, skip field
288
+ if (meta.expose.length > 0 && meta.expose.every(e => e.serializeOnly)) {
289
+ if (ctx.options?.debug) {
290
+ return `// [baker] field ${JSON.stringify(fieldKey)} excluded (all @Expose entries are serializeOnly)\n`;
291
+ }
292
+ return '';
293
+ }
294
+ const varName = toVarName(fieldKey, ctx.varPrefix);
295
+ const extractKey = getDeserializeExtractKey(fieldKey, meta.expose);
296
+ const exposeGroups = getDeserializeExposeGroups(meta.expose);
297
+ const inputObj = ctx.inputExpr || 'input';
298
+ // Create EmitContext
299
+ const emitCtx = makeEmitCtx(fieldKey, ctx);
300
+ let fieldCode = '';
301
+ // ① @ValidateIf guard
302
+ let validateIfIdx = null;
303
+ if (meta.flags.validateIf) {
304
+ validateIfIdx = ctx.refs.length;
305
+ ctx.refs.push(meta.flags.validateIf);
306
+ }
307
+ // ③ Extract + exposeDefaultValues — W7 (N-4): use Object.hasOwn to block prototype-inherited values
308
+ let extractCode;
309
+ const extractKeyJson = JSON.stringify(extractKey);
310
+ if (exposeDefaultValues && !meta.flags.isOptional) {
311
+ // exposeDefaultValues still needs hasOwn — must distinguish "missing key" (use default)
312
+ // from "explicit undefined" (no default). Prototype-only keys are treated as missing.
313
+ const defaultsSource = ctx.validateOnly ? '__bk$defs' : GEN.out;
314
+ extractCode = `var ${varName} = Object.hasOwn(${inputObj}, ${extractKeyJson}) ? ${inputObj}[${extractKeyJson}] : ${defaultsSource}[${JSON.stringify(fieldKey)}];\n`;
315
+ }
316
+ else {
317
+ // Direct property access (own or inherited), matching the fast-validator norm (e.g. ajv).
318
+ // A per-field `Object.hasOwn` guard would read own-only but cost ~10 ns per 5-field DTO
319
+ // (Bun 1.3.13 / i7-13700K) — a ~30% regression on the hot path. The only case it would change
320
+ // is an input whose prototype chain carries a declared field name, which requires a global
321
+ // `Object.prototype` pollution introduced elsewhere (a separate, pre-existing app vulnerability
322
+ // — baker's own input gate rejects `__proto__` payloads). Normal inputs (JSON.parse, framework
323
+ // request bodies) are always own-keyed, so this never triggers in practice.
324
+ extractCode = `var ${varName} = ${inputObj}[${extractKeyJson}];\n`;
325
+ }
326
+ // groups check wrap (§4.5)
327
+ let fieldStart = '';
328
+ let fieldEnd = '';
329
+ if (exposeGroups && exposeGroups.length > 0) {
330
+ fieldStart = `if ((${GEN.group0} !== null || ${GEN.groupsSet}) && (${buildGroupsHasExpr(GEN.group0, GEN.groupsSet, exposeGroups)})) {\n`;
331
+ fieldEnd = '}\n';
332
+ }
333
+ // inner content (extract + optional guard + validation + assign)
334
+ let innerCode = extractCode;
335
+ // ② null/undefined guard — @IsOptional, @IsNullable, @IsDefined combinations (§4.3, Phase5)
336
+ const useOptionalGuard = !!(meta.flags.isOptional && !meta.flags.isDefined);
337
+ const isNullable = meta.flags.isNullable === true;
338
+ const validationCode = generateValidationCode(fieldKey, varName, meta, ctx, emitCtx, exposeGroups);
339
+ const assignNull = ctx.validateOnly ? '' : `${GEN.out}[${JSON.stringify(fieldKey)}] = null;\n`;
340
+ const guardKey = resolveGuardKey(isNullable, useOptionalGuard, meta.flags.isDefined ?? false);
341
+ innerCode += GUARD_STRATEGIES[guardKey]({ varName, emitCtx, assignNull, validationCode });
342
+ // ① @ValidateIf outer wrap
343
+ if (validateIfIdx !== null) {
344
+ fieldCode += fieldStart + `if (refs[${validateIfIdx}](${inputObj})) {\n` + innerCode + '}\n' + fieldEnd;
345
+ }
346
+ else {
347
+ fieldCode += fieldStart + innerCode + fieldEnd;
348
+ }
349
+ return fieldCode;
350
+ }
351
+ // ─────────────────────────────────────────────────────────────────────────────
352
+ // Validation code generation — type guard + transform + validate + assign
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+ function generateValidationCode(fieldKey, varName, meta, ctx, emitCtx, fieldGroups) {
355
+ const { collectErrors } = ctx;
356
+ let code = '';
357
+ // @Transform (deserialize direction) — before validation (§4.3 ⑤)
358
+ const dsTransforms = meta.transform.filter(td => !td.options?.serializeOnly);
359
+ if (dsTransforms.length > 0) {
360
+ const fkJson = JSON.stringify(fieldKey);
361
+ const objExpr = ctx.inputExpr || 'input';
362
+ if (dsTransforms.length === 1) {
363
+ const td = dsTransforms[0];
364
+ const refIdx = ctx.refs.length;
365
+ ctx.refs.push(td.fn);
366
+ const callExpr = `refs[${refIdx}]({value:${varName},key:${fkJson},obj:${objExpr}})`;
367
+ code += `${varName} = ${td.isAsync ? 'await ' : ''}${callExpr};\n`;
368
+ }
369
+ else if (dsTransforms.length === 2) {
370
+ const td0 = dsTransforms[0];
371
+ const td1 = dsTransforms[1];
372
+ const refIdx0 = ctx.refs.length;
373
+ ctx.refs.push(td0.fn);
374
+ const refIdx1 = ctx.refs.length;
375
+ ctx.refs.push(td1.fn);
376
+ const call0 = `refs[${refIdx0}]({value:${varName},key:${fkJson},obj:${objExpr}})`;
377
+ const expr0 = td0.isAsync ? `await ${call0}` : call0;
378
+ const call1 = `refs[${refIdx1}]({value:${expr0},key:${fkJson},obj:${objExpr}})`;
379
+ code += `${varName} = ${td1.isAsync ? 'await ' : ''}${call1};\n`;
380
+ }
381
+ else {
382
+ for (const td of dsTransforms) {
383
+ const refIdx = ctx.refs.length;
384
+ ctx.refs.push(td.fn);
385
+ const callExpr = `refs[${refIdx}]({value:${varName},key:${fkJson},obj:${objExpr}})`;
386
+ code += `${varName} = ${td.isAsync ? 'await ' : ''}${callExpr};\n`;
387
+ }
388
+ }
389
+ }
390
+ // Collection (Map/Set) auto conversion
391
+ if (meta.type?.collection) {
392
+ code += ctx.validateOnly
393
+ ? generateCollectionCodeValidateOnly(fieldKey, varName, meta, ctx, emitCtx)
394
+ : generateCollectionCode(fieldKey, varName, meta, ctx, emitCtx);
395
+ return code;
396
+ }
397
+ // @ValidateNested + @Type (§8.1)
398
+ if (meta.flags.validateNested && meta.type?.fn) {
399
+ code += ctx.validateOnly
400
+ ? generateNestedCodeValidateOnly(fieldKey, varName, meta, ctx, emitCtx)
401
+ : generateNestedCode(fieldKey, varName, meta, ctx, emitCtx);
402
+ return code;
403
+ }
404
+ // No validation rules → direct assign (skip in validate mode)
405
+ if (meta.validation.length === 0) {
406
+ if (!ctx.validateOnly) {
407
+ code += `${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
408
+ }
409
+ return code;
410
+ }
411
+ // Build validation with type gate
412
+ code += buildRulesCode(fieldKey, varName, meta.validation, collectErrors, emitCtx, ctx, meta, fieldGroups);
413
+ return code;
414
+ }
415
+ // ─────────────────────────────────────────────────────────────────────────────
416
+ // Helper for computing per-rule extra fields (message/context) code strings
417
+ // ─────────────────────────────────────────────────────────────────────────────
418
+ /** Convert rule's message/context options into extra field strings within generated code */
419
+ function computeRuleExtras(rd, fieldKey, varName, ctx) {
420
+ let extra = '';
421
+ if (typeof rd.message === 'string') {
422
+ extra += `,message:${JSON.stringify(rd.message)}`;
423
+ }
424
+ else if (typeof rd.message === 'function') {
425
+ const msgIdx = ctx.refs.length;
426
+ ctx.refs.push(rd.message);
427
+ const constraintsIdx = ctx.refs.length;
428
+ ctx.refs.push(rd.rule.constraints ?? {});
429
+ extra += `,message:refs[${msgIdx}]({property:${JSON.stringify(fieldKey)},value:${varName},constraints:refs[${constraintsIdx}]})`;
430
+ }
431
+ if (rd.context !== undefined) {
432
+ const ctxIdx = ctx.refs.length;
433
+ ctx.refs.push(rd.context);
434
+ extra += `,context:refs[${ctxIdx}]`;
435
+ }
436
+ return extra;
437
+ }
438
+ /** Create per-rule EmitContext (with message/context overrides) */
439
+ function makeRuleEmitCtx(baseEmitCtx, fieldKey, varName, rd, ctx) {
440
+ const extra = computeRuleExtras(rd, fieldKey, varName, ctx);
441
+ if (!extra) {
442
+ return baseEmitCtx;
443
+ }
444
+ const pathExpr = baseEmitCtx.pathExpr ?? JSON.stringify(fieldKey);
445
+ return {
446
+ ...baseEmitCtx,
447
+ fail(code) {
448
+ if (baseEmitCtx.collectErrors) {
449
+ return `${GEN.errList}.push({path:${pathExpr},code:${JSON.stringify(code)}${extra}})`;
450
+ }
451
+ else if (ctx.validateOnly) {
452
+ return `return [{path:${pathExpr},code:${JSON.stringify(code)}${extra}}]`;
453
+ }
454
+ return `return err([{path:${pathExpr},code:${JSON.stringify(code)}${extra}}])`;
455
+ },
456
+ };
457
+ }
458
+ function emitRuleList(fieldKey, varName, rules, emitCtx, ctx, indent, fieldGroups, insideTypeGate) {
459
+ let code = '';
460
+ // Single-pass partition over rules, counting both cacheable categories without a filter[] alloc.
461
+ let lengthCount = 0;
462
+ let timeCount = 0;
463
+ for (const rd of rules) {
464
+ if (!sameGroups(rd.groups, fieldGroups)) {
465
+ continue;
466
+ }
467
+ if (rd.rule.plan?.cacheKey === 'length') {
468
+ lengthCount += 1;
469
+ }
470
+ else if (rd.rule.plan?.cacheKey === 'time') {
471
+ timeCount += 1;
472
+ }
473
+ }
474
+ const sk = sanitizeKey(fieldKey);
475
+ const lengthVar = lengthCount > 1 ? `${GEN.arr}${sk}len` : null;
476
+ const timeVar = timeCount > 1 ? `${GEN.arr}${sk}time` : null;
477
+ if (lengthVar) {
478
+ code += `${indent}var ${lengthVar} = ${varName}.length;\n`;
479
+ }
480
+ if (timeVar) {
481
+ code += `${indent}var ${timeVar} = ${varName}.getTime();\n`;
482
+ }
483
+ for (const rd of rules) {
484
+ const sg = sameGroups(rd.groups, fieldGroups); // cache once — was called 3× per rule
485
+ const ruleEmitCtx = makeRuleEmitCtx(emitCtx, fieldKey, varName, rd, ctx);
486
+ const gatedCtx = insideTypeGate ? { ...ruleEmitCtx, insideTypeGate: true } : ruleEmitCtx;
487
+ let emitted;
488
+ if (sg && rd.rule.plan && (lengthVar || timeVar)) {
489
+ const cache = {};
490
+ if (rd.rule.plan.cacheKey === 'length' && lengthVar) {
491
+ cache.length = lengthVar;
492
+ }
493
+ if (rd.rule.plan.cacheKey === 'time' && timeVar) {
494
+ cache.time = timeVar;
495
+ }
496
+ emitted = emitRulePlan(varName, gatedCtx, rd.rule.ruleName, rd.rule.plan, cache, insideTypeGate);
497
+ }
498
+ else {
499
+ emitted = rd.rule.emit(varName, gatedCtx);
500
+ }
501
+ if (!emitted) {
502
+ continue;
503
+ } // empty emit (e.g., asserter fully subsumed by gate)
504
+ const ruleCode = sg ? emitted : wrapGroupsGuard(rd, emitted);
505
+ code += indent + ruleCode.replace(/\n/g, '\n' + indent) + '\n';
506
+ }
507
+ return code;
508
+ }
509
+ // ─────────────────────────────────────────────────────────────────────────────
510
+ // wrapGroupsGuard — per-rule validation groups check wrapper (§M4)
511
+ // ─────────────────────────────────────────────────────────────────────────────
512
+ /**
513
+ * When rd.groups is set, only execute code if there is an intersection with runtime __bk$groups.
514
+ * Rules without groups always execute (preserves existing behavior).
515
+ */
516
+ function wrapGroupsGuard(rd, code) {
517
+ if (!rd.groups || rd.groups.length === 0) {
518
+ return code;
519
+ }
520
+ return `if ((${GEN.group0} === null && !${GEN.groupsSet}) || ${buildGroupsHasExpr(GEN.group0, GEN.groupsSet, rd.groups)}) {\n${code}\n}\n`;
521
+ }
522
+ function sameGroups(a, b) {
523
+ if (!a || a.length === 0) {
524
+ return !b || b.length === 0;
525
+ }
526
+ if (!b || a.length !== b.length) {
527
+ return false;
528
+ }
529
+ for (let i = 0; i < a.length; i++) {
530
+ if (a[i] !== b[i]) {
531
+ return false;
532
+ }
533
+ }
534
+ return true;
535
+ }
536
+ // ─────────────────────────────────────────────────────────────────────────────
537
+ // generateConversionCode — enableImplicitConversion conversion code generation
538
+ // ─────────────────────────────────────────────────────────────────────────────
539
+ function generateConversionCode(targetType, varName, fieldKey, skipVar, // null = stopAtFirstError
540
+ collectErrors, emitCtx) {
541
+ const failCode = collectErrors
542
+ ? `${emitCtx.fail('conversionFailed')}; ${skipVar} = true;`
543
+ : emitCtx.fail('conversionFailed') + ';';
544
+ switch (targetType) {
545
+ case 'string':
546
+ return ` ${varName} = String(${varName});\n`;
547
+ case 'number':
548
+ return ` ${varName} = Number(${varName});\n if (isNaN(${varName})) { ${failCode} }\n`;
549
+ case 'boolean':
550
+ return (` if (${varName} === 'true' || ${varName} === '1' || ${varName} === 1) ${varName} = true;\n` +
551
+ ` else if (${varName} === 'false' || ${varName} === '0' || ${varName} === 0) ${varName} = false;\n` +
552
+ ` else { ${failCode} }\n`);
553
+ case 'date':
554
+ return ` ${varName} = new Date(${varName});\n if (isNaN(${varName}.getTime())) { ${failCode} }\n`;
555
+ default:
556
+ throw new BakerError(`Unknown implicit conversion type: "${targetType}" for field "${fieldKey}"`);
557
+ }
558
+ }
559
+ /** `@Type`() primitive builtin → target type mapping */
560
+ const PRIMITIVE_TYPE_HINTS = {
561
+ Number: 'number',
562
+ Boolean: 'boolean',
563
+ String: 'string',
564
+ Date: 'date',
565
+ };
566
+ /** Asserter rule name → gate type mapping */
567
+ const ASSERTER_TO_GATE = {
568
+ isString: 'string',
569
+ isNumber: 'number',
570
+ isBoolean: 'boolean',
571
+ isDate: 'date',
572
+ isInt: 'number',
573
+ isArray: 'array',
574
+ isObject: 'object',
575
+ };
576
+ /** Asserters whose gate check fully subsumes the rule (skip emit inside gate) */
577
+ const GATE_ONLY_ASSERTERS = new Set(['isString', 'isBoolean', 'isDate', 'isArray', 'isObject']);
578
+ /** categorizeRules — separate each/nonEach rules, detect mixed gate conflicts */
579
+ function categorizeRules(fieldKey, validation) {
580
+ // Single-pass partition — was 9 separate .filter() passes over the same array, each allocating
581
+ // a fresh intermediate. For a field with N rules, runs at seal time only but adds up across DTOs.
582
+ const each = [];
583
+ const generalRules = [];
584
+ const typedBuckets = {
585
+ string: [],
586
+ number: [],
587
+ boolean: [],
588
+ date: [],
589
+ array: [],
590
+ object: [],
591
+ };
592
+ for (const rd of validation) {
593
+ if (rd.each) {
594
+ each.push(rd);
595
+ continue;
596
+ }
597
+ const reqType = rd.rule.requiresType;
598
+ if (reqType !== undefined) {
599
+ typedBuckets[reqType].push(rd);
600
+ }
601
+ else {
602
+ generalRules.push(rd);
603
+ }
604
+ }
605
+ // Mixed gate conflict detection — at most one bucket should be non-empty
606
+ let chosen = undefined;
607
+ let activeTypes = null;
608
+ for (const t of ['string', 'number', 'boolean', 'date', 'array', 'object']) {
609
+ const deps = typedBuckets[t];
610
+ if (deps.length === 0) {
611
+ continue;
612
+ }
613
+ if (chosen) {
614
+ // Late allocation: only build the array when we actually need to report a conflict
615
+ if (activeTypes === null) {
616
+ activeTypes = [chosen.type];
617
+ }
618
+ activeTypes.push(t);
619
+ }
620
+ else {
621
+ chosen = { type: t, deps };
622
+ }
623
+ }
624
+ if (activeTypes) {
625
+ throw new BakerError(`Field "${fieldKey}" has conflicting requiresType: ${activeTypes.join(', ')}`);
626
+ }
627
+ return { each, generalRules, typedDeps: chosen };
628
+ }
629
+ /** resolveTypeGate — determine effective gate type from asserters/conversion/type hints */
630
+ function resolveTypeGate(fieldKey, categorized, meta, ctx) {
631
+ const { generalRules, typedDeps } = categorized;
632
+ const hasTypedDeps = !!typedDeps;
633
+ const gateType = typedDeps?.type ?? null;
634
+ const gateDeps = typedDeps?.deps ?? [];
635
+ // Find type asserter in generalRules matching gate type
636
+ let typeAsserterIdx = -1;
637
+ if (gateType) {
638
+ typeAsserterIdx = generalRules.findIndex(rd => ASSERTER_TO_GATE[rd.rule.ruleName] === gateType);
639
+ }
640
+ // enableImplicitConversion check — skip if explicit @Transform for deserialize direction
641
+ const enableConversion = !!ctx.options?.enableImplicitConversion && !meta?.transform.some(td => !td.options?.serializeOnly);
642
+ // enableImplicitConversion: asserter-only gate inference — generate conversion gate even for standalone @IsNumber() usage
643
+ let asserterInferredGate = null;
644
+ if (!hasTypedDeps && enableConversion && typeAsserterIdx < 0) {
645
+ for (let i = 0; i < generalRules.length; i++) {
646
+ const gate = ASSERTER_TO_GATE[generalRules[i].rule.ruleName];
647
+ if (gate) {
648
+ typeAsserterIdx = i;
649
+ asserterInferredGate = gate;
650
+ break;
651
+ }
652
+ }
653
+ }
654
+ const typeAsserter = typeAsserterIdx >= 0 ? generalRules[typeAsserterIdx] : undefined;
655
+ // @Type() primitive hint — infer conversion target when no typed deps exist
656
+ let typeHintGate = null;
657
+ if (!hasTypedDeps && !asserterInferredGate && enableConversion && meta?.type?.fn) {
658
+ try {
659
+ const raw = meta.type.fn();
660
+ const typeCtor = Array.isArray(raw) ? raw[0] : raw;
661
+ typeHintGate = typeCtor ? (PRIMITIVE_TYPE_HINTS[typeCtor.name] ?? null) : null;
662
+ }
663
+ catch (e) {
664
+ throw new BakerError(`field "${fieldKey}": @Field type function threw: ${e.message}`, { cause: e });
665
+ }
666
+ }
667
+ return {
668
+ effectiveGateType: gateType ?? asserterInferredGate ?? typeHintGate,
669
+ gateDeps,
670
+ typeAsserterIdx,
671
+ typeAsserter,
672
+ enableConversion,
673
+ asserterInferredGate,
674
+ typeHintGate,
675
+ };
676
+ }
677
+ /** emitTypedRules — generate type gate + inner validation code */
678
+ function emitTypedRules(fieldKey, varName, collectErrors, emitCtx, ctx, config, fieldGroups) {
679
+ let code = '';
680
+ const sk = sanitizeKey(fieldKey); // cached — was called up to 4× in this function before
681
+ const { effectiveGateType, gateCondition, gateErrorCode, gateEmitCtx, otherGeneral, gateDeps, typeAsserter, enableConversion } = config;
682
+ // Helper: emit inner validation rules
683
+ const emitInnerRules = (indent) => {
684
+ const rules = [];
685
+ // typeAsserter emit — skip GATE_ONLY_ASSERTERS (isString, isBoolean) as they fully overlap with the gate
686
+ if (typeAsserter && !GATE_ONLY_ASSERTERS.has(typeAsserter.rule.ruleName)) {
687
+ rules.push(typeAsserter);
688
+ }
689
+ rules.push(...otherGeneral, ...gateDeps);
690
+ return emitRuleList(fieldKey, varName, rules, emitCtx, ctx, indent, fieldGroups, true);
691
+ };
692
+ if (collectErrors) {
693
+ const canConvert = enableConversion &&
694
+ (effectiveGateType === 'string' ||
695
+ effectiveGateType === 'number' ||
696
+ effectiveGateType === 'boolean' ||
697
+ effectiveGateType === 'date');
698
+ if (canConvert) {
699
+ // Conversion mode: try convert on gate failure, skip field if conversion fails
700
+ const skipVar = `${GEN.skip}${sk}`;
701
+ code += `var ${skipVar} = false;\n`;
702
+ code += `if (${gateCondition}) {\n`;
703
+ code += generateConversionCode(effectiveGateType, varName, fieldKey, skipVar, true, emitCtx);
704
+ code += `}\n`;
705
+ code += `if (!${skipVar}) {\n`;
706
+ if (ctx.validateOnly) {
707
+ code += emitInnerRules(' ');
708
+ }
709
+ else {
710
+ const markVar = `${GEN.mark}${sk}`;
711
+ code += ` var ${markVar} = ${GEN.errList}.length;\n`;
712
+ code += emitInnerRules(' ');
713
+ code += ` if (${GEN.errList}.length === ${markVar}) ${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
714
+ }
715
+ code += `}\n`;
716
+ }
717
+ else {
718
+ code += `if (${gateCondition}) ${gateEmitCtx.fail(gateErrorCode)};\n`;
719
+ code += `else {\n`;
720
+ if (ctx.validateOnly) {
721
+ code += emitInnerRules(' ');
722
+ }
723
+ else {
724
+ const markVar = `${GEN.mark}${sk}`;
725
+ code += ` var ${markVar} = ${GEN.errList}.length;\n`;
726
+ code += emitInnerRules(' ');
727
+ code += ` if (${GEN.errList}.length === ${markVar}) ${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
728
+ }
729
+ code += `}\n`;
730
+ }
731
+ }
732
+ else {
733
+ const canConvert = enableConversion &&
734
+ (effectiveGateType === 'string' ||
735
+ effectiveGateType === 'number' ||
736
+ effectiveGateType === 'boolean' ||
737
+ effectiveGateType === 'date');
738
+ if (canConvert) {
739
+ code += `if (${gateCondition}) {\n`;
740
+ code += generateConversionCode(effectiveGateType, varName, fieldKey, null, false, emitCtx);
741
+ code += `}\n`;
742
+ code += emitInnerRules('');
743
+ if (!ctx.validateOnly) {
744
+ code += `${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
745
+ }
746
+ }
747
+ else {
748
+ code += `if (${gateCondition}) ${gateEmitCtx.fail(gateErrorCode)};\n`;
749
+ code += emitInnerRules('');
750
+ if (!ctx.validateOnly) {
751
+ code += `${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
752
+ }
753
+ }
754
+ }
755
+ return code;
756
+ }
757
+ /** emitGeneralRules — generate type-agnostic rule code */
758
+ function emitGeneralRules(fieldKey, varName, generalRules, collectErrors, emitCtx, ctx, fieldGroups) {
759
+ let code = '';
760
+ if (collectErrors) {
761
+ if (generalRules.length === 0) {
762
+ if (!ctx.validateOnly) {
763
+ code += `${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
764
+ }
765
+ }
766
+ else if (ctx.validateOnly) {
767
+ code += emitRuleList(fieldKey, varName, generalRules, emitCtx, ctx, '', fieldGroups);
768
+ }
769
+ else {
770
+ const markVar = `${GEN.mark}${sanitizeKey(fieldKey)}`;
771
+ code += `var ${markVar} = ${GEN.errList}.length;\n`;
772
+ code += emitRuleList(fieldKey, varName, generalRules, emitCtx, ctx, '', fieldGroups);
773
+ code += `if (${GEN.errList}.length === ${markVar}) ${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
774
+ }
775
+ }
776
+ else {
777
+ code += emitRuleList(fieldKey, varName, generalRules, emitCtx, ctx, '', fieldGroups);
778
+ if (!ctx.validateOnly) {
779
+ code += `${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
780
+ }
781
+ }
782
+ return code;
783
+ }
784
+ /** emitEachRules — generate Array/Set/Map each code */
785
+ function emitEachRules(fieldKey, varName, eachRules, collectErrors, emitCtx, ctx, fieldGroups) {
786
+ let code = '';
787
+ for (const rd of eachRules) {
788
+ // pathKey must honor ctx.pathPrefix so inlined nested DTOs report full path.
789
+ // Without this, validate(Parent, ...) returned `tags[1]` while deserialize returned `nested.tags[1]`.
790
+ const pathKey = ctx.pathPrefix ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}` : JSON.stringify(fieldKey);
791
+ const sk = sanitizeKey(fieldKey);
792
+ const iVar = `${GEN.index}${sk}`;
793
+ const siVar = `${GEN.setIdx}${sk}`;
794
+ const svVar = `${GEN.setVal}${sk}`;
795
+ const miVar = `${GEN.mapIdx}${sk}`;
796
+ const mvVar = `${GEN.mapVal}${sk}`;
797
+ const extra = computeRuleExtras(rd, fieldKey, varName, ctx);
798
+ // Cache the groups-guard predicate once — was previously evaluated twice (open + close)
799
+ const rdGroups = rd.groups && rd.groups.length > 0 && !sameGroups(rd.groups, fieldGroups) ? rd.groups : null;
800
+ const eachGuardOpen = rdGroups
801
+ ? `if ((${GEN.group0} === null && !${GEN.groupsSet}) || ${buildGroupsHasExpr(GEN.group0, GEN.groupsSet, rdGroups)}) {\n`
802
+ : '';
803
+ const eachGuardClose = rdGroups ? '}\n' : '';
804
+ // Collection descriptors: [idxVar, elemExpr, loopHeader, counterDecl, counterInc]
805
+ const collections = [
806
+ {
807
+ guard: `Array.isArray(${varName})`,
808
+ idxVar: iVar,
809
+ elemExpr: `${varName}[${iVar}]`,
810
+ loopHeader: `for (var ${iVar}=0; ${iVar}<${varName}.length; ${iVar}++)`,
811
+ counterDecl: '',
812
+ counterInc: '',
813
+ },
814
+ {
815
+ guard: `${varName} instanceof Set`,
816
+ idxVar: siVar,
817
+ elemExpr: svVar,
818
+ loopHeader: `for (var ${svVar} of ${varName})`,
819
+ counterDecl: `var ${siVar} = 0;\n`,
820
+ counterInc: `${siVar}++;\n`,
821
+ },
822
+ {
823
+ guard: `${varName} instanceof Map`,
824
+ idxVar: miVar,
825
+ elemExpr: mvVar,
826
+ loopHeader: `for (var ${mvVar} of ${varName}.values())`,
827
+ counterDecl: `var ${miVar} = 0;\n`,
828
+ counterInc: `${miVar}++;\n`,
829
+ },
830
+ ];
831
+ // Single prefix var shared across all collection branches — only one branch executes
832
+ // at runtime, so reusing the same identifier avoids 3 hoisted-but-dead var declarations.
833
+ const prefixVar = `__bk$ep_${sk}`;
834
+ const emitCollectionBlock = (col) => {
835
+ const failFn = (c) => collectErrors
836
+ ? `${GEN.errList}.push({path:${prefixVar}+${col.idxVar}+']',code:${JSON.stringify(c)}${extra}})`
837
+ : ctx.validateOnly
838
+ ? `return [{path:${prefixVar}+${col.idxVar}+']',code:${JSON.stringify(c)}${extra}}]`
839
+ : `return err([{path:${prefixVar}+${col.idxVar}+']',code:${JSON.stringify(c)}${extra}}])`;
840
+ const colEmitCtx = { ...emitCtx, fail: failFn };
841
+ let block = '';
842
+ block += ` ${col.counterDecl}`;
843
+ block += ` ${col.loopHeader} {\n`;
844
+ block += ' ' + rd.rule.emit(col.elemExpr, colEmitCtx) + '\n';
845
+ if (col.counterInc) {
846
+ block += ` ${col.counterInc}`;
847
+ }
848
+ block += ` }\n`;
849
+ return block;
850
+ };
851
+ // Compute collection kind once via integer dispatch — eliminates 2-3× repeated
852
+ // Array.isArray / instanceof Set / instanceof Map evaluation.
853
+ // prefixVar is declared once here and reused by all three branches.
854
+ const kindVar = `__bk$ck${sk}`;
855
+ code += eachGuardOpen;
856
+ code += `var ${kindVar} = Array.isArray(${varName})?1:(${varName} instanceof Set?2:(${varName} instanceof Map?3:0));\n`;
857
+ code += `var ${prefixVar} = ${pathKey}+'[';\n`;
858
+ if (collectErrors) {
859
+ code += `if (${kindVar} === 1) {\n`;
860
+ code += emitCollectionBlock(collections[0]);
861
+ code += `} else if (${kindVar} === 2) {\n`;
862
+ code += emitCollectionBlock(collections[1]);
863
+ code += `} else if (${kindVar} === 3) {\n`;
864
+ code += emitCollectionBlock(collections[2]);
865
+ code += `} else { ${GEN.errList}.push({path:${pathKey},code:'isArray'}); }\n`;
866
+ }
867
+ else {
868
+ code += `if (${kindVar} === 0) ${emitCtx.fail('isArray')};\n`;
869
+ code += `if (${kindVar} === 1) {\n`;
870
+ code += emitCollectionBlock(collections[0]);
871
+ code += `} else if (${kindVar} === 2) {\n`;
872
+ code += emitCollectionBlock(collections[1]);
873
+ code += `} else if (${kindVar} === 3) {\n`;
874
+ code += emitCollectionBlock(collections[2]);
875
+ code += `}\n`;
876
+ }
877
+ code += eachGuardClose;
878
+ }
879
+ return code;
880
+ }
881
+ /** buildRulesCode — orchestrator that composes categorize → resolve → emit phases */
882
+ function buildRulesCode(fieldKey, varName, validation, collectErrors, emitCtx, ctx, meta, fieldGroups) {
883
+ // Phase 1: Categorize rules
884
+ const categorized = categorizeRules(fieldKey, validation);
885
+ // Phase 2: Resolve type gate
886
+ const resolved = resolveTypeGate(fieldKey, categorized, meta, ctx);
887
+ let code = '';
888
+ // Phase 3: Emit typed or general rules
889
+ const hasTypedDeps = !!categorized.typedDeps;
890
+ if (hasTypedDeps || resolved.asserterInferredGate || resolved.typeHintGate) {
891
+ // Other general rules (excluding the type asserter)
892
+ const otherGeneral = resolved.typeAsserter
893
+ ? categorized.generalRules.filter((_, i) => i !== resolved.typeAsserterIdx)
894
+ : categorized.generalRules;
895
+ // Generate type gate condition — date uses instanceof, others use typeof
896
+ let gateCondition;
897
+ let gateErrorCode;
898
+ if (resolved.typeAsserter) {
899
+ gateErrorCode = resolved.typeAsserter.rule.ruleName;
900
+ }
901
+ else if (resolved.gateDeps.length > 0) {
902
+ gateErrorCode = resolved.gateDeps[0].rule.ruleName;
903
+ }
904
+ else {
905
+ gateErrorCode = 'conversionFailed'; // @Type hint only — no asserter or deps
906
+ }
907
+ if (resolved.effectiveGateType === 'date') {
908
+ gateCondition = `!(${varName} instanceof Date) || isNaN(${varName}.getTime())`;
909
+ }
910
+ else if (resolved.effectiveGateType === 'array') {
911
+ gateCondition = `!Array.isArray(${varName})`;
912
+ }
913
+ else if (resolved.effectiveGateType === 'object') {
914
+ gateCondition = `typeof ${varName} !== 'object' || ${varName} === null || Array.isArray(${varName})`;
915
+ }
916
+ else if (resolved.effectiveGateType === 'number') {
917
+ gateCondition = `typeof ${varName} !== 'number' || isNaN(${varName})`;
918
+ }
919
+ else {
920
+ gateCondition = `typeof ${varName} !== '${resolved.effectiveGateType}'`;
921
+ }
922
+ // Type gate fail — reflect message/context if typeAsserter rd exists
923
+ const gateEmitCtx = resolved.typeAsserter ? makeRuleEmitCtx(emitCtx, fieldKey, varName, resolved.typeAsserter, ctx) : emitCtx;
924
+ code += emitTypedRules(fieldKey, varName, collectErrors, emitCtx, ctx, {
925
+ effectiveGateType: resolved.effectiveGateType,
926
+ gateCondition,
927
+ gateErrorCode,
928
+ gateEmitCtx,
929
+ otherGeneral,
930
+ gateDeps: resolved.gateDeps,
931
+ typeAsserter: resolved.typeAsserter,
932
+ enableConversion: resolved.enableConversion,
933
+ }, fieldGroups);
934
+ }
935
+ else {
936
+ code += emitGeneralRules(fieldKey, varName, categorized.generalRules, collectErrors, emitCtx, ctx, fieldGroups);
937
+ }
938
+ // Phase 4: Emit each rules
939
+ code += emitEachRules(fieldKey, varName, categorized.each, collectErrors, emitCtx, ctx, fieldGroups);
940
+ return code;
941
+ }
942
+ // ─────────────────────────────────────────────────────────────────────────────
943
+ // generateCollectionCode — Map/Set auto conversion
944
+ // ─────────────────────────────────────────────────────────────────────────────
945
+ function generateCollectionCode(fieldKey, varName, meta, ctx, emitCtx) {
946
+ const { collectErrors, execs } = ctx;
947
+ const sk = sanitizeKey(fieldKey);
948
+ const collection = meta.type.collection;
949
+ const awaitKw = ctx.isAsync ? 'await ' : '';
950
+ // nested DTO executor (if present)
951
+ let execIdx = -1;
952
+ if (meta.type.resolvedCollectionValue) {
953
+ const nestedSealed = getSealed(meta.type.resolvedCollectionValue);
954
+ execIdx = execs.length;
955
+ execs.push(nestedSealed);
956
+ }
957
+ let code = '';
958
+ if (collection === 'Set') {
959
+ // input: array → Set
960
+ code += `if (Array.isArray(${varName})) {\n`;
961
+ // array-level validation rules (e.g. arrayMinSize)
962
+ const nonEachRules = meta.validation.filter(rd => !rd.each);
963
+ code += emitRuleList(fieldKey, varName, nonEachRules, emitCtx, ctx, ' ');
964
+ if (execIdx >= 0) {
965
+ // nested DTO Set
966
+ const iVar = `${GEN.index}${sk}`;
967
+ code += ` var ${GEN.arr}${sk} = new Set();\n`;
968
+ code += ` for (var ${iVar}=0; ${iVar}<${varName}.length; ${iVar}++) {\n`;
969
+ code += ` var ${GEN.result}${sk} = ${awaitKw}execs[${execIdx}].deserialize(${varName}[${iVar}], opts);\n`;
970
+ code += ` if (isErr(${GEN.result}${sk})) {\n`;
971
+ if (collectErrors) {
972
+ code += ` var ${GEN.errors}${sk} = ${GEN.result}${sk}.data;\n`;
973
+ code += ` var __bk$pp${sk} = ${JSON.stringify(fieldKey)}+'['+${iVar}+'].';\n`;
974
+ code += ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${GEN.errors}${sk}.length; ${GEN.nestedIdx}${sk}++) {\n`;
975
+ code +=
976
+ ` ` +
977
+ nestedErrPush(GEN.errList, `__bk$pp${sk}+${GEN.errors}${sk}[${GEN.nestedIdx}${sk}].path`, `${GEN.errors}${sk}[${GEN.nestedIdx}${sk}]`, `__ne${sk}`);
978
+ code += ` }\n`;
979
+ }
980
+ else {
981
+ code += ` var ${GEN.errors}${sk} = ${GEN.result}${sk}.data;\n`;
982
+ code += ` var __bk$pp${sk} = ${JSON.stringify(fieldKey)}+'['+${iVar}+'].';\n`;
983
+ code += ` ` + nestedErrReturn(`__bk$pp${sk}+${GEN.errors}${sk}[0].path`, `${GEN.errors}${sk}[0]`, `__ne${sk}`);
984
+ }
985
+ code += ` } else { ${GEN.arr}${sk}.add(${GEN.result}${sk}); }\n`;
986
+ code += ` }\n`;
987
+ code += ` ${GEN.out}[${JSON.stringify(fieldKey)}] = ${GEN.arr}${sk};\n`;
988
+ }
989
+ else {
990
+ // primitive Set
991
+ code += ` ${GEN.out}[${JSON.stringify(fieldKey)}] = new Set(${varName});\n`;
992
+ }
993
+ // each validation rules (per element)
994
+ const eachRules = meta.validation.filter(rd => rd.each);
995
+ if (eachRules.length > 0) {
996
+ const siVar = `${GEN.setIdx}${sk}`;
997
+ const svVar = `${GEN.setVal}${sk}`;
998
+ code += ` var ${siVar} = 0;\n`;
999
+ code += ` for (var ${svVar} of ${GEN.out}[${JSON.stringify(fieldKey)}]) {\n`;
1000
+ for (const rd of eachRules) {
1001
+ 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)}}])`;
1004
+ const colEmitCtx = { ...emitCtx, fail: failFn };
1005
+ code += ` ${rd.rule.emit(svVar, colEmitCtx)}\n`;
1006
+ }
1007
+ code += ` ${siVar}++;\n`;
1008
+ code += ` }\n`;
1009
+ }
1010
+ code += `} else { ${emitCtx.fail('isArray')}; }\n`;
1011
+ }
1012
+ else {
1013
+ // Map: input plain object → Map
1014
+ code += `if (${varName} != null && typeof ${varName} === 'object' && !Array.isArray(${varName})) {\n`;
1015
+ if (execIdx >= 0) {
1016
+ // nested DTO Map — indexed Object.keys loop (measured 2-30× faster than for-in+hasOwn on Bun/JSC)
1017
+ const kVar = `${GEN.key}${sk}`;
1018
+ const ksVar = `__bk$mk${sk}`;
1019
+ const iVarMap = `__bk$mi${sk}`;
1020
+ code += ` var ${GEN.arr}${sk} = new Map();\n`;
1021
+ code += ` var ${ksVar} = Object.keys(${varName});\n`;
1022
+ code += ` for (var ${iVarMap}=0; ${iVarMap}<${ksVar}.length; ${iVarMap}++) {\n`;
1023
+ code += ` var ${kVar} = ${ksVar}[${iVarMap}];\n`;
1024
+ code += ` var ${GEN.result}${sk} = ${awaitKw}execs[${execIdx}].deserialize(${varName}[${kVar}], opts);\n`;
1025
+ code += ` if (isErr(${GEN.result}${sk})) {\n`;
1026
+ if (collectErrors) {
1027
+ code += ` var ${GEN.errors}${sk} = ${GEN.result}${sk}.data;\n`;
1028
+ code += ` var __bk$pp${sk} = ${JSON.stringify(fieldKey)}+'['+${kVar}+'].';\n`;
1029
+ code += ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${GEN.errors}${sk}.length; ${GEN.nestedIdx}${sk}++) {\n`;
1030
+ code +=
1031
+ ` ` +
1032
+ nestedErrPush(GEN.errList, `__bk$pp${sk}+${GEN.errors}${sk}[${GEN.nestedIdx}${sk}].path`, `${GEN.errors}${sk}[${GEN.nestedIdx}${sk}]`, `__ne${sk}`);
1033
+ code += ` }\n`;
1034
+ }
1035
+ else {
1036
+ code += ` var ${GEN.errors}${sk} = ${GEN.result}${sk}.data;\n`;
1037
+ code += ` var __bk$pp${sk} = ${JSON.stringify(fieldKey)}+'['+${kVar}+'].';\n`;
1038
+ code += ` ` + nestedErrReturn(`__bk$pp${sk}+${GEN.errors}${sk}[0].path`, `${GEN.errors}${sk}[0]`, `__ne${sk}`);
1039
+ }
1040
+ code += ` } else { ${GEN.arr}${sk}.set(${kVar}, ${GEN.result}${sk}); }\n`;
1041
+ code += ` }\n`;
1042
+ code += ` ${GEN.out}[${JSON.stringify(fieldKey)}] = ${GEN.arr}${sk};\n`;
1043
+ }
1044
+ else {
1045
+ // primitive Map — indexed Object.keys loop
1046
+ const ksVar = `__bk$mk${sk}`;
1047
+ const iVarMap = `__bk$mi${sk}`;
1048
+ code += ` var ${GEN.arr}${sk} = new Map();\n`;
1049
+ code += ` var ${ksVar} = Object.keys(${varName});\n`;
1050
+ code += ` for (var ${iVarMap}=0; ${iVarMap}<${ksVar}.length; ${iVarMap}++) {\n`;
1051
+ code += ` var ${GEN.key}${sk} = ${ksVar}[${iVarMap}];\n`;
1052
+ code += ` ${GEN.arr}${sk}.set(${GEN.key}${sk}, ${varName}[${GEN.key}${sk}]);\n`;
1053
+ code += ` }\n`;
1054
+ code += ` ${GEN.out}[${JSON.stringify(fieldKey)}] = ${GEN.arr}${sk};\n`;
1055
+ }
1056
+ code += `} else { ${emitCtx.fail('isObject')}; }\n`;
1057
+ }
1058
+ return code;
1059
+ }
1060
+ // ─────────────────────────────────────────────────────────────────────────────
1061
+ // generateNestedCode — @ValidateNested + @Type (§8.1, §8.2)
1062
+ // ─────────────────────────────────────────────────────────────────────────────
1063
+ function generateNestedCode(fieldKey, varName, meta, ctx, emitCtx) {
1064
+ const { collectErrors, execs } = ctx;
1065
+ if (!meta.type) {
1066
+ return `${GEN.out}[${JSON.stringify(fieldKey)}] = ${varName};\n`;
1067
+ }
1068
+ let code = '';
1069
+ const sk = sanitizeKey(fieldKey);
1070
+ if (meta.type.discriminator) {
1071
+ // §8.3 discriminator
1072
+ const discProp = JSON.stringify(meta.type.discriminator.property);
1073
+ code += `var ${GEN.disc}${sk} = ${varName} && ${varName}[${discProp}];\n`;
1074
+ code += `switch (${GEN.disc}${sk}) {\n`;
1075
+ for (const sub of meta.type.discriminator.subTypes) {
1076
+ const nestedSealed = getSealed(sub.value);
1077
+ const execIdx = execs.length;
1078
+ execs.push(nestedSealed);
1079
+ const awaitKwD = ctx.isAsync ? 'await ' : '';
1080
+ code += ` case ${JSON.stringify(sub.name)}:\n`;
1081
+ code += ` var ${GEN.result}${sk} = ${awaitKwD}execs[${execIdx}].deserialize(${varName}, opts);\n`;
1082
+ code += generateNestedResultCode(fieldKey, `${GEN.result}${sk}`, collectErrors, ctx.pathPrefix);
1083
+ code += ` break;\n`;
1084
+ }
1085
+ const validSubTypeNamesJson = JSON.stringify(meta.type.discriminator.subTypes.map(s => s.name));
1086
+ const discPathExpr = emitCtx.pathExpr ?? JSON.stringify(fieldKey);
1087
+ const discValueExpr = `${GEN.disc}${sk}`;
1088
+ if (collectErrors) {
1089
+ code += ` default: ${GEN.errList}.push({path:${discPathExpr},code:'invalidDiscriminator',context:{received:${discValueExpr},validSubTypes:${validSubTypeNamesJson}}});\n`;
1090
+ }
1091
+ else if (ctx.validateOnly) {
1092
+ code += ` default: return [{path:${discPathExpr},code:'invalidDiscriminator',context:{received:${discValueExpr},validSubTypes:${validSubTypeNamesJson}}}];\n`;
1093
+ }
1094
+ else {
1095
+ code += ` default: return err([{path:${discPathExpr},code:'invalidDiscriminator',context:{received:${discValueExpr},validSubTypes:${validSubTypeNamesJson}}}]);\n`;
1096
+ }
1097
+ code += `}\n`;
1098
+ // keepDiscriminatorProperty: preserve discriminator property in result object (PB-3)
1099
+ if (meta.type.keepDiscriminatorProperty) {
1100
+ const fkJson = JSON.stringify(fieldKey);
1101
+ code += `{var __dh=${GEN.out}[${fkJson}]; if(__dh!=null) __dh[${discProp}]=${GEN.disc}${sk};}\n`;
1102
+ }
1103
+ }
1104
+ else {
1105
+ // §8.1 simple nested or §8.2 each array
1106
+ const nestedCls = meta.type.resolvedClass ?? meta.type.fn();
1107
+ const nestedSealed = getSealed(nestedCls);
1108
+ const execIdx = execs.length;
1109
+ execs.push(nestedSealed);
1110
+ // Check if validateNested each (array) — meta.type is already proven non-null above
1111
+ const hasEach = meta.type.isArray || meta.flags.validateNestedEach || meta.validation.some(rd => rd.each);
1112
+ if (hasEach) {
1113
+ const iVar = `${GEN.index}${sk}`;
1114
+ const awaitKwE = ctx.isAsync ? 'await ' : '';
1115
+ code += `if (Array.isArray(${varName})) {\n`;
1116
+ // Emit non-each array-level validation rules (e.g. @ArrayMinSize, @ArrayMaxSize)
1117
+ const nonEachRules = meta.validation.filter(rd => !rd.each);
1118
+ code += emitRuleList(fieldKey, varName, nonEachRules, emitCtx, ctx, ' ');
1119
+ code += ` var ${GEN.arr}${sk} = [];\n`;
1120
+ code += ` for (var ${iVar}=0; ${iVar}<${varName}.length; ${iVar}++) {\n`;
1121
+ code += ` var ${GEN.result}${sk} = ${awaitKwE}execs[${execIdx}].deserialize(${varName}[${iVar}], opts);\n`;
1122
+ code += ` if (isErr(${GEN.result}${sk})) {\n`;
1123
+ if (collectErrors) {
1124
+ code += ` var ${GEN.errors}${sk} = ${GEN.result}${sk}.data;\n`;
1125
+ code += ` var __bk$pp${sk} = ${JSON.stringify(fieldKey)}+'['+${iVar}+'].';\n`;
1126
+ code += ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${GEN.errors}${sk}.length; ${GEN.nestedIdx}${sk}++) {\n`;
1127
+ code +=
1128
+ ` ` +
1129
+ nestedErrPush(GEN.errList, `__bk$pp${sk}+${GEN.errors}${sk}[${GEN.nestedIdx}${sk}].path`, `${GEN.errors}${sk}[${GEN.nestedIdx}${sk}]`, `__ne${sk}`);
1130
+ code += ` }\n`;
1131
+ }
1132
+ else {
1133
+ code += ` var ${GEN.errors}${sk} = ${GEN.result}${sk}.data;\n`;
1134
+ code += ` var __bk$pp${sk} = ${JSON.stringify(fieldKey)}+'['+${iVar}+'].';\n`;
1135
+ code += ` ` + nestedErrReturn(`__bk$pp${sk}+${GEN.errors}${sk}[0].path`, `${GEN.errors}${sk}[0]`, `__ne${sk}`);
1136
+ }
1137
+ code += ` } else { ${GEN.arr}${sk}.push(${GEN.result}${sk}); }\n`;
1138
+ code += ` }\n`;
1139
+ code += ` ${GEN.out}[${JSON.stringify(fieldKey)}] = ${GEN.arr}${sk};\n`;
1140
+ code += `} else { ${emitCtx.fail('isArray')}; }\n`;
1141
+ }
1142
+ else {
1143
+ const awaitKwS = ctx.isAsync ? 'await ' : '';
1144
+ code += `if (${varName} != null && typeof ${varName} === 'object' && !Array.isArray(${varName})) {\n`;
1145
+ code += ` var ${GEN.result}${sk} = ${awaitKwS}execs[${execIdx}].deserialize(${varName}, opts);\n`;
1146
+ code += generateNestedResultCode(fieldKey, `${GEN.result}${sk}`, collectErrors, ctx.pathPrefix);
1147
+ code += `} else { ${emitCtx.fail('isObject')}; }\n`;
1148
+ }
1149
+ }
1150
+ return code;
1151
+ }
1152
+ function generateNestedResultCode(fieldKey, resultVar, collectErrors, pathPrefix) {
1153
+ const sk = sanitizeKey(fieldKey);
1154
+ // Prepend the current scope's path prefix so an executor reached from inside an inlined block
1155
+ // (e.g. a circular nested DTO) keeps the full path, not just `fieldKey.`.
1156
+ const ppValue = pathPrefix ? `${pathPrefix}+${JSON.stringify(fieldKey + '.')}` : JSON.stringify(fieldKey + '.');
1157
+ if (collectErrors) {
1158
+ const errItem = `${GEN.errors}${sk}[${GEN.nestedIdx}${sk}]`;
1159
+ return (` if (isErr(${resultVar})) {\n` +
1160
+ ` var ${GEN.errors}${sk} = ${resultVar}.data;\n` +
1161
+ ` var __bk$pp${sk} = ${ppValue};\n` +
1162
+ ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${GEN.errors}${sk}.length; ${GEN.nestedIdx}${sk}++) {\n` +
1163
+ ` ` +
1164
+ nestedErrPush(GEN.errList, `__bk$pp${sk}+${errItem}.path`, errItem, `__ne${sk}`) +
1165
+ ` }\n` +
1166
+ ` } else { ${GEN.out}[${JSON.stringify(fieldKey)}] = ${resultVar}; }\n`);
1167
+ }
1168
+ const errFirst = `${GEN.errors}${sk}[0]`;
1169
+ return (` if (isErr(${resultVar})) {\n` +
1170
+ ` var ${GEN.errors}${sk} = ${resultVar}.data;\n` +
1171
+ ` var __bk$pp${sk} = ${ppValue};\n` +
1172
+ ` ` +
1173
+ nestedErrReturn(`__bk$pp${sk}+${errFirst}.path`, errFirst, `__ne${sk}`) +
1174
+ ` } else { ${GEN.out}[${JSON.stringify(fieldKey)}] = ${resultVar}; }\n`);
1175
+ }
1176
+ // ─────────────────────────────────────────────────────────────────────────────
1177
+ // generateNestedCodeValidateOnly — validate-only nested (inline when possible)
1178
+ // ─────────────────────────────────────────────────────────────────────────────
1179
+ // Inline-eligibility predicate: a nested DTO can be inlined unless it is already in the
1180
+ // active inline-set (circular reference). Inlined directly at the three call sites below
1181
+ // — no extra function call at seal time.
1182
+ /**
1183
+ * Emit inline validation code for all fields of a nested DTO.
1184
+ * Reuses generateFieldCode with modified ctx (pathPrefix, varPrefix, inputExpr).
1185
+ */
1186
+ function emitInlineNestedBlock(nestedMerged, nestedClass, inputExpr, pathPrefixExpr, varPrefix, ctx) {
1187
+ const inlinedSet = ctx.inlineNestedClasses;
1188
+ inlinedSet.add(nestedClass);
1189
+ const inlineCtx = {
1190
+ ...ctx,
1191
+ pathPrefix: pathPrefixExpr,
1192
+ varPrefix,
1193
+ inputExpr,
1194
+ exposeDefaultValues: false, // inline nested doesn't use exposeDefaultValues
1195
+ };
1196
+ let code = '';
1197
+ for (const [fieldKey, meta] of Object.entries(nestedMerged)) {
1198
+ code += generateFieldCode(fieldKey, meta, inlineCtx);
1199
+ }
1200
+ inlinedSet.delete(nestedClass);
1201
+ return code;
1202
+ }
1203
+ function generateNestedCodeValidateOnly(fieldKey, varName, meta, ctx, emitCtx) {
1204
+ const { collectErrors, execs } = ctx;
1205
+ if (!meta.type) {
1206
+ return '';
1207
+ }
1208
+ const sk = (ctx.varPrefix || '') + sanitizeKey(fieldKey);
1209
+ let code = '';
1210
+ // Initialize inline tracking set if not present
1211
+ if (!ctx.inlineNestedClasses) {
1212
+ ctx.inlineNestedClasses = new Set();
1213
+ }
1214
+ if (meta.type.discriminator) {
1215
+ // Discriminator — inline each subType's validation
1216
+ const discProp = JSON.stringify(meta.type.discriminator.property);
1217
+ code += `var ${GEN.disc}${sk} = ${varName} && ${varName}[${discProp}];\n`;
1218
+ code += `switch (${GEN.disc}${sk}) {\n`;
1219
+ for (const sub of meta.type.discriminator.subTypes) {
1220
+ const subSealed = getSealed(sub.value);
1221
+ const subMerged = subSealed.merged;
1222
+ const canInline = subMerged && !ctx.inlineNestedClasses.has(sub.value);
1223
+ code += ` case ${JSON.stringify(sub.name)}:\n`;
1224
+ if (canInline) {
1225
+ const ppExpr = ctx.pathPrefix ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey + '.')}` : JSON.stringify(fieldKey + '.');
1226
+ const vpPrefix = `${sk}_d${sanitizeKey(sub.name)}_`;
1227
+ code += emitInlineNestedBlock(subMerged, sub.value, varName, ppExpr, vpPrefix, ctx);
1228
+ }
1229
+ else {
1230
+ const execIdx = execs.length;
1231
+ execs.push(subSealed);
1232
+ const awaitKw = ctx.isAsync ? 'await ' : '';
1233
+ code += ` var ${GEN.result}${sk} = ${awaitKw}execs[${execIdx}].validate(${varName}, opts);\n`;
1234
+ code += generateValidateNestedResult(fieldKey, `${GEN.result}${sk}`, collectErrors, ctx.pathPrefix);
1235
+ }
1236
+ code += ` break;\n`;
1237
+ }
1238
+ const validSubTypeNamesJsonV = JSON.stringify(meta.type.discriminator.subTypes.map(s => s.name));
1239
+ const discPathExprV = emitCtx.pathExpr ?? JSON.stringify(fieldKey);
1240
+ const discValueExprV = `${GEN.disc}${sk}`;
1241
+ if (collectErrors) {
1242
+ code += ` default: ${GEN.errList}.push({path:${discPathExprV},code:'invalidDiscriminator',context:{received:${discValueExprV},validSubTypes:${validSubTypeNamesJsonV}}});\n`;
1243
+ }
1244
+ else {
1245
+ code += ` default: return [{path:${discPathExprV},code:'invalidDiscriminator',context:{received:${discValueExprV},validSubTypes:${validSubTypeNamesJsonV}}}];\n`;
1246
+ }
1247
+ code += `}\n`;
1248
+ }
1249
+ else {
1250
+ const nestedCls = meta.type.resolvedClass ?? meta.type.fn();
1251
+ const nestedSealed = getSealed(nestedCls);
1252
+ const nestedMerged = nestedSealed.merged;
1253
+ const hasEach = meta.type.isArray || meta.flags.validateNestedEach || meta.validation.some(rd => rd.each);
1254
+ // Decide: inline or function call
1255
+ const useInline = nestedMerged && !ctx.inlineNestedClasses.has(nestedCls);
1256
+ if (hasEach) {
1257
+ const iVar = `${GEN.index}${sk}`;
1258
+ code += `if (Array.isArray(${varName})) {\n`;
1259
+ const nonEachRules = meta.validation.filter(rd => !rd.each);
1260
+ code += emitRuleList(fieldKey, varName, nonEachRules, emitCtx, ctx, ' ');
1261
+ code += ` for (var ${iVar}=0; ${iVar}<${varName}.length; ${iVar}++) {\n`;
1262
+ if (useInline) {
1263
+ // INLINE: generate validation code directly in the loop body.
1264
+ // Emit the per-iteration path as a single local var — both the invalidInput error
1265
+ // path and the nested block reference it, avoiding two identical 3-string concats.
1266
+ const itemVar = `__il$${sk}item`;
1267
+ const ppVar = `__bk$pp${sk}`;
1268
+ const ppExpr = ppVar;
1269
+ const ppInit = ctx.pathPrefix
1270
+ ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`
1271
+ : `${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`;
1272
+ const vpPrefix = `${sk}i_`;
1273
+ code += ` var ${itemVar} = ${varName}[${iVar}];\n`;
1274
+ code += ` var ${ppVar} = ${ppInit};\n`;
1275
+ // Input type guard for the item — uses the cached prefix
1276
+ code += ` if (${itemVar} == null || typeof ${itemVar} !== 'object' || Array.isArray(${itemVar})) `;
1277
+ if (collectErrors) {
1278
+ code += `${GEN.errList}.push({path:${ppVar},code:'invalidInput'});\n`;
1279
+ }
1280
+ else {
1281
+ code += `return [{path:${ppVar},code:'invalidInput'}];\n`;
1282
+ }
1283
+ code += ` else {\n`;
1284
+ code += emitInlineNestedBlock(nestedMerged, nestedCls, itemVar, ppExpr, vpPrefix, ctx);
1285
+ code += ` }\n`;
1286
+ }
1287
+ else {
1288
+ // FALLBACK: function call to validate
1289
+ const execIdx = execs.length;
1290
+ execs.push(nestedSealed);
1291
+ const awaitKw = ctx.isAsync ? 'await ' : '';
1292
+ code += ` var ${GEN.result}${sk} = ${awaitKw}execs[${execIdx}].validate(${varName}[${iVar}], opts);\n`;
1293
+ code += ` if (${GEN.result}${sk} !== null) {\n`;
1294
+ const ppVar = `__bk$pp${sk}`;
1295
+ const ppInit = ctx.pathPrefix
1296
+ ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`
1297
+ : `${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`;
1298
+ code += ` var ${ppVar} = ${ppInit};\n`;
1299
+ if (collectErrors) {
1300
+ code += ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${GEN.result}${sk}.length; ${GEN.nestedIdx}${sk}++) {\n`;
1301
+ code +=
1302
+ ` ` +
1303
+ nestedErrPush(GEN.errList, `${ppVar}+${GEN.result}${sk}[${GEN.nestedIdx}${sk}].path`, `${GEN.result}${sk}[${GEN.nestedIdx}${sk}]`, `__ne${sk}`);
1304
+ code += ` }\n`;
1305
+ }
1306
+ else {
1307
+ code += ` ` + nestedErrReturn(`${ppVar}+${GEN.result}${sk}[0].path`, `${GEN.result}${sk}[0]`, `__ne${sk}`, true);
1308
+ }
1309
+ code += ` }\n`;
1310
+ }
1311
+ code += ` }\n`;
1312
+ code += `} else { ${emitCtx.fail('isArray')}; }\n`;
1313
+ }
1314
+ else {
1315
+ // Single nested object — arrays are objects by `typeof` but are not valid nested DTOs;
1316
+ // reject them here (matching the deserialize path) instead of descending into their fields.
1317
+ code += `if (${varName} != null && typeof ${varName} === 'object' && !Array.isArray(${varName})) {\n`;
1318
+ if (useInline) {
1319
+ const ppExpr = ctx.pathPrefix ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey + '.')}` : JSON.stringify(fieldKey + '.');
1320
+ const vpPrefix = `${sk}_`;
1321
+ code += emitInlineNestedBlock(nestedMerged, nestedCls, varName, ppExpr, vpPrefix, ctx);
1322
+ }
1323
+ else {
1324
+ const execIdx = execs.length;
1325
+ execs.push(nestedSealed);
1326
+ const awaitKw = ctx.isAsync ? 'await ' : '';
1327
+ code += ` var ${GEN.result}${sk} = ${awaitKw}execs[${execIdx}].validate(${varName}, opts);\n`;
1328
+ code += generateValidateNestedResult(fieldKey, `${GEN.result}${sk}`, collectErrors, ctx.pathPrefix);
1329
+ }
1330
+ code += `} else { ${emitCtx.fail('isObject')}; }\n`;
1331
+ }
1332
+ }
1333
+ return code;
1334
+ }
1335
+ /** Generate validate-mode nested result handling (null check instead of isErr) */
1336
+ function generateValidateNestedResult(fieldKey, resultVar, collectErrors, pathPrefix) {
1337
+ const sk = sanitizeKey(fieldKey);
1338
+ const ppVar = `__bk$pp${sk}`;
1339
+ // Prepend the current scope's path prefix (see generateNestedResultCode).
1340
+ const ppValue = pathPrefix ? `${pathPrefix}+${JSON.stringify(fieldKey + '.')}` : JSON.stringify(fieldKey + '.');
1341
+ if (collectErrors) {
1342
+ const errItem = `${resultVar}[${GEN.nestedIdx}${sk}]`;
1343
+ return (` if (${resultVar} !== null) {\n` +
1344
+ ` var ${ppVar} = ${ppValue};\n` +
1345
+ ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${resultVar}.length; ${GEN.nestedIdx}${sk}++) {\n` +
1346
+ ` ` +
1347
+ nestedErrPush(GEN.errList, `${ppVar}+${errItem}.path`, errItem, `__ne${sk}`) +
1348
+ ` }\n` +
1349
+ ` }\n`);
1350
+ }
1351
+ const errFirst = `${resultVar}[0]`;
1352
+ return (` if (${resultVar} !== null) {\n` +
1353
+ ` var ${ppVar} = ${ppValue};\n` +
1354
+ ` ` +
1355
+ nestedErrReturn(`${ppVar}+${errFirst}.path`, errFirst, `__ne${sk}`, true) +
1356
+ ` }\n`);
1357
+ }
1358
+ // ─────────────────────────────────────────────────────────────────────────────
1359
+ // generateCollectionCodeValidateOnly — validate-only collection (no Set/Map creation)
1360
+ // ─────────────────────────────────────────────────────────────────────────────
1361
+ function generateCollectionCodeValidateOnly(fieldKey, varName, meta, ctx, emitCtx) {
1362
+ const { collectErrors, execs } = ctx;
1363
+ const sk = (ctx.varPrefix || '') + sanitizeKey(fieldKey);
1364
+ const collection = meta.type.collection;
1365
+ const awaitKw = ctx.isAsync ? 'await ' : '';
1366
+ if (!ctx.inlineNestedClasses) {
1367
+ ctx.inlineNestedClasses = new Set();
1368
+ }
1369
+ // Resolve nested DTO for collection values
1370
+ let nestedCls;
1371
+ let nestedSealed;
1372
+ let nestedMerged;
1373
+ if (meta.type.resolvedCollectionValue) {
1374
+ nestedCls = meta.type.resolvedCollectionValue;
1375
+ nestedSealed = getSealed(nestedCls);
1376
+ nestedMerged = nestedSealed.merged;
1377
+ }
1378
+ const useInline = nestedCls && nestedMerged && !ctx.inlineNestedClasses.has(nestedCls);
1379
+ let code = '';
1380
+ if (collection === 'Set') {
1381
+ code += `if (Array.isArray(${varName})) {\n`;
1382
+ const nonEachRules = meta.validation.filter(rd => !rd.each);
1383
+ code += emitRuleList(fieldKey, varName, nonEachRules, emitCtx, ctx, ' ');
1384
+ if (nestedSealed) {
1385
+ const iVar = `${GEN.index}${sk}`;
1386
+ code += ` for (var ${iVar}=0; ${iVar}<${varName}.length; ${iVar}++) {\n`;
1387
+ if (useInline) {
1388
+ // Cache per-iteration path prefix into a single local var — itemInvalidPathExpr was
1389
+ // identical to ppExpr (two copies of the same 3-string concat in the emitted body).
1390
+ const itemVar = `__il$${sk}ci`;
1391
+ const ppVar = `__bk$pp${sk}`;
1392
+ const ppInit = ctx.pathPrefix
1393
+ ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`
1394
+ : `${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`;
1395
+ const vpPrefix = `${sk}c_`;
1396
+ code += ` var ${itemVar} = ${varName}[${iVar}];\n`;
1397
+ code += ` var ${ppVar} = ${ppInit};\n`;
1398
+ code += ` if (${itemVar} == null || typeof ${itemVar} !== 'object' || Array.isArray(${itemVar})) `;
1399
+ if (collectErrors) {
1400
+ code += `${GEN.errList}.push({path:${ppVar},code:'invalidInput'});\n`;
1401
+ }
1402
+ else {
1403
+ code += `return [{path:${ppVar},code:'invalidInput'}];\n`;
1404
+ }
1405
+ code += ` else {\n`;
1406
+ code += emitInlineNestedBlock(nestedMerged, nestedCls, itemVar, ppVar, vpPrefix, ctx);
1407
+ code += ` }\n`;
1408
+ }
1409
+ else {
1410
+ const execIdx = execs.length;
1411
+ execs.push(nestedSealed);
1412
+ code += ` var ${GEN.result}${sk} = ${awaitKw}execs[${execIdx}].validate(${varName}[${iVar}], opts);\n`;
1413
+ code += ` if (${GEN.result}${sk} !== null) {\n`;
1414
+ const ppVar = `__bk$pp${sk}`;
1415
+ const ppInit = ctx.pathPrefix
1416
+ ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`
1417
+ : `${JSON.stringify(fieldKey)}+'['+${iVar}+'].'`;
1418
+ code += ` var ${ppVar} = ${ppInit};\n`;
1419
+ if (collectErrors) {
1420
+ code += ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${GEN.result}${sk}.length; ${GEN.nestedIdx}${sk}++) {\n`;
1421
+ code +=
1422
+ ` ` +
1423
+ nestedErrPush(GEN.errList, `${ppVar}+${GEN.result}${sk}[${GEN.nestedIdx}${sk}].path`, `${GEN.result}${sk}[${GEN.nestedIdx}${sk}]`, `__ne${sk}`);
1424
+ code += ` }\n`;
1425
+ }
1426
+ else {
1427
+ code += ` ` + nestedErrReturn(`${ppVar}+${GEN.result}${sk}[0].path`, `${GEN.result}${sk}[0]`, `__ne${sk}`, true);
1428
+ }
1429
+ code += ` }\n`;
1430
+ }
1431
+ code += ` }\n`;
1432
+ }
1433
+ // each validation — iterate input array directly
1434
+ const eachRules = meta.validation.filter(rd => rd.each);
1435
+ if (eachRules.length > 0) {
1436
+ const eiVar = `${GEN.index}${sk}e`;
1437
+ code += ` for (var ${eiVar}=0; ${eiVar}<${varName}.length; ${eiVar}++) {\n`;
1438
+ for (const rd of eachRules) {
1439
+ const prefixVar = `__bk$ep_${sk}`;
1440
+ const extra = computeRuleExtras(rd, fieldKey, varName, ctx);
1441
+ const failFn = (c) => collectErrors
1442
+ ? `${GEN.errList}.push({path:${prefixVar}+${eiVar}+']',code:${JSON.stringify(c)}${extra}})`
1443
+ : `return [{path:${prefixVar}+${eiVar}+']',code:${JSON.stringify(c)}${extra}}]`;
1444
+ const colEmitCtx = { ...emitCtx, fail: failFn };
1445
+ if (!code.includes(`var ${prefixVar}`)) {
1446
+ const prefixInit = ctx.pathPrefix
1447
+ ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}+'['`
1448
+ : `${JSON.stringify(fieldKey)}+'['`;
1449
+ code += ` var ${prefixVar} = ${prefixInit};\n`;
1450
+ }
1451
+ code += ` ${rd.rule.emit(`${varName}[${eiVar}]`, colEmitCtx)}\n`;
1452
+ }
1453
+ code += ` }\n`;
1454
+ }
1455
+ code += `} else { ${emitCtx.fail('isArray')}; }\n`;
1456
+ }
1457
+ else {
1458
+ // Map: validate object values
1459
+ code += `if (${varName} != null && typeof ${varName} === 'object' && !Array.isArray(${varName})) {\n`;
1460
+ if (nestedSealed) {
1461
+ const kVar = `${GEN.key}${sk}`;
1462
+ const ksVar = `__bk$vk${sk}`;
1463
+ const iVar = `__bk$vi${sk}`;
1464
+ code += ` var ${ksVar} = Object.keys(${varName});\n`;
1465
+ code += ` for (var ${iVar}=0; ${iVar}<${ksVar}.length; ${iVar}++) {\n`;
1466
+ code += ` var ${kVar} = ${ksVar}[${iVar}];\n`;
1467
+ if (useInline) {
1468
+ const itemVar = `__il$${sk}mi`;
1469
+ const ppExpr = ctx.pathPrefix
1470
+ ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}+'['+${kVar}+'].'`
1471
+ : `${JSON.stringify(fieldKey)}+'['+${kVar}+'].'`;
1472
+ const vpPrefix = `${sk}m_`;
1473
+ const itemInvalidPathExpr = ppExpr;
1474
+ code += ` var ${itemVar} = ${varName}[${kVar}];\n`;
1475
+ code += ` if (${itemVar} == null || typeof ${itemVar} !== 'object' || Array.isArray(${itemVar})) `;
1476
+ if (collectErrors) {
1477
+ code += `${GEN.errList}.push({path:${itemInvalidPathExpr},code:'invalidInput'});\n`;
1478
+ }
1479
+ else {
1480
+ code += `return [{path:${itemInvalidPathExpr},code:'invalidInput'}];\n`;
1481
+ }
1482
+ code += ` else {\n`;
1483
+ code += emitInlineNestedBlock(nestedMerged, nestedCls, itemVar, ppExpr, vpPrefix, ctx);
1484
+ code += ` }\n`;
1485
+ }
1486
+ else {
1487
+ const execIdx = execs.length;
1488
+ execs.push(nestedSealed);
1489
+ code += ` var ${GEN.result}${sk} = ${awaitKw}execs[${execIdx}].validate(${varName}[${kVar}], opts);\n`;
1490
+ code += ` if (${GEN.result}${sk} !== null) {\n`;
1491
+ const ppVar = `__bk$pp${sk}`;
1492
+ const ppInit = ctx.pathPrefix
1493
+ ? `${ctx.pathPrefix}+${JSON.stringify(fieldKey)}+'['+${kVar}+'].'`
1494
+ : `${JSON.stringify(fieldKey)}+'['+${kVar}+'].'`;
1495
+ code += ` var ${ppVar} = ${ppInit};\n`;
1496
+ if (collectErrors) {
1497
+ code += ` for (var ${GEN.nestedIdx}${sk}=0; ${GEN.nestedIdx}${sk}<${GEN.result}${sk}.length; ${GEN.nestedIdx}${sk}++) {\n`;
1498
+ code +=
1499
+ ` ` +
1500
+ nestedErrPush(GEN.errList, `${ppVar}+${GEN.result}${sk}[${GEN.nestedIdx}${sk}].path`, `${GEN.result}${sk}[${GEN.nestedIdx}${sk}]`, `__ne${sk}`);
1501
+ code += ` }\n`;
1502
+ }
1503
+ else {
1504
+ code += ` ` + nestedErrReturn(`${ppVar}+${GEN.result}${sk}[0].path`, `${GEN.result}${sk}[0]`, `__ne${sk}`, true);
1505
+ }
1506
+ code += ` }\n`;
1507
+ }
1508
+ code += ` }\n`;
1509
+ }
1510
+ code += `} else { ${emitCtx.fail('isObject')}; }\n`;
1511
+ }
1512
+ return code;
1513
+ }
1514
+ // ─────────────────────────────────────────────────────────────────────────────
1515
+ // makeEmitCtx — create per-field EmitContext
1516
+ // ─────────────────────────────────────────────────────────────────────────────
1517
+ function makeEmitCtx(fieldKey, ctx) {
1518
+ const { collectErrors, regexes, refs, execs, validateOnly, pathPrefix } = ctx;
1519
+ const pathExpr = pathPrefix ? `${pathPrefix}+${JSON.stringify(fieldKey)}` : JSON.stringify(fieldKey);
1520
+ return {
1521
+ addRegex(re) {
1522
+ regexes.push(re);
1523
+ return regexes.length - 1;
1524
+ },
1525
+ addRef(fn) {
1526
+ refs.push(fn);
1527
+ return refs.length - 1;
1528
+ },
1529
+ addExecutor(executor) {
1530
+ execs.push(executor);
1531
+ return execs.length - 1;
1532
+ },
1533
+ fail(code) {
1534
+ if (collectErrors) {
1535
+ return `${GEN.errList}.push({path:${pathExpr},code:${JSON.stringify(code)}})`;
1536
+ }
1537
+ else if (validateOnly) {
1538
+ return `return [{path:${pathExpr},code:${JSON.stringify(code)}}]`;
1539
+ }
1540
+ return `return err([{path:${pathExpr},code:${JSON.stringify(code)}}])`;
1541
+ },
1542
+ collectErrors,
1543
+ pathExpr: pathExpr,
1544
+ };
1545
+ }
1546
+ export { buildDeserializeCode, buildValidateCode };