@zipbul/baker 2.1.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 (91) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/MIGRATION-3.0.md +104 -0
  3. package/README.md +121 -75
  4. package/dist/index.d.ts +8 -7
  5. package/dist/index.js +10 -229
  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 -11
  9. package/dist/src/configure.js +43 -0
  10. package/dist/src/create-rule.d.ts +1 -1
  11. package/dist/src/create-rule.js +41 -0
  12. package/dist/src/decorators/field.d.ts +22 -18
  13. package/dist/src/decorators/field.js +268 -0
  14. package/dist/src/decorators/index.d.ts +1 -0
  15. package/dist/src/decorators/index.js +2 -2
  16. package/dist/src/decorators/recipe.d.ts +17 -0
  17. package/dist/src/decorators/recipe.js +23 -0
  18. package/dist/src/errors.d.ts +28 -17
  19. package/dist/src/errors.js +52 -0
  20. package/dist/src/functions/check-call-options.d.ts +8 -0
  21. package/dist/src/functions/check-call-options.js +51 -0
  22. package/dist/src/functions/deserialize.d.ts +14 -6
  23. package/dist/src/functions/deserialize.js +57 -0
  24. package/dist/src/functions/serialize.d.ts +10 -3
  25. package/dist/src/functions/serialize.js +52 -0
  26. package/dist/src/functions/validate.d.ts +13 -8
  27. package/dist/src/functions/validate.js +49 -0
  28. package/dist/src/interfaces.d.ts +1 -1
  29. package/dist/src/interfaces.js +4 -0
  30. package/dist/src/meta-access.d.ts +19 -0
  31. package/dist/src/meta-access.js +75 -0
  32. package/dist/src/registry.js +8 -0
  33. package/dist/src/rule-metadata.d.ts +11 -0
  34. package/dist/src/rule-metadata.js +17 -0
  35. package/dist/src/rule-plan.d.ts +29 -0
  36. package/dist/src/rule-plan.js +117 -0
  37. package/dist/src/rules/array.d.ts +7 -6
  38. package/dist/src/rules/array.js +96 -0
  39. package/dist/src/rules/common.d.ts +2 -2
  40. package/dist/src/rules/common.js +77 -0
  41. package/dist/src/rules/date.js +35 -0
  42. package/dist/src/rules/index.d.ts +2 -4
  43. package/dist/src/rules/index.js +8 -11
  44. package/dist/src/rules/locales.d.ts +5 -4
  45. package/dist/src/rules/locales.js +249 -0
  46. package/dist/src/rules/number.d.ts +2 -2
  47. package/dist/src/rules/number.js +79 -0
  48. package/dist/src/rules/object.d.ts +1 -1
  49. package/dist/src/rules/object.js +49 -0
  50. package/dist/src/rules/string.d.ts +83 -80
  51. package/dist/src/rules/string.js +1998 -0
  52. package/dist/src/rules/typechecker.d.ts +6 -6
  53. package/dist/src/rules/typechecker.js +143 -0
  54. package/dist/src/seal/circular-analyzer.js +63 -0
  55. package/dist/src/seal/codegen-utils.d.ts +7 -0
  56. package/dist/src/seal/codegen-utils.js +18 -0
  57. package/dist/src/seal/deserialize-builder.d.ts +8 -3
  58. package/dist/src/seal/deserialize-builder.js +1546 -0
  59. package/dist/src/seal/expose-validator.d.ts +3 -2
  60. package/dist/src/seal/expose-validator.js +65 -0
  61. package/dist/src/seal/seal-state.d.ts +10 -0
  62. package/dist/src/seal/seal-state.js +18 -0
  63. package/dist/src/seal/seal.d.ts +22 -21
  64. package/dist/src/seal/seal.js +431 -0
  65. package/dist/src/seal/serialize-builder.d.ts +3 -2
  66. package/dist/src/seal/serialize-builder.js +374 -0
  67. package/dist/src/seal/validate-meta.d.ts +13 -0
  68. package/dist/src/seal/validate-meta.js +61 -0
  69. package/dist/src/symbols.d.ts +1 -1
  70. package/dist/src/symbols.js +13 -2
  71. package/dist/src/transformers/collection.transformer.js +25 -0
  72. package/dist/src/transformers/date.transformer.js +18 -0
  73. package/dist/src/transformers/index.js +6 -2
  74. package/dist/src/transformers/luxon.transformer.d.ts +4 -2
  75. package/dist/src/transformers/luxon.transformer.js +34 -0
  76. package/dist/src/transformers/moment.transformer.d.ts +4 -2
  77. package/dist/src/transformers/moment.transformer.js +32 -0
  78. package/dist/src/transformers/number.transformer.js +8 -0
  79. package/dist/src/transformers/string.transformer.js +12 -0
  80. package/dist/src/types.d.ts +68 -28
  81. package/dist/src/types.js +1 -0
  82. package/dist/src/utils.d.ts +4 -2
  83. package/dist/src/utils.js +10 -0
  84. package/package.json +80 -67
  85. package/dist/index-fnv35wrf.js +0 -3
  86. package/dist/index-k3d659ad.js +0 -3
  87. package/dist/index-s0n74vx1.js +0 -3
  88. package/dist/index-xdn55cz3.js +0 -1
  89. package/dist/src/functions/_run-sealed.d.ts +0 -7
  90. package/dist/src/functions/index.d.ts +0 -3
  91. package/dist/src/seal/index.d.ts +0 -5
@@ -1,14 +1,14 @@
1
1
  import type { EmittableRule } from '../types';
2
- export declare const isString: EmittableRule;
2
+ export declare const isString: import("../types").InternalRule;
3
3
  export interface IsNumberOptions {
4
4
  allowNaN?: boolean;
5
5
  allowInfinity?: boolean;
6
6
  maxDecimalPlaces?: number;
7
7
  }
8
8
  export declare function isNumber(options?: IsNumberOptions): EmittableRule;
9
- export declare const isBoolean: EmittableRule;
10
- export declare const isDate: EmittableRule;
9
+ export declare const isBoolean: import("../types").InternalRule;
10
+ export declare const isDate: import("../types").InternalRule;
11
11
  export declare function isEnum(entity: object): EmittableRule;
12
- export declare const isInt: EmittableRule;
13
- export declare const isArray: EmittableRule;
14
- export declare const isObject: EmittableRule;
12
+ export declare const isInt: import("../types").InternalRule;
13
+ export declare const isArray: import("../types").InternalRule;
14
+ export declare const isObject: import("../types").InternalRule;
@@ -0,0 +1,143 @@
1
+ import { makeRule } from '../rule-plan.js';
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // isString — typeof check (§4.8 A: operator inline)
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ export const isString = makeRule({
6
+ name: 'isString',
7
+ constraints: {},
8
+ validate: value => typeof value === 'string',
9
+ emit: (varName, ctx) => `if (typeof ${varName} !== 'string') ${ctx.fail('isString')};`,
10
+ });
11
+ export function isNumber(options) {
12
+ const allowNaN = options?.allowNaN ?? false;
13
+ const allowInfinity = options?.allowInfinity ?? false;
14
+ const maxDecimalPlaces = options?.maxDecimalPlaces;
15
+ const validate = (value) => {
16
+ if (typeof value !== 'number') {
17
+ return false;
18
+ }
19
+ // Check NaN first — since isFinite(NaN) is also false, order matters
20
+ if (isNaN(value)) {
21
+ return allowNaN;
22
+ }
23
+ // Non-NaN non-finite values (Infinity / -Infinity)
24
+ if (!isFinite(value)) {
25
+ return allowInfinity;
26
+ }
27
+ if (maxDecimalPlaces !== undefined) {
28
+ const parts = value.toExponential().split('e');
29
+ const mantissaDecimals = (parts[0].split('.')[1] || '').length;
30
+ const exponent = parseInt(parts[1], 10);
31
+ if (Math.max(0, mantissaDecimals - exponent) > maxDecimalPlaces) {
32
+ return false;
33
+ }
34
+ }
35
+ return true;
36
+ };
37
+ return makeRule({
38
+ name: 'isNumber',
39
+ constraints: {
40
+ allowNaN: options?.allowNaN,
41
+ allowInfinity: options?.allowInfinity,
42
+ maxDecimalPlaces: options?.maxDecimalPlaces,
43
+ },
44
+ validate,
45
+ emit: (varName, ctx) => {
46
+ if (ctx.insideTypeGate) {
47
+ // typeof + isNaN already verified by gate — emit only Infinity/maxDecimalPlaces checks
48
+ let code = '';
49
+ if (!allowInfinity) {
50
+ code += `if (${varName} === Infinity || ${varName} === -Infinity) ${ctx.fail('isNumber')};`;
51
+ }
52
+ if (maxDecimalPlaces !== undefined) {
53
+ code += `${code ? '\nelse ' : ''}{ var exp=${varName}.toExponential().split('e'); var mant=(exp[0].split('.')[1]||'').length; var exp2=parseInt(exp[1],10); if(Math.max(0,mant-exp2)>${maxDecimalPlaces}) ${ctx.fail('isNumber')}; }`;
54
+ }
55
+ return code;
56
+ }
57
+ let code = `if (typeof ${varName} !== 'number') ${ctx.fail('isNumber')};`;
58
+ if (!allowNaN) {
59
+ code += `\nelse if (isNaN(${varName})) ${ctx.fail('isNumber')};`;
60
+ }
61
+ if (!allowInfinity) {
62
+ code += `\nelse if (${varName} === Infinity || ${varName} === -Infinity) ${ctx.fail('isNumber')};`;
63
+ }
64
+ if (maxDecimalPlaces !== undefined) {
65
+ code += `\nelse { var exp=${varName}.toExponential().split('e'); var mant=(exp[0].split('.')[1]||'').length; var exp2=parseInt(exp[1],10); if(Math.max(0,mant-exp2)>${maxDecimalPlaces}) ${ctx.fail('isNumber')}; }`;
66
+ }
67
+ return code;
68
+ },
69
+ });
70
+ }
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+ // isBoolean — typeof check (§4.8 A)
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+ export const isBoolean = makeRule({
75
+ name: 'isBoolean',
76
+ constraints: {},
77
+ validate: value => typeof value === 'boolean',
78
+ emit: (varName, ctx) => `if (typeof ${varName} !== 'boolean') ${ctx.fail('isBoolean')};`,
79
+ });
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+ // isDate — instanceof Date + getTime() NaN check (§4.8 A)
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ export const isDate = makeRule({
84
+ name: 'isDate',
85
+ constraints: {},
86
+ validate: value => value instanceof Date && !isNaN(value.getTime()),
87
+ emit: (varName, ctx) => `if (!(${varName} instanceof Date) || isNaN(${varName}.getTime())) ${ctx.fail('isDate')};`,
88
+ });
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ // isEnum — factory: indexOf check using Object.values array (§4.8 C)
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ export function isEnum(entity) {
93
+ const values = Object.values(entity);
94
+ // Set lookup is O(1); array indexOf is O(n). Measured (Bun/JSC):
95
+ // - 4 items: indexOf 1.2 ns vs Set.has 2.2 ns (indexOf marginally faster)
96
+ // - 50 items: indexOf 64 ns vs Set.has 8.4 ns (Set 7.5x faster)
97
+ // Use Set when there are enough values to overcome its constant-factor overhead.
98
+ const useSet = values.length >= 8;
99
+ const valuesSet = useSet ? new Set(values) : null;
100
+ return makeRule({
101
+ name: 'isEnum',
102
+ constraints: { values },
103
+ validate: useSet ? value => valuesSet.has(value) : value => values.includes(value),
104
+ emit: (varName, ctx) => {
105
+ if (useSet) {
106
+ const i = ctx.addRef(valuesSet);
107
+ return `if (!refs[${i}].has(${varName})) ${ctx.fail('isEnum')};`;
108
+ }
109
+ const i = ctx.addRef(values);
110
+ return `if (!refs[${i}].includes(${varName})) ${ctx.fail('isEnum')};`;
111
+ },
112
+ });
113
+ }
114
+ // ─────────────────────────────────────────────────────────────────────────────
115
+ // isInt — typeof + Number.isInteger check (§4.8 A)
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+ export const isInt = makeRule({
118
+ name: 'isInt',
119
+ requiresType: 'number',
120
+ constraints: {},
121
+ validate: value => typeof value === 'number' && Number.isInteger(value),
122
+ emit: (varName, ctx) => ctx.insideTypeGate
123
+ ? `if (!Number.isInteger(${varName})) ${ctx.fail('isInt')};`
124
+ : `if (typeof ${varName} !== 'number' || !Number.isInteger(${varName})) ${ctx.fail('isInt')};`,
125
+ });
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+ // isArray — Array.isArray check (§4.8 A: operator inline)
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+ export const isArray = makeRule({
130
+ name: 'isArray',
131
+ constraints: {},
132
+ validate: value => Array.isArray(value),
133
+ emit: (varName, ctx) => `if (!Array.isArray(${varName})) ${ctx.fail('isArray')};`,
134
+ });
135
+ // ─────────────────────────────────────────────────────────────────────────────
136
+ // isObject — typeof object + non-null + non-array (§4.8 A)
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+ export const isObject = makeRule({
139
+ name: 'isObject',
140
+ constraints: {},
141
+ validate: value => typeof value === 'object' && value !== null && !Array.isArray(value),
142
+ emit: (varName, ctx) => `if (typeof ${varName} !== 'object' || ${varName} === null || Array.isArray(${varName})) ${ctx.fail('isObject')};`,
143
+ });
@@ -0,0 +1,63 @@
1
+ import { BakerError } from '../errors.js';
2
+ import { getRaw } from '../meta-access.js';
3
+ /**
4
+ * Static analysis for circular references (§4.6)
5
+ *
6
+ * Traverses the @Type reference graph via DFS to detect cycles.
7
+ *
8
+ * Flat DTO without cycles → false (zero WeakSet overhead)
9
+ * DTO with cycles → true (WeakSet automatically inserted)
10
+ */
11
+ export function analyzeCircular(Class) {
12
+ // @Type reference graph DFS — detect back-edges via visited set
13
+ const visited = new Set();
14
+ function walk(cls) {
15
+ if (visited.has(cls)) {
16
+ return true;
17
+ } // back-edge → cycle detected
18
+ visited.add(cls);
19
+ const raw = getRaw(cls);
20
+ if (raw) {
21
+ for (const meta of Object.values(raw)) {
22
+ // Simple @Type
23
+ if (meta.type?.fn) {
24
+ let typeResult;
25
+ try {
26
+ typeResult = meta.type.fn();
27
+ }
28
+ catch (e) {
29
+ throw new BakerError(`${cls.name}: type function threw: ${e.message}`, { cause: e });
30
+ }
31
+ const nested = Array.isArray(typeResult) ? typeResult[0] : typeResult;
32
+ if (walk(nested)) {
33
+ return true;
34
+ }
35
+ }
36
+ // discriminator subTypes
37
+ if (meta.type?.discriminator) {
38
+ for (const sub of meta.type.discriminator.subTypes) {
39
+ if (walk(sub.value)) {
40
+ return true;
41
+ }
42
+ }
43
+ }
44
+ // W1 (F-1): collectionValue thunk (Set/Map nested DTO) — invoke and walk
45
+ if (meta.type?.collectionValue) {
46
+ let resolved;
47
+ try {
48
+ resolved = meta.type.collectionValue();
49
+ }
50
+ catch (e) {
51
+ throw new BakerError(`${cls.name}: collectionValue function threw: ${e.message}`, { cause: e });
52
+ }
53
+ if (typeof resolved === 'function' && walk(resolved)) {
54
+ return true;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ visited.delete(cls); // Release tree edge — prevent false positives for diamond patterns
60
+ return false;
61
+ }
62
+ return walk(Class);
63
+ }
@@ -0,0 +1,7 @@
1
+ /** Convert key to a valid JS identifier suffix (encode non-alphanumeric chars via charCode to prevent collisions) */
2
+ export declare function sanitizeKey(key: string): string;
3
+ /**
4
+ * Generate a groups-has expression for the fast-path single-group / Set pattern.
5
+ * Checks if any of the given groups match the runtime groups.
6
+ */
7
+ export declare function buildGroupsHasExpr(singleGroupVar: string, groupsVar: string, groups: string[]): string;
@@ -0,0 +1,18 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Shared code-generation utilities for deserialize/serialize builders
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ /** Convert key to a valid JS identifier suffix (encode non-alphanumeric chars via charCode to prevent collisions) */
5
+ export function sanitizeKey(key) {
6
+ return key.replace(/[^a-zA-Z0-9_]/g, ch => `$${ch.charCodeAt(0)}$`);
7
+ }
8
+ /**
9
+ * Generate a groups-has expression for the fast-path single-group / Set pattern.
10
+ * Checks if any of the given groups match the runtime groups.
11
+ */
12
+ export function buildGroupsHasExpr(singleGroupVar, groupsVar, groups) {
13
+ const checks = groups.map(group => {
14
+ const q = JSON.stringify(group);
15
+ return `(${singleGroupVar}===${q} || (${groupsVar} && ${groupsVar}.has(${q})))`;
16
+ });
17
+ return checks.join(' || ');
18
+ }
@@ -1,5 +1,10 @@
1
1
  import type { Result, ResultAsync } from '@zipbul/result';
2
- import type { RawClassMeta } from '../types';
3
2
  import type { SealOptions, RuntimeOptions } from '../interfaces';
4
- import { type BakerError } from '../errors';
5
- export declare function buildDeserializeCode<T>(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean): (input: unknown, opts?: RuntimeOptions) => Result<T, BakerError[]> | ResultAsync<T, BakerError[]>;
3
+ import type { RawClassMeta } from '../types';
4
+ import { type BakerIssue } from '../errors';
5
+ type DeserializeExecutor<T> = (input: unknown, opts?: RuntimeOptions) => Result<T, BakerIssue[]> | ResultAsync<T, BakerIssue[]>;
6
+ type ValidateExecutor = (input: unknown, opts?: RuntimeOptions) => BakerIssue[] | null | Promise<BakerIssue[] | null>;
7
+ declare function buildDeserializeCode<T>(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean): DeserializeExecutor<T>;
8
+ declare function buildDeserializeCode(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean, validateOnly: true): ValidateExecutor;
9
+ declare function buildValidateCode(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean): ValidateExecutor;
10
+ export { buildDeserializeCode, buildValidateCode };