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.
- package/CHANGELOG.md +20 -0
- package/LICENSE.md +55 -0
- package/README.md +170 -0
- package/package.json +164 -0
- package/src/api.ts +149 -0
- package/src/assertion/assertion-async.ts +203 -0
- package/src/assertion/assertion-sync.ts +351 -0
- package/src/assertion/assertion-types.ts +964 -0
- package/src/assertion/assertion.ts +234 -0
- package/src/assertion/create.ts +226 -0
- package/src/assertion/impl/README.md +13 -0
- package/src/assertion/impl/async.ts +284 -0
- package/src/assertion/impl/index.ts +2 -0
- package/src/assertion/impl/sync-basic.ts +111 -0
- package/src/assertion/impl/sync-collection.ts +108 -0
- package/src/assertion/impl/sync-esoteric.ts +25 -0
- package/src/assertion/impl/sync-parametric.ts +488 -0
- package/src/assertion/impl/sync.ts +29 -0
- package/src/assertion/index.ts +15 -0
- package/src/assertion/slotify.ts +89 -0
- package/src/bootstrap.ts +55 -0
- package/src/constant.ts +37 -0
- package/src/error.ts +47 -0
- package/src/expect.ts +364 -0
- package/src/guards.ts +223 -0
- package/src/index.ts +29 -0
- package/src/metadata.ts +60 -0
- package/src/schema.md +15 -0
- package/src/schema.ts +464 -0
- package/src/types.ts +159 -0
- package/src/use.ts +63 -0
- package/src/util.ts +264 -0
package/src/constant.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants and symbols used throughout the library.
|
|
3
|
+
*
|
|
4
|
+
* This module defines unique symbols and constants that are used internally for
|
|
5
|
+
* marking and identifying special values and types within the assertion
|
|
6
|
+
* system.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Symbol flagging the value as a Bupkis-created string literal, which will be
|
|
14
|
+
* omitted from the parameters to an `AssertionImpl`.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const kStringLiteral: unique symbol = Symbol('bupkis:string-literal');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Symbol used to flag an `AssertionError` as our own.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const kBupkisAssertionError: unique symbol = Symbol('bupkis-error');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Symbol used to flag a `NegatedAssertionError`
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export const kBupkisNegatedAssertionError: unique symbol = Symbol(
|
|
36
|
+
'bupkis-negated-error',
|
|
37
|
+
);
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types thrown by _BUPKIS_, including {@link AssertionError}.
|
|
3
|
+
*
|
|
4
|
+
* @privateRemarks
|
|
5
|
+
* Other custom errors should go here.
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AssertionError as NodeAssertionError } from 'node:assert';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
kBupkisAssertionError,
|
|
13
|
+
kBupkisNegatedAssertionError,
|
|
14
|
+
} from './constant.js';
|
|
15
|
+
import { isA } from './guards.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* _BUPKIS_' s custom `AssertionError` class, which is just a thin wrapper
|
|
19
|
+
* around Node.js' {@link NodeAssertionError AssertionError}.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export class AssertionError extends NodeAssertionError {
|
|
24
|
+
[kBupkisAssertionError] = true;
|
|
25
|
+
|
|
26
|
+
static isAssertionError(err: unknown): err is AssertionError {
|
|
27
|
+
return (
|
|
28
|
+
isA(err, NodeAssertionError) && Object.hasOwn(err, kBupkisAssertionError)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error type used internally to catch failed negated assertions.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export class NegatedAssertionError extends AssertionError {
|
|
39
|
+
[kBupkisNegatedAssertionError] = true;
|
|
40
|
+
|
|
41
|
+
static isNegatedAssertionError(err: unknown): err is NegatedAssertionError {
|
|
42
|
+
return (
|
|
43
|
+
isA(err, AssertionError) &&
|
|
44
|
+
Object.hasOwn(err, kBupkisNegatedAssertionError)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/expect.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import Debug from 'debug';
|
|
2
|
+
import { inspect } from 'util';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type Expect,
|
|
6
|
+
type ExpectAsync,
|
|
7
|
+
type ExpectAsyncFunction,
|
|
8
|
+
type ExpectAsyncProps,
|
|
9
|
+
type ExpectFunction,
|
|
10
|
+
type ExpectSyncProps,
|
|
11
|
+
} from './api.js';
|
|
12
|
+
import {
|
|
13
|
+
type AnyAsyncAssertion,
|
|
14
|
+
type AnyAsyncAssertions,
|
|
15
|
+
type AnySyncAssertion,
|
|
16
|
+
type AnySyncAssertions,
|
|
17
|
+
type AssertionAsync,
|
|
18
|
+
type AssertionImplAsync,
|
|
19
|
+
type AssertionImplSync,
|
|
20
|
+
type AssertionParts,
|
|
21
|
+
type AssertionSlots,
|
|
22
|
+
type AssertionSync,
|
|
23
|
+
type ParsedResult,
|
|
24
|
+
type ParsedValues,
|
|
25
|
+
} from './assertion/assertion-types.js';
|
|
26
|
+
import { createAssertion, createAsyncAssertion } from './assertion/create.js';
|
|
27
|
+
import { AssertionError, NegatedAssertionError } from './error.js';
|
|
28
|
+
import { isAssertionFailure, isString } from './guards.js';
|
|
29
|
+
import { createUse } from './use.js';
|
|
30
|
+
|
|
31
|
+
const debug = Debug('bupkis:expect');
|
|
32
|
+
|
|
33
|
+
export function createExpectAsyncFunction<
|
|
34
|
+
T extends AnyAsyncAssertions,
|
|
35
|
+
U extends ExpectAsync<AnyAsyncAssertions>,
|
|
36
|
+
>(assertions: T, expect: U): ExpectAsyncFunction<T & U['assertions']>;
|
|
37
|
+
export function createExpectAsyncFunction<T extends AnyAsyncAssertions>(
|
|
38
|
+
assertions: T,
|
|
39
|
+
): ExpectAsyncFunction<T>;
|
|
40
|
+
export function createExpectAsyncFunction<
|
|
41
|
+
T extends AnyAsyncAssertions,
|
|
42
|
+
U extends ExpectAsync<AnyAsyncAssertions>,
|
|
43
|
+
>(assertions: T, expect?: U) {
|
|
44
|
+
debug(
|
|
45
|
+
'Creating expectAsync function with %d assertions',
|
|
46
|
+
assertions.length + (expect?.assertions.length ?? 0),
|
|
47
|
+
);
|
|
48
|
+
const expectAsyncFunction = async (...args: readonly unknown[]) => {
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
const [isNegated, processedArgs] = maybeProcessNegation(args);
|
|
51
|
+
const candidates: Array<{
|
|
52
|
+
assertion: AnyAsyncAssertion;
|
|
53
|
+
parseResult: ParsedResult<AssertionParts>;
|
|
54
|
+
}> = [];
|
|
55
|
+
for (const assertion of [...(expect?.assertions ?? []), ...assertions]) {
|
|
56
|
+
const parseResult = await assertion.parseValuesAsync(processedArgs);
|
|
57
|
+
const { exactMatch, parsedValues, success } = parseResult;
|
|
58
|
+
|
|
59
|
+
if (success) {
|
|
60
|
+
if (exactMatch) {
|
|
61
|
+
return executeAsync(
|
|
62
|
+
assertion,
|
|
63
|
+
parsedValues,
|
|
64
|
+
[...args],
|
|
65
|
+
expectAsyncFunction,
|
|
66
|
+
isNegated,
|
|
67
|
+
parseResult,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
candidates.push({ assertion, parseResult });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (candidates.length) {
|
|
74
|
+
const { assertion, parseResult } = candidates[0]!;
|
|
75
|
+
return executeAsync(
|
|
76
|
+
assertion as any,
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
78
|
+
parseResult.parsedValues as any,
|
|
79
|
+
[...args],
|
|
80
|
+
expectAsyncFunction,
|
|
81
|
+
isNegated,
|
|
82
|
+
parseResult,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
throwInvalidParametersError(args);
|
|
86
|
+
};
|
|
87
|
+
return expectAsyncFunction;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createExpectSyncFunction<
|
|
91
|
+
T extends AnySyncAssertions,
|
|
92
|
+
U extends Expect<AnySyncAssertions>,
|
|
93
|
+
>(assertions: T, expect: U): ExpectFunction<T & U['assertions']>;
|
|
94
|
+
|
|
95
|
+
export function createExpectSyncFunction<T extends AnySyncAssertions>(
|
|
96
|
+
assertions: T,
|
|
97
|
+
): ExpectFunction<T>;
|
|
98
|
+
export function createExpectSyncFunction<
|
|
99
|
+
T extends AnySyncAssertions,
|
|
100
|
+
U extends Expect<AnySyncAssertions>,
|
|
101
|
+
>(assertions: T, expect?: U) {
|
|
102
|
+
debug(
|
|
103
|
+
'Creating expect function with %d assertions',
|
|
104
|
+
assertions.length + (expect?.assertions.length ?? 0),
|
|
105
|
+
);
|
|
106
|
+
const expectFunction = (...args: readonly unknown[]) => {
|
|
107
|
+
const [isNegated, processedArgs] = maybeProcessNegation(args);
|
|
108
|
+
const candidates: Array<{
|
|
109
|
+
assertion: AnySyncAssertion;
|
|
110
|
+
parseResult: ParsedResult<AssertionParts>;
|
|
111
|
+
}> = [];
|
|
112
|
+
for (const assertion of [...(expect?.assertions ?? []), ...assertions]) {
|
|
113
|
+
const parseResult = assertion.parseValues(processedArgs);
|
|
114
|
+
const { exactMatch, parsedValues, success } = parseResult;
|
|
115
|
+
|
|
116
|
+
if (success) {
|
|
117
|
+
if (exactMatch) {
|
|
118
|
+
return execute(
|
|
119
|
+
assertion,
|
|
120
|
+
parsedValues,
|
|
121
|
+
[...args],
|
|
122
|
+
expectFunction,
|
|
123
|
+
isNegated,
|
|
124
|
+
parseResult,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
candidates.push({ assertion, parseResult });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (candidates.length) {
|
|
131
|
+
const { assertion, parseResult } = candidates[0]!;
|
|
132
|
+
return execute(
|
|
133
|
+
assertion as any,
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
135
|
+
parseResult.parsedValues as any,
|
|
136
|
+
[...args],
|
|
137
|
+
expectFunction,
|
|
138
|
+
isNegated,
|
|
139
|
+
parseResult,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
throwInvalidParametersError(args);
|
|
143
|
+
};
|
|
144
|
+
return expectFunction;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Executes an assertion with optional negation logic.
|
|
149
|
+
*
|
|
150
|
+
* @privateRemarks
|
|
151
|
+
* This is here because `Assertion` doesn't know anything about negation and
|
|
152
|
+
* probably shouldn't.
|
|
153
|
+
* @param assertion - The assertion to execute
|
|
154
|
+
* @param parsedValues - Parsed values for the assertion
|
|
155
|
+
* @param args - Original arguments passed to expect
|
|
156
|
+
* @param stackStartFn - Function for stack trace management
|
|
157
|
+
* @param isNegated - Whether the assertion should be negated
|
|
158
|
+
*/
|
|
159
|
+
const execute = <
|
|
160
|
+
T extends AssertionSync<Parts, AssertionImplSync<Parts>, Slots>,
|
|
161
|
+
Parts extends AssertionParts,
|
|
162
|
+
Slots extends AssertionSlots<Parts>,
|
|
163
|
+
>(
|
|
164
|
+
assertion: T,
|
|
165
|
+
parsedValues: ParsedValues<Parts>,
|
|
166
|
+
args: unknown[],
|
|
167
|
+
stackStartFn: (...args: any[]) => any,
|
|
168
|
+
isNegated: boolean,
|
|
169
|
+
parseResult?: ParsedResult<Parts>,
|
|
170
|
+
): void => {
|
|
171
|
+
if (!isNegated) {
|
|
172
|
+
return assertion.execute(parsedValues, args, stackStartFn, parseResult);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
debug('Executing negated assertion: %s', assertion);
|
|
177
|
+
const result = assertion.execute(
|
|
178
|
+
parsedValues,
|
|
179
|
+
args,
|
|
180
|
+
stackStartFn,
|
|
181
|
+
parseResult,
|
|
182
|
+
);
|
|
183
|
+
if (isAssertionFailure(result)) {
|
|
184
|
+
throw new NegatedAssertionError({
|
|
185
|
+
actual: result.actual,
|
|
186
|
+
expected: result.expected,
|
|
187
|
+
message:
|
|
188
|
+
result.message ??
|
|
189
|
+
`Expected assertion ${assertion} to fail (due to negation), but it passed`,
|
|
190
|
+
stackStartFn,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// If we reach here, the assertion passed but we expected it to fail
|
|
194
|
+
throw new NegatedAssertionError({
|
|
195
|
+
message: `Expected assertion to fail (due to negation), but it passed: ${assertion}`,
|
|
196
|
+
stackStartFn,
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// Check if this is the negation error we just threw
|
|
200
|
+
if (NegatedAssertionError.isNegatedAssertionError(error)) {
|
|
201
|
+
// This is our negation error, re-throw it
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (AssertionError.isAssertionError(error)) {
|
|
206
|
+
// The assertion failed as expected for negation - this is success
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
debug('Non-assertion error thrown during negated assertion: %O', error);
|
|
211
|
+
// Re-throw non-assertion errors (like TypeErrors, etc.)
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Executes an assertion with optional negation logic (async version).
|
|
218
|
+
*
|
|
219
|
+
* @privateRemarks
|
|
220
|
+
* This is here because `Assertion` doesn't know anything about negation and
|
|
221
|
+
* probably shouldn't.
|
|
222
|
+
* @param assertion - The assertion to execute
|
|
223
|
+
* @param parsedValues - Parsed values for the assertion
|
|
224
|
+
* @param args - Original arguments passed to expectAsync
|
|
225
|
+
* @param stackStartFn - Function for stack trace management
|
|
226
|
+
* @param isNegated - Whether the assertion should be negated
|
|
227
|
+
*/
|
|
228
|
+
export const executeAsync = async <
|
|
229
|
+
T extends AssertionAsync<Parts, AssertionImplAsync<Parts>, Slots>,
|
|
230
|
+
Parts extends AssertionParts,
|
|
231
|
+
Slots extends AssertionSlots<Parts>,
|
|
232
|
+
>(
|
|
233
|
+
assertion: T,
|
|
234
|
+
parsedValues: ParsedValues<Parts>,
|
|
235
|
+
args: unknown[],
|
|
236
|
+
stackStartFn: (...args: any[]) => any,
|
|
237
|
+
isNegated: boolean,
|
|
238
|
+
parseResult?: ParsedResult<Parts>,
|
|
239
|
+
): Promise<void> => {
|
|
240
|
+
if (!isNegated) {
|
|
241
|
+
return assertion.executeAsync(
|
|
242
|
+
parsedValues,
|
|
243
|
+
args,
|
|
244
|
+
stackStartFn,
|
|
245
|
+
parseResult,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
debug('Executing negated async assertion: %s', assertion);
|
|
251
|
+
await assertion.executeAsync(parsedValues, args, stackStartFn, parseResult);
|
|
252
|
+
// If we reach here, the assertion passed but we expected it to fail
|
|
253
|
+
throw new NegatedAssertionError({
|
|
254
|
+
message: `Expected assertion to fail (due to negation), but it passed: ${assertion}`,
|
|
255
|
+
stackStartFn,
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Check if this is the negation error we just threw
|
|
259
|
+
if (NegatedAssertionError.isNegatedAssertionError(error)) {
|
|
260
|
+
// This is our negation error, re-throw it
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (AssertionError.isAssertionError(error)) {
|
|
265
|
+
// The assertion failed as expected for negation - this is success
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
debug(
|
|
269
|
+
'Non-assertion error thrown during negated async assertion: %O',
|
|
270
|
+
error,
|
|
271
|
+
);
|
|
272
|
+
// Re-throw non-assertion errors (like TypeErrors, etc.)
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
const maybeProcessNegation = (
|
|
281
|
+
args: readonly unknown[],
|
|
282
|
+
): [isNegated: boolean, processedArgs: readonly unknown[]] => {
|
|
283
|
+
let isNegated = false;
|
|
284
|
+
let processedArgs = args;
|
|
285
|
+
|
|
286
|
+
if (args.length >= 2 && isString(args[1])) {
|
|
287
|
+
const { cleanedPhrase, isNegated: detected } = detectNegation(args[1]);
|
|
288
|
+
if (detected) {
|
|
289
|
+
isNegated = true;
|
|
290
|
+
processedArgs = [args[0], cleanedPhrase, ...args.slice(2)];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return [isNegated, processedArgs];
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @internal
|
|
298
|
+
*/
|
|
299
|
+
const throwInvalidParametersError = (args: readonly unknown[]): never => {
|
|
300
|
+
const inspectedArgs = inspect(args, { depth: 1 });
|
|
301
|
+
debug(`Invalid arguments. No assertion matched: ${inspectedArgs}`);
|
|
302
|
+
throw new TypeError(
|
|
303
|
+
`Invalid arguments. No assertion matched: ${inspectedArgs}`,
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Detects if an assertion phrase starts with "not " and returns the cleaned
|
|
309
|
+
* phrase.
|
|
310
|
+
*
|
|
311
|
+
* @param phrase - The assertion phrase to check
|
|
312
|
+
* @returns Object with `isNegated` flag and `cleanedPhrase`
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
const detectNegation = (
|
|
316
|
+
phrase: string,
|
|
317
|
+
): {
|
|
318
|
+
cleanedPhrase: string;
|
|
319
|
+
isNegated: boolean;
|
|
320
|
+
} => {
|
|
321
|
+
if (phrase.startsWith('not ')) {
|
|
322
|
+
return {
|
|
323
|
+
cleanedPhrase: phrase.substring(4), // Remove "not "
|
|
324
|
+
isNegated: true,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
cleanedPhrase: phrase,
|
|
329
|
+
isNegated: false,
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const fail = (reason?: string): never => {
|
|
334
|
+
throw new AssertionError({ message: reason });
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export function createBaseExpect<
|
|
338
|
+
T extends AnySyncAssertions,
|
|
339
|
+
U extends AnyAsyncAssertions,
|
|
340
|
+
>(syncAssertions: T, asyncAssertions: U, type: 'sync'): ExpectSyncProps<T, U>;
|
|
341
|
+
export function createBaseExpect<
|
|
342
|
+
T extends AnySyncAssertions,
|
|
343
|
+
U extends AnyAsyncAssertions,
|
|
344
|
+
>(syncAssertions: T, asyncAssertions: U, type: 'async'): ExpectAsyncProps<U, T>;
|
|
345
|
+
export function createBaseExpect<
|
|
346
|
+
T extends AnySyncAssertions,
|
|
347
|
+
U extends AnyAsyncAssertions,
|
|
348
|
+
>(syncAssertions: T, asyncAssertions: U, type: 'async' | 'sync') {
|
|
349
|
+
return type === 'sync'
|
|
350
|
+
? {
|
|
351
|
+
assertions: syncAssertions,
|
|
352
|
+
createAssertion,
|
|
353
|
+
createAsyncAssertion,
|
|
354
|
+
fail,
|
|
355
|
+
use: createUse(syncAssertions, asyncAssertions),
|
|
356
|
+
}
|
|
357
|
+
: {
|
|
358
|
+
assertions: asyncAssertions,
|
|
359
|
+
createAssertion,
|
|
360
|
+
createAsyncAssertion,
|
|
361
|
+
fail,
|
|
362
|
+
use: createUse(syncAssertions, asyncAssertions),
|
|
363
|
+
};
|
|
364
|
+
}
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard functions and runtime type checking utilities.
|
|
3
|
+
*
|
|
4
|
+
* This module provides various type guard functions for runtime type checking,
|
|
5
|
+
* including guards for Zod schemas, constructors, Promise-like objects, and
|
|
6
|
+
* assertion parts. These are used throughout the library for safe type
|
|
7
|
+
* narrowing and validation.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { type Primitive } from 'type-fest';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
AssertionFailure,
|
|
17
|
+
AssertionPart,
|
|
18
|
+
PhraseLiteralChoice,
|
|
19
|
+
} from './assertion/assertion-types.js';
|
|
20
|
+
import type { Constructor } from './types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the given value looks like a Zod schema (v4), determined by
|
|
24
|
+
* the presence of an internal `def.type` field.
|
|
25
|
+
*
|
|
26
|
+
* Note: This relies on Zod's internal shape and is intended for runtime
|
|
27
|
+
* discrimination within this library.
|
|
28
|
+
*
|
|
29
|
+
* @template T
|
|
30
|
+
* @param value - Value to test
|
|
31
|
+
* @returns Whether the value is Zod-like
|
|
32
|
+
*/
|
|
33
|
+
export const isZodType = (value: unknown): value is z.ZodType =>
|
|
34
|
+
!!(
|
|
35
|
+
value &&
|
|
36
|
+
typeof value === 'object' &&
|
|
37
|
+
'def' in value &&
|
|
38
|
+
value.def &&
|
|
39
|
+
typeof value.def === 'object' &&
|
|
40
|
+
'type' in value.def
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns true if the given value is a {@link z.ZodPromise} schema.
|
|
45
|
+
*
|
|
46
|
+
* @param value - Value to test
|
|
47
|
+
* @returns `true` if the value is a `ZodPromise` schema; `false` otherwise
|
|
48
|
+
*/
|
|
49
|
+
export const isZodPromise = (value: unknown): value is z.ZodPromise =>
|
|
50
|
+
isZodType(value) && value.def.type === 'promise';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if a value is "promise-like", meaning it is a "thenable" object.
|
|
54
|
+
*
|
|
55
|
+
* @param value - Value to test
|
|
56
|
+
* @returns `true` if the value is promise-like, `false` otherwise
|
|
57
|
+
*/
|
|
58
|
+
export const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
|
|
59
|
+
!!(
|
|
60
|
+
value &&
|
|
61
|
+
typeof value === 'object' &&
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
63
|
+
typeof (value as any).then === 'function'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the given value is a constructable function (i.e., a class).
|
|
68
|
+
*
|
|
69
|
+
* This may be the only way we can determine, at runtime, if a function is a
|
|
70
|
+
* constructor without actually calling it.
|
|
71
|
+
*
|
|
72
|
+
* @param fn - Function to test
|
|
73
|
+
* @returns Whether the function is constructable
|
|
74
|
+
*/
|
|
75
|
+
export const isConstructable = (fn: any): fn is Constructor => {
|
|
76
|
+
try {
|
|
77
|
+
// this will throw if there is no `[[construct]]` slot.. or so I've heard.
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
79
|
+
new new Proxy(fn, { construct: () => ({}) })();
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Type guard for a boolean value
|
|
88
|
+
*
|
|
89
|
+
* @param value Value to check
|
|
90
|
+
* @returns `true` if the value is a boolean, `false` otherwise
|
|
91
|
+
*/
|
|
92
|
+
export const isBoolean = (value: unknown): value is boolean =>
|
|
93
|
+
typeof value === 'boolean';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Type guard for a function value
|
|
97
|
+
*
|
|
98
|
+
* @param value Value to check
|
|
99
|
+
* @returns `true` if the value is a function, `false` otherwise
|
|
100
|
+
*/
|
|
101
|
+
export const isFunction = (value: unknown): value is (...args: any[]) => any =>
|
|
102
|
+
typeof value === 'function';
|
|
103
|
+
|
|
104
|
+
const AssertionFailureSchema: z.ZodType<AssertionFailure> = z.object({
|
|
105
|
+
actual: z
|
|
106
|
+
.unknown()
|
|
107
|
+
.optional()
|
|
108
|
+
.describe('The actual value or description of what actually occurred'),
|
|
109
|
+
expected: z
|
|
110
|
+
.unknown()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe(
|
|
113
|
+
'The expected value or description of what was expected to occur',
|
|
114
|
+
),
|
|
115
|
+
message: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('A human-readable message describing the failure'),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const isAssertionFailure = (value: unknown): value is AssertionFailure =>
|
|
122
|
+
AssertionFailureSchema.safeParse(value).success;
|
|
123
|
+
|
|
124
|
+
export const isAsyncFunction = (
|
|
125
|
+
value: unknown,
|
|
126
|
+
): value is (...args: any[]) => Promise<any> =>
|
|
127
|
+
isFunction(value) && value.constructor.name === 'AsyncFunction';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Type guard for a string value
|
|
131
|
+
*
|
|
132
|
+
* @param value Value to check
|
|
133
|
+
* @returns `true` if the value is a string, `false` otherwise
|
|
134
|
+
*/
|
|
135
|
+
export const isString = (value: unknown): value is string =>
|
|
136
|
+
typeof value === 'string';
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Type guard for a non-null object value
|
|
140
|
+
*
|
|
141
|
+
* @param value Value to check
|
|
142
|
+
* @returns `true` if the value is an object and not null, `false` otherwise
|
|
143
|
+
*/
|
|
144
|
+
export const isNonNullObject = (value: unknown): value is object =>
|
|
145
|
+
typeof value === 'object' && value !== null;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Type guard for a null or non-object value
|
|
149
|
+
*
|
|
150
|
+
* @param value Value to check
|
|
151
|
+
* @returns `true` if the value is null or not an object, `false` otherwise
|
|
152
|
+
*/
|
|
153
|
+
export const isNullOrNonObject = (value: unknown): value is null | Primitive =>
|
|
154
|
+
typeof value !== 'object' || value === null;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Type guard for a {@link PhraseLiteralChoice}, which is a tuple of strings.
|
|
158
|
+
*
|
|
159
|
+
* @param value Assertion part to check
|
|
160
|
+
* @returns `true` if the part is a `PhraseLiteralChoice`, `false` otherwise
|
|
161
|
+
* @internal
|
|
162
|
+
*/
|
|
163
|
+
export const isPhraseLiteralChoice = (
|
|
164
|
+
value: AssertionPart,
|
|
165
|
+
): value is PhraseLiteralChoice =>
|
|
166
|
+
Array.isArray(value) && value.every(isPhraseLiteral);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Type guard for a {@link PhraseLiteral}, which is just a string that does not
|
|
170
|
+
* begin with `not `.
|
|
171
|
+
*
|
|
172
|
+
* @param value Assertion part to check
|
|
173
|
+
* @returns `true` if the part is a `PhraseLiteral`, `false` otherwise
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
export const isPhraseLiteral = (value: AssertionPart): value is string =>
|
|
177
|
+
isString(value) && !value.startsWith('not ');
|
|
178
|
+
|
|
179
|
+
export type PrimitiveTypeName =
|
|
180
|
+
| 'bigint'
|
|
181
|
+
| 'boolean'
|
|
182
|
+
| 'function'
|
|
183
|
+
| 'null'
|
|
184
|
+
| 'number'
|
|
185
|
+
| 'object'
|
|
186
|
+
| 'string'
|
|
187
|
+
| 'symbol'
|
|
188
|
+
| 'undefined';
|
|
189
|
+
|
|
190
|
+
export type PrimitiveTypeNameToType<T extends PrimitiveTypeName> =
|
|
191
|
+
T extends 'undefined'
|
|
192
|
+
? undefined
|
|
193
|
+
: T extends 'object'
|
|
194
|
+
? null | object
|
|
195
|
+
: T extends 'function'
|
|
196
|
+
? (...args: any[]) => any
|
|
197
|
+
: T extends 'string'
|
|
198
|
+
? string
|
|
199
|
+
: T extends 'number'
|
|
200
|
+
? number
|
|
201
|
+
: T extends 'boolean'
|
|
202
|
+
? boolean
|
|
203
|
+
: T extends 'bigint'
|
|
204
|
+
? bigint
|
|
205
|
+
: T extends 'symbol'
|
|
206
|
+
? symbol
|
|
207
|
+
: never;
|
|
208
|
+
|
|
209
|
+
export const isType = <T extends PrimitiveTypeName>(
|
|
210
|
+
a: unknown,
|
|
211
|
+
b: T,
|
|
212
|
+
): a is PrimitiveTypeNameToType<T> => {
|
|
213
|
+
return typeof a === b;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const isA = <T extends Constructor>(
|
|
217
|
+
value: unknown,
|
|
218
|
+
ctor: T,
|
|
219
|
+
): value is InstanceType<T> => {
|
|
220
|
+
return isNonNullObject(value) && value instanceof ctor;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const isError = (value: unknown): value is Error => isA(value, Error);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for the Bupkis assertion library.
|
|
3
|
+
*
|
|
4
|
+
* This module exports all public APIs including the main `expect` function,
|
|
5
|
+
* asynchronous `expectAsync` function, assertion creation utilities, type
|
|
6
|
+
* guards, schema definitions, utility functions, and error types.
|
|
7
|
+
*
|
|
8
|
+
* @module bupkis
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { expect as sacrificialExpect } from './bootstrap.js';
|
|
12
|
+
export type * from './api.js';
|
|
13
|
+
|
|
14
|
+
export * as assertion from './assertion/index.js';
|
|
15
|
+
export { expect, expectAsync } from './bootstrap.js';
|
|
16
|
+
|
|
17
|
+
export * as error from './error.js';
|
|
18
|
+
export * as guards from './guards.js';
|
|
19
|
+
|
|
20
|
+
export type * as metadata from './metadata.js';
|
|
21
|
+
export * as schema from './schema.js';
|
|
22
|
+
export type * as types from './types.js';
|
|
23
|
+
export * as util from './util.js';
|
|
24
|
+
|
|
25
|
+
export { z } from 'zod/v4';
|
|
26
|
+
|
|
27
|
+
export { createAssertion, createAsyncAssertion, fail, use };
|
|
28
|
+
const { createAssertion, createAsyncAssertion, fail, use, ..._rest } =
|
|
29
|
+
sacrificialExpect;
|