bupkis 0.0.2

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.
@@ -0,0 +1,488 @@
1
+ import { inspect } from 'node:util';
2
+ import { z } from 'zod/v4';
3
+
4
+ import { isA, isError, isNonNullObject, isString } from '../../guards.js';
5
+ import {
6
+ ArrayLikeSchema,
7
+ ClassSchema,
8
+ FunctionSchema,
9
+ RegExpSchema,
10
+ StrongMapSchema,
11
+ StrongSetSchema,
12
+ WrappedPromiseLikeSchema,
13
+ } from '../../schema.js';
14
+ import { valueToSchema } from '../../util.js';
15
+ import { createAssertion } from '../create.js';
16
+
17
+ const trapError = (fn: () => unknown): unknown => {
18
+ try {
19
+ fn();
20
+ } catch (err) {
21
+ if (err === undefined) {
22
+ return new Error('Function threw undefined');
23
+ }
24
+ return err;
25
+ }
26
+ };
27
+
28
+ const knownTypes = Object.freeze(
29
+ new Set([
30
+ 'string',
31
+ 'number',
32
+ 'boolean',
33
+ 'undefined',
34
+ 'null',
35
+ 'BigInt',
36
+ 'Symbol',
37
+ 'Object',
38
+ 'Function',
39
+ 'Array',
40
+ 'Date',
41
+ 'Map',
42
+ 'Set',
43
+ 'WeakMap',
44
+ 'WeakSet',
45
+ 'RegExp',
46
+ 'Promise',
47
+ 'Error',
48
+ 'WeakRef',
49
+ ] as const),
50
+ );
51
+
52
+ export const ParametricAssertions = [
53
+ createAssertion(
54
+ [['to be an instance of', 'to be a'], ClassSchema],
55
+ (_, ctor) => z.instanceof(ctor),
56
+ ),
57
+ createAssertion(
58
+ [
59
+ z.any(),
60
+ ['to be a', 'to be an'],
61
+ z.enum(
62
+ [...knownTypes].flatMap((t) => [t, t.toLowerCase()]) as [
63
+ string,
64
+ ...string[],
65
+ ],
66
+ ),
67
+ ],
68
+ (_, type) => {
69
+ type = type.toLowerCase() as Lowercase<typeof type>;
70
+ // these first three are names that are _not_ results of the `typeof` operator; i.e. `typeof x` will never return these strings
71
+ switch (type) {
72
+ case 'array':
73
+ return z.array(z.any());
74
+ case 'bigint':
75
+ return z.bigint();
76
+ case 'boolean':
77
+ return z.boolean();
78
+ case 'date':
79
+ return z.date();
80
+ case 'error':
81
+ return z.instanceof(Error);
82
+ case 'function':
83
+ return z.function();
84
+ case 'map':
85
+ return StrongMapSchema;
86
+ case 'null':
87
+ return z.null();
88
+ case 'number':
89
+ return z.number();
90
+ case 'object':
91
+ return z.looseObject({});
92
+ case 'promise':
93
+ return WrappedPromiseLikeSchema;
94
+ case 'regex': // fallthrough
95
+ case 'regexp':
96
+ return z.instanceof(RegExp);
97
+ case 'set':
98
+ return StrongSetSchema;
99
+ case 'string':
100
+ return z.string();
101
+ case 'symbol':
102
+ return z.symbol();
103
+ case 'undefined':
104
+ return z.undefined();
105
+ case 'weakmap':
106
+ return z.instanceof(WeakMap);
107
+ case 'weakref':
108
+ return z.instanceof(WeakRef);
109
+ case 'weakset':
110
+ return z.instanceof(WeakSet);
111
+ // c8 ignore next 2
112
+ default:
113
+ throw new TypeError(`Unknown type: "${type}"`);
114
+ }
115
+ },
116
+ ),
117
+ createAssertion([z.number(), 'to be greater than', z.number()], (_, other) =>
118
+ z.number().gt(other),
119
+ ),
120
+ createAssertion([z.number(), 'to be less than', z.number()], (_, other) =>
121
+ z.number().lt(other),
122
+ ),
123
+ createAssertion(
124
+ [
125
+ z.number(),
126
+ ['to be greater than or equal to', 'to be at least'],
127
+ z.number(),
128
+ ],
129
+ (_, other) => z.number().gte(other),
130
+ ),
131
+ createAssertion(
132
+ [z.number(), ['to be less than or equal to', 'to be at most'], z.number()],
133
+ (_, other) => z.number().lte(other),
134
+ ),
135
+
136
+ // Number range and approximation assertions
137
+ createAssertion(
138
+ [z.number(), 'to be within', z.number(), z.number()],
139
+ (subject, min, max) => {
140
+ if (subject < min || subject > max) {
141
+ return {
142
+ actual: subject,
143
+ expected: `number between ${min} and ${max}`,
144
+ message: `Expected ${subject} to be within range [${min}, ${max}]`,
145
+ };
146
+ }
147
+ },
148
+ ),
149
+ createAssertion(
150
+ [z.number(), 'to be close to', z.number(), z.number().optional()],
151
+ (subject, expected, tolerance = 1e-9) => {
152
+ const diff = Math.abs(subject - expected);
153
+ if (diff > tolerance) {
154
+ return {
155
+ actual: subject,
156
+ expected: expected,
157
+ message: `Expected ${subject} to be close to ${expected} (within ${tolerance}), but difference was ${diff}`,
158
+ };
159
+ }
160
+ },
161
+ ),
162
+
163
+ // String comparison assertions (lexicographic)
164
+ createAssertion(
165
+ [z.string(), 'to be greater than', z.string()],
166
+ (subject, other) => {
167
+ if (!(subject > other)) {
168
+ return {
169
+ actual: subject,
170
+ expected: `string greater than "${other}"`,
171
+ message: `Expected "${subject}" to be greater than "${other}"`,
172
+ };
173
+ }
174
+ },
175
+ ),
176
+ createAssertion(
177
+ [z.string(), 'to be less than', z.string()],
178
+ (subject, other) => {
179
+ if (!(subject < other)) {
180
+ return {
181
+ actual: subject,
182
+ expected: `string less than "${other}"`,
183
+ message: `Expected "${subject}" to be less than "${other}"`,
184
+ };
185
+ }
186
+ },
187
+ ),
188
+ createAssertion(
189
+ [z.string(), 'to be greater than or equal to', z.string()],
190
+ (subject, other) => {
191
+ if (!(subject >= other)) {
192
+ return {
193
+ actual: subject,
194
+ expected: `string greater than or equal to "${other}"`,
195
+ message: `Expected "${subject}" to be greater than or equal to "${other}"`,
196
+ };
197
+ }
198
+ },
199
+ ),
200
+ createAssertion(
201
+ [z.string(), 'to be less than or equal to', z.string()],
202
+ (subject, other) => {
203
+ if (!(subject <= other)) {
204
+ return {
205
+ actual: subject,
206
+ expected: `string less than or equal to "${other}"`,
207
+ message: `Expected "${subject}" to be less than or equal to "${other}"`,
208
+ };
209
+ }
210
+ },
211
+ ),
212
+
213
+ // String endpoint assertions
214
+ createAssertion(
215
+ [z.string(), ['to begin with', 'to start with'], z.string()],
216
+ (subject, prefix) => {
217
+ if (!subject.startsWith(prefix)) {
218
+ return {
219
+ actual: subject,
220
+ expected: `string beginning with "${prefix}"`,
221
+ message: `Expected "${subject}" to begin with "${prefix}"`,
222
+ };
223
+ }
224
+ },
225
+ ),
226
+ createAssertion(
227
+ [z.string(), 'to end with', z.string()],
228
+ (subject, suffix) => {
229
+ if (!subject.endsWith(suffix)) {
230
+ return {
231
+ actual: subject,
232
+ expected: `string ending with "${suffix}"`,
233
+ message: `Expected "${subject}" to end with "${suffix}"`,
234
+ };
235
+ }
236
+ },
237
+ ),
238
+
239
+ // One-of assertion
240
+ createAssertion(
241
+ [z.any(), 'to be one of', z.array(z.any())],
242
+ (subject, values) => {
243
+ if (!values.includes(subject)) {
244
+ return {
245
+ actual: subject as unknown,
246
+ expected: `one of [${values.map((v) => inspect(v)).join(', ')}]`,
247
+ message: `Expected ${inspect(subject)} to be one of [${values.map((v) => inspect(v)).join(', ')}]`,
248
+ };
249
+ }
250
+ },
251
+ ),
252
+
253
+ // Function arity assertion
254
+ createAssertion(
255
+ [FunctionSchema, 'to have arity', z.number().int().nonnegative()],
256
+ (subject, expectedArity) => {
257
+ if (subject.length !== expectedArity) {
258
+ return {
259
+ actual: subject.length,
260
+ expected: expectedArity,
261
+ message: `Expected function to have arity ${expectedArity}, but it has arity ${subject.length}`,
262
+ };
263
+ }
264
+ },
265
+ ),
266
+
267
+ // Error message assertions
268
+ createAssertion(
269
+ [z.instanceof(Error), 'to have message', z.string()],
270
+ (subject, expectedMessage) => {
271
+ if (subject.message !== expectedMessage) {
272
+ return {
273
+ actual: subject.message,
274
+ expected: expectedMessage,
275
+ message: `Expected error message "${subject.message}" to equal "${expectedMessage}"`,
276
+ };
277
+ }
278
+ },
279
+ ),
280
+ createAssertion(
281
+ [z.instanceof(Error), 'to have message matching', RegExpSchema],
282
+ (subject, pattern) => {
283
+ if (!pattern.test(subject.message)) {
284
+ return {
285
+ actual: subject.message,
286
+ expected: `message matching ${pattern}`,
287
+ message: `Expected error message "${subject.message}" to match ${pattern}`,
288
+ };
289
+ }
290
+ },
291
+ ),
292
+ createAssertion(
293
+ [
294
+ ['to be', 'to equal', 'equals', 'is', 'is equal to', 'to strictly equal'],
295
+ z.unknown(),
296
+ ],
297
+ (subject, value) => {
298
+ if (subject !== value) {
299
+ return {
300
+ actual: subject,
301
+ expected: value,
302
+ message: `Expected ${inspect(subject)} to equal ${inspect(value)}`,
303
+ };
304
+ }
305
+ },
306
+ ),
307
+ // @ts-expect-error fix later
308
+ createAssertion(
309
+ [
310
+ z.looseObject({}),
311
+ ['to deep equal', 'to deeply equal'],
312
+ z.looseObject({}),
313
+ ],
314
+ (_, expected) => valueToSchema(expected, { strict: true }),
315
+ ),
316
+ // @ts-expect-error fix later
317
+ createAssertion(
318
+ [ArrayLikeSchema, ['to deep equal', 'to deeply equal'], ArrayLikeSchema],
319
+ (_, expected) => valueToSchema(expected, { strict: true }),
320
+ ),
321
+ createAssertion([FunctionSchema, 'to throw'], (subject) => {
322
+ const error = trapError(subject);
323
+ if (!error) {
324
+ return {
325
+ actual: error,
326
+ message: `Expected function to throw, but it did not`,
327
+ };
328
+ }
329
+ }),
330
+ createAssertion(
331
+ [FunctionSchema, ['to throw a', 'to thrown an'], ClassSchema],
332
+ (subject, ctor) => {
333
+ const error = trapError(subject);
334
+ if (!error) {
335
+ return false;
336
+ }
337
+ if (!(error instanceof ctor)) {
338
+ let message: string;
339
+ if (isError(error)) {
340
+ message = `Expected function to throw an instance of ${ctor.name}, but it threw ${error.constructor.name}`;
341
+ } else {
342
+ message = `Expected function to throw an instance of ${ctor.name}, but it threw a non-object value: ${error as unknown}`;
343
+ }
344
+ return {
345
+ actual: error,
346
+ expected: ctor,
347
+ message,
348
+ };
349
+ }
350
+ },
351
+ ),
352
+ createAssertion(
353
+ [
354
+ FunctionSchema,
355
+ ['to throw'],
356
+ z.union([z.string(), z.instanceof(RegExp), z.looseObject({})]),
357
+ ],
358
+ (subject, param) => {
359
+ const error = trapError(subject);
360
+ if (!error) {
361
+ return false;
362
+ }
363
+
364
+ if (isString(param)) {
365
+ return z
366
+ .looseObject({
367
+ message: z.coerce.string().pipe(z.literal(param)),
368
+ })
369
+ .or(z.coerce.string().pipe(z.literal(param)))
370
+ .safeParse(error).success;
371
+ } else if (isA(param, RegExp)) {
372
+ return z
373
+ .looseObject({
374
+ message: z.coerce.string().regex(param),
375
+ })
376
+ .or(z.coerce.string().regex(param))
377
+ .safeParse(error).success;
378
+ } else if (isNonNullObject(param)) {
379
+ const schema = valueToSchema(param, {
380
+ literalPrimitives: true,
381
+ strict: true,
382
+ });
383
+ return schema.safeParse(error).success;
384
+ } else {
385
+ throw new TypeError(`Invalid parameter schema: ${inspect(param)}`);
386
+ }
387
+ },
388
+ ),
389
+ createAssertion(
390
+ [
391
+ FunctionSchema,
392
+ ['to throw a', 'to thrown an'],
393
+ ClassSchema,
394
+ 'satisfying',
395
+ z.union([z.string(), z.instanceof(RegExp), z.looseObject({})]),
396
+ ],
397
+ (subject, ctor, param) => {
398
+ const error = trapError(subject);
399
+ if (!isA(error, ctor)) {
400
+ return {
401
+ actual: error,
402
+ expected: `instance of ${ctor.name}`,
403
+ message: isError(error)
404
+ ? `Expected function to throw an instance of ${ctor.name}, but it threw ${(error as Error).constructor.name}`
405
+ : `Expected function to throw an instance of ${ctor.name}, but it threw a non-object value: ${error as unknown}`,
406
+ };
407
+ }
408
+
409
+ if (isString(param)) {
410
+ const result = z
411
+ .looseObject({
412
+ message: z.coerce.string().refine((msg) => msg.includes(param)),
413
+ })
414
+ .or(z.coerce.string().refine((str) => str.includes(param)))
415
+ .safeParse(error);
416
+ if (!result.success) {
417
+ return {
418
+ actual: isError(error) ? error.message : String(error),
419
+ expected: `error with message containing "${param}"`,
420
+ message: `Expected error message to contain "${param}", but got: ${isError(error) ? error.message : String(error)}`,
421
+ };
422
+ }
423
+ } else if (isA(param, RegExp)) {
424
+ const result = z
425
+ .looseObject({
426
+ message: z.coerce.string().regex(param),
427
+ })
428
+ .or(z.coerce.string().regex(param))
429
+ .safeParse(error);
430
+ if (!result.success) {
431
+ return {
432
+ actual: isError(error) ? error.message : String(error),
433
+ expected: `error with message matching ${param}`,
434
+ message: `Expected error message to match ${param}, but got: ${isError(error) ? error.message : String(error)}`,
435
+ };
436
+ }
437
+ } else if (isNonNullObject(param)) {
438
+ const schema = valueToSchema(param);
439
+ const result = schema.safeParse(error);
440
+ if (!result.success) {
441
+ return {
442
+ actual: error as unknown,
443
+ expected: param,
444
+ message: `Expected error to match object: ${inspect(param)}, but got: ${inspect(error)}`,
445
+ };
446
+ }
447
+ } else {
448
+ throw new TypeError(`Invalid parameter schema: ${inspect(param)}`);
449
+ }
450
+ },
451
+ ),
452
+ createAssertion(
453
+ [
454
+ z.string(),
455
+ ['includes', 'contains', 'to include', 'to contain'],
456
+ z.string(),
457
+ ],
458
+ (subject, expected) => subject.includes(expected),
459
+ ),
460
+
461
+ createAssertion([z.string(), 'to match', RegExpSchema], (subject, regex) =>
462
+ regex.test(subject),
463
+ ),
464
+ createAssertion(
465
+ [
466
+ z.looseObject({}).nonoptional(),
467
+ ['to satisfy', 'to be like'],
468
+ z.looseObject({}),
469
+ ],
470
+ (_subject, shape) => valueToSchema(shape) as z.ZodType<typeof _subject>,
471
+ ),
472
+ createAssertion(
473
+ [ArrayLikeSchema, ['to satisfy', 'to be like'], ArrayLikeSchema],
474
+ (_subject, shape) => valueToSchema(shape) as z.ZodType<typeof _subject>,
475
+ ),
476
+ createAssertion(
477
+ [FunctionSchema, 'to have arity', z.number().int().nonnegative()],
478
+ (subject, expectedArity) => {
479
+ if (subject.length !== expectedArity) {
480
+ return {
481
+ actual: subject.length,
482
+ expected: expectedArity,
483
+ message: `Expected function to have arity ${expectedArity}, but it has arity ${subject.length}`,
484
+ };
485
+ }
486
+ },
487
+ ),
488
+ ] as const; // Shared validation/match helper
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Synchronous assertion implementations.
3
+ *
4
+ * This module contains all built-in synchronous assertion implementations
5
+ * including type checks, comparisons, equality tests, object satisfaction,
6
+ * function behavior validation, and property checks. Each assertion is
7
+ * implemented with proper error handling and type safety.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import { BasicAssertions } from './sync-basic.js';
13
+ import { CollectionAssertions } from './sync-collection.js';
14
+ import { EsotericAssertions } from './sync-esoteric.js';
15
+ import { ParametricAssertions } from './sync-parametric.js';
16
+
17
+ export const SyncAssertions = [
18
+ ...CollectionAssertions,
19
+ ...BasicAssertions,
20
+ ...EsotericAssertions,
21
+ ...ParametricAssertions,
22
+ ] as const;
23
+
24
+ export {
25
+ BasicAssertions,
26
+ CollectionAssertions,
27
+ EsotericAssertions,
28
+ ParametricAssertions,
29
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Assertion creation and management utilities.
3
+ *
4
+ * This module provides the main `Assertion` class and factory functions for
5
+ * creating custom assertions. It serves as the primary interface for assertion
6
+ * construction and configuration.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ export type * from './assertion-types.js';
12
+ export { BupkisAssertion } from './assertion.js';
13
+ export { createAssertion, createAsyncAssertion } from './create.js';
14
+ export * from './impl/async.js';
15
+ export * from './impl/sync.js';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Provides {@link slotify}, which converts {@link AssertionParts} (phrases, Zod
3
+ * schemas) into {@link AssertionSlots} (Zod schemas only).
4
+ *
5
+ * `AssertionSlots` are used to match assertions against arguments to
6
+ * `expect()`.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import { inspect } from 'util';
12
+ import { z } from 'zod/v4';
13
+
14
+ import type { AssertionParts, AssertionSlots } from './assertion-types.js';
15
+
16
+ import { kStringLiteral } from '../constant.js';
17
+ import {
18
+ isPhraseLiteral,
19
+ isPhraseLiteralChoice,
20
+ isZodType,
21
+ } from '../guards.js';
22
+ import { BupkisRegistry } from '../metadata.js';
23
+
24
+ /**
25
+ * Builds slots out of assertion parts.
26
+ *
27
+ * @remarks
28
+ * This function converts {@link AssertionParts} into {@link AssertionSlots} by
29
+ * processing string literals and Zod schemas, registering metadata for runtime
30
+ * introspection, and handling validation constraints such as preventing "not "
31
+ * prefixes in string literal parts.
32
+ * @param parts Assertion parts
33
+ * @returns Slots
34
+ */
35
+ export const slotify = <const Parts extends AssertionParts>(
36
+ parts: Parts,
37
+ ): AssertionSlots<Parts> =>
38
+ parts.flatMap((part, index) => {
39
+ const result: z.ZodType[] = [];
40
+ if (index === 0 && (isPhraseLiteralChoice(part) || isPhraseLiteral(part))) {
41
+ result.push(z.unknown().describe('subject'));
42
+ }
43
+
44
+ if (isPhraseLiteralChoice(part)) {
45
+ if (part.some((p) => p.startsWith('not '))) {
46
+ throw new TypeError(
47
+ `PhraseLiteralChoice at parts[${index}] must not include phrases starting with "not ": ${inspect(
48
+ part,
49
+ )}`,
50
+ );
51
+ }
52
+ result.push(
53
+ z
54
+ .literal(part)
55
+ .brand('string-literal')
56
+ .register(BupkisRegistry, {
57
+ [kStringLiteral]: true,
58
+ values: part,
59
+ }),
60
+ );
61
+ } else if (isPhraseLiteral(part)) {
62
+ if (part.startsWith('not ')) {
63
+ throw new TypeError(
64
+ `PhraseLiteral at parts[${index}] must not start with "not ": ${inspect(
65
+ part,
66
+ )}`,
67
+ );
68
+ }
69
+ result.push(
70
+ z
71
+ .literal(part)
72
+ .brand('string-literal')
73
+ .register(BupkisRegistry, {
74
+ [kStringLiteral]: true,
75
+ value: part,
76
+ }),
77
+ );
78
+ } else {
79
+ if (!isZodType(part)) {
80
+ throw new TypeError(
81
+ `Expected Zod schema, phrase literal, or phrase literal choice at parts[${index}] but received ${inspect(
82
+ part,
83
+ )} (${typeof part})`,
84
+ );
85
+ }
86
+ result.push(part);
87
+ }
88
+ return result;
89
+ }) as unknown as AssertionSlots<Parts>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Factory function for creating the main assertion functions.
3
+ *
4
+ * This module provides the `bootstrap()` function that creates both synchronous
5
+ * and asynchronous assertion engines. It contains the core implementation
6
+ * previously split between `expect.ts` and `expect-async.ts`.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import { type Expect, type ExpectAsync } from './api.js';
12
+ import { SyncAssertions } from './assertion/impl/sync.js';
13
+ import { AsyncAssertions } from './assertion/index.js';
14
+ import {
15
+ createBaseExpect,
16
+ createExpectAsyncFunction,
17
+ createExpectSyncFunction,
18
+ } from './expect.js';
19
+
20
+ /**
21
+ * Factory function that creates both synchronous and asynchronous assertion
22
+ * engines.
23
+ *
24
+ * @returns Object containing `expect` and `expectAsync` functions
25
+ * @internal
26
+ */
27
+ export const bootstrap = (): {
28
+ expect: Expect<typeof SyncAssertions>;
29
+ expectAsync: ExpectAsync<typeof AsyncAssertions>;
30
+ } => {
31
+ const expect: Expect<typeof SyncAssertions, typeof AsyncAssertions> =
32
+ Object.assign(
33
+ createExpectSyncFunction(SyncAssertions),
34
+ createBaseExpect(SyncAssertions, AsyncAssertions, 'sync'),
35
+ );
36
+
37
+ const expectAsync: ExpectAsync<
38
+ typeof AsyncAssertions,
39
+ typeof SyncAssertions
40
+ > = Object.assign(
41
+ createExpectAsyncFunction(AsyncAssertions),
42
+ createBaseExpect(SyncAssertions, AsyncAssertions, 'async'),
43
+ );
44
+
45
+ return { expect, expectAsync };
46
+ };
47
+
48
+ const api = bootstrap();
49
+
50
+ /** {@inheritDoc Expect} */
51
+ const { expect } = api;
52
+ /** {@inheritDoc ExpectAsync} */
53
+ const { expectAsync } = api;
54
+
55
+ export { expect, expectAsync };