bupkis 0.2.0 → 0.4.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.
- package/CHANGELOG.md +27 -0
- package/README.md +35 -11
- package/dist/commonjs/assertion/assertion-async.d.ts +2 -1
- package/dist/commonjs/assertion/assertion-async.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion-async.js +84 -2
- package/dist/commonjs/assertion/assertion-async.js.map +1 -1
- package/dist/commonjs/assertion/assertion-sync.d.ts +1 -1
- package/dist/commonjs/assertion/assertion-sync.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion-sync.js +5 -1
- package/dist/commonjs/assertion/assertion-sync.js.map +1 -1
- package/dist/commonjs/assertion/assertion-types.d.ts +6 -2
- package/dist/commonjs/assertion/assertion-types.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion.d.ts +1 -1
- package/dist/commonjs/assertion/assertion.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion.js +1 -14
- package/dist/commonjs/assertion/assertion.js.map +1 -1
- package/dist/commonjs/assertion/impl/async.d.ts +122 -21
- package/dist/commonjs/assertion/impl/async.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/async.js +118 -90
- package/dist/commonjs/assertion/impl/async.js.map +1 -1
- package/dist/commonjs/assertion/impl/callback.d.ts +104 -0
- package/dist/commonjs/assertion/impl/callback.d.ts.map +1 -0
- package/dist/commonjs/assertion/impl/callback.js +694 -0
- package/dist/commonjs/assertion/impl/callback.js.map +1 -0
- package/dist/commonjs/assertion/impl/index.d.ts +1 -1
- package/dist/commonjs/assertion/impl/index.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/index.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-basic.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-basic.js +1 -1
- package/dist/commonjs/assertion/impl/sync-basic.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-collection.d.ts +1 -1
- package/dist/commonjs/assertion/impl/sync-collection.js +3 -3
- package/dist/commonjs/assertion/impl/sync-collection.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-esoteric.js +1 -1
- package/dist/commonjs/assertion/impl/sync-esoteric.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-parametric.d.ts +22 -28
- package/dist/commonjs/assertion/impl/sync-parametric.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-parametric.js +35 -50
- package/dist/commonjs/assertion/impl/sync-parametric.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync.d.ts +68 -30
- package/dist/commonjs/assertion/impl/sync.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync.js +4 -1
- package/dist/commonjs/assertion/impl/sync.js.map +1 -1
- package/dist/commonjs/bootstrap.d.ts +147 -52
- package/dist/commonjs/bootstrap.d.ts.map +1 -1
- package/dist/commonjs/bootstrap.js +2 -3
- package/dist/commonjs/bootstrap.js.map +1 -1
- package/dist/commonjs/constant.d.ts +1 -1
- package/dist/commonjs/constant.d.ts.map +1 -1
- package/dist/commonjs/constant.js +8 -1
- package/dist/commonjs/constant.js.map +1 -1
- package/dist/commonjs/error.d.ts +22 -2
- package/dist/commonjs/error.d.ts.map +1 -1
- package/dist/commonjs/error.js +44 -4
- package/dist/commonjs/error.js.map +1 -1
- package/dist/commonjs/expect.d.ts.map +1 -1
- package/dist/commonjs/expect.js +1 -1
- package/dist/commonjs/expect.js.map +1 -1
- package/dist/commonjs/guards.d.ts +96 -5
- package/dist/commonjs/guards.d.ts.map +1 -1
- package/dist/commonjs/guards.js +104 -25
- package/dist/commonjs/guards.js.map +1 -1
- package/dist/commonjs/index.d.ts +146 -51
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/schema.d.ts +84 -18
- package/dist/commonjs/schema.d.ts.map +1 -1
- package/dist/commonjs/schema.js +107 -22
- package/dist/commonjs/schema.js.map +1 -1
- package/dist/commonjs/types.d.ts +171 -9
- package/dist/commonjs/types.d.ts.map +1 -1
- package/dist/commonjs/use.d.ts.map +1 -1
- package/dist/commonjs/use.js +15 -1
- package/dist/commonjs/use.js.map +1 -1
- package/dist/commonjs/util.d.ts +66 -50
- package/dist/commonjs/util.d.ts.map +1 -1
- package/dist/commonjs/util.js +169 -156
- package/dist/commonjs/util.js.map +1 -1
- package/dist/commonjs/value-to-schema.d.ts +122 -0
- package/dist/commonjs/value-to-schema.d.ts.map +1 -0
- package/dist/commonjs/value-to-schema.js +329 -0
- package/dist/commonjs/value-to-schema.js.map +1 -0
- package/dist/esm/assertion/assertion-async.d.ts +2 -1
- package/dist/esm/assertion/assertion-async.d.ts.map +1 -1
- package/dist/esm/assertion/assertion-async.js +85 -3
- package/dist/esm/assertion/assertion-async.js.map +1 -1
- package/dist/esm/assertion/assertion-sync.d.ts +1 -1
- package/dist/esm/assertion/assertion-sync.d.ts.map +1 -1
- package/dist/esm/assertion/assertion-sync.js +6 -2
- package/dist/esm/assertion/assertion-sync.js.map +1 -1
- package/dist/esm/assertion/assertion-types.d.ts +6 -2
- package/dist/esm/assertion/assertion-types.d.ts.map +1 -1
- package/dist/esm/assertion/assertion.d.ts +1 -1
- package/dist/esm/assertion/assertion.d.ts.map +1 -1
- package/dist/esm/assertion/assertion.js +1 -14
- package/dist/esm/assertion/assertion.js.map +1 -1
- package/dist/esm/assertion/impl/async.d.ts +122 -21
- package/dist/esm/assertion/impl/async.d.ts.map +1 -1
- package/dist/esm/assertion/impl/async.js +118 -90
- package/dist/esm/assertion/impl/async.js.map +1 -1
- package/dist/esm/assertion/impl/callback.d.ts +104 -0
- package/dist/esm/assertion/impl/callback.d.ts.map +1 -0
- package/dist/esm/assertion/impl/callback.js +691 -0
- package/dist/esm/assertion/impl/callback.js.map +1 -0
- package/dist/esm/assertion/impl/index.d.ts +1 -1
- package/dist/esm/assertion/impl/index.d.ts.map +1 -1
- package/dist/esm/assertion/impl/index.js +1 -1
- package/dist/esm/assertion/impl/index.js.map +1 -1
- package/dist/esm/assertion/impl/sync-basic.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync-basic.js +2 -2
- package/dist/esm/assertion/impl/sync-basic.js.map +1 -1
- package/dist/esm/assertion/impl/sync-collection.d.ts +1 -1
- package/dist/esm/assertion/impl/sync-collection.js +3 -3
- package/dist/esm/assertion/impl/sync-collection.js.map +1 -1
- package/dist/esm/assertion/impl/sync-esoteric.js +2 -2
- package/dist/esm/assertion/impl/sync-esoteric.js.map +1 -1
- package/dist/esm/assertion/impl/sync-parametric.d.ts +22 -28
- package/dist/esm/assertion/impl/sync-parametric.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync-parametric.js +36 -51
- package/dist/esm/assertion/impl/sync-parametric.js.map +1 -1
- package/dist/esm/assertion/impl/sync.d.ts +68 -30
- package/dist/esm/assertion/impl/sync.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync.js +3 -1
- package/dist/esm/assertion/impl/sync.js.map +1 -1
- package/dist/esm/bootstrap.d.ts +147 -52
- package/dist/esm/bootstrap.d.ts.map +1 -1
- package/dist/esm/bootstrap.js +1 -2
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/constant.d.ts +1 -1
- package/dist/esm/constant.d.ts.map +1 -1
- package/dist/esm/constant.js +7 -0
- package/dist/esm/constant.js.map +1 -1
- package/dist/esm/error.d.ts +22 -2
- package/dist/esm/error.d.ts.map +1 -1
- package/dist/esm/error.js +43 -4
- package/dist/esm/error.js.map +1 -1
- package/dist/esm/expect.d.ts.map +1 -1
- package/dist/esm/expect.js +2 -2
- package/dist/esm/expect.js.map +1 -1
- package/dist/esm/guards.d.ts +96 -5
- package/dist/esm/guards.d.ts.map +1 -1
- package/dist/esm/guards.js +98 -21
- package/dist/esm/guards.js.map +1 -1
- package/dist/esm/index.d.ts +146 -51
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/schema.d.ts +84 -18
- package/dist/esm/schema.d.ts.map +1 -1
- package/dist/esm/schema.js +107 -22
- package/dist/esm/schema.js.map +1 -1
- package/dist/esm/types.d.ts +171 -9
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/use.d.ts.map +1 -1
- package/dist/esm/use.js +15 -1
- package/dist/esm/use.js.map +1 -1
- package/dist/esm/util.d.ts +66 -50
- package/dist/esm/util.d.ts.map +1 -1
- package/dist/esm/util.js +153 -154
- package/dist/esm/util.js.map +1 -1
- package/dist/esm/value-to-schema.d.ts +122 -0
- package/dist/esm/value-to-schema.d.ts.map +1 -0
- package/dist/esm/value-to-schema.js +325 -0
- package/dist/esm/value-to-schema.js.map +1 -0
- package/package.json +16 -13
- package/src/assertion/assertion-async.ts +113 -3
- package/src/assertion/assertion-sync.ts +5 -2
- package/src/assertion/assertion-types.ts +14 -4
- package/src/assertion/assertion.ts +2 -17
- package/src/assertion/impl/async.ts +137 -93
- package/src/assertion/impl/callback.ts +882 -0
- package/src/assertion/impl/index.ts +1 -1
- package/src/assertion/impl/sync-basic.ts +5 -2
- package/src/assertion/impl/sync-collection.ts +3 -3
- package/src/assertion/impl/sync-esoteric.ts +2 -2
- package/src/assertion/impl/sync-parametric.ts +47 -54
- package/src/assertion/impl/sync.ts +3 -0
- package/src/bootstrap.ts +1 -2
- package/src/constant.ts +10 -0
- package/src/error.ts +57 -3
- package/src/expect.ts +6 -2
- package/src/guards.ts +125 -18
- package/src/index.ts +3 -0
- package/src/schema.ts +121 -23
- package/src/types.ts +205 -10
- package/src/use.ts +22 -0
- package/src/util.ts +168 -223
- package/src/value-to-schema.ts +489 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isExpectItExecutor,
|
|
5
|
+
isNonNullObject,
|
|
6
|
+
isObject,
|
|
7
|
+
isPromiseLike,
|
|
8
|
+
isString,
|
|
9
|
+
isZodType,
|
|
10
|
+
} from './guards.js';
|
|
11
|
+
import {
|
|
12
|
+
RegExpSchema,
|
|
13
|
+
StrongMapSchema,
|
|
14
|
+
StrongSetSchema,
|
|
15
|
+
WrappedPromiseLikeSchema,
|
|
16
|
+
} from './schema.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recursively converts an arbitrary value to a Zod v4 schema that would
|
|
20
|
+
* validate values with the same structure.
|
|
21
|
+
*
|
|
22
|
+
* This function analyzes the runtime value and generates a corresponding Zod
|
|
23
|
+
* schema that captures the value's structure and type information. It handles
|
|
24
|
+
* primitives, objects, arrays, functions, and various built-in types, with
|
|
25
|
+
* support for circular reference detection.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
*
|
|
29
|
+
* ```typescript
|
|
30
|
+
* // Primitive types
|
|
31
|
+
* valueToSchema('hello'); // z.string()
|
|
32
|
+
* valueToSchema(42); // z.number()
|
|
33
|
+
* valueToSchema(true); // z.boolean()
|
|
34
|
+
*
|
|
35
|
+
* // Objects
|
|
36
|
+
* valueToSchema({ name: 'John', age: 30 });
|
|
37
|
+
* // z.object({ name: z.string(), age: z.number() })
|
|
38
|
+
*
|
|
39
|
+
* // Arrays
|
|
40
|
+
* valueToSchema(['a', 'b', 'c']); // z.array(z.string())
|
|
41
|
+
* valueToSchema([1, 'mixed']); // z.array(z.union([z.number(), z.string()]))
|
|
42
|
+
*
|
|
43
|
+
* // Nested structures
|
|
44
|
+
* valueToSchema({ users: [{ name: 'John' }] });
|
|
45
|
+
* // z.object({ users: z.array(z.object({ name: z.string() })) })
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @param value - The value to convert to a schema
|
|
49
|
+
* @param options - Configuration options for schema generation
|
|
50
|
+
* @param visited - Internal WeakSet for circular reference detection
|
|
51
|
+
* @returns A Zod schema that validates values matching the input's structure.
|
|
52
|
+
* This value is unfortunately untyped due to the complexity involved. But the
|
|
53
|
+
* schema works!
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
export const valueToSchema = (
|
|
57
|
+
value: unknown,
|
|
58
|
+
options: ValueToSchemaOptions = {},
|
|
59
|
+
visited = new WeakSet<object>(),
|
|
60
|
+
): z.ZodType<any> => {
|
|
61
|
+
const {
|
|
62
|
+
_currentDepth = 0,
|
|
63
|
+
literalEmptyObjects = false,
|
|
64
|
+
literalPrimitives = false,
|
|
65
|
+
literalRegExp = false,
|
|
66
|
+
literalTuples = false,
|
|
67
|
+
maxDepth = 10,
|
|
68
|
+
noMixedArrays = false,
|
|
69
|
+
strict = false,
|
|
70
|
+
} = options;
|
|
71
|
+
|
|
72
|
+
// Prevent infinite recursion
|
|
73
|
+
if (_currentDepth >= maxDepth) {
|
|
74
|
+
return z.unknown();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle primitives
|
|
78
|
+
if (value === null) {
|
|
79
|
+
return z.null();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (value === undefined) {
|
|
83
|
+
return literalPrimitives
|
|
84
|
+
? z.custom<undefined>((val: unknown) => val === undefined, {
|
|
85
|
+
message: 'Expected undefined',
|
|
86
|
+
})
|
|
87
|
+
: z.undefined();
|
|
88
|
+
}
|
|
89
|
+
if (Number.isNaN(value as number)) {
|
|
90
|
+
return z.nan();
|
|
91
|
+
}
|
|
92
|
+
if (value === Infinity || value === -Infinity) {
|
|
93
|
+
return z.literal(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const valueType = typeof value;
|
|
97
|
+
|
|
98
|
+
switch (valueType) {
|
|
99
|
+
case 'bigint':
|
|
100
|
+
return literalPrimitives ? z.literal(value as bigint) : z.bigint();
|
|
101
|
+
case 'boolean':
|
|
102
|
+
return literalPrimitives ? z.literal(value as boolean) : z.boolean();
|
|
103
|
+
case 'function':
|
|
104
|
+
// Check if this is an ExpectItExecutor
|
|
105
|
+
if (isExpectItExecutor(value)) {
|
|
106
|
+
// Only allow nested assertions when strict is false (e.g., "to satisfy" semantics)
|
|
107
|
+
if (strict) {
|
|
108
|
+
throw new TypeError(
|
|
109
|
+
'ExpectItExecutor (expect.it) functions are not allowed in strict mode. ' +
|
|
110
|
+
'Use "to satisfy" assertions for nested expectations.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
// Return a schema that executes the ExpectItExecutor when validated
|
|
114
|
+
return z.custom<unknown>(
|
|
115
|
+
(subject: unknown) => {
|
|
116
|
+
try {
|
|
117
|
+
value(subject);
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
message: 'Failed expect.it assertion',
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return z.function();
|
|
129
|
+
case 'number':
|
|
130
|
+
return literalPrimitives ? z.literal(value as number) : z.number();
|
|
131
|
+
case 'string':
|
|
132
|
+
return literalPrimitives ? z.literal(value as string) : z.string();
|
|
133
|
+
case 'symbol':
|
|
134
|
+
return z.symbol();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle objects
|
|
138
|
+
if (typeof value === 'object' && value !== null) {
|
|
139
|
+
// Check for circular references
|
|
140
|
+
if (visited.has(value)) {
|
|
141
|
+
// Return a recursive schema reference or unknown for circular refs
|
|
142
|
+
return z.unknown();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
visited.add(value);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Check for objects with own __proto__ property - these can cause unexpected behavior
|
|
149
|
+
if (Object.hasOwn(value, '__proto__')) {
|
|
150
|
+
throw new TypeError(
|
|
151
|
+
'Objects with an own "__proto__" property are not supported by valueToSchema',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle built-in object types
|
|
156
|
+
if (value instanceof Date) {
|
|
157
|
+
// Check if it's a valid date
|
|
158
|
+
if (isNaN(value.getTime())) {
|
|
159
|
+
// For invalid dates, use a literal or custom validator
|
|
160
|
+
return z.custom<Date>(
|
|
161
|
+
(val) => val instanceof Date && isNaN(val.getTime()),
|
|
162
|
+
{
|
|
163
|
+
message: 'Expected an invalid Date',
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return z.date();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (value instanceof RegExp) {
|
|
171
|
+
if (literalRegExp) {
|
|
172
|
+
return RegExpSchema;
|
|
173
|
+
}
|
|
174
|
+
return z.coerce.string().regex(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (value instanceof Map) {
|
|
178
|
+
return StrongMapSchema;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (value instanceof Set) {
|
|
182
|
+
return StrongSetSchema;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (value instanceof WeakMap) {
|
|
186
|
+
return z.instanceof(WeakMap);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (value instanceof WeakSet) {
|
|
190
|
+
return z.instanceof(WeakSet);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (value instanceof Error) {
|
|
194
|
+
return z.instanceof(Error);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isPromiseLike(value)) {
|
|
198
|
+
return WrappedPromiseLikeSchema;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Handle arrays
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
// For arrays, we need to preserve undefined values while allowing
|
|
204
|
+
// other elements to use the original literalPrimitives setting
|
|
205
|
+
const filteredValue = value; // Always process all elements
|
|
206
|
+
|
|
207
|
+
if (filteredValue.length === 0) {
|
|
208
|
+
// For empty arrays, use z.tuple() if literalTuples is enabled
|
|
209
|
+
if (literalTuples) {
|
|
210
|
+
return z.tuple([]);
|
|
211
|
+
}
|
|
212
|
+
return z.array(z.never());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const elementSchemas = filteredValue.map((item) => {
|
|
216
|
+
// Use literal mode for undefined values to preserve them exactly,
|
|
217
|
+
// but use the original setting for other values
|
|
218
|
+
const itemLiteralPrimitives =
|
|
219
|
+
item === undefined ? true : literalPrimitives;
|
|
220
|
+
|
|
221
|
+
return valueToSchema(
|
|
222
|
+
item,
|
|
223
|
+
{
|
|
224
|
+
...options,
|
|
225
|
+
_currentDepth: _currentDepth + 1,
|
|
226
|
+
literalPrimitives: itemLiteralPrimitives,
|
|
227
|
+
},
|
|
228
|
+
visited,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Use z.tuple() if literalTuples is enabled
|
|
233
|
+
if (literalTuples) {
|
|
234
|
+
return z.tuple(elementSchemas as [z.ZodType, ...z.ZodType[]]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!noMixedArrays) {
|
|
238
|
+
// Helper function to generate structural keys for schemas
|
|
239
|
+
const getSchemaKey = <T extends z.core.SomeType | z.ZodType>(
|
|
240
|
+
zodType: T,
|
|
241
|
+
): string => {
|
|
242
|
+
const schema = zodType as z.ZodType;
|
|
243
|
+
if (isZodType(schema, 'literal')) {
|
|
244
|
+
return `${schema.constructor.name}:${String(schema.def.values)}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (isZodType(schema, 'array')) {
|
|
248
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
249
|
+
const elementKey = getSchemaKey((schema.def as any).element);
|
|
250
|
+
return `ZodArray<${elementKey}>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (isZodType(schema, 'object')) {
|
|
254
|
+
// For objects, create a key based on the property keys and their types
|
|
255
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
256
|
+
const shape = (schema.def as any).shape as Record<
|
|
257
|
+
string,
|
|
258
|
+
z.ZodType
|
|
259
|
+
>;
|
|
260
|
+
const shapeKeys = Object.keys(shape)
|
|
261
|
+
.sort()
|
|
262
|
+
.map((key) => {
|
|
263
|
+
const propSchema = shape[key]!;
|
|
264
|
+
return `${key}:${getSchemaKey(propSchema)}`;
|
|
265
|
+
});
|
|
266
|
+
return `ZodObject<{${shapeKeys.join(',')}}>`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (isZodType(schema, 'union')) {
|
|
270
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
271
|
+
const optionKeys = ((schema.def as any).options as z.ZodType[])
|
|
272
|
+
.map((option) => getSchemaKey(option))
|
|
273
|
+
.sort();
|
|
274
|
+
return `ZodUnion<[${optionKeys.join(',')}]>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// For other types, use the constructor name
|
|
278
|
+
return schema.constructor.name;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const seenSchemaKeys = new Set<string>();
|
|
282
|
+
const uniqueSchemas: z.ZodType[] = [];
|
|
283
|
+
|
|
284
|
+
for (const schema of elementSchemas) {
|
|
285
|
+
const schemaKey = getSchemaKey(schema);
|
|
286
|
+
|
|
287
|
+
if (!seenSchemaKeys.has(schemaKey)) {
|
|
288
|
+
seenSchemaKeys.add(schemaKey);
|
|
289
|
+
uniqueSchemas.push(schema);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (uniqueSchemas.length === 1) {
|
|
294
|
+
return z.array(uniqueSchemas[0]!);
|
|
295
|
+
} else {
|
|
296
|
+
return z.array(
|
|
297
|
+
z.union(uniqueSchemas as [z.ZodType, z.ZodType, ...z.ZodType[]]),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
// Use the first element's schema for all elements
|
|
302
|
+
return z.array(elementSchemas[0]!);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Handle plain objects
|
|
307
|
+
if (isNonNullObject(value)) {
|
|
308
|
+
const schemaShape: Record<string, z.ZodType<any>> = {};
|
|
309
|
+
const undefinedKeys: string[] = [];
|
|
310
|
+
|
|
311
|
+
for (const [key, val] of Object.entries(value)) {
|
|
312
|
+
if (isString(key)) {
|
|
313
|
+
// Skip undefined values unless we're in literalPrimitives mode
|
|
314
|
+
// This prevents objects with only undefined values from matching any object
|
|
315
|
+
if (val === undefined && !literalPrimitives) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (val === undefined && literalPrimitives) {
|
|
320
|
+
// Track keys that should have undefined values
|
|
321
|
+
undefinedKeys.push(key);
|
|
322
|
+
schemaShape[key] = z.undefined();
|
|
323
|
+
} else {
|
|
324
|
+
schemaShape[key] = valueToSchema(
|
|
325
|
+
val,
|
|
326
|
+
{
|
|
327
|
+
...options,
|
|
328
|
+
_currentDepth: _currentDepth + 1,
|
|
329
|
+
},
|
|
330
|
+
visited,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create the base object schema
|
|
337
|
+
const baseSchema = strict
|
|
338
|
+
? z.strictObject(schemaShape)
|
|
339
|
+
: z.looseObject(schemaShape);
|
|
340
|
+
|
|
341
|
+
// If we have undefined keys in literalPrimitives mode, add validation to ensure they exist
|
|
342
|
+
if (undefinedKeys.length > 0 && literalPrimitives) {
|
|
343
|
+
return baseSchema.superRefine((data, ctx) => {
|
|
344
|
+
if (typeof data !== 'object' || data === null) {
|
|
345
|
+
ctx.addIssue({
|
|
346
|
+
code: z.ZodIssueCode.custom,
|
|
347
|
+
message: 'Expected an object',
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const obj = data as Record<string, unknown>;
|
|
353
|
+
for (const key of undefinedKeys) {
|
|
354
|
+
if (!Object.hasOwn(obj, key)) {
|
|
355
|
+
ctx.addIssue({
|
|
356
|
+
code: z.ZodIssueCode.custom,
|
|
357
|
+
message: `Expected property "${key}" to exist with value undefined`,
|
|
358
|
+
path: [key],
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check if this is an empty object and literalEmptyObjects is enabled
|
|
366
|
+
if (Object.keys(schemaShape).length === 0 && literalEmptyObjects) {
|
|
367
|
+
// Create a schema that only matches empty objects
|
|
368
|
+
return z.custom<Record<string, never>>(
|
|
369
|
+
(val) => isObject(val) && Object.keys(val).length === 0,
|
|
370
|
+
{
|
|
371
|
+
message: 'Expected an empty object with no own properties',
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return baseSchema;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Handle other object types (ArrayBuffer, etc.)
|
|
380
|
+
return z.custom<object>(
|
|
381
|
+
(val) => typeof val === 'object' && val !== null,
|
|
382
|
+
{ message: 'Expected an object' },
|
|
383
|
+
);
|
|
384
|
+
} finally {
|
|
385
|
+
visited.delete(value);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Fallback for unknown types
|
|
390
|
+
return z.unknown();
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Options for {@link valueToSchema}
|
|
395
|
+
*/
|
|
396
|
+
export interface ValueToSchemaOptions {
|
|
397
|
+
/**
|
|
398
|
+
* Current depth (internal)
|
|
399
|
+
*
|
|
400
|
+
* @internal
|
|
401
|
+
*/
|
|
402
|
+
_currentDepth?: number;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* If `true`, treat empty objects `{}` as literal empty objects that only
|
|
406
|
+
* match objects with zero own properties
|
|
407
|
+
*
|
|
408
|
+
* @defaultValue false
|
|
409
|
+
*/
|
|
410
|
+
literalEmptyObjects?: boolean;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* If `true`, use literal schema for primitive values instead of type schema
|
|
414
|
+
*
|
|
415
|
+
* @defaultValue false
|
|
416
|
+
*/
|
|
417
|
+
literalPrimitives?: boolean;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* If `true`, treat `RegExp` literals as `RegExp` literals; otherwise treat as
|
|
421
|
+
* strings and attempt match
|
|
422
|
+
*
|
|
423
|
+
* @defaultValue false
|
|
424
|
+
*/
|
|
425
|
+
literalRegExp?: boolean;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* If `true`, treat arrays as tuples wherever possible.
|
|
429
|
+
*
|
|
430
|
+
* Implies `false` for {@link noMixedArrays}.
|
|
431
|
+
*
|
|
432
|
+
* @defaultValue false
|
|
433
|
+
*/
|
|
434
|
+
literalTuples?: boolean;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Maximum nesting depth to prevent stack overflow
|
|
438
|
+
*
|
|
439
|
+
* @defaultValue 10
|
|
440
|
+
*/
|
|
441
|
+
maxDepth?: number;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Whether to allow mixed types in arrays
|
|
445
|
+
*
|
|
446
|
+
* If {@link literalTuples} is `true`, this option is ignored and treated as
|
|
447
|
+
* `false`.
|
|
448
|
+
*
|
|
449
|
+
* @defaultValue false
|
|
450
|
+
*/
|
|
451
|
+
noMixedArrays?: boolean;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* If `true`, will disallow unknown properties in parsed objects
|
|
455
|
+
*
|
|
456
|
+
* @defaultValue false
|
|
457
|
+
*/
|
|
458
|
+
strict?: boolean;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Predefined options for {@link valueToSchema} optimized for object satisfaction
|
|
463
|
+
* checks.
|
|
464
|
+
*
|
|
465
|
+
* Uses literal primitives and tuples for exact matching while allowing extra
|
|
466
|
+
* properties.
|
|
467
|
+
*/
|
|
468
|
+
export const valueToSchemaOptionsForSatisfies = Object.freeze({
|
|
469
|
+
literalEmptyObjects: true,
|
|
470
|
+
literalPrimitives: true,
|
|
471
|
+
literalRegExp: false,
|
|
472
|
+
literalTuples: true,
|
|
473
|
+
strict: false,
|
|
474
|
+
} as const) satisfies ValueToSchemaOptions;
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Predefined options for {@link valueToSchema} optimized for deep equality
|
|
478
|
+
* checks.
|
|
479
|
+
*
|
|
480
|
+
* Uses literal primitives, regexp, and tuples with strict validation for exact
|
|
481
|
+
* matching.
|
|
482
|
+
*/
|
|
483
|
+
export const valueToSchemaOptionsForDeepEqual = Object.freeze({
|
|
484
|
+
literalEmptyObjects: true,
|
|
485
|
+
literalPrimitives: true,
|
|
486
|
+
literalRegExp: true,
|
|
487
|
+
literalTuples: true,
|
|
488
|
+
strict: true,
|
|
489
|
+
} as const) satisfies ValueToSchemaOptions;
|