@oxog/vld 1.4.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +449 -333
- package/README.md +1171 -1130
- package/dist/chunks/bigint-ZKkWLdmi.js +1628 -0
- package/dist/chunks/bigint-ZKkWLdmi.js.map +1 -0
- package/dist/{coercion/string.js → chunks/date-7Iz2BGsL.js} +271 -5
- package/dist/chunks/date-7Iz2BGsL.js.map +1 -0
- package/dist/chunks/index-BoV-4S1F.js +894 -0
- package/dist/chunks/index-BoV-4S1F.js.map +1 -0
- package/dist/chunks/index-eWXdjyyy.js +426 -0
- package/dist/chunks/index-eWXdjyyy.js.map +1 -0
- package/dist/chunks/json-nIhyRPOU.js +1934 -0
- package/dist/chunks/json-nIhyRPOU.js.map +1 -0
- package/dist/{validators/unknown.js → chunks/unknown-CjSRcELI.js} +6 -3
- package/dist/chunks/unknown-CjSRcELI.js.map +1 -0
- package/dist/cjs/errors.cjs +387 -0
- package/dist/cjs/errors.cjs.map +1 -0
- package/dist/cjs/index.cjs +9815 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/locales/index.cjs +2877 -0
- package/dist/cjs/locales/index.cjs.map +1 -0
- package/dist/cjs/locales/lazy.cjs +3169 -0
- package/dist/cjs/locales/lazy.cjs.map +1 -0
- package/dist/cjs/mini.cjs +7161 -0
- package/dist/cjs/mini.cjs.map +1 -0
- package/dist/cli/bin.d.ts +8 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/commands/benchmark.d.ts +11 -0
- package/dist/cli/commands/benchmark.d.ts.map +1 -0
- package/dist/cli/commands/validate.d.ts +11 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/index.d.ts +59 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/codecs/index.d.ts.map +1 -1
- package/dist/codecs/index.js +4 -256
- package/dist/codecs/index.js.map +1 -1
- package/dist/coercion/boolean.d.ts.map +1 -1
- package/dist/coercion/index.d.ts +11 -0
- package/dist/coercion/index.d.ts.map +1 -0
- package/dist/coercion/{bigint.js → index.js} +7 -4
- package/dist/coercion/index.js.map +1 -0
- package/dist/compat/emitter.d.ts +73 -0
- package/dist/compat/emitter.d.ts.map +1 -0
- package/dist/compat/emitter.js +181 -0
- package/dist/compat/emitter.js.map +1 -0
- package/dist/compat/index.d.ts +15 -0
- package/dist/compat/index.d.ts.map +1 -0
- package/dist/compat/result.d.ts +128 -0
- package/dist/compat/result.d.ts.map +1 -0
- package/dist/compat/result.js +171 -0
- package/dist/compat/result.js.map +1 -0
- package/dist/errors.d.ts +58 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +106 -15
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +140 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -103
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +29 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +405 -0
- package/dist/kernel.js.map +1 -0
- package/dist/locales/af.js +4 -2
- package/dist/locales/af.js.map +1 -1
- package/dist/locales/ar.js +4 -2
- package/dist/locales/ar.js.map +1 -1
- package/dist/locales/bn.js +4 -2
- package/dist/locales/bn.js.map +1 -1
- package/dist/locales/da.js +4 -2
- package/dist/locales/da.js.map +1 -1
- package/dist/locales/de.js +4 -2
- package/dist/locales/de.js.map +1 -1
- package/dist/locales/en.js +4 -2
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es-MX.js +4 -2
- package/dist/locales/es-MX.js.map +1 -1
- package/dist/locales/es.js +4 -2
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fi.js +4 -2
- package/dist/locales/fi.js.map +1 -1
- package/dist/locales/fr.js +4 -2
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/hi.js +4 -2
- package/dist/locales/hi.js.map +1 -1
- package/dist/locales/id.js +4 -2
- package/dist/locales/id.js.map +1 -1
- package/dist/locales/index.js +3 -91
- package/dist/locales/index.js.map +1 -1
- package/dist/locales/it.js +4 -2
- package/dist/locales/it.js.map +1 -1
- package/dist/locales/ja.js +4 -2
- package/dist/locales/ja.js.map +1 -1
- package/dist/locales/ko.js +4 -2
- package/dist/locales/ko.js.map +1 -1
- package/dist/locales/lazy.d.ts +111 -0
- package/dist/locales/lazy.d.ts.map +1 -0
- package/dist/locales/lazy.js +220 -0
- package/dist/locales/lazy.js.map +1 -0
- package/dist/locales/nl.js +4 -2
- package/dist/locales/nl.js.map +1 -1
- package/dist/locales/no.js +4 -2
- package/dist/locales/no.js.map +1 -1
- package/dist/locales/pl.js +4 -2
- package/dist/locales/pl.js.map +1 -1
- package/dist/locales/pt-BR.js +4 -2
- package/dist/locales/pt-BR.js.map +1 -1
- package/dist/locales/pt.js +4 -2
- package/dist/locales/pt.js.map +1 -1
- package/dist/locales/ru.js +4 -2
- package/dist/locales/ru.js.map +1 -1
- package/dist/locales/sv.js +4 -2
- package/dist/locales/sv.js.map +1 -1
- package/dist/locales/sw.js +4 -2
- package/dist/locales/sw.js.map +1 -1
- package/dist/locales/th.js +4 -2
- package/dist/locales/th.js.map +1 -1
- package/dist/locales/tr.js +4 -2
- package/dist/locales/tr.js.map +1 -1
- package/dist/locales/vi.js +4 -2
- package/dist/locales/vi.js.map +1 -1
- package/dist/locales/zh.js +4 -2
- package/dist/locales/zh.js.map +1 -1
- package/dist/logger.d.ts +94 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/mini.d.ts +182 -0
- package/dist/mini.d.ts.map +1 -0
- package/dist/mini.js +132 -0
- package/dist/mini.js.map +1 -0
- package/dist/pigment.d.ts +124 -0
- package/dist/pigment.d.ts.map +1 -0
- package/dist/pigment.js +178 -0
- package/dist/pigment.js.map +1 -0
- package/dist/plugins/index.d.ts +8 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/types.d.ts +186 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/utils/codec-utils.d.ts.map +1 -1
- package/dist/utils/ip-validation.d.ts.map +1 -1
- package/dist/validators/base.d.ts +3 -0
- package/dist/validators/base.d.ts.map +1 -1
- package/dist/validators/index.js +5 -45
- package/dist/validators/index.js.map +1 -1
- package/dist/validators/object.d.ts.map +1 -1
- package/dist/validators/template-literal.d.ts.map +1 -1
- package/package.json +153 -79
- package/dist/coercion/bigint.js.map +0 -1
- package/dist/coercion/boolean.js +0 -67
- package/dist/coercion/boolean.js.map +0 -1
- package/dist/coercion/date.js +0 -52
- package/dist/coercion/date.js.map +0 -1
- package/dist/coercion/number.js +0 -160
- package/dist/coercion/number.js.map +0 -1
- package/dist/coercion/string.js.map +0 -1
- package/dist/locales/backup-en.js +0 -105
- package/dist/locales/backup-en.js.map +0 -1
- package/dist/locales/types.js +0 -2
- package/dist/locales/types.js.map +0 -1
- package/dist/utils/codec-utils.js +0 -223
- package/dist/utils/codec-utils.js.map +0 -1
- package/dist/utils/deep-merge.js +0 -76
- package/dist/utils/deep-merge.js.map +0 -1
- package/dist/utils/ip-validation.js +0 -78
- package/dist/utils/ip-validation.js.map +0 -1
- package/dist/utils/security.js +0 -61
- package/dist/utils/security.js.map +0 -1
- package/dist/validators/any.js +0 -25
- package/dist/validators/any.js.map +0 -1
- package/dist/validators/array.js +0 -190
- package/dist/validators/array.js.map +0 -1
- package/dist/validators/base.js +0 -491
- package/dist/validators/base.js.map +0 -1
- package/dist/validators/base64.js +0 -65
- package/dist/validators/base64.js.map +0 -1
- package/dist/validators/bigint.js +0 -142
- package/dist/validators/bigint.js.map +0 -1
- package/dist/validators/boolean.js +0 -114
- package/dist/validators/boolean.js.map +0 -1
- package/dist/validators/codec.js +0 -182
- package/dist/validators/codec.js.map +0 -1
- package/dist/validators/custom.js +0 -50
- package/dist/validators/custom.js.map +0 -1
- package/dist/validators/date.js +0 -227
- package/dist/validators/date.js.map +0 -1
- package/dist/validators/discriminated-union.js +0 -98
- package/dist/validators/discriminated-union.js.map +0 -1
- package/dist/validators/enum.js +0 -82
- package/dist/validators/enum.js.map +0 -1
- package/dist/validators/file.js +0 -114
- package/dist/validators/file.js.map +0 -1
- package/dist/validators/function.js +0 -45
- package/dist/validators/function.js.map +0 -1
- package/dist/validators/hex.js +0 -62
- package/dist/validators/hex.js.map +0 -1
- package/dist/validators/intersection.js +0 -64
- package/dist/validators/intersection.js.map +0 -1
- package/dist/validators/json.js +0 -64
- package/dist/validators/json.js.map +0 -1
- package/dist/validators/lazy.js +0 -42
- package/dist/validators/lazy.js.map +0 -1
- package/dist/validators/literal.js +0 -45
- package/dist/validators/literal.js.map +0 -1
- package/dist/validators/map.js +0 -54
- package/dist/validators/map.js.map +0 -1
- package/dist/validators/nan.js +0 -32
- package/dist/validators/nan.js.map +0 -1
- package/dist/validators/never.js +0 -29
- package/dist/validators/never.js.map +0 -1
- package/dist/validators/null.js +0 -31
- package/dist/validators/null.js.map +0 -1
- package/dist/validators/number.js +0 -225
- package/dist/validators/number.js.map +0 -1
- package/dist/validators/object.js +0 -458
- package/dist/validators/object.js.map +0 -1
- package/dist/validators/record.js +0 -128
- package/dist/validators/record.js.map +0 -1
- package/dist/validators/set.js +0 -51
- package/dist/validators/set.js.map +0 -1
- package/dist/validators/string-bool.js +0 -127
- package/dist/validators/string-bool.js.map +0 -1
- package/dist/validators/string-formats.js +0 -120
- package/dist/validators/string-formats.js.map +0 -1
- package/dist/validators/string.js +0 -253
- package/dist/validators/string.js.map +0 -1
- package/dist/validators/symbol.js +0 -42
- package/dist/validators/symbol.js.map +0 -1
- package/dist/validators/template-literal.js +0 -101
- package/dist/validators/template-literal.js.map +0 -1
- package/dist/validators/tuple.js +0 -55
- package/dist/validators/tuple.js.map +0 -1
- package/dist/validators/uint8array.js +0 -98
- package/dist/validators/uint8array.js.map +0 -1
- package/dist/validators/undefined.js +0 -31
- package/dist/validators/undefined.js.map +0 -1
- package/dist/validators/union.js +0 -126
- package/dist/validators/union.js.map +0 -1
- package/dist/validators/unknown.js.map +0 -1
- package/dist/validators/void.js +0 -35
- package/dist/validators/void.js.map +0 -1
- package/dist/validators/xor.js +0 -64
- package/dist/validators/xor.js.map +0 -1
|
@@ -0,0 +1,1934 @@
|
|
|
1
|
+
import { i as VldBase, t as getMessages, V as VldString, a as VldNumber, b as VldBoolean, c as VldDate, e as VldOptional } from './bigint-ZKkWLdmi.js';
|
|
2
|
+
import { c as VldCoerceString, b as VldCoerceNumber, a as VldCoerceBoolean, V as VldCoerceDate } from './date-7Iz2BGsL.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default truthy and falsy value sets
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_TRUTHY = ['true', '1', 'yes', 'on', 'y', 'enabled'];
|
|
8
|
+
const DEFAULT_FALSY = ['false', '0', 'no', 'off', 'n', 'disabled'];
|
|
9
|
+
/**
|
|
10
|
+
* Immutable string boolean validator for flexible boolean parsing
|
|
11
|
+
* Supports string representations like "true", "yes", "on", "1", etc.
|
|
12
|
+
*/
|
|
13
|
+
class VldStringBool extends VldBase {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
super();
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a new string boolean validator
|
|
20
|
+
*/
|
|
21
|
+
static create(options = {}) {
|
|
22
|
+
return new VldStringBool(options);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a string value based on case sensitivity setting
|
|
26
|
+
*/
|
|
27
|
+
normalizeValue(value) {
|
|
28
|
+
return this.options.caseSensitive ? value : value.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get the effective truthy values (custom or default)
|
|
32
|
+
*/
|
|
33
|
+
getTruthyValues() {
|
|
34
|
+
return this.options.truthy ?? DEFAULT_TRUTHY;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the effective falsy values (custom or default)
|
|
38
|
+
*/
|
|
39
|
+
getFalsyValues() {
|
|
40
|
+
return this.options.falsy ?? DEFAULT_FALSY;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse and validate a string boolean value
|
|
44
|
+
*/
|
|
45
|
+
parse(value) {
|
|
46
|
+
// First, ensure we have a string
|
|
47
|
+
if (typeof value !== 'string') {
|
|
48
|
+
// Also accept actual boolean values for convenience
|
|
49
|
+
if (typeof value === 'boolean') {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(getMessages().stringExpected(typeof value, 'string'));
|
|
53
|
+
}
|
|
54
|
+
const normalized = this.normalizeValue(value);
|
|
55
|
+
const truthyValues = this.getTruthyValues();
|
|
56
|
+
const falsyValues = this.getFalsyValues();
|
|
57
|
+
// Normalize the value sets for comparison
|
|
58
|
+
const normalizedTruthy = this.options.caseSensitive
|
|
59
|
+
? truthyValues
|
|
60
|
+
: truthyValues.map(v => v.toLowerCase());
|
|
61
|
+
const normalizedFalsy = this.options.caseSensitive
|
|
62
|
+
? falsyValues
|
|
63
|
+
: falsyValues.map(v => v.toLowerCase());
|
|
64
|
+
// Check if value is in truthy set
|
|
65
|
+
if (normalizedTruthy.includes(normalized)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// Check if value is in falsy set
|
|
69
|
+
if (normalizedFalsy.includes(normalized)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Value is not recognized
|
|
73
|
+
const allValidValues = [...normalizedTruthy, ...normalizedFalsy];
|
|
74
|
+
throw new Error(getMessages().stringBoolExpected(this.options.caseSensitive ? allValidValues.join(', ') : allValidValues.join(', '), value));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Safely parse and validate a string boolean value
|
|
78
|
+
*/
|
|
79
|
+
safeParse(value) {
|
|
80
|
+
try {
|
|
81
|
+
const result = this.parse(value);
|
|
82
|
+
return { success: true, data: result };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: error
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create a new validator with custom truthy values
|
|
93
|
+
*/
|
|
94
|
+
withTruthy(values) {
|
|
95
|
+
return new VldStringBool({
|
|
96
|
+
...this.options,
|
|
97
|
+
truthy: values
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a new validator with custom falsy values
|
|
102
|
+
*/
|
|
103
|
+
withFalsy(values) {
|
|
104
|
+
return new VldStringBool({
|
|
105
|
+
...this.options,
|
|
106
|
+
falsy: values
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create a new validator with case-sensitive matching
|
|
111
|
+
*/
|
|
112
|
+
caseSensitive() {
|
|
113
|
+
return new VldStringBool({
|
|
114
|
+
...this.options,
|
|
115
|
+
caseSensitive: true
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Create a new validator with case-insensitive matching (default)
|
|
120
|
+
*/
|
|
121
|
+
caseInsensitive() {
|
|
122
|
+
return new VldStringBool({
|
|
123
|
+
...this.options,
|
|
124
|
+
caseSensitive: false
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Immutable array validator with chainable methods
|
|
131
|
+
*/
|
|
132
|
+
class VldArray extends VldBase {
|
|
133
|
+
/**
|
|
134
|
+
* Private constructor to enforce immutability
|
|
135
|
+
*/
|
|
136
|
+
constructor(config) {
|
|
137
|
+
super();
|
|
138
|
+
this.config = config;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create a new array validator
|
|
142
|
+
*/
|
|
143
|
+
static create(itemValidator) {
|
|
144
|
+
return new VldArray({ itemValidator });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Parse and validate an array value
|
|
148
|
+
*/
|
|
149
|
+
parse(value) {
|
|
150
|
+
if (!Array.isArray(value)) {
|
|
151
|
+
throw new Error(this.config.errorMessage || getMessages().invalidArray);
|
|
152
|
+
}
|
|
153
|
+
// Validate length constraints
|
|
154
|
+
if (this.config.exactLength !== undefined && value.length !== this.config.exactLength) {
|
|
155
|
+
throw new Error(this.config.errorMessage || getMessages().arrayLength(this.config.exactLength));
|
|
156
|
+
}
|
|
157
|
+
if (this.config.minLength !== undefined && value.length < this.config.minLength) {
|
|
158
|
+
throw new Error(this.config.errorMessage || getMessages().arrayMin(this.config.minLength));
|
|
159
|
+
}
|
|
160
|
+
if (this.config.maxLength !== undefined && value.length > this.config.maxLength) {
|
|
161
|
+
throw new Error(this.config.errorMessage || getMessages().arrayMax(this.config.maxLength));
|
|
162
|
+
}
|
|
163
|
+
// Validate each item with optimized loop and safeParse
|
|
164
|
+
const result = [];
|
|
165
|
+
const length = value.length;
|
|
166
|
+
for (let i = 0; i < length; i++) {
|
|
167
|
+
const parseResult = this.config.itemValidator.safeParse(value[i]);
|
|
168
|
+
if (!parseResult.success) {
|
|
169
|
+
throw new Error(getMessages().arrayItem(i, parseResult.error.message));
|
|
170
|
+
}
|
|
171
|
+
result[i] = parseResult.data; // Direct assignment is faster than push
|
|
172
|
+
}
|
|
173
|
+
// Check uniqueness if required
|
|
174
|
+
if (this.config.unique) {
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const item of result) {
|
|
177
|
+
const key = typeof item === 'object' ? this.stableStringify(item) : item;
|
|
178
|
+
if (seen.has(key)) {
|
|
179
|
+
throw new Error('Array must contain unique items');
|
|
180
|
+
}
|
|
181
|
+
seen.add(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Safely parse and validate an array value
|
|
188
|
+
*/
|
|
189
|
+
safeParse(value) {
|
|
190
|
+
try {
|
|
191
|
+
return { success: true, data: this.parse(value) };
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return { success: false, error: error };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create a stable string representation of an object for hashing
|
|
199
|
+
* Handles circular references and deep nesting gracefully
|
|
200
|
+
* BUG-006 FIX: Added depth limit to prevent stack overflow
|
|
201
|
+
* BUG-NEW-003 FIX: Fixed depth tracking to properly track recursion depth
|
|
202
|
+
*/
|
|
203
|
+
stableStringify(obj) {
|
|
204
|
+
const seen = new WeakSet();
|
|
205
|
+
const MAX_DEPTH = 100; // Reasonable depth limit to prevent stack overflow
|
|
206
|
+
// Helper function to sort keys and create stable representation
|
|
207
|
+
const sortedStringify = (value, depth = 0) => {
|
|
208
|
+
// Check depth limit
|
|
209
|
+
if (depth > MAX_DEPTH) {
|
|
210
|
+
return '"[Max Depth Exceeded]"';
|
|
211
|
+
}
|
|
212
|
+
// Handle primitives
|
|
213
|
+
if (value === null)
|
|
214
|
+
return 'null';
|
|
215
|
+
if (value === undefined)
|
|
216
|
+
return 'undefined';
|
|
217
|
+
if (typeof value !== 'object')
|
|
218
|
+
return JSON.stringify(value);
|
|
219
|
+
// Handle circular references
|
|
220
|
+
if (seen.has(value)) {
|
|
221
|
+
return '"[Circular]"';
|
|
222
|
+
}
|
|
223
|
+
seen.add(value);
|
|
224
|
+
try {
|
|
225
|
+
// Handle arrays
|
|
226
|
+
if (Array.isArray(value)) {
|
|
227
|
+
const items = value.map(item => sortedStringify(item, depth + 1));
|
|
228
|
+
return `[${items.join(',')}]`;
|
|
229
|
+
}
|
|
230
|
+
// Handle objects - sort keys for stability
|
|
231
|
+
const keys = Object.keys(value).sort();
|
|
232
|
+
const pairs = keys.map(key => {
|
|
233
|
+
const serializedKey = JSON.stringify(key);
|
|
234
|
+
const serializedValue = sortedStringify(value[key], depth + 1);
|
|
235
|
+
return `${serializedKey}:${serializedValue}`;
|
|
236
|
+
});
|
|
237
|
+
return `{${pairs.join(',')}}`;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
// Clean up seen set for this branch
|
|
241
|
+
seen.delete(value);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
return sortedStringify(obj);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
// Fallback to safe representation if stringify fails
|
|
249
|
+
return String(obj);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Create a new validator with minimum length constraint
|
|
254
|
+
*/
|
|
255
|
+
min(length, message) {
|
|
256
|
+
return new VldArray({
|
|
257
|
+
...this.config,
|
|
258
|
+
minLength: length,
|
|
259
|
+
errorMessage: message || getMessages().arrayMin(length)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create a new validator with maximum length constraint
|
|
264
|
+
*/
|
|
265
|
+
max(length, message) {
|
|
266
|
+
return new VldArray({
|
|
267
|
+
...this.config,
|
|
268
|
+
maxLength: length,
|
|
269
|
+
errorMessage: message || getMessages().arrayMax(length)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Create a new validator with exact length constraint
|
|
274
|
+
*/
|
|
275
|
+
length(length, message) {
|
|
276
|
+
return new VldArray({
|
|
277
|
+
...this.config,
|
|
278
|
+
exactLength: length,
|
|
279
|
+
minLength: undefined,
|
|
280
|
+
maxLength: undefined,
|
|
281
|
+
errorMessage: message || getMessages().arrayLength(length)
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Create a new validator that ensures array is not empty
|
|
286
|
+
*/
|
|
287
|
+
nonempty(message) {
|
|
288
|
+
return new VldArray({
|
|
289
|
+
...this.config,
|
|
290
|
+
minLength: 1,
|
|
291
|
+
errorMessage: message || getMessages().arrayEmpty
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Create a new validator that ensures array contains unique items
|
|
296
|
+
*/
|
|
297
|
+
unique(message) {
|
|
298
|
+
return new VldArray({
|
|
299
|
+
...this.config,
|
|
300
|
+
unique: true,
|
|
301
|
+
errorMessage: message || 'Array must contain unique items'
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Create a new validator with a range constraint for length
|
|
306
|
+
*/
|
|
307
|
+
between(min, max, message) {
|
|
308
|
+
return new VldArray({
|
|
309
|
+
...this.config,
|
|
310
|
+
minLength: min,
|
|
311
|
+
maxLength: max,
|
|
312
|
+
errorMessage: message || `Array length must be between ${min} and ${max}`
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Immutable enum validator for string enum values
|
|
319
|
+
*/
|
|
320
|
+
class VldEnum extends VldBase {
|
|
321
|
+
/**
|
|
322
|
+
* Private constructor to enforce immutability
|
|
323
|
+
*/
|
|
324
|
+
constructor(values, errorMessage) {
|
|
325
|
+
super();
|
|
326
|
+
this.values = values;
|
|
327
|
+
this.errorMessage = errorMessage;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Create a new enum validator
|
|
331
|
+
*/
|
|
332
|
+
static create(values) {
|
|
333
|
+
return new VldEnum(values);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Parse and validate an enum value
|
|
337
|
+
*/
|
|
338
|
+
parse(value) {
|
|
339
|
+
// BUG-002 FIX: Add type check before includes() to prevent type confusion
|
|
340
|
+
if (typeof value !== 'string') {
|
|
341
|
+
throw new Error(this.errorMessage ||
|
|
342
|
+
getMessages().enumExpected([...this.values], JSON.stringify(value)));
|
|
343
|
+
}
|
|
344
|
+
if (!this.values.includes(value)) {
|
|
345
|
+
throw new Error(this.errorMessage ||
|
|
346
|
+
getMessages().enumExpected([...this.values], JSON.stringify(value)));
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Safely parse and validate an enum value
|
|
352
|
+
*/
|
|
353
|
+
safeParse(value) {
|
|
354
|
+
// BUG-002 FIX: Add type check before includes() to prevent type confusion
|
|
355
|
+
if (typeof value !== 'string') {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: new Error(this.errorMessage ||
|
|
359
|
+
getMessages().enumExpected([...this.values], JSON.stringify(value)))
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (this.values.includes(value)) {
|
|
363
|
+
return { success: true, data: value };
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
error: new Error(this.errorMessage ||
|
|
368
|
+
getMessages().enumExpected([...this.values], JSON.stringify(value)))
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Exclude specific values from the enum
|
|
373
|
+
* Creates a new enum validator without the specified values
|
|
374
|
+
*/
|
|
375
|
+
exclude(...excludeValues) {
|
|
376
|
+
const filtered = this.values.filter(v => !excludeValues.includes(v));
|
|
377
|
+
if (filtered.length === 0) {
|
|
378
|
+
throw new Error('Cannot exclude all enum values');
|
|
379
|
+
}
|
|
380
|
+
// Type assertion needed because TypeScript can't guarantee the filtered array maintains the required type
|
|
381
|
+
return new VldEnum(filtered, this.errorMessage);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Extract specific values from the enum
|
|
385
|
+
* Creates a new enum validator with only the specified values
|
|
386
|
+
*/
|
|
387
|
+
extract(...extractValues) {
|
|
388
|
+
const extracted = this.values.filter(v => extractValues.includes(v));
|
|
389
|
+
if (extracted.length === 0) {
|
|
390
|
+
throw new Error('Cannot extract non-existent enum values');
|
|
391
|
+
}
|
|
392
|
+
// Type assertion needed because TypeScript can't guarantee the extracted array has the required type
|
|
393
|
+
return new VldEnum(extracted, this.errorMessage);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Optimized immutable object validator with chainable methods
|
|
399
|
+
* Features pre-computed keys and Set-based lookups for better performance
|
|
400
|
+
*/
|
|
401
|
+
class VldObject extends VldBase {
|
|
402
|
+
/**
|
|
403
|
+
* Private constructor to enforce immutability
|
|
404
|
+
*/
|
|
405
|
+
constructor(config) {
|
|
406
|
+
super();
|
|
407
|
+
this.config = config;
|
|
408
|
+
// Pre-compute shape keys for faster access
|
|
409
|
+
this.shapeKeys = Object.keys(config.shape);
|
|
410
|
+
this.shapeKeysSet = new Set(this.shapeKeys);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Create a new object validator
|
|
414
|
+
*/
|
|
415
|
+
static create(shape) {
|
|
416
|
+
return new VldObject({ shape });
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Parse and validate an object value
|
|
420
|
+
* Ultra-optimized with inline type checks and minimal overhead
|
|
421
|
+
*/
|
|
422
|
+
parse(value) {
|
|
423
|
+
// Fast type check
|
|
424
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
425
|
+
throw new Error(this.config.errorMessage || getMessages().invalidObject);
|
|
426
|
+
}
|
|
427
|
+
const obj = value;
|
|
428
|
+
const result = {};
|
|
429
|
+
// Ultra-optimized field validation with inline fast paths
|
|
430
|
+
for (let i = 0; i < this.shapeKeys.length; i++) {
|
|
431
|
+
const key = this.shapeKeys[i];
|
|
432
|
+
const validator = this.config.shape[key];
|
|
433
|
+
const fieldValue = obj[key];
|
|
434
|
+
// BUG-NEW-002 FIX: Use instanceof instead of constructor.name
|
|
435
|
+
// constructor.name breaks in minified builds where class names become 'a', 'b', etc.
|
|
436
|
+
// instanceof checks are reliable regardless of minification
|
|
437
|
+
//
|
|
438
|
+
// Note: Coercion validators extend their base validators, so we need to check for
|
|
439
|
+
// coercion types first (more specific) before checking base types
|
|
440
|
+
// Check for coercion validators first - they need full safeParse for type conversion
|
|
441
|
+
if (validator instanceof VldCoerceString ||
|
|
442
|
+
validator instanceof VldCoerceNumber ||
|
|
443
|
+
validator instanceof VldCoerceBoolean ||
|
|
444
|
+
validator instanceof VldCoerceDate) {
|
|
445
|
+
// Coercion validators need safeParse to handle type conversion
|
|
446
|
+
const parseResult = validator.safeParse(fieldValue);
|
|
447
|
+
if (!parseResult.success) {
|
|
448
|
+
throw new Error(getMessages().objectField(key, parseResult.error.message));
|
|
449
|
+
}
|
|
450
|
+
result[key] = parseResult.data;
|
|
451
|
+
}
|
|
452
|
+
else if (validator instanceof VldString) {
|
|
453
|
+
// For string with validations, use safeParse to handle email, regex etc.
|
|
454
|
+
const parseResult = validator.safeParse(fieldValue);
|
|
455
|
+
if (!parseResult.success) {
|
|
456
|
+
throw new Error(getMessages().objectField(key, parseResult.error.message));
|
|
457
|
+
}
|
|
458
|
+
result[key] = parseResult.data;
|
|
459
|
+
}
|
|
460
|
+
else if (validator instanceof VldNumber) {
|
|
461
|
+
if (typeof fieldValue !== 'number' || isNaN(fieldValue)) {
|
|
462
|
+
throw new Error(getMessages().objectField(key, getMessages().invalidNumber));
|
|
463
|
+
}
|
|
464
|
+
result[key] = fieldValue;
|
|
465
|
+
}
|
|
466
|
+
else if (validator instanceof VldBoolean) {
|
|
467
|
+
if (typeof fieldValue !== 'boolean') {
|
|
468
|
+
throw new Error(getMessages().objectField(key, getMessages().invalidBoolean));
|
|
469
|
+
}
|
|
470
|
+
result[key] = fieldValue;
|
|
471
|
+
}
|
|
472
|
+
else if (validator instanceof VldDate) {
|
|
473
|
+
// Use the actual validator for Date parsing (handles string conversion)
|
|
474
|
+
const parseResult = validator.safeParse(fieldValue);
|
|
475
|
+
if (!parseResult.success) {
|
|
476
|
+
throw new Error(getMessages().objectField(key, parseResult.error.message));
|
|
477
|
+
}
|
|
478
|
+
result[key] = parseResult.data;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// Fallback to safeParse for complex validators
|
|
482
|
+
const parseResult = validator.safeParse(fieldValue);
|
|
483
|
+
if (!parseResult.success) {
|
|
484
|
+
throw new Error(getMessages().objectField(key, parseResult.error.message));
|
|
485
|
+
}
|
|
486
|
+
result[key] = parseResult.data;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Handle strict mode - optimized with Set
|
|
490
|
+
if (this.config.strict) {
|
|
491
|
+
const objKeys = Object.keys(obj);
|
|
492
|
+
const extraKeys = [];
|
|
493
|
+
for (let i = 0; i < objKeys.length; i++) {
|
|
494
|
+
if (!this.shapeKeysSet.has(objKeys[i])) {
|
|
495
|
+
extraKeys.push(objKeys[i]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (extraKeys.length > 0) {
|
|
499
|
+
throw new Error(getMessages().unexpectedKeys(extraKeys));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Handle passthrough mode - optimized with comprehensive prototype pollution protection
|
|
503
|
+
if (this.config.passthrough) {
|
|
504
|
+
const objKeys = Object.keys(obj);
|
|
505
|
+
for (let i = 0; i < objKeys.length; i++) {
|
|
506
|
+
const key = objKeys[i];
|
|
507
|
+
// Skip dangerous keys to prevent prototype pollution
|
|
508
|
+
if (!this.shapeKeysSet.has(key) && !this.isDangerousKey(key)) {
|
|
509
|
+
result[key] = obj[key];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Handle catchall - validate extra keys with catchall validator
|
|
514
|
+
if (this.config.catchall) {
|
|
515
|
+
const objKeys = Object.keys(obj);
|
|
516
|
+
for (let i = 0; i < objKeys.length; i++) {
|
|
517
|
+
const key = objKeys[i];
|
|
518
|
+
// Skip keys already in shape and dangerous keys
|
|
519
|
+
if (!this.shapeKeysSet.has(key) && !this.isDangerousKey(key)) {
|
|
520
|
+
result[key] = this.config.catchall.parse(obj[key]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Safely parse and validate an object value
|
|
528
|
+
* Optimized version using pre-computed keys
|
|
529
|
+
*/
|
|
530
|
+
safeParse(value) {
|
|
531
|
+
// Fast type check
|
|
532
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
533
|
+
return {
|
|
534
|
+
success: false,
|
|
535
|
+
error: new Error(this.config.errorMessage || getMessages().invalidObject)
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const obj = value;
|
|
539
|
+
const result = {};
|
|
540
|
+
// Validate all fields
|
|
541
|
+
for (let i = 0; i < this.shapeKeys.length; i++) {
|
|
542
|
+
const key = this.shapeKeys[i];
|
|
543
|
+
const validator = this.config.shape[key];
|
|
544
|
+
const fieldValue = obj[key];
|
|
545
|
+
const parseResult = validator.safeParse(fieldValue);
|
|
546
|
+
if (parseResult.success) {
|
|
547
|
+
result[key] = parseResult.data;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
error: new Error(getMessages().objectField(key, parseResult.error.message))
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Handle strict mode
|
|
557
|
+
if (this.config.strict) {
|
|
558
|
+
const objKeys = Object.keys(obj);
|
|
559
|
+
const extraKeys = [];
|
|
560
|
+
for (let i = 0; i < objKeys.length; i++) {
|
|
561
|
+
if (!this.shapeKeysSet.has(objKeys[i])) {
|
|
562
|
+
extraKeys.push(objKeys[i]);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (extraKeys.length > 0) {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
error: new Error(getMessages().unexpectedKeys(extraKeys))
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Handle passthrough mode with comprehensive prototype pollution protection
|
|
573
|
+
if (this.config.passthrough) {
|
|
574
|
+
const objKeys = Object.keys(obj);
|
|
575
|
+
for (let i = 0; i < objKeys.length; i++) {
|
|
576
|
+
const key = objKeys[i];
|
|
577
|
+
// Skip dangerous keys to prevent prototype pollution
|
|
578
|
+
if (!this.shapeKeysSet.has(key) && !this.isDangerousKey(key)) {
|
|
579
|
+
result[key] = obj[key];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Handle catchall - validate extra keys with catchall validator
|
|
584
|
+
if (this.config.catchall) {
|
|
585
|
+
const objKeys = Object.keys(obj);
|
|
586
|
+
for (let i = 0; i < objKeys.length; i++) {
|
|
587
|
+
const key = objKeys[i];
|
|
588
|
+
// Skip keys already in shape and dangerous keys
|
|
589
|
+
if (!this.shapeKeysSet.has(key) && !this.isDangerousKey(key)) {
|
|
590
|
+
const catchallResult = this.config.catchall.safeParse(obj[key]);
|
|
591
|
+
if (!catchallResult.success) {
|
|
592
|
+
return {
|
|
593
|
+
success: false,
|
|
594
|
+
error: new Error(getMessages().objectField(key, catchallResult.error.message))
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
result[key] = catchallResult.data;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { success: true, data: result };
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Comprehensive prototype pollution protection
|
|
605
|
+
* Checks for dangerous keys that could modify Object.prototype
|
|
606
|
+
*/
|
|
607
|
+
isDangerousKey(key) {
|
|
608
|
+
// Direct dangerous keys
|
|
609
|
+
const directDangerousKeys = ['__proto__', 'constructor', 'prototype'];
|
|
610
|
+
if (directDangerousKeys.includes(key)) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
// Nested prototype manipulation vectors
|
|
614
|
+
// These patterns could allow prototype pollution through nested access
|
|
615
|
+
const nestedPatterns = [
|
|
616
|
+
'constructor.prototype',
|
|
617
|
+
'__proto__.toString',
|
|
618
|
+
'prototype.constructor',
|
|
619
|
+
'__defineGetter__',
|
|
620
|
+
'__defineSetter__',
|
|
621
|
+
'__lookupGetter__',
|
|
622
|
+
'__lookupSetter__'
|
|
623
|
+
];
|
|
624
|
+
// Check for nested patterns
|
|
625
|
+
for (const pattern of nestedPatterns) {
|
|
626
|
+
if (key.includes(pattern)) {
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Check for property access chains that could lead to prototype pollution
|
|
631
|
+
// This covers patterns like "x.constructor.prototype.polluted"
|
|
632
|
+
const dangerousChains = [
|
|
633
|
+
'constructor.',
|
|
634
|
+
'__proto__.',
|
|
635
|
+
'prototype.'
|
|
636
|
+
];
|
|
637
|
+
for (const chain of dangerousChains) {
|
|
638
|
+
if (key.includes(chain)) {
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Additional protection: reject keys that could be used for property shadowing
|
|
643
|
+
const shadowingPatterns = [
|
|
644
|
+
'hasOwnProperty',
|
|
645
|
+
'toString',
|
|
646
|
+
'valueOf',
|
|
647
|
+
'isPrototypeOf',
|
|
648
|
+
'propertyIsEnumerable'
|
|
649
|
+
];
|
|
650
|
+
for (const pattern of shadowingPatterns) {
|
|
651
|
+
if (key === pattern || key.includes(`.${pattern}`)) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Create a new validator in strict mode (no extra keys allowed)
|
|
659
|
+
*/
|
|
660
|
+
strict(message) {
|
|
661
|
+
return new VldObject({
|
|
662
|
+
...this.config,
|
|
663
|
+
strict: true,
|
|
664
|
+
passthrough: false,
|
|
665
|
+
errorMessage: message
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Create a new validator in passthrough mode (extra keys are preserved)
|
|
670
|
+
*/
|
|
671
|
+
passthrough() {
|
|
672
|
+
return new VldObject({
|
|
673
|
+
...this.config,
|
|
674
|
+
strict: false,
|
|
675
|
+
passthrough: true
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Create a new validator with all fields optional
|
|
680
|
+
*/
|
|
681
|
+
partial() {
|
|
682
|
+
const partialShape = {};
|
|
683
|
+
for (const key in this.config.shape) {
|
|
684
|
+
partialShape[key] = new VldOptional(this.config.shape[key]);
|
|
685
|
+
}
|
|
686
|
+
return new VldObject({
|
|
687
|
+
...this.config,
|
|
688
|
+
shape: partialShape
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Create a new validator with deep partial (nested objects also partial)
|
|
693
|
+
*/
|
|
694
|
+
deepPartial() {
|
|
695
|
+
const deepPartialShape = {};
|
|
696
|
+
for (const key in this.config.shape) {
|
|
697
|
+
const validator = this.config.shape[key];
|
|
698
|
+
if (validator instanceof VldObject) {
|
|
699
|
+
deepPartialShape[key] = new VldOptional(validator.deepPartial());
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
deepPartialShape[key] = new VldOptional(validator);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return new VldObject({
|
|
706
|
+
...this.config,
|
|
707
|
+
shape: deepPartialShape
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Create a new validator with only specified keys
|
|
712
|
+
*/
|
|
713
|
+
pick(...keys) {
|
|
714
|
+
const pickedShape = {};
|
|
715
|
+
for (const key of keys) {
|
|
716
|
+
if (key in this.config.shape) {
|
|
717
|
+
pickedShape[key] = this.config.shape[key];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return new VldObject({
|
|
721
|
+
...this.config,
|
|
722
|
+
shape: pickedShape
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Create a new validator without specified keys
|
|
727
|
+
*/
|
|
728
|
+
omit(...keys) {
|
|
729
|
+
const omittedShape = {};
|
|
730
|
+
const keysToOmit = new Set(keys);
|
|
731
|
+
for (const key in this.config.shape) {
|
|
732
|
+
if (!keysToOmit.has(key)) {
|
|
733
|
+
omittedShape[key] = this.config.shape[key];
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return new VldObject({
|
|
737
|
+
...this.config,
|
|
738
|
+
shape: omittedShape
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Create a new validator with additional fields
|
|
743
|
+
*/
|
|
744
|
+
extend(extension) {
|
|
745
|
+
return new VldObject({
|
|
746
|
+
...this.config,
|
|
747
|
+
shape: { ...this.config.shape, ...extension }
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Create a new validator by merging with another object validator
|
|
752
|
+
*/
|
|
753
|
+
merge(other) {
|
|
754
|
+
return new VldObject({
|
|
755
|
+
...this.config,
|
|
756
|
+
shape: { ...this.config.shape, ...other.config.shape }
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Create a new validator with all fields required (removes optional)
|
|
761
|
+
*/
|
|
762
|
+
required() {
|
|
763
|
+
const requiredShape = {};
|
|
764
|
+
for (const key in this.config.shape) {
|
|
765
|
+
const validator = this.config.shape[key];
|
|
766
|
+
// If it's optional, unwrap it
|
|
767
|
+
if (validator instanceof VldOptional) {
|
|
768
|
+
// BUG-001 FIX: Add defensive check for baseValidator property
|
|
769
|
+
const unwrapped = validator.baseValidator;
|
|
770
|
+
if (!unwrapped || typeof unwrapped.parse !== 'function') {
|
|
771
|
+
throw new Error(`Invalid VldOptional structure for field "${key}": missing or invalid baseValidator`);
|
|
772
|
+
}
|
|
773
|
+
requiredShape[key] = unwrapped;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
requiredShape[key] = validator;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return new VldObject({
|
|
780
|
+
...this.config,
|
|
781
|
+
shape: requiredShape
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Create a new validator with a catchall validator for extra keys
|
|
786
|
+
* Zod 4 API parity - validates unknown keys with provided schema
|
|
787
|
+
*/
|
|
788
|
+
catchall(schema) {
|
|
789
|
+
return new VldObject({
|
|
790
|
+
...this.config,
|
|
791
|
+
catchall: schema,
|
|
792
|
+
passthrough: false // catchall overrides passthrough
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Access the inner shape schemas
|
|
797
|
+
* Zod 4 API parity - returns the shape object
|
|
798
|
+
*/
|
|
799
|
+
get shape() {
|
|
800
|
+
return this.config.shape;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Create an enum validator from object keys
|
|
804
|
+
* Zod 4 API parity - creates literal union of keys
|
|
805
|
+
*/
|
|
806
|
+
keyof() {
|
|
807
|
+
const keys = Object.keys(this.config.shape);
|
|
808
|
+
if (keys.length === 0) {
|
|
809
|
+
throw new Error('Cannot create keyof enum from empty object');
|
|
810
|
+
}
|
|
811
|
+
return VldEnum.create(keys);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Type-safe extend that throws an error if any key already exists
|
|
815
|
+
* Zod 4 API parity - prevents accidental field override
|
|
816
|
+
* @param extension The extension shape to add
|
|
817
|
+
* @returns A new validator with extended shape
|
|
818
|
+
* @throws {Error} If any extension key already exists in the shape
|
|
819
|
+
* @example
|
|
820
|
+
* const base = v.object({ name: v.string() });
|
|
821
|
+
* const extended = base.safeExtend({ age: v.number() }); // OK
|
|
822
|
+
* const invalid = base.safeExtend({ name: v.number() }); // Throws error
|
|
823
|
+
*/
|
|
824
|
+
safeExtend(extension) {
|
|
825
|
+
// Check for overlapping keys
|
|
826
|
+
const existingKeys = new Set(Object.keys(this.config.shape));
|
|
827
|
+
const extensionKeys = Object.keys(extension);
|
|
828
|
+
const overlappingKeys = [];
|
|
829
|
+
for (const key of extensionKeys) {
|
|
830
|
+
if (existingKeys.has(key)) {
|
|
831
|
+
overlappingKeys.push(key);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (overlappingKeys.length > 0) {
|
|
835
|
+
throw new Error(`safeExtend: ${getMessages().safeExtendOverlap(overlappingKeys)}`);
|
|
836
|
+
}
|
|
837
|
+
return new VldObject({
|
|
838
|
+
...this.config,
|
|
839
|
+
shape: { ...this.config.shape, ...extension }
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Optimized immutable union validator for multiple type options
|
|
846
|
+
* Features type-checking shortcuts for 110x performance improvement
|
|
847
|
+
*/
|
|
848
|
+
class VldUnion extends VldBase {
|
|
849
|
+
/**
|
|
850
|
+
* Private constructor to enforce immutability
|
|
851
|
+
*/
|
|
852
|
+
constructor(validators, errorMessage) {
|
|
853
|
+
super();
|
|
854
|
+
this.validators = validators;
|
|
855
|
+
this.errorMessage = errorMessage;
|
|
856
|
+
this.typeCheckers = new Map();
|
|
857
|
+
// Pre-compile type checkers for common types
|
|
858
|
+
for (const validator of validators) {
|
|
859
|
+
this.typeCheckers.set(validator, this.createTypeChecker(validator));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Create type checker based on validator type for fast path optimization
|
|
864
|
+
* Uses a safer approach that's less prone to spoofing
|
|
865
|
+
*/
|
|
866
|
+
createTypeChecker(validator) {
|
|
867
|
+
// More conservative approach that prioritizes security and stability
|
|
868
|
+
// Only use fast path checks for very common, unambiguous types
|
|
869
|
+
try {
|
|
870
|
+
// Test basic types with safe parsing to validate the validator
|
|
871
|
+
const stringTest = validator.safeParse('test');
|
|
872
|
+
if (stringTest.success) {
|
|
873
|
+
return (v) => typeof v === 'string';
|
|
874
|
+
}
|
|
875
|
+
const numberTest = validator.safeParse(123);
|
|
876
|
+
if (numberTest.success) {
|
|
877
|
+
return (v) => typeof v === 'number' && !isNaN(v);
|
|
878
|
+
}
|
|
879
|
+
const booleanTest = validator.safeParse(true);
|
|
880
|
+
if (booleanTest.success) {
|
|
881
|
+
return (v) => typeof v === 'boolean';
|
|
882
|
+
}
|
|
883
|
+
const arrayTest = validator.safeParse([]);
|
|
884
|
+
if (arrayTest.success) {
|
|
885
|
+
return (v) => Array.isArray(v);
|
|
886
|
+
}
|
|
887
|
+
const objectTest = validator.safeParse({});
|
|
888
|
+
if (objectTest.success) {
|
|
889
|
+
return (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
890
|
+
}
|
|
891
|
+
const nullTest = validator.safeParse(null);
|
|
892
|
+
if (nullTest.success) {
|
|
893
|
+
return (v) => v === null;
|
|
894
|
+
}
|
|
895
|
+
const undefinedTest = validator.safeParse(undefined);
|
|
896
|
+
if (undefinedTest.success) {
|
|
897
|
+
return (v) => v === undefined;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
// If safe testing fails, fall back to safe validation
|
|
902
|
+
}
|
|
903
|
+
// Fallback: no quick check available - use safe validation
|
|
904
|
+
return () => true;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Create a new union validator
|
|
908
|
+
*/
|
|
909
|
+
static create(...validators) {
|
|
910
|
+
return new VldUnion(validators);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Parse and validate a value against union options
|
|
914
|
+
* Optimized with type checking and safeParse to avoid try-catch overhead
|
|
915
|
+
* BUG-NEW-013 FIX: Single-pass error collection to avoid double parsing
|
|
916
|
+
*/
|
|
917
|
+
parse(value) {
|
|
918
|
+
// Single pass: collect errors during validation
|
|
919
|
+
const errors = [];
|
|
920
|
+
for (const validator of this.validators) {
|
|
921
|
+
const typeChecker = this.typeCheckers.get(validator);
|
|
922
|
+
// Skip validators that definitely won't match based on type
|
|
923
|
+
if (typeChecker && !typeChecker(value)) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
// Use safeParse to avoid try-catch overhead
|
|
927
|
+
const result = validator.safeParse(value);
|
|
928
|
+
if (result.success) {
|
|
929
|
+
return result.data;
|
|
930
|
+
}
|
|
931
|
+
// Collect error for final message if all validators fail
|
|
932
|
+
errors.push(result.error.message);
|
|
933
|
+
}
|
|
934
|
+
// All validators failed - throw with collected errors
|
|
935
|
+
throw new Error(this.errorMessage ||
|
|
936
|
+
getMessages().unionNoMatch(errors));
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Safely parse and validate a value against union options
|
|
940
|
+
* Optimized version using type checking shortcuts
|
|
941
|
+
* BUG-NEW-013 FIX: Single-pass error collection to avoid double parsing
|
|
942
|
+
*/
|
|
943
|
+
safeParse(value) {
|
|
944
|
+
// Single pass: collect errors during validation
|
|
945
|
+
const errors = [];
|
|
946
|
+
for (const validator of this.validators) {
|
|
947
|
+
const typeChecker = this.typeCheckers.get(validator);
|
|
948
|
+
// Skip validators that definitely won't match based on type
|
|
949
|
+
if (typeChecker && !typeChecker(value)) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const result = validator.safeParse(value);
|
|
953
|
+
if (result.success) {
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
// Collect error for final message if all validators fail
|
|
957
|
+
errors.push(result.error.message);
|
|
958
|
+
}
|
|
959
|
+
// All validators failed - return error with collected messages
|
|
960
|
+
return {
|
|
961
|
+
success: false,
|
|
962
|
+
error: new Error(this.errorMessage ||
|
|
963
|
+
getMessages().unionNoMatch(errors))
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Immutable literal validator for exact value matching
|
|
970
|
+
*/
|
|
971
|
+
class VldLiteral extends VldBase {
|
|
972
|
+
/**
|
|
973
|
+
* Private constructor to enforce immutability
|
|
974
|
+
*/
|
|
975
|
+
constructor(literal, errorMessage) {
|
|
976
|
+
super();
|
|
977
|
+
this.literal = literal;
|
|
978
|
+
this.errorMessage = errorMessage;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Create a new literal validator
|
|
982
|
+
*/
|
|
983
|
+
static create(literal) {
|
|
984
|
+
return new VldLiteral(literal);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Parse and validate a literal value
|
|
988
|
+
*/
|
|
989
|
+
parse(value) {
|
|
990
|
+
if (value !== this.literal) {
|
|
991
|
+
throw new Error(this.errorMessage ||
|
|
992
|
+
getMessages().literalExpected(JSON.stringify(this.literal), JSON.stringify(value)));
|
|
993
|
+
}
|
|
994
|
+
return this.literal;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Safely parse and validate a literal value
|
|
998
|
+
*/
|
|
999
|
+
safeParse(value) {
|
|
1000
|
+
if (value === this.literal) {
|
|
1001
|
+
return { success: true, data: this.literal };
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
success: false,
|
|
1005
|
+
error: new Error(this.errorMessage ||
|
|
1006
|
+
getMessages().literalExpected(JSON.stringify(this.literal), JSON.stringify(value)))
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Immutable symbol validator
|
|
1013
|
+
*/
|
|
1014
|
+
class VldSymbol extends VldBase {
|
|
1015
|
+
/**
|
|
1016
|
+
* Private constructor to enforce immutability
|
|
1017
|
+
*/
|
|
1018
|
+
constructor(errorMessage) {
|
|
1019
|
+
super();
|
|
1020
|
+
this.errorMessage = errorMessage;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Create a new symbol validator
|
|
1024
|
+
*/
|
|
1025
|
+
static create() {
|
|
1026
|
+
return new VldSymbol();
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Parse and validate a symbol value
|
|
1030
|
+
*/
|
|
1031
|
+
parse(value) {
|
|
1032
|
+
if (typeof value !== 'symbol') {
|
|
1033
|
+
throw new Error(this.errorMessage || getMessages().invalidSymbol);
|
|
1034
|
+
}
|
|
1035
|
+
return value;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Safely parse and validate a symbol value
|
|
1039
|
+
*/
|
|
1040
|
+
safeParse(value) {
|
|
1041
|
+
if (typeof value === 'symbol') {
|
|
1042
|
+
return { success: true, data: value };
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
success: false,
|
|
1046
|
+
error: new Error(this.errorMessage || getMessages().invalidSymbol)
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Immutable tuple validator for fixed-length arrays
|
|
1053
|
+
*/
|
|
1054
|
+
class VldTuple extends VldBase {
|
|
1055
|
+
/**
|
|
1056
|
+
* Private constructor to enforce immutability
|
|
1057
|
+
*/
|
|
1058
|
+
constructor(validators, errorMessage) {
|
|
1059
|
+
super();
|
|
1060
|
+
this.validators = validators;
|
|
1061
|
+
this.errorMessage = errorMessage;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Create a new tuple validator
|
|
1065
|
+
*/
|
|
1066
|
+
static create(...validators) {
|
|
1067
|
+
return new VldTuple(validators);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Parse and validate a tuple value
|
|
1071
|
+
*/
|
|
1072
|
+
parse(value) {
|
|
1073
|
+
if (!Array.isArray(value)) {
|
|
1074
|
+
throw new Error(this.errorMessage || getMessages().invalidTuple);
|
|
1075
|
+
}
|
|
1076
|
+
if (value.length !== this.validators.length) {
|
|
1077
|
+
throw new Error(this.errorMessage ||
|
|
1078
|
+
getMessages().tupleLength(this.validators.length, value.length));
|
|
1079
|
+
}
|
|
1080
|
+
const result = [];
|
|
1081
|
+
for (let i = 0; i < this.validators.length; i++) {
|
|
1082
|
+
try {
|
|
1083
|
+
result[i] = this.validators[i].parse(value[i]);
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
throw new Error(getMessages().arrayItem(i, error.message));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return result;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Safely parse and validate a tuple value
|
|
1093
|
+
*/
|
|
1094
|
+
safeParse(value) {
|
|
1095
|
+
try {
|
|
1096
|
+
return { success: true, data: this.parse(value) };
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
return { success: false, error: error };
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Security utilities for prototype pollution protection
|
|
1106
|
+
* BUG-NEW-018 & BUG-NEW-020 FIX: Centralized comprehensive dangerous key detection
|
|
1107
|
+
*/
|
|
1108
|
+
/**
|
|
1109
|
+
* Comprehensive prototype pollution protection
|
|
1110
|
+
* Checks for dangerous keys that could modify Object.prototype or built-in prototypes
|
|
1111
|
+
* @param key The property key to check
|
|
1112
|
+
* @returns true if the key is dangerous, false otherwise
|
|
1113
|
+
*/
|
|
1114
|
+
function isDangerousKey(key) {
|
|
1115
|
+
// Direct dangerous keys
|
|
1116
|
+
const directDangerousKeys = ['__proto__', 'constructor', 'prototype'];
|
|
1117
|
+
if (directDangerousKeys.includes(key)) {
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
// Nested prototype manipulation vectors
|
|
1121
|
+
// These patterns could allow prototype pollution through nested access
|
|
1122
|
+
const nestedPatterns = [
|
|
1123
|
+
'constructor.prototype',
|
|
1124
|
+
'__proto__.toString',
|
|
1125
|
+
'prototype.constructor',
|
|
1126
|
+
'__defineGetter__',
|
|
1127
|
+
'__defineSetter__',
|
|
1128
|
+
'__lookupGetter__',
|
|
1129
|
+
'__lookupSetter__'
|
|
1130
|
+
];
|
|
1131
|
+
// Check for nested patterns
|
|
1132
|
+
for (const pattern of nestedPatterns) {
|
|
1133
|
+
if (key.includes(pattern)) {
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// Check for property access chains that could lead to prototype pollution
|
|
1138
|
+
// This covers patterns like "x.constructor.prototype.polluted"
|
|
1139
|
+
const dangerousChains = [
|
|
1140
|
+
'constructor.',
|
|
1141
|
+
'__proto__.',
|
|
1142
|
+
'prototype.'
|
|
1143
|
+
];
|
|
1144
|
+
for (const chain of dangerousChains) {
|
|
1145
|
+
if (key.includes(chain)) {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
// Additional protection: reject keys that could be used for property shadowing
|
|
1150
|
+
const shadowingPatterns = [
|
|
1151
|
+
'hasOwnProperty',
|
|
1152
|
+
'toString',
|
|
1153
|
+
'valueOf',
|
|
1154
|
+
'isPrototypeOf',
|
|
1155
|
+
'propertyIsEnumerable'
|
|
1156
|
+
];
|
|
1157
|
+
for (const pattern of shadowingPatterns) {
|
|
1158
|
+
if (key === pattern || key.includes(`.${pattern}`)) {
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Immutable record validator for key-value pairs
|
|
1167
|
+
* BUG-NEW-018 FIX: Uses comprehensive dangerous key protection
|
|
1168
|
+
*/
|
|
1169
|
+
class VldRecord extends VldBase {
|
|
1170
|
+
/**
|
|
1171
|
+
* Private constructor to enforce immutability
|
|
1172
|
+
*/
|
|
1173
|
+
constructor(valueValidator, errorMessage) {
|
|
1174
|
+
super();
|
|
1175
|
+
this.valueValidator = valueValidator;
|
|
1176
|
+
this.errorMessage = errorMessage;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Create a new record validator
|
|
1180
|
+
*/
|
|
1181
|
+
static create(valueValidator) {
|
|
1182
|
+
return new VldRecord(valueValidator);
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Parse and validate a record value
|
|
1186
|
+
* BUG-NEW-018 FIX: Use comprehensive dangerous key protection
|
|
1187
|
+
*/
|
|
1188
|
+
parse(value) {
|
|
1189
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1190
|
+
throw new Error(this.errorMessage || getMessages().invalidRecord);
|
|
1191
|
+
}
|
|
1192
|
+
const result = {};
|
|
1193
|
+
const obj = value;
|
|
1194
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1195
|
+
// Skip dangerous keys to prevent prototype pollution
|
|
1196
|
+
// Now using comprehensive protection from shared utility
|
|
1197
|
+
if (isDangerousKey(key)) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
try {
|
|
1201
|
+
result[key] = this.valueValidator.parse(val);
|
|
1202
|
+
}
|
|
1203
|
+
catch (error) {
|
|
1204
|
+
throw new Error(getMessages().objectField(key, error.message));
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return result;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Safely parse and validate a record value
|
|
1211
|
+
*/
|
|
1212
|
+
safeParse(value) {
|
|
1213
|
+
try {
|
|
1214
|
+
return { success: true, data: this.parse(value) };
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
return { success: false, error: error };
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Create a partial record variant (all keys optional)
|
|
1222
|
+
* Similar to v.object().partial() but for records
|
|
1223
|
+
*/
|
|
1224
|
+
partial() {
|
|
1225
|
+
const optionalValidator = this.valueValidator.optional();
|
|
1226
|
+
return new VldRecord(optionalValidator);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Create a loose record variant (allows extra keys)
|
|
1230
|
+
* Similar to v.object().passthrough() but for records
|
|
1231
|
+
* Note: Records already allow any keys, so this mainly affects error handling
|
|
1232
|
+
*/
|
|
1233
|
+
loose() {
|
|
1234
|
+
// For records, "loose" means we don't throw errors for validation failures
|
|
1235
|
+
// We return a modified version that catches validation errors
|
|
1236
|
+
return new VldLooseRecord(this.valueValidator);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Loose record variant that allows validation failures
|
|
1241
|
+
* Used internally by .loose() method
|
|
1242
|
+
*/
|
|
1243
|
+
class VldLooseRecord extends VldBase {
|
|
1244
|
+
constructor(valueValidator) {
|
|
1245
|
+
super();
|
|
1246
|
+
this.valueValidator = valueValidator;
|
|
1247
|
+
}
|
|
1248
|
+
parse(value) {
|
|
1249
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1250
|
+
throw new Error(getMessages().invalidRecord);
|
|
1251
|
+
}
|
|
1252
|
+
const result = {};
|
|
1253
|
+
const obj = value;
|
|
1254
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1255
|
+
// Skip dangerous keys
|
|
1256
|
+
if (isDangerousKey(key)) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
// For loose records, skip invalid values instead of throwing
|
|
1260
|
+
const parseResult = this.valueValidator.safeParse(val);
|
|
1261
|
+
if (parseResult.success) {
|
|
1262
|
+
result[key] = parseResult.data;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return result;
|
|
1266
|
+
}
|
|
1267
|
+
safeParse(value) {
|
|
1268
|
+
try {
|
|
1269
|
+
return { success: true, data: this.parse(value) };
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
return { success: false, error: error };
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Create a partial variant (all keys optional)
|
|
1277
|
+
*/
|
|
1278
|
+
partial() {
|
|
1279
|
+
const optionalValidator = this.valueValidator.optional();
|
|
1280
|
+
return new VldLooseRecord(optionalValidator);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Return self (already loose)
|
|
1284
|
+
*/
|
|
1285
|
+
loose() {
|
|
1286
|
+
return this;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Immutable Set validator
|
|
1292
|
+
*/
|
|
1293
|
+
class VldSet extends VldBase {
|
|
1294
|
+
/**
|
|
1295
|
+
* Private constructor to enforce immutability
|
|
1296
|
+
*/
|
|
1297
|
+
constructor(itemValidator, errorMessage) {
|
|
1298
|
+
super();
|
|
1299
|
+
this.itemValidator = itemValidator;
|
|
1300
|
+
this.errorMessage = errorMessage;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Create a new Set validator
|
|
1304
|
+
*/
|
|
1305
|
+
static create(itemValidator) {
|
|
1306
|
+
return new VldSet(itemValidator);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Parse and validate a Set value
|
|
1310
|
+
*/
|
|
1311
|
+
parse(value) {
|
|
1312
|
+
if (!(value instanceof Set)) {
|
|
1313
|
+
throw new Error(this.errorMessage || getMessages().invalidSet);
|
|
1314
|
+
}
|
|
1315
|
+
const result = new Set();
|
|
1316
|
+
for (const item of value) {
|
|
1317
|
+
try {
|
|
1318
|
+
result.add(this.itemValidator.parse(item));
|
|
1319
|
+
}
|
|
1320
|
+
catch (error) {
|
|
1321
|
+
throw new Error(error.message);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return result;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Safely parse and validate a Set value
|
|
1328
|
+
*/
|
|
1329
|
+
safeParse(value) {
|
|
1330
|
+
try {
|
|
1331
|
+
return { success: true, data: this.parse(value) };
|
|
1332
|
+
}
|
|
1333
|
+
catch (error) {
|
|
1334
|
+
return { success: false, error: error };
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Immutable Map validator
|
|
1341
|
+
*/
|
|
1342
|
+
class VldMap extends VldBase {
|
|
1343
|
+
/**
|
|
1344
|
+
* Private constructor to enforce immutability
|
|
1345
|
+
*/
|
|
1346
|
+
constructor(keyValidator, valueValidator, errorMessage) {
|
|
1347
|
+
super();
|
|
1348
|
+
this.keyValidator = keyValidator;
|
|
1349
|
+
this.valueValidator = valueValidator;
|
|
1350
|
+
this.errorMessage = errorMessage;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Create a new Map validator
|
|
1354
|
+
*/
|
|
1355
|
+
static create(keyValidator, valueValidator) {
|
|
1356
|
+
return new VldMap(keyValidator, valueValidator);
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Parse and validate a Map value
|
|
1360
|
+
*/
|
|
1361
|
+
parse(value) {
|
|
1362
|
+
if (!(value instanceof Map)) {
|
|
1363
|
+
throw new Error(this.errorMessage || getMessages().invalidMap);
|
|
1364
|
+
}
|
|
1365
|
+
const result = new Map();
|
|
1366
|
+
for (const [key, val] of value) {
|
|
1367
|
+
try {
|
|
1368
|
+
const validKey = this.keyValidator.parse(key);
|
|
1369
|
+
const validValue = this.valueValidator.parse(val);
|
|
1370
|
+
result.set(validKey, validValue);
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
throw new Error(error.message);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return result;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Safely parse and validate a Map value
|
|
1380
|
+
*/
|
|
1381
|
+
safeParse(value) {
|
|
1382
|
+
try {
|
|
1383
|
+
return { success: true, data: this.parse(value) };
|
|
1384
|
+
}
|
|
1385
|
+
catch (error) {
|
|
1386
|
+
return { success: false, error: error };
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Type guard to check if a value is a plain object
|
|
1393
|
+
*/
|
|
1394
|
+
function isPlainObject(obj) {
|
|
1395
|
+
return (obj !== null &&
|
|
1396
|
+
typeof obj === 'object' &&
|
|
1397
|
+
obj.constructor === Object &&
|
|
1398
|
+
Object.prototype.toString.call(obj) === '[object Object]');
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Safely deep merge two objects without prototype pollution vulnerability
|
|
1402
|
+
* Optimized for performance with minimal object allocations
|
|
1403
|
+
* BUG-NEW-020 FIX: Use comprehensive dangerous key protection from shared utility
|
|
1404
|
+
* @param target The target object
|
|
1405
|
+
* @param source The source object to merge from
|
|
1406
|
+
* @returns A new merged object
|
|
1407
|
+
*/
|
|
1408
|
+
function deepMerge(target, source) {
|
|
1409
|
+
// Create a new object to avoid mutations (only once)
|
|
1410
|
+
const result = { ...target };
|
|
1411
|
+
// Get source keys directly for better performance
|
|
1412
|
+
const sourceKeys = Object.keys(source);
|
|
1413
|
+
for (let i = 0; i < sourceKeys.length; i++) {
|
|
1414
|
+
const key = sourceKeys[i];
|
|
1415
|
+
// Skip dangerous keys that could lead to prototype pollution
|
|
1416
|
+
if (isDangerousKey(key)) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
// Only process own properties (redundant with Object.keys but safe)
|
|
1420
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
const sourceValue = source[key];
|
|
1424
|
+
const targetValue = target[key];
|
|
1425
|
+
// If both values are plain objects, merge them recursively
|
|
1426
|
+
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
|
|
1427
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
// Otherwise, use the source value (including undefined)
|
|
1431
|
+
result[key] = sourceValue;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return result;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Immutable intersection validator for combining validators
|
|
1439
|
+
*/
|
|
1440
|
+
class VldIntersection extends VldBase {
|
|
1441
|
+
/**
|
|
1442
|
+
* Private constructor to enforce immutability
|
|
1443
|
+
*/
|
|
1444
|
+
constructor(validatorA, validatorB) {
|
|
1445
|
+
super();
|
|
1446
|
+
this.validatorA = validatorA;
|
|
1447
|
+
this.validatorB = validatorB;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Create a new intersection validator
|
|
1451
|
+
*/
|
|
1452
|
+
static create(validatorA, validatorB) {
|
|
1453
|
+
return new VldIntersection(validatorA, validatorB);
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Parse and validate a value against both validators
|
|
1457
|
+
*/
|
|
1458
|
+
parse(value) {
|
|
1459
|
+
try {
|
|
1460
|
+
// Both validators must pass
|
|
1461
|
+
const resultA = this.validatorA.parse(value);
|
|
1462
|
+
const resultB = this.validatorB.parse(value);
|
|
1463
|
+
// BUG-NEW-015 FIX: Check type consistency before merging
|
|
1464
|
+
const aIsObject = isPlainObject(resultA);
|
|
1465
|
+
const bIsObject = isPlainObject(resultB);
|
|
1466
|
+
// Both are objects - safe to merge
|
|
1467
|
+
if (aIsObject && bIsObject) {
|
|
1468
|
+
return deepMerge(resultA, resultB);
|
|
1469
|
+
}
|
|
1470
|
+
// Neither are objects - must be identical primitives
|
|
1471
|
+
if (!aIsObject && !bIsObject) {
|
|
1472
|
+
if (resultA === resultB) {
|
|
1473
|
+
return resultA;
|
|
1474
|
+
}
|
|
1475
|
+
throw new Error('Values must be identical for intersection of primitive types');
|
|
1476
|
+
}
|
|
1477
|
+
// One is object, one is primitive - invalid intersection
|
|
1478
|
+
throw new Error('Cannot create intersection of object and primitive types. ' +
|
|
1479
|
+
'Both validators must produce the same type category.');
|
|
1480
|
+
}
|
|
1481
|
+
catch (error) {
|
|
1482
|
+
throw new Error(getMessages().intersectionError(error.message));
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Safely parse and validate a value against both validators
|
|
1487
|
+
*/
|
|
1488
|
+
safeParse(value) {
|
|
1489
|
+
try {
|
|
1490
|
+
return { success: true, data: this.parse(value) };
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
return { success: false, error: error };
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Validator that accepts any value
|
|
1500
|
+
*/
|
|
1501
|
+
class VldAny extends VldBase {
|
|
1502
|
+
/**
|
|
1503
|
+
* Create a new any validator
|
|
1504
|
+
*/
|
|
1505
|
+
static create() {
|
|
1506
|
+
return new VldAny();
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Parse any value (always succeeds)
|
|
1510
|
+
*/
|
|
1511
|
+
parse(value) {
|
|
1512
|
+
return value;
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Safely parse any value (always succeeds)
|
|
1516
|
+
*/
|
|
1517
|
+
safeParse(value) {
|
|
1518
|
+
return { success: true, data: value };
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Validator that only accepts undefined
|
|
1524
|
+
*/
|
|
1525
|
+
class VldVoid extends VldBase {
|
|
1526
|
+
/**
|
|
1527
|
+
* Create a new void validator
|
|
1528
|
+
*/
|
|
1529
|
+
static create() {
|
|
1530
|
+
return new VldVoid();
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Parse void value (must be undefined)
|
|
1534
|
+
*/
|
|
1535
|
+
parse(value) {
|
|
1536
|
+
if (value !== undefined) {
|
|
1537
|
+
throw new Error(getMessages().expectedUndefined);
|
|
1538
|
+
}
|
|
1539
|
+
return undefined;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Safely parse void value
|
|
1543
|
+
*/
|
|
1544
|
+
safeParse(value) {
|
|
1545
|
+
if (value === undefined) {
|
|
1546
|
+
return { success: true, data: undefined };
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
success: false,
|
|
1550
|
+
error: new Error(getMessages().expectedUndefined)
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Validator that never succeeds
|
|
1557
|
+
*/
|
|
1558
|
+
class VldNever extends VldBase {
|
|
1559
|
+
/**
|
|
1560
|
+
* Create a new never validator
|
|
1561
|
+
*/
|
|
1562
|
+
static create() {
|
|
1563
|
+
return new VldNever();
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Parse never value (always fails)
|
|
1567
|
+
*/
|
|
1568
|
+
parse(_value) {
|
|
1569
|
+
throw new Error(getMessages().neverType);
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Safely parse never value (always fails)
|
|
1573
|
+
*/
|
|
1574
|
+
safeParse(_value) {
|
|
1575
|
+
return {
|
|
1576
|
+
success: false,
|
|
1577
|
+
error: new Error(getMessages().neverType)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* VldNull - Validates that a value is exactly null
|
|
1584
|
+
* Part of Zod 4 API parity implementation
|
|
1585
|
+
*/
|
|
1586
|
+
/**
|
|
1587
|
+
* Null validator - validates that a value is exactly null
|
|
1588
|
+
*/
|
|
1589
|
+
class VldNull extends VldBase {
|
|
1590
|
+
constructor() {
|
|
1591
|
+
super();
|
|
1592
|
+
}
|
|
1593
|
+
static create() {
|
|
1594
|
+
return new VldNull();
|
|
1595
|
+
}
|
|
1596
|
+
parse(value) {
|
|
1597
|
+
if (value !== null) {
|
|
1598
|
+
throw new Error(`Expected null, received ${typeof value}`);
|
|
1599
|
+
}
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
safeParse(value) {
|
|
1603
|
+
try {
|
|
1604
|
+
return { success: true, data: this.parse(value) };
|
|
1605
|
+
}
|
|
1606
|
+
catch (error) {
|
|
1607
|
+
return { success: false, error: error };
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* VldUndefined - Validates that a value is exactly undefined
|
|
1614
|
+
* Part of Zod 4 API parity implementation
|
|
1615
|
+
*/
|
|
1616
|
+
/**
|
|
1617
|
+
* Undefined validator - validates that a value is exactly undefined
|
|
1618
|
+
*/
|
|
1619
|
+
class VldUndefined extends VldBase {
|
|
1620
|
+
constructor() {
|
|
1621
|
+
super();
|
|
1622
|
+
}
|
|
1623
|
+
static create() {
|
|
1624
|
+
return new VldUndefined();
|
|
1625
|
+
}
|
|
1626
|
+
parse(value) {
|
|
1627
|
+
if (value !== undefined) {
|
|
1628
|
+
throw new Error(`Expected undefined, received ${typeof value}`);
|
|
1629
|
+
}
|
|
1630
|
+
return undefined;
|
|
1631
|
+
}
|
|
1632
|
+
safeParse(value) {
|
|
1633
|
+
try {
|
|
1634
|
+
return { success: true, data: this.parse(value) };
|
|
1635
|
+
}
|
|
1636
|
+
catch (error) {
|
|
1637
|
+
return { success: false, error: error };
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* VldNan - Validates that a value is NaN (Not a Number)
|
|
1644
|
+
* Part of Zod 4 API parity implementation
|
|
1645
|
+
*/
|
|
1646
|
+
/**
|
|
1647
|
+
* NaN validator - validates that a value is NaN
|
|
1648
|
+
* Note: Uses Number.isNaN() which is more strict than global isNaN()
|
|
1649
|
+
*/
|
|
1650
|
+
class VldNan extends VldBase {
|
|
1651
|
+
constructor() {
|
|
1652
|
+
super();
|
|
1653
|
+
}
|
|
1654
|
+
static create() {
|
|
1655
|
+
return new VldNan();
|
|
1656
|
+
}
|
|
1657
|
+
parse(value) {
|
|
1658
|
+
if (typeof value !== 'number' || !Number.isNaN(value)) {
|
|
1659
|
+
throw new Error(`Expected NaN, received ${typeof value === 'number' ? 'a valid number' : typeof value}`);
|
|
1660
|
+
}
|
|
1661
|
+
return value;
|
|
1662
|
+
}
|
|
1663
|
+
safeParse(value) {
|
|
1664
|
+
try {
|
|
1665
|
+
return { success: true, data: this.parse(value) };
|
|
1666
|
+
}
|
|
1667
|
+
catch (error) {
|
|
1668
|
+
return { success: false, error: error };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* VldLazy - Lazy evaluation for recursive schemas
|
|
1675
|
+
* Part of Zod 4 API parity implementation
|
|
1676
|
+
* Allows schemas that reference themselves (e.g., tree structures, nested categories)
|
|
1677
|
+
*/
|
|
1678
|
+
/**
|
|
1679
|
+
* Lazy validator - defers schema evaluation until runtime
|
|
1680
|
+
* Essential for recursive and self-referencing types
|
|
1681
|
+
*/
|
|
1682
|
+
class VldLazy extends VldBase {
|
|
1683
|
+
constructor(_schemaGetter) {
|
|
1684
|
+
super();
|
|
1685
|
+
this._schemaGetter = _schemaGetter;
|
|
1686
|
+
this._cachedSchema = null;
|
|
1687
|
+
}
|
|
1688
|
+
static create(schemaGetter) {
|
|
1689
|
+
return new VldLazy(schemaGetter);
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Get the actual schema, caching it after first retrieval
|
|
1693
|
+
*/
|
|
1694
|
+
_getSchema() {
|
|
1695
|
+
if (!this._cachedSchema) {
|
|
1696
|
+
this._cachedSchema = this._schemaGetter();
|
|
1697
|
+
}
|
|
1698
|
+
return this._cachedSchema;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Get the inner schema (unwrap)
|
|
1702
|
+
*/
|
|
1703
|
+
unwrap() {
|
|
1704
|
+
return this._getSchema();
|
|
1705
|
+
}
|
|
1706
|
+
parse(value) {
|
|
1707
|
+
return this._getSchema().parse(value);
|
|
1708
|
+
}
|
|
1709
|
+
safeParse(value) {
|
|
1710
|
+
return this._getSchema().safeParse(value);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* VldDiscriminatedUnion - Fast union validation using discriminator field
|
|
1716
|
+
* Part of Zod 4 API parity implementation
|
|
1717
|
+
* Provides O(1) lookup performance by using a discriminator key
|
|
1718
|
+
*/
|
|
1719
|
+
/**
|
|
1720
|
+
* Extract literal values from a schema
|
|
1721
|
+
*/
|
|
1722
|
+
function extractLiteralValues(schema) {
|
|
1723
|
+
if (schema instanceof VldLiteral) {
|
|
1724
|
+
const value = schema.literal;
|
|
1725
|
+
return [value];
|
|
1726
|
+
}
|
|
1727
|
+
if (schema instanceof VldEnum) {
|
|
1728
|
+
return schema.values;
|
|
1729
|
+
}
|
|
1730
|
+
throw new Error('Discriminator must be a literal or enum schema');
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Discriminated union validator - validates union based on discriminator key
|
|
1734
|
+
* Much faster than regular union when you have a discriminator field
|
|
1735
|
+
*/
|
|
1736
|
+
class VldDiscriminatedUnion extends VldBase {
|
|
1737
|
+
constructor(_discriminator, _options) {
|
|
1738
|
+
super();
|
|
1739
|
+
this._discriminator = _discriminator;
|
|
1740
|
+
this._options = _options;
|
|
1741
|
+
// Build discriminator map for O(1) lookup
|
|
1742
|
+
this._discriminatorMap = new Map();
|
|
1743
|
+
for (const option of _options) {
|
|
1744
|
+
if (!(option instanceof VldObject)) {
|
|
1745
|
+
throw new Error('All options in a discriminated union must be objects');
|
|
1746
|
+
}
|
|
1747
|
+
const discriminatorSchema = option.config?.shape?.[this._discriminator];
|
|
1748
|
+
if (!discriminatorSchema) {
|
|
1749
|
+
throw new Error(`Missing discriminator key "${this._discriminator}" in one of the options`);
|
|
1750
|
+
}
|
|
1751
|
+
const values = extractLiteralValues(discriminatorSchema);
|
|
1752
|
+
for (const value of values) {
|
|
1753
|
+
if (this._discriminatorMap.has(value)) {
|
|
1754
|
+
throw new Error(`Duplicate discriminator value "${String(value)}" found in discriminated union`);
|
|
1755
|
+
}
|
|
1756
|
+
this._discriminatorMap.set(value, option);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
static create(discriminator, options) {
|
|
1761
|
+
return new VldDiscriminatedUnion(discriminator, options);
|
|
1762
|
+
}
|
|
1763
|
+
parse(value) {
|
|
1764
|
+
const result = this.safeParse(value);
|
|
1765
|
+
if (!result.success) {
|
|
1766
|
+
throw result.error;
|
|
1767
|
+
}
|
|
1768
|
+
return result.data;
|
|
1769
|
+
}
|
|
1770
|
+
safeParse(value) {
|
|
1771
|
+
// Check if input is an object
|
|
1772
|
+
if (typeof value !== 'object' || value === null) {
|
|
1773
|
+
return {
|
|
1774
|
+
success: false,
|
|
1775
|
+
error: new Error(`Expected object, received ${value === null ? 'null' : typeof value}`)
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
// Get discriminator value
|
|
1779
|
+
const discriminatorValue = value[this._discriminator];
|
|
1780
|
+
// Look up matching schema
|
|
1781
|
+
const matchedSchema = this._discriminatorMap.get(discriminatorValue);
|
|
1782
|
+
if (!matchedSchema) {
|
|
1783
|
+
const validValues = Array.from(this._discriminatorMap.keys());
|
|
1784
|
+
return {
|
|
1785
|
+
success: false,
|
|
1786
|
+
error: new Error(`Invalid discriminator value for "${this._discriminator}". ` +
|
|
1787
|
+
`Expected one of: ${JSON.stringify(validValues)}, ` +
|
|
1788
|
+
`received: ${JSON.stringify(discriminatorValue)}`)
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
// Validate against matched schema
|
|
1792
|
+
return matchedSchema.safeParse(value);
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Get the discriminator key
|
|
1796
|
+
*/
|
|
1797
|
+
getDiscriminator() {
|
|
1798
|
+
return this._discriminator;
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Get all options
|
|
1802
|
+
*/
|
|
1803
|
+
getOptions() {
|
|
1804
|
+
return this._options;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* VldXor - Exclusive union validator
|
|
1810
|
+
* Part of Zod 4 API parity implementation
|
|
1811
|
+
* Ensures exactly one schema in the union matches
|
|
1812
|
+
*/
|
|
1813
|
+
/**
|
|
1814
|
+
* XOR validator - ensures exactly one option matches
|
|
1815
|
+
* Unlike regular union which allows multiple matches, XOR requires exactly one
|
|
1816
|
+
*/
|
|
1817
|
+
class VldXor extends VldBase {
|
|
1818
|
+
constructor(_options) {
|
|
1819
|
+
super();
|
|
1820
|
+
this._options = _options;
|
|
1821
|
+
}
|
|
1822
|
+
static create(options) {
|
|
1823
|
+
if (options.length < 2) {
|
|
1824
|
+
throw new Error('XOR requires at least 2 options');
|
|
1825
|
+
}
|
|
1826
|
+
return new VldXor(options);
|
|
1827
|
+
}
|
|
1828
|
+
parse(value) {
|
|
1829
|
+
const result = this.safeParse(value);
|
|
1830
|
+
if (!result.success) {
|
|
1831
|
+
throw result.error;
|
|
1832
|
+
}
|
|
1833
|
+
return result.data;
|
|
1834
|
+
}
|
|
1835
|
+
safeParse(value) {
|
|
1836
|
+
let matchCount = 0;
|
|
1837
|
+
let lastSuccess = null;
|
|
1838
|
+
for (const option of this._options) {
|
|
1839
|
+
const result = option.safeParse(value);
|
|
1840
|
+
if (result.success) {
|
|
1841
|
+
matchCount++;
|
|
1842
|
+
lastSuccess = result;
|
|
1843
|
+
if (matchCount > 1) {
|
|
1844
|
+
// Early exit if more than one match
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (matchCount === 0) {
|
|
1850
|
+
return {
|
|
1851
|
+
success: false,
|
|
1852
|
+
error: new Error('No schema matched in XOR union')
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
if (matchCount > 1) {
|
|
1856
|
+
return {
|
|
1857
|
+
success: false,
|
|
1858
|
+
error: new Error(`Input matches ${matchCount} schemas in XOR union, but exactly one is required`)
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
return lastSuccess;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Get all options
|
|
1865
|
+
*/
|
|
1866
|
+
getOptions() {
|
|
1867
|
+
return this._options;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Immutable JSON validator that validates and parses JSON strings
|
|
1873
|
+
* Accepts both strings (which are parsed) and already-parsed objects
|
|
1874
|
+
*/
|
|
1875
|
+
class VldJson extends VldBase {
|
|
1876
|
+
constructor(schema) {
|
|
1877
|
+
super();
|
|
1878
|
+
this.schema = schema;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Create a new JSON validator
|
|
1882
|
+
*/
|
|
1883
|
+
static create(schema) {
|
|
1884
|
+
return new VldJson(schema);
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Parse and validate a JSON value
|
|
1888
|
+
*/
|
|
1889
|
+
parse(value) {
|
|
1890
|
+
let parsed;
|
|
1891
|
+
// If it's a string, try to parse it as JSON first
|
|
1892
|
+
if (typeof value === 'string') {
|
|
1893
|
+
try {
|
|
1894
|
+
parsed = JSON.parse(value);
|
|
1895
|
+
}
|
|
1896
|
+
catch (e) {
|
|
1897
|
+
throw new Error(getMessages().invalidJson);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
// Not a string - assume it's already parsed
|
|
1902
|
+
parsed = value;
|
|
1903
|
+
}
|
|
1904
|
+
// If we have a schema, validate against it
|
|
1905
|
+
if (this.schema) {
|
|
1906
|
+
return this.schema.parse(parsed);
|
|
1907
|
+
}
|
|
1908
|
+
// No schema - just return the parsed value
|
|
1909
|
+
return parsed;
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Safely parse and validate a JSON value
|
|
1913
|
+
*/
|
|
1914
|
+
safeParse(value) {
|
|
1915
|
+
try {
|
|
1916
|
+
return { success: true, data: this.parse(value) };
|
|
1917
|
+
}
|
|
1918
|
+
catch (error) {
|
|
1919
|
+
return {
|
|
1920
|
+
success: false,
|
|
1921
|
+
error: error
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Create a new JSON validator with a schema for validation
|
|
1927
|
+
*/
|
|
1928
|
+
withSchema(schema) {
|
|
1929
|
+
return new VldJson(schema);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
export { VldSymbol as V, VldStringBool as a, VldArray as b, VldObject as c, VldTuple as d, VldRecord as e, VldSet as f, VldMap as g, VldUnion as h, VldIntersection as i, VldDiscriminatedUnion as j, VldXor as k, VldLiteral as l, VldEnum as m, VldAny as n, VldVoid as o, VldNever as p, VldNull as q, VldUndefined as r, VldNan as s, VldLazy as t, VldJson as u };
|
|
1934
|
+
//# sourceMappingURL=json-nIhyRPOU.js.map
|