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.
Files changed (215) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +19 -2
  3. package/dist/commonjs/assertion/assertion-async.d.ts.map +1 -1
  4. package/dist/commonjs/assertion/assertion-async.js +37 -7
  5. package/dist/commonjs/assertion/assertion-async.js.map +1 -1
  6. package/dist/commonjs/assertion/assertion-sync.d.ts.map +1 -1
  7. package/dist/commonjs/assertion/assertion-sync.js +32 -8
  8. package/dist/commonjs/assertion/assertion-sync.js.map +1 -1
  9. package/dist/commonjs/assertion/assertion-types.d.ts +37 -31
  10. package/dist/commonjs/assertion/assertion-types.d.ts.map +1 -1
  11. package/dist/commonjs/assertion/assertion-types.js +0 -32
  12. package/dist/commonjs/assertion/assertion-types.js.map +1 -1
  13. package/dist/commonjs/assertion/assertion.d.ts +3 -21
  14. package/dist/commonjs/assertion/assertion.d.ts.map +1 -1
  15. package/dist/commonjs/assertion/assertion.js +42 -27
  16. package/dist/commonjs/assertion/assertion.js.map +1 -1
  17. package/dist/commonjs/assertion/create.d.ts +2 -0
  18. package/dist/commonjs/assertion/create.d.ts.map +1 -1
  19. package/dist/commonjs/assertion/create.js +38 -42
  20. package/dist/commonjs/assertion/create.js.map +1 -1
  21. package/dist/commonjs/assertion/impl/assertion-util.d.ts +16 -4
  22. package/dist/commonjs/assertion/impl/assertion-util.d.ts.map +1 -1
  23. package/dist/commonjs/assertion/impl/assertion-util.js +20 -15
  24. package/dist/commonjs/assertion/impl/assertion-util.js.map +1 -1
  25. package/dist/commonjs/assertion/impl/async-parametric.d.ts +63 -11
  26. package/dist/commonjs/assertion/impl/async-parametric.d.ts.map +1 -1
  27. package/dist/commonjs/assertion/impl/async-parametric.js +89 -52
  28. package/dist/commonjs/assertion/impl/async-parametric.js.map +1 -1
  29. package/dist/commonjs/assertion/impl/async.d.ts +116 -12
  30. package/dist/commonjs/assertion/impl/async.d.ts.map +1 -1
  31. package/dist/commonjs/assertion/impl/async.js +1 -1
  32. package/dist/commonjs/assertion/impl/sync-basic.d.ts.map +1 -1
  33. package/dist/commonjs/assertion/impl/sync-basic.js +6 -4
  34. package/dist/commonjs/assertion/impl/sync-basic.js.map +1 -1
  35. package/dist/commonjs/assertion/impl/sync-collection.d.ts.map +1 -1
  36. package/dist/commonjs/assertion/impl/sync-collection.js +24 -14
  37. package/dist/commonjs/assertion/impl/sync-collection.js.map +1 -1
  38. package/dist/commonjs/assertion/impl/sync-esoteric.d.ts +1 -5
  39. package/dist/commonjs/assertion/impl/sync-esoteric.d.ts.map +1 -1
  40. package/dist/commonjs/assertion/impl/sync-esoteric.js +11 -13
  41. package/dist/commonjs/assertion/impl/sync-esoteric.js.map +1 -1
  42. package/dist/commonjs/assertion/impl/sync-parametric.d.ts +27 -7
  43. package/dist/commonjs/assertion/impl/sync-parametric.d.ts.map +1 -1
  44. package/dist/commonjs/assertion/impl/sync-parametric.js +75 -51
  45. package/dist/commonjs/assertion/impl/sync-parametric.js.map +1 -1
  46. package/dist/commonjs/assertion/impl/sync.d.ts +54 -22
  47. package/dist/commonjs/assertion/impl/sync.d.ts.map +1 -1
  48. package/dist/commonjs/assertion/impl/sync.js +1 -1
  49. package/dist/commonjs/assertion/impl/sync.js.map +1 -1
  50. package/dist/commonjs/assertion/index.d.ts +1 -1
  51. package/dist/commonjs/assertion/index.d.ts.map +1 -1
  52. package/dist/commonjs/assertion/index.js +0 -1
  53. package/dist/commonjs/assertion/index.js.map +1 -1
  54. package/dist/commonjs/assertion/slotify.d.ts +1 -13
  55. package/dist/commonjs/assertion/slotify.d.ts.map +1 -1
  56. package/dist/commonjs/assertion/slotify.js +49 -16
  57. package/dist/commonjs/assertion/slotify.js.map +1 -1
  58. package/dist/commonjs/bootstrap.d.ts +85 -17
  59. package/dist/commonjs/bootstrap.d.ts.map +1 -1
  60. package/dist/commonjs/bootstrap.js +1 -0
  61. package/dist/commonjs/bootstrap.js.map +1 -1
  62. package/dist/commonjs/diff.d.ts +51 -0
  63. package/dist/commonjs/diff.d.ts.map +1 -0
  64. package/dist/commonjs/diff.js +279 -0
  65. package/dist/commonjs/diff.js.map +1 -0
  66. package/dist/commonjs/error.d.ts +37 -18
  67. package/dist/commonjs/error.d.ts.map +1 -1
  68. package/dist/commonjs/error.js +44 -30
  69. package/dist/commonjs/error.js.map +1 -1
  70. package/dist/commonjs/expect.d.ts.map +1 -1
  71. package/dist/commonjs/expect.js +131 -78
  72. package/dist/commonjs/expect.js.map +1 -1
  73. package/dist/commonjs/guards.d.ts +24 -10
  74. package/dist/commonjs/guards.d.ts.map +1 -1
  75. package/dist/commonjs/guards.js +56 -39
  76. package/dist/commonjs/guards.js.map +1 -1
  77. package/dist/commonjs/index.d.ts +85 -17
  78. package/dist/commonjs/index.d.ts.map +1 -1
  79. package/dist/commonjs/internal-schema.d.ts +25 -0
  80. package/dist/commonjs/internal-schema.d.ts.map +1 -0
  81. package/dist/commonjs/internal-schema.js +209 -0
  82. package/dist/commonjs/internal-schema.js.map +1 -0
  83. package/dist/commonjs/schema.d.ts.map +1 -1
  84. package/dist/commonjs/schema.js +3 -2
  85. package/dist/commonjs/schema.js.map +1 -1
  86. package/dist/commonjs/use.js +22 -8
  87. package/dist/commonjs/use.js.map +1 -1
  88. package/dist/commonjs/util.d.ts +1 -0
  89. package/dist/commonjs/util.d.ts.map +1 -1
  90. package/dist/commonjs/util.js +14 -10
  91. package/dist/commonjs/util.js.map +1 -1
  92. package/dist/commonjs/value-to-schema.d.ts +1 -0
  93. package/dist/commonjs/value-to-schema.d.ts.map +1 -1
  94. package/dist/commonjs/value-to-schema.js +19 -12
  95. package/dist/commonjs/value-to-schema.js.map +1 -1
  96. package/dist/esm/assertion/assertion-async.d.ts.map +1 -1
  97. package/dist/esm/assertion/assertion-async.js +37 -7
  98. package/dist/esm/assertion/assertion-async.js.map +1 -1
  99. package/dist/esm/assertion/assertion-sync.d.ts.map +1 -1
  100. package/dist/esm/assertion/assertion-sync.js +32 -8
  101. package/dist/esm/assertion/assertion-sync.js.map +1 -1
  102. package/dist/esm/assertion/assertion-types.d.ts +37 -31
  103. package/dist/esm/assertion/assertion-types.d.ts.map +1 -1
  104. package/dist/esm/assertion/assertion-types.js +1 -31
  105. package/dist/esm/assertion/assertion-types.js.map +1 -1
  106. package/dist/esm/assertion/assertion.d.ts +3 -21
  107. package/dist/esm/assertion/assertion.d.ts.map +1 -1
  108. package/dist/esm/assertion/assertion.js +42 -27
  109. package/dist/esm/assertion/assertion.js.map +1 -1
  110. package/dist/esm/assertion/create.d.ts +2 -0
  111. package/dist/esm/assertion/create.d.ts.map +1 -1
  112. package/dist/esm/assertion/create.js +37 -41
  113. package/dist/esm/assertion/create.js.map +1 -1
  114. package/dist/esm/assertion/impl/assertion-util.d.ts +16 -4
  115. package/dist/esm/assertion/impl/assertion-util.d.ts.map +1 -1
  116. package/dist/esm/assertion/impl/assertion-util.js +20 -15
  117. package/dist/esm/assertion/impl/assertion-util.js.map +1 -1
  118. package/dist/esm/assertion/impl/async-parametric.d.ts +63 -11
  119. package/dist/esm/assertion/impl/async-parametric.d.ts.map +1 -1
  120. package/dist/esm/assertion/impl/async-parametric.js +89 -52
  121. package/dist/esm/assertion/impl/async-parametric.js.map +1 -1
  122. package/dist/esm/assertion/impl/async.d.ts +116 -12
  123. package/dist/esm/assertion/impl/async.d.ts.map +1 -1
  124. package/dist/esm/assertion/impl/async.js +2 -2
  125. package/dist/esm/assertion/impl/async.js.map +1 -1
  126. package/dist/esm/assertion/impl/sync-basic.d.ts.map +1 -1
  127. package/dist/esm/assertion/impl/sync-basic.js +6 -4
  128. package/dist/esm/assertion/impl/sync-basic.js.map +1 -1
  129. package/dist/esm/assertion/impl/sync-collection.d.ts.map +1 -1
  130. package/dist/esm/assertion/impl/sync-collection.js +24 -14
  131. package/dist/esm/assertion/impl/sync-collection.js.map +1 -1
  132. package/dist/esm/assertion/impl/sync-esoteric.d.ts +1 -5
  133. package/dist/esm/assertion/impl/sync-esoteric.d.ts.map +1 -1
  134. package/dist/esm/assertion/impl/sync-esoteric.js +11 -13
  135. package/dist/esm/assertion/impl/sync-esoteric.js.map +1 -1
  136. package/dist/esm/assertion/impl/sync-parametric.d.ts +27 -7
  137. package/dist/esm/assertion/impl/sync-parametric.d.ts.map +1 -1
  138. package/dist/esm/assertion/impl/sync-parametric.js +75 -51
  139. package/dist/esm/assertion/impl/sync-parametric.js.map +1 -1
  140. package/dist/esm/assertion/impl/sync.d.ts +54 -22
  141. package/dist/esm/assertion/impl/sync.d.ts.map +1 -1
  142. package/dist/esm/assertion/impl/sync.js +2 -2
  143. package/dist/esm/assertion/impl/sync.js.map +1 -1
  144. package/dist/esm/assertion/index.d.ts +1 -1
  145. package/dist/esm/assertion/index.d.ts.map +1 -1
  146. package/dist/esm/assertion/index.js +0 -1
  147. package/dist/esm/assertion/index.js.map +1 -1
  148. package/dist/esm/assertion/slotify.d.ts +1 -13
  149. package/dist/esm/assertion/slotify.d.ts.map +1 -1
  150. package/dist/esm/assertion/slotify.js +50 -17
  151. package/dist/esm/assertion/slotify.js.map +1 -1
  152. package/dist/esm/bootstrap.d.ts +85 -17
  153. package/dist/esm/bootstrap.d.ts.map +1 -1
  154. package/dist/esm/bootstrap.js +1 -0
  155. package/dist/esm/bootstrap.js.map +1 -1
  156. package/dist/esm/diff.d.ts +51 -0
  157. package/dist/esm/diff.d.ts.map +1 -0
  158. package/dist/esm/diff.js +273 -0
  159. package/dist/esm/diff.js.map +1 -0
  160. package/dist/esm/error.d.ts +37 -18
  161. package/dist/esm/error.d.ts.map +1 -1
  162. package/dist/esm/error.js +41 -27
  163. package/dist/esm/error.js.map +1 -1
  164. package/dist/esm/expect.d.ts.map +1 -1
  165. package/dist/esm/expect.js +133 -80
  166. package/dist/esm/expect.js.map +1 -1
  167. package/dist/esm/guards.d.ts +24 -10
  168. package/dist/esm/guards.d.ts.map +1 -1
  169. package/dist/esm/guards.js +52 -36
  170. package/dist/esm/guards.js.map +1 -1
  171. package/dist/esm/index.d.ts +85 -17
  172. package/dist/esm/index.d.ts.map +1 -1
  173. package/dist/esm/internal-schema.d.ts +25 -0
  174. package/dist/esm/internal-schema.d.ts.map +1 -0
  175. package/dist/esm/internal-schema.js +203 -0
  176. package/dist/esm/internal-schema.js.map +1 -0
  177. package/dist/esm/schema.d.ts.map +1 -1
  178. package/dist/esm/schema.js +3 -2
  179. package/dist/esm/schema.js.map +1 -1
  180. package/dist/esm/use.js +19 -6
  181. package/dist/esm/use.js.map +1 -1
  182. package/dist/esm/util.d.ts +1 -0
  183. package/dist/esm/util.d.ts.map +1 -1
  184. package/dist/esm/util.js +14 -10
  185. package/dist/esm/util.js.map +1 -1
  186. package/dist/esm/value-to-schema.d.ts +1 -0
  187. package/dist/esm/value-to-schema.d.ts.map +1 -1
  188. package/dist/esm/value-to-schema.js +20 -13
  189. package/dist/esm/value-to-schema.js.map +1 -1
  190. package/package.json +29 -11
  191. package/src/assertion/assertion-async.ts +42 -14
  192. package/src/assertion/assertion-sync.ts +40 -17
  193. package/src/assertion/assertion-types.ts +55 -45
  194. package/src/assertion/assertion.ts +49 -32
  195. package/src/assertion/create.ts +46 -65
  196. package/src/assertion/impl/assertion-util.ts +31 -18
  197. package/src/assertion/impl/async-parametric.ts +93 -52
  198. package/src/assertion/impl/async.ts +2 -2
  199. package/src/assertion/impl/sync-basic.ts +7 -4
  200. package/src/assertion/impl/sync-collection.ts +34 -14
  201. package/src/assertion/impl/sync-esoteric.ts +17 -13
  202. package/src/assertion/impl/sync-parametric.ts +79 -52
  203. package/src/assertion/impl/sync.ts +2 -2
  204. package/src/assertion/index.ts +1 -1
  205. package/src/assertion/slotify.ts +67 -21
  206. package/src/bootstrap.ts +1 -0
  207. package/src/diff.ts +343 -0
  208. package/src/error.ts +66 -31
  209. package/src/expect.ts +195 -129
  210. package/src/guards.ts +74 -48
  211. package/src/internal-schema.ts +246 -0
  212. package/src/schema.ts +4 -2
  213. package/src/use.ts +21 -7
  214. package/src/util.ts +15 -12
  215. 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 InvalidSchemaError}
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) && Object.hasOwn(err, kBupkisError);
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 the result of `valueToSchema` is invalid.
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 InvalidSchemaError extends BupkisError {
188
- readonly code = 'ERR_BUPKIS_INVALID_SCHEMA';
223
+ export class InvalidObjectSchemaError extends BupkisError {
224
+ readonly code = 'ERR_BUPKIS_INVALID_OBJECT_SCHEMA';
189
225
 
190
- override name = 'InvalidSchemaError';
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
- }