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
@@ -3,19 +3,18 @@ import z from 'zod/v4';
3
3
 
4
4
  import { kStringLiteral } from '../constant.js';
5
5
  import { AssertionError, AssertionImplementationError } from '../error.js';
6
+ import { isA, isBoolean, isError, isZodType } from '../guards.js';
6
7
  import {
7
- isA,
8
8
  isAssertionFailure,
9
- isBoolean,
10
- isError,
11
- isZodType,
12
- } from '../guards.js';
9
+ isAssertionParseRequest,
10
+ } from '../internal-schema.js';
13
11
  import { BupkisRegistry } from '../metadata.js';
14
12
  import {
15
13
  type AssertionAsync,
16
14
  type AssertionFunctionAsync,
17
15
  type AssertionImplAsync,
18
16
  type AssertionImplFnAsync,
17
+ type AssertionImplFnReturnType,
19
18
  type AssertionImplSchemaAsync,
20
19
  type AssertionParts,
21
20
  type AssertionSchemaAsync,
@@ -114,8 +113,25 @@ export class BupkisAssertionFunctionAsync<
114
113
  stackStartFn: (...args: any[]) => any,
115
114
  _parseResult?: ParsedResult<Parts>,
116
115
  ): Promise<void> {
117
- const { impl } = this;
118
- const result = await impl(...parsedValues);
116
+ let result: AssertionImplFnReturnType<Parts>;
117
+ try {
118
+ result = await (this.impl as AssertionImplFnAsync<Parts>).call(
119
+ null,
120
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
121
+ ...(parsedValues as any),
122
+ );
123
+ } catch (err) {
124
+ if (AssertionError.isAssertionError(err)) {
125
+ throw err;
126
+ }
127
+ if (isError(err) && err instanceof z.ZodError) {
128
+ throw this.fromZodError(err, stackStartFn, parsedValues);
129
+ }
130
+ throw new AssertionImplementationError(
131
+ `Unexpected error thrown from assertion ${this}: ${err}`,
132
+ { cause: err },
133
+ );
134
+ }
119
135
  if (isZodType(result)) {
120
136
  try {
121
137
  await result.parseAsync(parsedValues[0]);
@@ -131,14 +147,27 @@ export class BupkisAssertionFunctionAsync<
131
147
  message: `Assertion ${this} failed for arguments: ${inspect(args)}`,
132
148
  });
133
149
  }
150
+ } else if (isError(result) && result instanceof z.ZodError) {
151
+ throw this.fromZodError(result, stackStartFn, parsedValues);
152
+ } else if (isAssertionParseRequest(result)) {
153
+ const { asyncSchema, schema, subject } = result;
154
+ let zodResult: z.ZodSafeParseResult<unknown>;
155
+ if (schema) {
156
+ zodResult = schema.safeParse(subject);
157
+ } else {
158
+ zodResult = asyncSchema.safeParse(subject);
159
+ }
160
+ if (!zodResult.success) {
161
+ throw this.fromZodError(zodResult.error, stackStartFn, subject);
162
+ }
134
163
  } else if (isAssertionFailure(result)) {
135
164
  throw new AssertionError({
136
165
  actual: result.actual,
137
166
  expected: result.expected,
138
- message: result.message ?? `Assertion ${this} failed`,
167
+ message:
168
+ result.message ??
169
+ `Assertion ${this} failed for arguments: ${inspect(args)}`,
139
170
  });
140
- } else if (isError(result) && result instanceof z.ZodError) {
141
- throw this.fromZodError(result, stackStartFn, parsedValues);
142
171
  } else if (result as unknown) {
143
172
  throw new AssertionImplementationError(
144
173
  `Invalid return type from assertion ${this}; expected boolean, ZodType, or AssertionFailure`,
@@ -197,6 +226,8 @@ export class BupkisAssertionSchemaAsync<
197
226
  if (isA(error, z.ZodError)) {
198
227
  throw this.fromZodError(error, stackStartFn, parsedValues);
199
228
  }
229
+ /* c8 ignore next */
230
+ throw error;
200
231
  }
201
232
  }
202
233
 
@@ -211,10 +242,7 @@ export class BupkisAssertionSchemaAsync<
211
242
  }
212
243
 
213
244
  let exactMatch = true;
214
- let subjectValidationResult:
215
- | undefined
216
- | { data: any; success: true }
217
- | { error: z.ZodError; success: false };
245
+ let subjectValidationResult: ParsedResultSuccess<Parts>['subjectValidationResult'];
218
246
 
219
247
  for (let i = 0; i < slots.length; i++) {
220
248
  const slot = slots[i]!;
@@ -16,16 +16,15 @@ import {
16
16
  AssertionImplementationError,
17
17
  UnexpectedAsyncError,
18
18
  } from '../error.js';
19
+ import { isBoolean, isError, isPromiseLike, isZodType } from '../guards.js';
19
20
  import {
20
21
  isAssertionFailure,
21
- isBoolean,
22
- isError,
23
- isPromiseLike,
24
- isZodType,
25
- } from '../guards.js';
22
+ isAssertionParseRequest,
23
+ } from '../internal-schema.js';
26
24
  import { BupkisRegistry } from '../metadata.js';
27
25
  import {
28
26
  type AssertionFunctionSync,
27
+ type AssertionImplFnReturnType,
29
28
  type AssertionImplFnSync,
30
29
  type AssertionImplSchemaSync,
31
30
  type AssertionImplSync,
@@ -145,11 +144,25 @@ export class BupkisAssertionFunctionSync<
145
144
  stackStartFn: (...args: any[]) => any,
146
145
  _parseResult?: ParsedResult<Parts>,
147
146
  ): void {
148
- const result = (this.impl as AssertionImplFnSync<Parts>).call(
149
- null,
150
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
151
- ...(parsedValues as any),
152
- );
147
+ let result: AssertionImplFnReturnType<Parts>;
148
+ try {
149
+ result = (this.impl as AssertionImplFnSync<Parts>).call(
150
+ null,
151
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
152
+ ...(parsedValues as any),
153
+ );
154
+ } catch (err) {
155
+ if (AssertionError.isAssertionError(err)) {
156
+ throw err;
157
+ }
158
+ if (isError(err) && err instanceof z.ZodError) {
159
+ throw this.fromZodError(err, stackStartFn, parsedValues);
160
+ }
161
+ throw new AssertionImplementationError(
162
+ `Unexpected error thrown from assertion ${this}: ${err}`,
163
+ { cause: err },
164
+ );
165
+ }
153
166
  if (isPromiseLike(result)) {
154
167
  // Avoid unhandled promise rejection
155
168
  Promise.resolve(result).catch((err) => {
@@ -171,14 +184,27 @@ export class BupkisAssertionFunctionSync<
171
184
  message: `Assertion ${this} failed for arguments: ${inspect(args)}`,
172
185
  });
173
186
  }
187
+ } else if (isError(result) && result instanceof z.ZodError) {
188
+ throw this.fromZodError(result, stackStartFn, parsedValues);
189
+ } else if (isAssertionParseRequest(result)) {
190
+ const { asyncSchema, schema, subject } = result;
191
+ if (asyncSchema) {
192
+ throw new AssertionImplementationError(
193
+ `Sync assertion ${this} returned an async schema in its AssertionParseRequest`,
194
+ );
195
+ }
196
+ const zodResult = schema.safeParse(subject);
197
+ if (!zodResult.success) {
198
+ throw this.fromZodError(zodResult.error, stackStartFn, subject);
199
+ }
174
200
  } else if (isAssertionFailure(result)) {
175
201
  throw new AssertionError({
176
202
  actual: result.actual,
177
203
  expected: result.expected,
178
- message: result.message ?? `Assertion ${this} failed`,
204
+ message:
205
+ result.message ??
206
+ `Assertion ${this} failed for arguments: ${inspect(args)}`,
179
207
  });
180
- } else if (isError(result) && result instanceof z.ZodError) {
181
- throw this.fromZodError(result, stackStartFn, parsedValues);
182
208
  } else if (result as unknown) {
183
209
  throw new AssertionImplementationError(
184
210
  `Invalid return type from assertion ${this}; expected boolean, ZodType, or AssertionFailure`,
@@ -252,10 +278,7 @@ export class BupkisAssertionSchemaSync<
252
278
  }
253
279
 
254
280
  let exactMatch = true;
255
- let subjectValidationResult:
256
- | undefined
257
- | { data: any; success: true }
258
- | { error: z.ZodError; success: false };
281
+ let subjectValidationResult: ParsedResultSuccess<Parts>['subjectValidationResult'];
259
282
 
260
283
  for (let i = 0; i < slots.length; i++) {
261
284
  const slot = slots[i]!;
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { type ArrayValues, type NonEmptyTuple } from 'type-fest';
20
- import { z } from 'zod/v4';
20
+ import { type z } from 'zod/v4';
21
21
 
22
22
  import type { AsyncAssertions, SyncAssertions } from './impl/index.js';
23
23
 
@@ -271,6 +271,7 @@ export type AssertionImplFnAsync<Parts extends AssertionParts> = (
271
271
  */
272
272
  export type AssertionImplFnReturnType<Parts extends AssertionParts> =
273
273
  | AssertionFailure
274
+ | AssertionParseRequest
274
275
  | boolean
275
276
  | void
276
277
  | z.ZodError
@@ -379,6 +380,57 @@ export type AssertionImplSync<Parts extends AssertionParts> =
379
380
  | AssertionImplFnSync<Parts>
380
381
  | AssertionImplSchemaSync<Parts>;
381
382
 
383
+ /**
384
+ * Internal metadata for assertions.
385
+ *
386
+ * For internal use by documentation tooling.
387
+ */
388
+ export interface AssertionMetadata {
389
+ /**
390
+ * Anchor ID for linking to this assertion
391
+ */
392
+ anchor: string;
393
+ /**
394
+ * Category to map to page of logically grouped assertions
395
+ */
396
+ category:
397
+ | 'collections'
398
+ | 'date'
399
+ | 'equality'
400
+ | 'error'
401
+ | 'function'
402
+ | 'numeric'
403
+ | 'object'
404
+ | 'other'
405
+ | 'primitives'
406
+ | 'promise'
407
+ | 'strings';
408
+ /**
409
+ * Redirect for assertion to its documentation page, including anchor
410
+ */
411
+ redirect?: string | undefined;
412
+ }
413
+
414
+ /**
415
+ * When you want to use a Zod schema in an assertion implementation function
416
+ * against some value that _isn't_ the subject, you can return this object and
417
+ * <span class="bupkis">BUPKIS</span> will do it for you (with better diffs).
418
+ *
419
+ * @group Assertion Creation
420
+ */
421
+ export type AssertionParseRequest = {
422
+ subject: unknown;
423
+ } & (
424
+ | {
425
+ asyncSchema: z.ZodType;
426
+ schema?: never;
427
+ }
428
+ | {
429
+ asyncSchema?: never;
430
+ schema: z.ZodType;
431
+ }
432
+ );
433
+
382
434
  /**
383
435
  * Union type representing the fundamental building blocks of an assertion.
384
436
  *
@@ -740,7 +792,6 @@ export interface CreateAssertionFn {
740
792
  metadata?: AssertionMetadata,
741
793
  ): AssertionFunctionSync<Parts, Impl, Slots>;
742
794
  }
743
-
744
795
  /**
745
796
  * The main factory function for creating asynchronous assertions.
746
797
  *
@@ -789,6 +840,7 @@ export interface CreateAsyncAssertionFn {
789
840
  metadata?: AssertionMetadata,
790
841
  ): AssertionFunctionAsync<Parts, Impl, Slots>;
791
842
  }
843
+
792
844
  /**
793
845
  * Utility type for parsed values that may be empty.
794
846
  *
@@ -1014,7 +1066,6 @@ export type ParsedValues<Parts extends AssertionParts = AssertionParts> =
1014
1066
  * @see {@link AssertionPart} for how phrases fit into assertion structure
1015
1067
  */
1016
1068
  export type Phrase = PhraseLiteral | PhraseLiteralChoice;
1017
-
1018
1069
  /**
1019
1070
  * Type representing a single phrase literal string.
1020
1071
  *
@@ -1044,6 +1095,7 @@ export type Phrase = PhraseLiteral | PhraseLiteralChoice;
1044
1095
  * @see {@link AssertionPart} for how phrases fit into assertion structure
1045
1096
  */
1046
1097
  export type PhraseLiteral = string;
1098
+
1047
1099
  /**
1048
1100
  * Type representing a choice between multiple phrase literals.
1049
1101
  *
@@ -1152,45 +1204,3 @@ export type RawAssertionImplSchemaAsync<Parts extends AssertionParts> =
1152
1204
  */
1153
1205
  export type RawAssertionImplSchemaSync<Parts extends AssertionParts> =
1154
1206
  z.ZodType<ParsedSubject<Parts>>;
1155
-
1156
- /**
1157
- * Metadata associated with an assertion, for internal use by documentation
1158
- * tooling.
1159
- *
1160
- * @private
1161
- */
1162
- export const AssertionMetadataSchema = z
1163
- .looseObject({
1164
- anchor: z.string().describe('Anchor ID for linking to this assertion.'),
1165
- category: z
1166
- .enum([
1167
- 'collections',
1168
- 'date',
1169
- 'equality',
1170
- 'error',
1171
- 'function',
1172
- 'numeric',
1173
- 'object',
1174
- 'other',
1175
- 'primitives',
1176
- 'promise',
1177
- 'strings',
1178
- ])
1179
- .describe('Category to map to page of logically grouped assertions'),
1180
- redirectName: z
1181
- .string()
1182
- .optional()
1183
- .describe(
1184
- 'Redirect for assertion to its documentation page, including anchor',
1185
- ),
1186
- })
1187
- .describe(
1188
- 'Metadata associated with an assertion, for internal use by documentation tooling.',
1189
- );
1190
-
1191
- /**
1192
- * {@inheritDoc AssertionMetadataSchema}
1193
- *
1194
- * @private
1195
- */
1196
- export type AssertionMetadata = z.infer<typeof AssertionMetadataSchema>;
@@ -16,6 +16,11 @@ import { inspect } from 'util';
16
16
  import { z } from 'zod/v4';
17
17
 
18
18
  import { kStringLiteral } from '../constant.js';
19
+ import {
20
+ extractDiffValues,
21
+ generateDiff,
22
+ shouldGenerateDiff,
23
+ } from '../diff.js';
19
24
  import { AssertionError, InvalidMetadataError } from '../error.js';
20
25
  import { BupkisRegistry } from '../metadata.js';
21
26
  import {
@@ -25,11 +30,11 @@ import {
25
30
  type AssertionParts,
26
31
  type AssertionSlots,
27
32
  type ParsedResult,
28
- type ParsedValues,
29
33
  } from './assertion-types.js';
30
34
 
31
35
  const debug = createDebug('bupkis:assertion');
32
-
36
+ const { hasOwn, keys } = Object;
37
+ const { isArray } = Array;
33
38
  /**
34
39
  * Modified charmap for {@link slug} to use underscores to replace hyphens (`-`;
35
40
  * and for hyphens to replace everything else that needs replacing) and `<` with
@@ -75,6 +80,14 @@ export abstract class BupkisAssertion<
75
80
  * @returns String representation
76
81
  */
77
82
  public toString(): string {
83
+ /**
84
+ * Expands a Zod type into a human-readable string representation.
85
+ *
86
+ * @function
87
+ * @param zodType The Zod type to expand
88
+ * @param wrapCurlies Whether to wrap the result in curly braces
89
+ * @returns String representation of the Zod type
90
+ */
78
91
  const expand = (
79
92
  zodType: z.core.$ZodType | z.ZodType,
80
93
  wrapCurlies = false,
@@ -107,7 +120,7 @@ export abstract class BupkisAssertion<
107
120
  repr = `{${expand((def as z.core.$ZodDefaultDef).innerType)}}`;
108
121
  break;
109
122
  case 'enum':
110
- repr = `${Object.keys((def as z.core.$ZodEnumDef<any>).entries as Record<PropertyKey, unknown>).join(' / ')}`;
123
+ repr = `${keys((def as z.core.$ZodEnumDef<any>).entries as Record<PropertyKey, unknown>).join(' / ')}`;
111
124
  break;
112
125
  case 'intersection':
113
126
  repr = `${expand((def as z.core.$ZodIntersectionDef<z.core.$ZodType>).left)} & ${expand((def as z.core.$ZodIntersectionDef<z.core.$ZodType>).right)}`;
@@ -154,7 +167,7 @@ export abstract class BupkisAssertion<
154
167
  };
155
168
  return `"${this.slots
156
169
  .map((slot) =>
157
- Object.hasOwn(BupkisRegistry.get(slot) ?? {}, kStringLiteral)
170
+ hasOwn(BupkisRegistry.get(slot) ?? {}, kStringLiteral)
158
171
  ? expand(slot)
159
172
  : expand(slot, true),
160
173
  )
@@ -174,40 +187,44 @@ export abstract class BupkisAssertion<
174
187
  * @param values Values which caused the error
175
188
  * @returns New `AssertionError`
176
189
  */
177
- /**
178
- * Translates a {@link z.ZodError} into an {@link AssertionError} with a
179
- * human-friendly message.
180
- *
181
- * @remarks
182
- * This does not handle parameterized assertions with more than one parameter
183
- * too cleanly; it's unclear how a test runner would display the expected
184
- * values. This will probably need a fix in the future.
185
- * @param stackStartFn The function to start the stack trace from
186
- * @param zodError The original `ZodError`
187
- * @param values Values which caused the error
188
- * @returns New `AssertionError`
189
- */
190
- protected fromZodError<Parts extends AssertionParts>(
190
+ protected fromZodError<Values>(
191
191
  zodError: z.ZodError,
192
192
  stackStartFn: (...args: any[]) => any,
193
- values: ParsedValues<Parts>,
193
+ values: Values,
194
194
  ): AssertionError {
195
- const flat = z.flattenError(zodError);
195
+ const subject: unknown = isArray(values) ? values[0] : values;
196
196
 
197
- let pretty = flat.formErrors.join('; ');
198
- for (const [keypath, errors] of Object.entries(flat.fieldErrors)) {
199
- pretty += `; ${keypath}: ${(errors as unknown[]).join('; ')}`;
200
- }
197
+ const { actual, expected } = extractDiffValues(zodError, subject);
201
198
 
202
- const [actual, ...expected] = values as unknown as [unknown, ...unknown[]];
199
+ // Only use custom message if we could extract diff values
200
+ if (shouldGenerateDiff(actual, expected)) {
201
+ // Use jest-diff to generate rich, colored diff output
202
+ const diffOutput = generateDiff(expected, actual, {
203
+ aAnnotation: 'expected',
204
+ bAnnotation: 'actual',
205
+ expand: false,
206
+ includeChangeCounts: true,
207
+ });
203
208
 
204
- return new AssertionError({
205
- actual,
206
- expected: expected.length === 1 ? expected[0] : expected,
207
- message: `Assertion ${this} failed: ${pretty}`,
208
- operator: `${this}`,
209
- stackStartFn,
210
- });
209
+ const message = diffOutput
210
+ ? `Assertion ${this} failed:\n${diffOutput}`
211
+ : `Assertion ${this} failed: values are not equal`;
212
+
213
+ return new AssertionError({
214
+ actual,
215
+ expected,
216
+ message,
217
+ stackStartFn,
218
+ });
219
+ } else {
220
+ // Fall back to Zod's prettified error message
221
+ const pretty = z.prettifyError(zodError).slice(2);
222
+ return new AssertionError({
223
+ actual: subject,
224
+ message: `Assertion ${this} failed:\n${pretty}`,
225
+ stackStartFn,
226
+ });
227
+ }
211
228
  }
212
229
 
213
230
  protected maybeParseValuesArgMismatch<Args extends readonly unknown[]>(
@@ -76,7 +76,12 @@ import type {
76
76
  } from './assertion-types.js';
77
77
 
78
78
  import { AssertionImplementationError } from '../error.js';
79
- import { isFunction, isString, isZodType } from '../guards.js';
79
+ import { isFunction, isZodType } from '../guards.js';
80
+ import {
81
+ AssertionMetadataSchema,
82
+ CreateAssertionInputSchema,
83
+ CreateAssertionInputSchemaAsync,
84
+ } from '../internal-schema.js';
80
85
  import {
81
86
  BupkisAssertionFunctionAsync,
82
87
  BupkisAssertionSchemaAsync,
@@ -86,7 +91,6 @@ import {
86
91
  BupkisAssertionSchemaSync,
87
92
  } from './assertion-sync.js';
88
93
  import {
89
- AssertionMetadataSchema,
90
94
  type CreateAssertionFn,
91
95
  type CreateAsyncAssertionFn,
92
96
  } from './assertion-types.js';
@@ -96,6 +100,7 @@ import { slotify } from './slotify.js';
96
100
  /**
97
101
  * {@inheritDoc CreateAssertionFn}
98
102
  *
103
+ * @function
99
104
  * @throws {TypeError} Invalid assertion implementation type
100
105
  * @group Assertion Creation
101
106
  */
@@ -107,56 +112,37 @@ export const createAssertion: CreateAssertionFn = <
107
112
  impl: Impl,
108
113
  metadata?: AssertionMetadata,
109
114
  ) => {
110
- if (!Array.isArray(parts)) {
111
- throw new AssertionImplementationError('First parameter must be an array');
112
- }
113
- if (parts.length === 0) {
114
- throw new AssertionImplementationError(
115
- 'At least one value is required for an assertion',
116
- );
117
- }
118
- if (
119
- !parts.every(
120
- (part) => isString(part) || Array.isArray(part) || isZodType(part),
121
- )
122
- ) {
123
- throw new AssertionImplementationError(
124
- 'All assertion parts must be strings or Zod schemas',
125
- );
126
- }
127
- if (!impl) {
128
- throw new AssertionImplementationError(
129
- 'An assertion implementation is required',
130
- );
131
- }
115
+ // Validate inputs using Zod schema
132
116
  try {
133
- const slots = slotify<Parts>(parts);
134
-
135
- if (isZodType(impl)) {
136
- const assertion = new BupkisAssertionSchemaSync(parts, slots, impl);
137
- if (metadata) {
138
- AssertionMetadataRegistry.set(assertion, metadata);
139
- }
140
- return assertion;
141
- } else if (isFunction(impl)) {
142
- const assertion = new BupkisAssertionFunctionSync(parts, slots, impl);
143
- if (metadata) {
144
- AssertionMetadataRegistry.set(assertion, metadata);
145
- }
146
- return assertion;
147
- }
117
+ CreateAssertionInputSchema.parse([parts, impl, metadata]);
148
118
  } catch (err) {
149
119
  if (err instanceof z.ZodError) {
150
120
  throw new AssertionImplementationError(
151
- `Failed to slotify assertion parts: ${z.prettifyError(err)}`,
121
+ `Invalid input parameters: ${z.prettifyError(err)}`,
152
122
  { cause: err },
153
123
  );
154
124
  }
155
- throw new AssertionImplementationError(
156
- `Failed to slotify assertion parts: ${err}`,
157
- { cause: err },
158
- );
125
+ /* c8 ignore next */
126
+ throw err;
127
+ }
128
+
129
+ const slots = slotify<Parts>(parts);
130
+
131
+ if (isZodType(impl)) {
132
+ const assertion = new BupkisAssertionSchemaSync(parts, slots, impl);
133
+ if (metadata) {
134
+ AssertionMetadataRegistry.set(assertion, metadata);
135
+ }
136
+ return assertion;
137
+ } else if (isFunction(impl)) {
138
+ const assertion = new BupkisAssertionFunctionSync(parts, slots, impl);
139
+ if (metadata) {
140
+ AssertionMetadataRegistry.set(assertion, metadata);
141
+ }
142
+ return assertion;
159
143
  }
144
+ // should be impossible if CreateAssertionInputSchema is correct
145
+ /* c8 ignore next */
160
146
  throw new AssertionImplementationError(
161
147
  'Assertion implementation must be a function, Zod schema or Zod schema factory',
162
148
  );
@@ -165,6 +151,7 @@ export const createAssertion: CreateAssertionFn = <
165
151
  /**
166
152
  * {@inheritDoc CreateAsyncAssertionFn}
167
153
  *
154
+ * @function
168
155
  * @throws {TypeError} Invalid assertion implementation type
169
156
  * @group Assertion Creation
170
157
  */
@@ -176,28 +163,20 @@ export const createAsyncAssertion: CreateAsyncAssertionFn = <
176
163
  impl: Impl,
177
164
  metadata?: AssertionMetadata,
178
165
  ) => {
179
- if (!Array.isArray(parts)) {
180
- throw new AssertionImplementationError('First parameter must be an array');
181
- }
182
- if (parts.length === 0) {
183
- throw new AssertionImplementationError(
184
- 'At least one value is required for an assertion',
185
- );
186
- }
187
- if (
188
- !parts.every(
189
- (part) => isString(part) || Array.isArray(part) || isZodType(part),
190
- )
191
- ) {
192
- throw new AssertionImplementationError(
193
- 'All assertion parts must be strings or Zod schemas',
194
- );
195
- }
196
- if (!impl) {
197
- throw new AssertionImplementationError(
198
- 'An assertion implementation is required',
199
- );
166
+ // Validate inputs using Zod schema
167
+ try {
168
+ CreateAssertionInputSchemaAsync.parse([parts, impl, metadata]);
169
+ } catch (err) {
170
+ if (err instanceof z.ZodError) {
171
+ throw new AssertionImplementationError(
172
+ `Invalid input parameters: ${z.prettifyError(err)}`,
173
+ { cause: err },
174
+ );
175
+ }
176
+ /* c8 ignore next */
177
+ throw err;
200
178
  }
179
+
201
180
  const slots = slotify<Parts>(parts);
202
181
 
203
182
  if (isZodType(impl)) {
@@ -219,6 +198,8 @@ export const createAsyncAssertion: CreateAsyncAssertionFn = <
219
198
  }
220
199
  return assertion;
221
200
  }
201
+ // should be impossible if CreateAssertionInputSchemaAsync is correct
202
+ /* c8 ignore next */
222
203
  throw new AssertionImplementationError(
223
204
  'Assertion implementation must be a function, Zod schema or Zod schema factory',
224
205
  );