bupkis 0.7.2 → 0.9.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 +15 -0
- package/README.md +19 -2
- package/dist/commonjs/assertion/assertion-async.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion-async.js +37 -7
- package/dist/commonjs/assertion/assertion-async.js.map +1 -1
- package/dist/commonjs/assertion/assertion-sync.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion-sync.js +32 -8
- package/dist/commonjs/assertion/assertion-sync.js.map +1 -1
- package/dist/commonjs/assertion/assertion-types.d.ts +37 -31
- package/dist/commonjs/assertion/assertion-types.d.ts.map +1 -1
- package/dist/commonjs/assertion/assertion-types.js +0 -32
- package/dist/commonjs/assertion/assertion-types.js.map +1 -1
- package/dist/commonjs/assertion/assertion.d.ts +3 -21
- 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 +38 -42
- package/dist/commonjs/assertion/create.js.map +1 -1
- package/dist/commonjs/assertion/impl/assertion-util.d.ts +16 -4
- package/dist/commonjs/assertion/impl/assertion-util.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/assertion-util.js +20 -15
- package/dist/commonjs/assertion/impl/assertion-util.js.map +1 -1
- package/dist/commonjs/assertion/impl/async-parametric.d.ts +63 -11
- package/dist/commonjs/assertion/impl/async-parametric.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/async-parametric.js +89 -52
- package/dist/commonjs/assertion/impl/async-parametric.js.map +1 -1
- package/dist/commonjs/assertion/impl/async.d.ts +116 -12
- package/dist/commonjs/assertion/impl/async.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/async.js +1 -1
- package/dist/commonjs/assertion/impl/sync-basic.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-basic.js +6 -4
- 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 +27 -7
- package/dist/commonjs/assertion/impl/sync-parametric.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync-parametric.js +75 -51
- package/dist/commonjs/assertion/impl/sync-parametric.js.map +1 -1
- package/dist/commonjs/assertion/impl/sync.d.ts +54 -22
- package/dist/commonjs/assertion/impl/sync.d.ts.map +1 -1
- package/dist/commonjs/assertion/impl/sync.js +1 -1
- package/dist/commonjs/assertion/impl/sync.js.map +1 -1
- package/dist/commonjs/assertion/index.d.ts +1 -1
- package/dist/commonjs/assertion/index.d.ts.map +1 -1
- package/dist/commonjs/assertion/index.js +0 -1
- package/dist/commonjs/assertion/index.js.map +1 -1
- package/dist/commonjs/assertion/slotify.d.ts +1 -13
- package/dist/commonjs/assertion/slotify.d.ts.map +1 -1
- package/dist/commonjs/assertion/slotify.js +49 -16
- package/dist/commonjs/assertion/slotify.js.map +1 -1
- package/dist/commonjs/bootstrap.d.ts +85 -17
- 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 +37 -18
- package/dist/commonjs/error.d.ts.map +1 -1
- package/dist/commonjs/error.js +44 -30
- package/dist/commonjs/error.js.map +1 -1
- package/dist/commonjs/expect.d.ts.map +1 -1
- package/dist/commonjs/expect.js +131 -78
- package/dist/commonjs/expect.js.map +1 -1
- package/dist/commonjs/guards.d.ts +24 -10
- package/dist/commonjs/guards.d.ts.map +1 -1
- package/dist/commonjs/guards.js +56 -39
- package/dist/commonjs/guards.js.map +1 -1
- package/dist/commonjs/index.d.ts +85 -17
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/internal-schema.d.ts +25 -0
- package/dist/commonjs/internal-schema.d.ts.map +1 -0
- package/dist/commonjs/internal-schema.js +209 -0
- package/dist/commonjs/internal-schema.js.map +1 -0
- 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 +19 -12
- package/dist/commonjs/value-to-schema.js.map +1 -1
- package/dist/esm/assertion/assertion-async.d.ts.map +1 -1
- package/dist/esm/assertion/assertion-async.js +37 -7
- package/dist/esm/assertion/assertion-async.js.map +1 -1
- package/dist/esm/assertion/assertion-sync.d.ts.map +1 -1
- package/dist/esm/assertion/assertion-sync.js +32 -8
- package/dist/esm/assertion/assertion-sync.js.map +1 -1
- package/dist/esm/assertion/assertion-types.d.ts +37 -31
- package/dist/esm/assertion/assertion-types.d.ts.map +1 -1
- package/dist/esm/assertion/assertion-types.js +1 -31
- package/dist/esm/assertion/assertion-types.js.map +1 -1
- package/dist/esm/assertion/assertion.d.ts +3 -21
- 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 +37 -41
- package/dist/esm/assertion/create.js.map +1 -1
- package/dist/esm/assertion/impl/assertion-util.d.ts +16 -4
- package/dist/esm/assertion/impl/assertion-util.d.ts.map +1 -1
- package/dist/esm/assertion/impl/assertion-util.js +20 -15
- package/dist/esm/assertion/impl/assertion-util.js.map +1 -1
- package/dist/esm/assertion/impl/async-parametric.d.ts +63 -11
- package/dist/esm/assertion/impl/async-parametric.d.ts.map +1 -1
- package/dist/esm/assertion/impl/async-parametric.js +89 -52
- package/dist/esm/assertion/impl/async-parametric.js.map +1 -1
- package/dist/esm/assertion/impl/async.d.ts +116 -12
- package/dist/esm/assertion/impl/async.d.ts.map +1 -1
- package/dist/esm/assertion/impl/async.js +2 -2
- package/dist/esm/assertion/impl/async.js.map +1 -1
- package/dist/esm/assertion/impl/sync-basic.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync-basic.js +6 -4
- 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 +27 -7
- package/dist/esm/assertion/impl/sync-parametric.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync-parametric.js +75 -51
- package/dist/esm/assertion/impl/sync-parametric.js.map +1 -1
- package/dist/esm/assertion/impl/sync.d.ts +54 -22
- package/dist/esm/assertion/impl/sync.d.ts.map +1 -1
- package/dist/esm/assertion/impl/sync.js +2 -2
- package/dist/esm/assertion/impl/sync.js.map +1 -1
- package/dist/esm/assertion/index.d.ts +1 -1
- package/dist/esm/assertion/index.d.ts.map +1 -1
- package/dist/esm/assertion/index.js +0 -1
- package/dist/esm/assertion/index.js.map +1 -1
- package/dist/esm/assertion/slotify.d.ts +1 -13
- package/dist/esm/assertion/slotify.d.ts.map +1 -1
- package/dist/esm/assertion/slotify.js +50 -17
- package/dist/esm/assertion/slotify.js.map +1 -1
- package/dist/esm/bootstrap.d.ts +85 -17
- 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 +37 -18
- package/dist/esm/error.d.ts.map +1 -1
- package/dist/esm/error.js +41 -27
- package/dist/esm/error.js.map +1 -1
- package/dist/esm/expect.d.ts.map +1 -1
- package/dist/esm/expect.js +133 -80
- package/dist/esm/expect.js.map +1 -1
- package/dist/esm/guards.d.ts +24 -10
- package/dist/esm/guards.d.ts.map +1 -1
- package/dist/esm/guards.js +52 -36
- package/dist/esm/guards.js.map +1 -1
- package/dist/esm/index.d.ts +85 -17
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/internal-schema.d.ts +25 -0
- package/dist/esm/internal-schema.d.ts.map +1 -0
- package/dist/esm/internal-schema.js +203 -0
- package/dist/esm/internal-schema.js.map +1 -0
- 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 +20 -13
- package/dist/esm/value-to-schema.js.map +1 -1
- package/package.json +29 -11
- package/src/assertion/assertion-async.ts +42 -14
- package/src/assertion/assertion-sync.ts +40 -17
- package/src/assertion/assertion-types.ts +55 -45
- package/src/assertion/assertion.ts +49 -32
- package/src/assertion/create.ts +46 -65
- package/src/assertion/impl/assertion-util.ts +31 -18
- package/src/assertion/impl/async-parametric.ts +93 -52
- package/src/assertion/impl/async.ts +2 -2
- package/src/assertion/impl/sync-basic.ts +7 -4
- 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 +79 -52
- package/src/assertion/impl/sync.ts +2 -2
- package/src/assertion/index.ts +1 -1
- package/src/assertion/slotify.ts +67 -21
- package/src/bootstrap.ts +1 -0
- package/src/diff.ts +343 -0
- package/src/error.ts +66 -31
- package/src/expect.ts +195 -129
- package/src/guards.ts +74 -48
- package/src/internal-schema.ts +246 -0
- package/src/schema.ts +4 -2
- package/src/use.ts +21 -7
- package/src/util.ts +15 -12
- package/src/value-to-schema.ts +21 -13
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
|
*
|
|
@@ -40,7 +57,7 @@ export interface InvalidMetadataErrorOptions extends ErrorOptions {
|
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
/**
|
|
43
|
-
* Options for {@link
|
|
60
|
+
* Options for {@link InvalidObjectSchemaError}
|
|
44
61
|
*
|
|
45
62
|
* @group Error Options
|
|
46
63
|
*/
|
|
@@ -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
|
|
|
@@ -133,6 +163,12 @@ export class AssertionImplementationError extends BupkisError {
|
|
|
133
163
|
super(message, rest);
|
|
134
164
|
this.result = result;
|
|
135
165
|
}
|
|
166
|
+
|
|
167
|
+
static isAssertionImplementationError(
|
|
168
|
+
err: unknown,
|
|
169
|
+
): err is AssertionImplementationError {
|
|
170
|
+
return isA(err, AssertionImplementationError);
|
|
171
|
+
}
|
|
136
172
|
}
|
|
137
173
|
|
|
138
174
|
/**
|
|
@@ -151,8 +187,7 @@ export class FailAssertionError extends AssertionError {
|
|
|
151
187
|
|
|
152
188
|
static isFailAssertionError(err: unknown): err is FailAssertionError {
|
|
153
189
|
return (
|
|
154
|
-
isA(err, FailAssertionError) &&
|
|
155
|
-
Object.hasOwn(err, kBupkisFailAssertionError)
|
|
190
|
+
isA(err, FailAssertionError) && hasOwn(err, kBupkisFailAssertionError)
|
|
156
191
|
);
|
|
157
192
|
}
|
|
158
193
|
}
|
|
@@ -180,14 +215,15 @@ export class InvalidMetadataError extends BupkisError {
|
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
/**
|
|
183
|
-
* Thrown from certain assertions when
|
|
218
|
+
* Thrown from certain assertions when a parameter cannot be used as an "object
|
|
219
|
+
* schema"
|
|
184
220
|
*
|
|
185
221
|
* @group Errors
|
|
186
222
|
*/
|
|
187
|
-
export class
|
|
188
|
-
readonly code = '
|
|
223
|
+
export class InvalidObjectSchemaError extends BupkisError {
|
|
224
|
+
readonly code = 'ERR_BUPKIS_INVALID_OBJECT_SCHEMA';
|
|
189
225
|
|
|
190
|
-
override name = '
|
|
226
|
+
override name = 'InvalidObjectSchemaError';
|
|
191
227
|
|
|
192
228
|
readonly schema?: unknown;
|
|
193
229
|
|
|
@@ -211,12 +247,30 @@ export class NegatedAssertionError extends AssertionError {
|
|
|
211
247
|
|
|
212
248
|
static isNegatedAssertionError(err: unknown): err is NegatedAssertionError {
|
|
213
249
|
return (
|
|
214
|
-
isA(err, AssertionError) &&
|
|
215
|
-
Object.hasOwn(err, kBupkisNegatedAssertionError)
|
|
250
|
+
isA(err, AssertionError) && hasOwn(err, kBupkisNegatedAssertionError)
|
|
216
251
|
);
|
|
217
252
|
}
|
|
218
253
|
}
|
|
219
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Thrown when a value cannot be converted to a schema using `valueToSchema()`.
|
|
257
|
+
*
|
|
258
|
+
* @remarks
|
|
259
|
+
* Currently, this includes the presence of an own property `__proto__` or an
|
|
260
|
+
* empty object (because this will match anything; though maybe we should change
|
|
261
|
+
* that).
|
|
262
|
+
* @group Errors
|
|
263
|
+
*/
|
|
264
|
+
export class SatisfactionError extends BupkisError {
|
|
265
|
+
readonly code = 'ERR_BUPKIS_SATISFACTION';
|
|
266
|
+
|
|
267
|
+
override name = 'SatisfactionError';
|
|
268
|
+
|
|
269
|
+
constructor(message: string, options: ErrorOptions = {}) {
|
|
270
|
+
super(message, options);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
220
274
|
/**
|
|
221
275
|
* Thrown when `expect()` is called with something async.
|
|
222
276
|
*
|
|
@@ -249,22 +303,3 @@ export class UnknownAssertionError<
|
|
|
249
303
|
this.args = args;
|
|
250
304
|
}
|
|
251
305
|
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Thrown when a value cannot be converted to a schema using `valueToSchema()`.
|
|
255
|
-
*
|
|
256
|
-
* @remarks
|
|
257
|
-
* Currently, this includes the presence of an own property `__proto__` or an
|
|
258
|
-
* empty object (because this will match anything; though maybe we should change
|
|
259
|
-
* that).
|
|
260
|
-
* @group Errors
|
|
261
|
-
*/
|
|
262
|
-
export class ValueToSchemaError extends BupkisError {
|
|
263
|
-
readonly code = 'ERR_BUPKIS_SATISFACTION';
|
|
264
|
-
|
|
265
|
-
override name = 'SatisfactionError';
|
|
266
|
-
|
|
267
|
-
constructor(message: string, options: ErrorOptions = {}) {
|
|
268
|
-
super(message, options);
|
|
269
|
-
}
|
|
270
|
-
}
|