bupkis 0.7.2 → 0.8.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 +7 -0
- package/dist/commonjs/assertion/assertion.d.ts +0 -13
- package/dist/commonjs/assertion/assertion.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion.js +42 -27
- package/dist/commonjs/assertion/assertion.js.map +1 -1
- package/dist/commonjs/assertion/create.d.ts +2 -0
- package/dist/commonjs/assertion/create.d.ts.map +1 -1
- package/dist/commonjs/assertion/create.js +7 -4
- package/dist/commonjs/assertion/create.js.map +1 -1
- package/dist/commonjs/assertion/impl/assertion-util.d.ts +2 -0
- package/dist/commonjs/assertion/impl/assertion-util.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/assertion-util.js +13 -2
- package/dist/commonjs/assertion/impl/assertion-util.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-basic.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-basic.js +2 -3
- package/dist/commonjs/assertion/impl/sync-basic.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-collection.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-collection.js +24 -14
- package/dist/commonjs/assertion/impl/sync-collection.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-esoteric.d.ts +1 -5
- package/dist/commonjs/assertion/impl/sync-esoteric.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-esoteric.js +11 -13
- package/dist/commonjs/assertion/impl/sync-esoteric.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync-parametric.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-parametric.js +4 -2
- package/dist/commonjs/assertion/impl/sync-parametric.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync.d.ts +2 -10
- package/dist/commonjs/assertion/impl/sync.d.ts.map +1 -1
- package/dist/commonjs/assertion/slotify.d.ts +1 -0
- package/dist/commonjs/assertion/slotify.d.ts.map +1 -1
- package/dist/commonjs/assertion/slotify.js +1 -0
- package/dist/commonjs/assertion/slotify.js.map +1 -1
- package/dist/commonjs/bootstrap.d.ts +1 -5
- package/dist/commonjs/bootstrap.d.ts.map +1 -1
- package/dist/commonjs/bootstrap.js +1 -0
- package/dist/commonjs/bootstrap.js.map +1 -1
- package/dist/commonjs/diff.d.ts +51 -0
- package/dist/commonjs/diff.d.ts.map +1 -0
- package/dist/commonjs/diff.js +279 -0
- package/dist/commonjs/diff.js.map +1 -0
- package/dist/commonjs/error.d.ts +17 -0
- package/dist/commonjs/error.d.ts.map +1 -1
- package/dist/commonjs/error.js +17 -7
- package/dist/commonjs/error.js.map +1 -1
- package/dist/commonjs/expect.d.ts.map +1 -1
- package/dist/commonjs/expect.js +13 -0
- package/dist/commonjs/expect.js.map +1 -1
- package/dist/commonjs/guards.d.ts +12 -8
- package/dist/commonjs/guards.d.ts.map +1 -1
- package/dist/commonjs/guards.js +24 -14
- package/dist/commonjs/guards.js.map +1 -1
- package/dist/commonjs/index.d.ts +1 -5
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/schema.d.ts.map +1 -1
- package/dist/commonjs/schema.js +3 -2
- package/dist/commonjs/schema.js.map +1 -1
- package/dist/commonjs/use.js +22 -8
- package/dist/commonjs/use.js.map +1 -1
- package/dist/commonjs/util.d.ts +1 -0
- package/dist/commonjs/util.d.ts.map +1 -1
- package/dist/commonjs/util.js +14 -10
- package/dist/commonjs/util.js.map +1 -1
- package/dist/commonjs/value-to-schema.d.ts +1 -0
- package/dist/commonjs/value-to-schema.d.ts.map +1 -1
- package/dist/commonjs/value-to-schema.js +17 -10
- package/dist/commonjs/value-to-schema.js.map +1 -1
- package/dist/esm/assertion/assertion.d.ts +0 -13
- package/dist/esm/assertion/assertion.d.ts.map +1 -1
- package/dist/esm/assertion/assertion.js +42 -27
- package/dist/esm/assertion/assertion.js.map +1 -1
- package/dist/esm/assertion/create.d.ts +2 -0
- package/dist/esm/assertion/create.d.ts.map +1 -1
- package/dist/esm/assertion/create.js +7 -4
- package/dist/esm/assertion/create.js.map +1 -1
- package/dist/esm/assertion/impl/assertion-util.d.ts +2 -0
- package/dist/esm/assertion/impl/assertion-util.d.ts.map +1 -1
- package/dist/esm/assertion/impl/assertion-util.js +13 -2
- package/dist/esm/assertion/impl/assertion-util.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 -3
- package/dist/esm/assertion/impl/sync-basic.js.map +1 -1
- package/dist/esm/assertion/impl/sync-collection.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync-collection.js +24 -14
- package/dist/esm/assertion/impl/sync-collection.js.map +1 -1
- package/dist/esm/assertion/impl/sync-esoteric.d.ts +1 -5
- package/dist/esm/assertion/impl/sync-esoteric.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync-esoteric.js +11 -13
- package/dist/esm/assertion/impl/sync-esoteric.js.map +1 -1
- package/dist/esm/assertion/impl/sync-parametric.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync-parametric.js +4 -2
- package/dist/esm/assertion/impl/sync-parametric.js.map +1 -1
- package/dist/esm/assertion/impl/sync.d.ts +2 -10
- package/dist/esm/assertion/impl/sync.d.ts.map +1 -1
- package/dist/esm/assertion/slotify.d.ts +1 -0
- package/dist/esm/assertion/slotify.d.ts.map +1 -1
- package/dist/esm/assertion/slotify.js +1 -0
- package/dist/esm/assertion/slotify.js.map +1 -1
- package/dist/esm/bootstrap.d.ts +1 -5
- package/dist/esm/bootstrap.d.ts.map +1 -1
- package/dist/esm/bootstrap.js +1 -0
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/diff.d.ts +51 -0
- package/dist/esm/diff.d.ts.map +1 -0
- package/dist/esm/diff.js +273 -0
- package/dist/esm/diff.js.map +1 -0
- package/dist/esm/error.d.ts +17 -0
- package/dist/esm/error.d.ts.map +1 -1
- package/dist/esm/error.js +17 -7
- package/dist/esm/error.js.map +1 -1
- package/dist/esm/expect.d.ts.map +1 -1
- package/dist/esm/expect.js +13 -0
- package/dist/esm/expect.js.map +1 -1
- package/dist/esm/guards.d.ts +12 -8
- package/dist/esm/guards.d.ts.map +1 -1
- package/dist/esm/guards.js +23 -12
- package/dist/esm/guards.js.map +1 -1
- package/dist/esm/index.d.ts +1 -5
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/schema.d.ts.map +1 -1
- package/dist/esm/schema.js +3 -2
- package/dist/esm/schema.js.map +1 -1
- package/dist/esm/use.js +19 -6
- package/dist/esm/use.js.map +1 -1
- package/dist/esm/util.d.ts +1 -0
- package/dist/esm/util.d.ts.map +1 -1
- package/dist/esm/util.js +14 -10
- package/dist/esm/util.js.map +1 -1
- package/dist/esm/value-to-schema.d.ts +1 -0
- package/dist/esm/value-to-schema.d.ts.map +1 -1
- package/dist/esm/value-to-schema.js +17 -10
- package/dist/esm/value-to-schema.js.map +1 -1
- package/package.json +22 -6
- package/src/assertion/assertion.ts +47 -28
- package/src/assertion/create.ts +8 -8
- package/src/assertion/impl/assertion-util.ts +27 -16
- package/src/assertion/impl/sync-basic.ts +3 -3
- package/src/assertion/impl/sync-collection.ts +34 -14
- package/src/assertion/impl/sync-esoteric.ts +17 -13
- package/src/assertion/impl/sync-parametric.ts +5 -2
- package/src/assertion/slotify.ts +1 -0
- package/src/bootstrap.ts +1 -0
- package/src/diff.ts +343 -0
- package/src/error.ts +35 -7
- package/src/expect.ts +13 -0
- package/src/guards.ts +27 -15
- package/src/schema.ts +4 -2
- package/src/use.ts +21 -7
- package/src/util.ts +15 -12
- package/src/value-to-schema.ts +18 -10
package/src/diff.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff utilities for generating rich assertion failure output.
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for extracting meaningful actual/expected
|
|
5
|
+
* values from Zod validation errors and generating high-quality diff output
|
|
6
|
+
* using `jest-diff` for assertion failures.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
* @see {@link https://npm.im/jest-diff | jest-diff}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DiffOptions } from 'jest-diff';
|
|
13
|
+
import type { z } from 'zod/v4';
|
|
14
|
+
|
|
15
|
+
import { diff } from 'jest-diff';
|
|
16
|
+
export type { DiffOptions };
|
|
17
|
+
|
|
18
|
+
const { isArray } = Array;
|
|
19
|
+
const { max } = Math;
|
|
20
|
+
const { stringify } = JSON;
|
|
21
|
+
const { entries } = Object;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Result of extracting diff values from a `ZodError`
|
|
25
|
+
*/
|
|
26
|
+
export interface DiffValues {
|
|
27
|
+
/** The actual value that was validated */
|
|
28
|
+
actual: unknown;
|
|
29
|
+
/** The expected value (corrected version) */
|
|
30
|
+
expected: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts actual and expected values from a `ZodError` for meaningful diffs.
|
|
35
|
+
*
|
|
36
|
+
* @function
|
|
37
|
+
* @param zodError The `ZodError` to extract values from
|
|
38
|
+
* @param subject The original subject that failed validation
|
|
39
|
+
* @returns Object with actual and expected values for diff
|
|
40
|
+
*/
|
|
41
|
+
export const extractDiffValues = (
|
|
42
|
+
zodError: z.ZodError,
|
|
43
|
+
subject: unknown,
|
|
44
|
+
): DiffValues => {
|
|
45
|
+
try {
|
|
46
|
+
// Start with the original subject as the actual value
|
|
47
|
+
const actual = subject;
|
|
48
|
+
|
|
49
|
+
// Create a copy to modify as the expected value
|
|
50
|
+
let expected = subject != null ? deepClone(subject) : subject;
|
|
51
|
+
|
|
52
|
+
// Process each issue to build the expected value
|
|
53
|
+
for (const issue of zodError.issues) {
|
|
54
|
+
try {
|
|
55
|
+
// Filter path to only string/number keys (symbols are rare in practice)
|
|
56
|
+
const filteredPath = issue.path.filter(
|
|
57
|
+
(key): key is number | string =>
|
|
58
|
+
typeof key === 'string' || typeof key === 'number',
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
switch (issue.code) {
|
|
62
|
+
case 'invalid_type': {
|
|
63
|
+
const correctedValue = createCorrectValueForType(
|
|
64
|
+
issue.expected,
|
|
65
|
+
getValueAtPath(actual, filteredPath),
|
|
66
|
+
);
|
|
67
|
+
expected = setValueAtPath(expected, filteredPath, correctedValue);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case 'invalid_value': {
|
|
72
|
+
// For literal/enum errors, use the first valid value
|
|
73
|
+
const correctedValue =
|
|
74
|
+
issue.values && issue.values.length > 0
|
|
75
|
+
? issue.values[0]
|
|
76
|
+
: '<valid value>';
|
|
77
|
+
expected = setValueAtPath(expected, filteredPath, correctedValue);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'too_big': {
|
|
82
|
+
const actualValue = getValueAtPath(actual, filteredPath);
|
|
83
|
+
let correctedValue;
|
|
84
|
+
|
|
85
|
+
if (issue.origin === 'string' && typeof actualValue === 'string') {
|
|
86
|
+
correctedValue = actualValue.slice(0, Number(issue.maximum));
|
|
87
|
+
} else if (issue.origin === 'array' && isArray(actualValue)) {
|
|
88
|
+
correctedValue = actualValue.slice(0, Number(issue.maximum));
|
|
89
|
+
} else {
|
|
90
|
+
correctedValue = issue.maximum;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
expected = setValueAtPath(expected, filteredPath, correctedValue);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'too_small': {
|
|
98
|
+
const actualValue = getValueAtPath(actual, filteredPath);
|
|
99
|
+
let correctedValue;
|
|
100
|
+
|
|
101
|
+
if (issue.origin === 'string' && typeof actualValue === 'string') {
|
|
102
|
+
const needed = Number(issue.minimum) - actualValue.length;
|
|
103
|
+
correctedValue = actualValue + 'x'.repeat(max(0, needed));
|
|
104
|
+
} else if (issue.origin === 'array' && isArray(actualValue)) {
|
|
105
|
+
const needed = Number(issue.minimum) - actualValue.length;
|
|
106
|
+
correctedValue = actualValue.concat(
|
|
107
|
+
new Array(max(0, needed)).fill(null),
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
correctedValue = issue.minimum;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
expected = setValueAtPath(expected, filteredPath, correctedValue);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case 'unrecognized_keys': {
|
|
118
|
+
// Remove unrecognized keys from expected
|
|
119
|
+
if (
|
|
120
|
+
filteredPath.length === 0 &&
|
|
121
|
+
typeof expected === 'object' &&
|
|
122
|
+
expected !== null
|
|
123
|
+
) {
|
|
124
|
+
const expectedCopy = {
|
|
125
|
+
...(expected as Record<string, unknown>),
|
|
126
|
+
};
|
|
127
|
+
for (const key of issue.keys) {
|
|
128
|
+
delete expectedCopy[key];
|
|
129
|
+
}
|
|
130
|
+
expected = expectedCopy;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
default: {
|
|
136
|
+
// For other error types, just mark that something should be different
|
|
137
|
+
// but don't try to guess what the correct value should be
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// If we can't process an individual issue, continue with others
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { actual, expected };
|
|
148
|
+
} catch {
|
|
149
|
+
// If anything goes wrong, return undefined to fall back to pretty error
|
|
150
|
+
return { actual: undefined, expected: undefined };
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generates a rich diff string using jest-diff for assertion failures.
|
|
156
|
+
*
|
|
157
|
+
* @function
|
|
158
|
+
* @param expected The expected value
|
|
159
|
+
* @param actual The actual value
|
|
160
|
+
* @param options Configuration options for diff generation
|
|
161
|
+
* @returns A formatted diff string, or null if values are identical
|
|
162
|
+
*/
|
|
163
|
+
export const generateDiff = (
|
|
164
|
+
expected: unknown,
|
|
165
|
+
actual: unknown,
|
|
166
|
+
options: DiffOptions = {},
|
|
167
|
+
): null | string => {
|
|
168
|
+
if (!shouldGenerateDiff(actual, expected)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return diff(expected, actual, {
|
|
173
|
+
aAnnotation: 'expected',
|
|
174
|
+
bAnnotation: 'actual',
|
|
175
|
+
expand: false,
|
|
176
|
+
includeChangeCounts: true,
|
|
177
|
+
...options,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Checks if two values should generate a meaningful diff.
|
|
183
|
+
*
|
|
184
|
+
* @function
|
|
185
|
+
* @param actual The actual value
|
|
186
|
+
* @param expected The expected value
|
|
187
|
+
* @returns True if a diff should be generated, false otherwise
|
|
188
|
+
*/
|
|
189
|
+
export const shouldGenerateDiff = (
|
|
190
|
+
actual: unknown,
|
|
191
|
+
expected: unknown,
|
|
192
|
+
): boolean =>
|
|
193
|
+
actual !== undefined && expected !== undefined && actual !== expected;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Creates a corrected value for a given type
|
|
197
|
+
*
|
|
198
|
+
* @function
|
|
199
|
+
*/
|
|
200
|
+
const createCorrectValueForType = (
|
|
201
|
+
expectedType: string,
|
|
202
|
+
actualValue: unknown,
|
|
203
|
+
): unknown => {
|
|
204
|
+
switch (expectedType) {
|
|
205
|
+
case 'array':
|
|
206
|
+
return isArray(actualValue) ? actualValue : [];
|
|
207
|
+
case 'boolean':
|
|
208
|
+
return typeof actualValue === 'boolean'
|
|
209
|
+
? actualValue
|
|
210
|
+
: Boolean(actualValue);
|
|
211
|
+
case 'null':
|
|
212
|
+
return null;
|
|
213
|
+
case 'number':
|
|
214
|
+
return typeof actualValue === 'number'
|
|
215
|
+
? actualValue
|
|
216
|
+
: Number(actualValue) || 0;
|
|
217
|
+
case 'object':
|
|
218
|
+
return typeof actualValue === 'object' && actualValue !== null
|
|
219
|
+
? actualValue
|
|
220
|
+
: {};
|
|
221
|
+
case 'string':
|
|
222
|
+
if (typeof actualValue === 'string') {
|
|
223
|
+
return actualValue;
|
|
224
|
+
}
|
|
225
|
+
if (actualValue == null) {
|
|
226
|
+
return '';
|
|
227
|
+
}
|
|
228
|
+
if (typeof actualValue === 'object') {
|
|
229
|
+
return stringify(actualValue);
|
|
230
|
+
}
|
|
231
|
+
// For primitive types (number, boolean, etc.) - we know it's not an object here
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
233
|
+
return `${actualValue}`;
|
|
234
|
+
case 'undefined':
|
|
235
|
+
return undefined;
|
|
236
|
+
default:
|
|
237
|
+
return `<${expectedType}>`;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Custom deep clone implementation for values that structuredClone can't
|
|
243
|
+
* handle. Used as fallback when structuredClone fails.
|
|
244
|
+
*
|
|
245
|
+
* @function
|
|
246
|
+
*/
|
|
247
|
+
const customDeepClone = (value: unknown): unknown => {
|
|
248
|
+
if (value === null || typeof value !== 'object') {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
if (isArray(value)) {
|
|
252
|
+
return value.map(customDeepClone);
|
|
253
|
+
}
|
|
254
|
+
// For objects, create a new object and copy properties
|
|
255
|
+
const cloned: Record<string, unknown> = {};
|
|
256
|
+
for (const [key, val] of entries(value)) {
|
|
257
|
+
cloned[key] = customDeepClone(val);
|
|
258
|
+
}
|
|
259
|
+
return cloned;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Deep clone a value to avoid mutating the original. Uses the native
|
|
264
|
+
* structuredClone API which handles more types and circular references better
|
|
265
|
+
* than a custom implementation. Falls back to custom implementation for values
|
|
266
|
+
* containing functions.
|
|
267
|
+
*
|
|
268
|
+
* @function
|
|
269
|
+
*/
|
|
270
|
+
const deepClone = (value: unknown): unknown => {
|
|
271
|
+
try {
|
|
272
|
+
return structuredClone(value);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// Handle DataCloneError for values that can't be cloned (functions, symbols, etc.)
|
|
275
|
+
if (error instanceof Error && error.name === 'DataCloneError') {
|
|
276
|
+
return customDeepClone(value);
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Gets a value at a specific path in an object
|
|
284
|
+
*
|
|
285
|
+
* @function
|
|
286
|
+
*/
|
|
287
|
+
const getValueAtPath = (
|
|
288
|
+
obj: unknown,
|
|
289
|
+
path: readonly (number | string)[],
|
|
290
|
+
): unknown => {
|
|
291
|
+
if (path.length === 0) {
|
|
292
|
+
return obj;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let current = obj;
|
|
296
|
+
for (const key of path) {
|
|
297
|
+
if (current == null) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
current = (current as Record<number | string, unknown>)[key];
|
|
301
|
+
}
|
|
302
|
+
return current;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Returns a new object with the value at the specified path set to the given
|
|
307
|
+
* value.
|
|
308
|
+
*
|
|
309
|
+
* @function
|
|
310
|
+
*/
|
|
311
|
+
const setValueAtPath = (
|
|
312
|
+
obj: unknown,
|
|
313
|
+
path: readonly (number | string)[],
|
|
314
|
+
value: unknown,
|
|
315
|
+
): unknown => {
|
|
316
|
+
if (path.length === 0) {
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (obj == null) {
|
|
321
|
+
// Need to create the structure
|
|
322
|
+
obj = typeof path[0] === 'number' ? [] : {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const result = isArray(obj)
|
|
326
|
+
? [...(obj as unknown[])]
|
|
327
|
+
: { ...(obj as Record<string, unknown>) };
|
|
328
|
+
const [head, ...tail] = path;
|
|
329
|
+
|
|
330
|
+
if (head !== undefined) {
|
|
331
|
+
if (tail.length === 0) {
|
|
332
|
+
(result as Record<number | string, unknown>)[head] = value;
|
|
333
|
+
} else {
|
|
334
|
+
(result as Record<number | string, unknown>)[head] = setValueAtPath(
|
|
335
|
+
(result as Record<number | string, unknown>)[head],
|
|
336
|
+
tail,
|
|
337
|
+
value,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return result;
|
|
343
|
+
};
|
package/src/error.ts
CHANGED
|
@@ -16,6 +16,23 @@ import {
|
|
|
16
16
|
} from './constant.js';
|
|
17
17
|
import { isA } from './guards.js';
|
|
18
18
|
|
|
19
|
+
const { hasOwn } = Object;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for {@link AssertionError}'s constructor
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Based on Node.js' {@link NodeAssertionError}'s constructor options, sans
|
|
26
|
+
* `operator`.
|
|
27
|
+
* @group Error Options
|
|
28
|
+
*/
|
|
29
|
+
export type AssertionErrorOptions = Partial<
|
|
30
|
+
Omit<
|
|
31
|
+
NonNullable<ConstructorParameters<typeof NodeAssertionError>[0]>,
|
|
32
|
+
'operator'
|
|
33
|
+
>
|
|
34
|
+
>;
|
|
35
|
+
|
|
19
36
|
/**
|
|
20
37
|
* Options for {@link AssertionImplementationError}
|
|
21
38
|
*
|
|
@@ -80,6 +97,10 @@ export class AssertionError extends NodeAssertionError {
|
|
|
80
97
|
|
|
81
98
|
override name = 'AssertionError';
|
|
82
99
|
|
|
100
|
+
constructor(options?: AssertionErrorOptions) {
|
|
101
|
+
super(options);
|
|
102
|
+
}
|
|
103
|
+
|
|
83
104
|
/**
|
|
84
105
|
* Type guard for an instance of this error.
|
|
85
106
|
*
|
|
@@ -88,10 +109,19 @@ export class AssertionError extends NodeAssertionError {
|
|
|
88
109
|
*/
|
|
89
110
|
static isAssertionError(value: unknown): value is AssertionError {
|
|
90
111
|
return (
|
|
91
|
-
isA(value, NodeAssertionError) &&
|
|
92
|
-
Object.hasOwn(value, kBupkisAssertionError)
|
|
112
|
+
isA(value, NodeAssertionError) && hasOwn(value, kBupkisAssertionError)
|
|
93
113
|
);
|
|
94
114
|
}
|
|
115
|
+
|
|
116
|
+
toJSON() {
|
|
117
|
+
return {
|
|
118
|
+
actual: this.actual,
|
|
119
|
+
expected: this.expected,
|
|
120
|
+
message: this.message,
|
|
121
|
+
name: this.name,
|
|
122
|
+
stack: this.stack,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
95
125
|
}
|
|
96
126
|
|
|
97
127
|
/**
|
|
@@ -108,7 +138,7 @@ export class BupkisError extends Error {
|
|
|
108
138
|
override name = 'BupkisError';
|
|
109
139
|
|
|
110
140
|
static isBupkisError(err: unknown): err is BupkisError {
|
|
111
|
-
return isA(err, Error) &&
|
|
141
|
+
return isA(err, Error) && hasOwn(err, kBupkisError);
|
|
112
142
|
}
|
|
113
143
|
}
|
|
114
144
|
|
|
@@ -151,8 +181,7 @@ export class FailAssertionError extends AssertionError {
|
|
|
151
181
|
|
|
152
182
|
static isFailAssertionError(err: unknown): err is FailAssertionError {
|
|
153
183
|
return (
|
|
154
|
-
isA(err, FailAssertionError) &&
|
|
155
|
-
Object.hasOwn(err, kBupkisFailAssertionError)
|
|
184
|
+
isA(err, FailAssertionError) && hasOwn(err, kBupkisFailAssertionError)
|
|
156
185
|
);
|
|
157
186
|
}
|
|
158
187
|
}
|
|
@@ -211,8 +240,7 @@ export class NegatedAssertionError extends AssertionError {
|
|
|
211
240
|
|
|
212
241
|
static isNegatedAssertionError(err: unknown): err is NegatedAssertionError {
|
|
213
242
|
return (
|
|
214
|
-
isA(err, AssertionError) &&
|
|
215
|
-
Object.hasOwn(err, kBupkisNegatedAssertionError)
|
|
243
|
+
isA(err, AssertionError) && hasOwn(err, kBupkisNegatedAssertionError)
|
|
216
244
|
);
|
|
217
245
|
}
|
|
218
246
|
}
|
package/src/expect.ts
CHANGED
|
@@ -181,6 +181,9 @@ export function createExpectAsyncFunction<
|
|
|
181
181
|
expect?.assertions.length ?? 0,
|
|
182
182
|
assertions.length + (expect?.assertions.length ?? 0),
|
|
183
183
|
);
|
|
184
|
+
/**
|
|
185
|
+
* @function
|
|
186
|
+
*/
|
|
184
187
|
const expectAsyncFunction = async (...args: readonly unknown[]) => {
|
|
185
188
|
await Promise.resolve();
|
|
186
189
|
const [isNegated, processedArgs] = maybeProcessNegation(args);
|
|
@@ -359,6 +362,9 @@ export function createExpectSyncFunction<
|
|
|
359
362
|
expect?.assertions.length ?? 0,
|
|
360
363
|
assertions.length + (expect?.assertions.length ?? 0),
|
|
361
364
|
);
|
|
365
|
+
/**
|
|
366
|
+
* @function
|
|
367
|
+
*/
|
|
362
368
|
const expectFunction = (...args: readonly unknown[]) => {
|
|
363
369
|
const [isNegated, processedArgs] = maybeProcessNegation(args);
|
|
364
370
|
const candidates: Array<{
|
|
@@ -406,6 +412,7 @@ export function createExpectSyncFunction<
|
|
|
406
412
|
* @privateRemarks
|
|
407
413
|
* This is here because `Assertion` doesn't know anything about negation and
|
|
408
414
|
* probably shouldn't.
|
|
415
|
+
* @function
|
|
409
416
|
* @param assertion - The assertion to execute
|
|
410
417
|
* @param parsedValues - Parsed values for the assertion
|
|
411
418
|
* @param args - Original arguments passed to expect
|
|
@@ -474,6 +481,7 @@ const execute = <
|
|
|
474
481
|
* @privateRemarks
|
|
475
482
|
* This is here because `Assertion` doesn't know anything about negation and
|
|
476
483
|
* probably shouldn't.
|
|
484
|
+
* @function
|
|
477
485
|
* @param assertion - The assertion to execute
|
|
478
486
|
* @param parsedValues - Parsed values for the assertion
|
|
479
487
|
* @param args - Original arguments passed to expectAsync
|
|
@@ -533,6 +541,7 @@ const executeAsync = async <
|
|
|
533
541
|
* requested along with arguments stripped of the leading negation (to enable
|
|
534
542
|
* assertion matching).
|
|
535
543
|
*
|
|
544
|
+
* @function
|
|
536
545
|
* @internal
|
|
537
546
|
*/
|
|
538
547
|
const maybeProcessNegation = (
|
|
@@ -555,6 +564,7 @@ const maybeProcessNegation = (
|
|
|
555
564
|
* Throws an error indicating that no valid assertion could be found for the
|
|
556
565
|
* provided arguments.
|
|
557
566
|
*
|
|
567
|
+
* @function
|
|
558
568
|
* @param args The arguments that were passed to the expect function
|
|
559
569
|
* @internal
|
|
560
570
|
*/
|
|
@@ -571,6 +581,7 @@ const throwInvalidParametersError = (args: readonly unknown[]): never => {
|
|
|
571
581
|
* Detects if an assertion phrase starts with "not " and returns the cleaned
|
|
572
582
|
* phrase.
|
|
573
583
|
*
|
|
584
|
+
* @function
|
|
574
585
|
* @param phrase - The assertion phrase to check
|
|
575
586
|
* @returns Object with `isNegated` flag and `cleanedPhrase`
|
|
576
587
|
*/
|
|
@@ -595,6 +606,8 @@ const detectNegation = (
|
|
|
595
606
|
|
|
596
607
|
/**
|
|
597
608
|
* {@inheritdoc FailFn}
|
|
609
|
+
*
|
|
610
|
+
* @function
|
|
598
611
|
*/
|
|
599
612
|
const fail: FailFn = (reason?: string): never => {
|
|
600
613
|
throw new FailAssertionError({ message: reason });
|
package/src/guards.ts
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { type Primitive } from 'type-fest';
|
|
20
19
|
import { z } from 'zod/v4';
|
|
21
20
|
|
|
22
21
|
import type {
|
|
@@ -28,6 +27,8 @@ import type { Constructor, ExpectItExecutor, ZodTypeMap } from './types.js';
|
|
|
28
27
|
|
|
29
28
|
import { kExpectIt } from './constant.js';
|
|
30
29
|
|
|
30
|
+
const { isArray } = Array;
|
|
31
|
+
|
|
31
32
|
/**
|
|
32
33
|
* Returns `true` if the given value looks like a Zod v4 schema, determined by
|
|
33
34
|
* the presence of an internal {@link z.core.$ZodTypeDef} field.
|
|
@@ -43,6 +44,7 @@ export function isZodType<T extends keyof ZodTypeMap>(
|
|
|
43
44
|
value: unknown,
|
|
44
45
|
type: T,
|
|
45
46
|
): value is ZodTypeMap[T];
|
|
47
|
+
|
|
46
48
|
/**
|
|
47
49
|
* Returns `true` if the given value looks like a Zod v4 schema, determined by
|
|
48
50
|
* the presence of an internal {@link z.core.$ZodTypeDef} field.
|
|
@@ -65,8 +67,12 @@ export function isZodType<T extends keyof ZodTypeMap>(
|
|
|
65
67
|
typeof value.def === 'object' &&
|
|
66
68
|
'type' in value.def;
|
|
67
69
|
|
|
68
|
-
if (!isValid)
|
|
69
|
-
|
|
70
|
+
if (!isValid) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (type === undefined) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
return (value as z.ZodType).def.type === type;
|
|
72
78
|
}
|
|
@@ -74,16 +80,18 @@ export function isZodType<T extends keyof ZodTypeMap>(
|
|
|
74
80
|
/**
|
|
75
81
|
* Type guard for a plain object.
|
|
76
82
|
*
|
|
83
|
+
* @function
|
|
77
84
|
* @param value Value to test
|
|
78
85
|
* @returns `true` if the value is a plain object, `false` otherwise
|
|
79
86
|
*/
|
|
80
87
|
export const isObject = (value: unknown): value is NonNullable<object> => {
|
|
81
|
-
return typeof value === 'object' && value !== null && !
|
|
88
|
+
return typeof value === 'object' && value !== null && !isArray(value);
|
|
82
89
|
};
|
|
83
90
|
|
|
84
91
|
/**
|
|
85
92
|
* Returns `true` if the given value is a {@link z.ZodPromise} schema.
|
|
86
93
|
*
|
|
94
|
+
* @function
|
|
87
95
|
* @param value - Value to test
|
|
88
96
|
* @returns `true` if the value is a `ZodPromise` schema; `false` otherwise
|
|
89
97
|
*/
|
|
@@ -93,6 +101,7 @@ export const isZodPromise = (value: unknown): value is z.ZodPromise =>
|
|
|
93
101
|
/**
|
|
94
102
|
* Checks if a value is "promise-like", meaning it is a "thenable" object.
|
|
95
103
|
*
|
|
104
|
+
* @function
|
|
96
105
|
* @param value - Value to test
|
|
97
106
|
* @returns `true` if the value is promise-like, `false` otherwise
|
|
98
107
|
*/
|
|
@@ -114,6 +123,7 @@ export const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
|
|
|
114
123
|
* This may be the only way we can determine, at runtime, if a function is a
|
|
115
124
|
* constructor without actually calling it. I am unsure if this only works for
|
|
116
125
|
* classes.
|
|
126
|
+
* @function
|
|
117
127
|
* @param fn - Function to test
|
|
118
128
|
* @returns Whether the function is constructable
|
|
119
129
|
*/
|
|
@@ -123,7 +133,7 @@ export const isConstructible = (fn: unknown): fn is Constructor => {
|
|
|
123
133
|
}
|
|
124
134
|
try {
|
|
125
135
|
// this will throw if there is no `[[construct]]` slot.. or so I've heard.
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, custom/require-function-tag-in-arrow-functions
|
|
127
137
|
new new Proxy(fn as any, { construct: () => ({}) })();
|
|
128
138
|
return true;
|
|
129
139
|
} catch {
|
|
@@ -134,6 +144,7 @@ export const isConstructible = (fn: unknown): fn is Constructor => {
|
|
|
134
144
|
/**
|
|
135
145
|
* Type guard for a boolean value
|
|
136
146
|
*
|
|
147
|
+
* @function
|
|
137
148
|
* @param value Value to check
|
|
138
149
|
* @returns `true` if the value is a boolean, `false` otherwise
|
|
139
150
|
*/
|
|
@@ -143,6 +154,7 @@ export const isBoolean = (value: unknown): value is boolean =>
|
|
|
143
154
|
/**
|
|
144
155
|
* Type guard for a function value
|
|
145
156
|
*
|
|
157
|
+
* @function
|
|
146
158
|
* @param value Value to check
|
|
147
159
|
* @returns `true` if the value is a function, `false` otherwise
|
|
148
160
|
*/
|
|
@@ -169,6 +181,7 @@ const AssertionFailureSchema: z.ZodType<AssertionFailure> = z.object({
|
|
|
169
181
|
/**
|
|
170
182
|
* Type guard for a {@link AssertionFailure} object
|
|
171
183
|
*
|
|
184
|
+
* @function
|
|
172
185
|
* @param value Value to check
|
|
173
186
|
* @returns `true` if the value is an `AssertionFailure`, `false` otherwise
|
|
174
187
|
* @internal
|
|
@@ -179,6 +192,7 @@ export const isAssertionFailure = (value: unknown): value is AssertionFailure =>
|
|
|
179
192
|
/**
|
|
180
193
|
* Type guard for a string value
|
|
181
194
|
*
|
|
195
|
+
* @function
|
|
182
196
|
* @param value Value to check
|
|
183
197
|
* @returns `true` if the value is a string, `false` otherwise
|
|
184
198
|
*/
|
|
@@ -188,27 +202,20 @@ export const isString = (value: unknown): value is string =>
|
|
|
188
202
|
/**
|
|
189
203
|
* Type guard for a non-null object value
|
|
190
204
|
*
|
|
205
|
+
* @function
|
|
191
206
|
* @param value Value to check
|
|
192
207
|
* @returns `true` if the value is an object and not null, `false` otherwise
|
|
193
208
|
*/
|
|
194
209
|
export const isNonNullObject = (value: unknown): value is object =>
|
|
195
210
|
typeof value === 'object' && value !== null;
|
|
196
211
|
|
|
197
|
-
/**
|
|
198
|
-
* Type guard for a null or non-object value
|
|
199
|
-
*
|
|
200
|
-
* @param value Value to check
|
|
201
|
-
* @returns `true` if the value is null or not an object, `false` otherwise
|
|
202
|
-
*/
|
|
203
|
-
export const isNullOrNonObject = (value: unknown): value is null | Primitive =>
|
|
204
|
-
typeof value !== 'object' || value === null;
|
|
205
|
-
|
|
206
212
|
/**
|
|
207
213
|
* Type guard for a valid WeakKey (object, function, or symbol).
|
|
208
214
|
*
|
|
209
215
|
* WeakMaps and WeakSets can only use objects (including functions) or symbols
|
|
210
216
|
* as keys, not primitives like strings, numbers, booleans, null, or undefined.
|
|
211
217
|
*
|
|
218
|
+
* @function
|
|
212
219
|
* @param value Value to check
|
|
213
220
|
* @returns `true` if the value is a valid WeakKey (object, function, or
|
|
214
221
|
* symbol), `false` otherwise
|
|
@@ -221,6 +228,7 @@ export const isWeakKey = (value: unknown): value is WeakKey =>
|
|
|
221
228
|
/**
|
|
222
229
|
* Type guard for a {@link PhraseLiteralChoice}, which is a tuple of strings.
|
|
223
230
|
*
|
|
231
|
+
* @function
|
|
224
232
|
* @param value Assertion part to check
|
|
225
233
|
* @returns `true` if the part is a `PhraseLiteralChoice`, `false` otherwise
|
|
226
234
|
* @internal
|
|
@@ -228,12 +236,13 @@ export const isWeakKey = (value: unknown): value is WeakKey =>
|
|
|
228
236
|
export const isPhraseLiteralChoice = (
|
|
229
237
|
value: AssertionPart,
|
|
230
238
|
): value is PhraseLiteralChoice =>
|
|
231
|
-
|
|
239
|
+
isArray(value) && value.every(isPhraseLiteral);
|
|
232
240
|
|
|
233
241
|
/**
|
|
234
242
|
* Type guard for a {@link PhraseLiteral}, which is just a string that does not
|
|
235
243
|
* begin with `not `.
|
|
236
244
|
*
|
|
245
|
+
* @function
|
|
237
246
|
* @param value Assertion part to check
|
|
238
247
|
* @returns `true` if the part is a `PhraseLiteral`, `false` otherwise
|
|
239
248
|
* @internal
|
|
@@ -260,6 +269,7 @@ export const isPhraseLiteral = (value: AssertionPart): value is string =>
|
|
|
260
269
|
* ```
|
|
261
270
|
*
|
|
262
271
|
* @template T - The constructor type to check against
|
|
272
|
+
* @function
|
|
263
273
|
* @param value - Value to test
|
|
264
274
|
* @param ctor - Constructor function to check instanceof
|
|
265
275
|
* @returns `true` if the value is an instance of the constructor, `false`
|
|
@@ -292,6 +302,7 @@ export const isA = <T extends Constructor>(
|
|
|
292
302
|
* }
|
|
293
303
|
* ```
|
|
294
304
|
*
|
|
305
|
+
* @function
|
|
295
306
|
* @param value - Value to test
|
|
296
307
|
* @returns `true` if the value is an Error instance, `false` otherwise
|
|
297
308
|
*/
|
|
@@ -318,6 +329,7 @@ export const isError = (value: unknown): value is Error => isA(value, Error);
|
|
|
318
329
|
* ```
|
|
319
330
|
*
|
|
320
331
|
* @template Subject - The subject type that the executor function operates on
|
|
332
|
+
* @function
|
|
321
333
|
* @param value - Value to test
|
|
322
334
|
* @returns `true` if the value is an ExpectItExecutor function, `false`
|
|
323
335
|
* otherwise
|
package/src/schema.ts
CHANGED
|
@@ -50,6 +50,8 @@ import {
|
|
|
50
50
|
type MutableOrReadonly,
|
|
51
51
|
} from './types.js';
|
|
52
52
|
|
|
53
|
+
const { getPrototypeOf, prototype: objectPrototype } = Object;
|
|
54
|
+
|
|
53
55
|
/**
|
|
54
56
|
* A Zod schema that validates JavaScript constructible functions.
|
|
55
57
|
*
|
|
@@ -386,7 +388,7 @@ export const WrappedPromiseLikeSchema = z
|
|
|
386
388
|
*/
|
|
387
389
|
export const DictionarySchema = z
|
|
388
390
|
.custom<Record<PropertyKey, unknown>>(
|
|
389
|
-
(value) => isNonNullObject(value) &&
|
|
391
|
+
(value) => isNonNullObject(value) && getPrototypeOf(value) === null,
|
|
390
392
|
)
|
|
391
393
|
.describe('Object with null prototype')
|
|
392
394
|
.register(BupkisRegistry, { name: 'dictionary' });
|
|
@@ -450,7 +452,7 @@ export const NullProtoObjectSchema = DictionarySchema;
|
|
|
450
452
|
* @group Schema
|
|
451
453
|
*/
|
|
452
454
|
export const AsyncFunctionSchema = FunctionSchema.refine(
|
|
453
|
-
(value) =>
|
|
455
|
+
(value) => objectPrototype.toString.call(value) === '[object AsyncFunction]',
|
|
454
456
|
)
|
|
455
457
|
.describe('Function declared with the `async` keyword')
|
|
456
458
|
.register(BupkisRegistry, { name: 'async-function' });
|