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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import Debug from 'debug';
|
|
2
|
+
import { inspect } from 'util';
|
|
3
|
+
import z from 'zod/v4';
|
|
4
|
+
|
|
5
|
+
import { kStringLiteral } from '../constant.js';
|
|
6
|
+
import { AssertionError } from '../error.js';
|
|
7
|
+
import { isA, isAssertionFailure, isBoolean, isZodType } from '../guards.js';
|
|
8
|
+
import { BupkisRegistry } from '../metadata.js';
|
|
9
|
+
import {
|
|
10
|
+
type AssertionAsync,
|
|
11
|
+
type AssertionFunctionAsync,
|
|
12
|
+
type AssertionImplAsync,
|
|
13
|
+
type AssertionImplFnAsync,
|
|
14
|
+
type AssertionImplSchemaAsync,
|
|
15
|
+
type AssertionParts,
|
|
16
|
+
type AssertionSchemaAsync,
|
|
17
|
+
type AssertionSlots,
|
|
18
|
+
type ParsedResult,
|
|
19
|
+
type ParsedValues,
|
|
20
|
+
} from './assertion-types.js';
|
|
21
|
+
import { BupkisAssertion } from './assertion.js';
|
|
22
|
+
const debug = Debug('bupkis:assertion:async');
|
|
23
|
+
|
|
24
|
+
export abstract class BupkisAssertionAsync<
|
|
25
|
+
Parts extends AssertionParts,
|
|
26
|
+
Impl extends AssertionImplAsync<Parts>,
|
|
27
|
+
Slots extends AssertionSlots<Parts>,
|
|
28
|
+
>
|
|
29
|
+
extends BupkisAssertion<Parts, Impl, Slots>
|
|
30
|
+
implements AssertionAsync<Parts, Impl, Slots>
|
|
31
|
+
{
|
|
32
|
+
abstract executeAsync(
|
|
33
|
+
parsedValues: ParsedValues<Parts>,
|
|
34
|
+
args: unknown[],
|
|
35
|
+
stackStartFn: (...args: any[]) => any,
|
|
36
|
+
parseResult?: ParsedResult<Parts>,
|
|
37
|
+
): Promise<void>;
|
|
38
|
+
|
|
39
|
+
async parseValuesAsync<Args extends readonly unknown[]>(
|
|
40
|
+
args: Args,
|
|
41
|
+
): Promise<ParsedResult<Parts>> {
|
|
42
|
+
const { slots } = this;
|
|
43
|
+
const parsedValues: any[] = [];
|
|
44
|
+
if (slots.length !== args.length) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
let exactMatch = true;
|
|
50
|
+
for (let i = 0; i < slots.length; i++) {
|
|
51
|
+
const slot = slots[i]!;
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
|
|
54
|
+
const parsedLiteralResult = this.parseSlotForLiteral(slot, i, arg);
|
|
55
|
+
if (parsedLiteralResult === true) {
|
|
56
|
+
continue;
|
|
57
|
+
} else if (parsedLiteralResult !== false) {
|
|
58
|
+
return parsedLiteralResult;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// unknown/any accept anything
|
|
62
|
+
// IMPORTANT: do not use a type guard here; it will break inference
|
|
63
|
+
if (slot.def.type === 'unknown' || slot.def.type === 'any') {
|
|
64
|
+
debug('Skipping unknown/any slot validation for arg', arg);
|
|
65
|
+
parsedValues.push(arg);
|
|
66
|
+
exactMatch = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const result = await slot.safeParseAsync(arg);
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
parsedValues.push(result.data);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
exactMatch,
|
|
79
|
+
parsedValues: parsedValues as unknown as ParsedValues<Parts>,
|
|
80
|
+
success: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* An assertion implemented as a Zod schema.
|
|
86
|
+
*
|
|
87
|
+
* Async schemas are supported via {@link expectAsync}.
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* Optimized schema assertion that performs subject validation during
|
|
91
|
+
* parseValues() to eliminate double parsing for simple schema-based
|
|
92
|
+
* assertions.
|
|
93
|
+
*
|
|
94
|
+
* This class implements Option 2 from the z.function() analysis - it caches the
|
|
95
|
+
* subject validation result during argument parsing and reuses it during
|
|
96
|
+
* execution, eliminating the double parsing overhead.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
export class BupkisAssertionFunctionAsync<
|
|
100
|
+
Parts extends AssertionParts,
|
|
101
|
+
Impl extends AssertionImplFnAsync<Parts>,
|
|
102
|
+
Slots extends AssertionSlots<Parts>,
|
|
103
|
+
>
|
|
104
|
+
extends BupkisAssertionAsync<Parts, Impl, Slots>
|
|
105
|
+
implements AssertionFunctionAsync<Parts, Impl, Slots>
|
|
106
|
+
{
|
|
107
|
+
override async executeAsync(
|
|
108
|
+
parsedValues: ParsedValues<Parts>,
|
|
109
|
+
args: unknown[],
|
|
110
|
+
stackStartFn: (...args: any[]) => any,
|
|
111
|
+
_parseResult?: ParsedResult<Parts>,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const { impl } = this;
|
|
114
|
+
const result = await impl(...parsedValues);
|
|
115
|
+
if (isZodType(result)) {
|
|
116
|
+
try {
|
|
117
|
+
await result.parseAsync(parsedValues[0]);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (isA(error, z.ZodError)) {
|
|
120
|
+
throw this.translateZodError(stackStartFn, error, ...parsedValues);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
} else if (isBoolean(result)) {
|
|
125
|
+
if (!result) {
|
|
126
|
+
throw new AssertionError({
|
|
127
|
+
message: `Assertion ${this} failed for arguments: ${inspect(args)}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} else if (isAssertionFailure(result)) {
|
|
131
|
+
throw new AssertionError({
|
|
132
|
+
actual: result.actual,
|
|
133
|
+
expected: result.expected,
|
|
134
|
+
message: result.message ?? `Assertion ${this} failed`,
|
|
135
|
+
});
|
|
136
|
+
} else if (result as unknown) {
|
|
137
|
+
throw new TypeError(
|
|
138
|
+
`Invalid return type from assertion ${this}; expected boolean, ZodType, or AssertionFailure`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A class representing an assertion implemented as a function.
|
|
146
|
+
*
|
|
147
|
+
* This function may:
|
|
148
|
+
*
|
|
149
|
+
* 1. Return a `boolean` indicating pass/fail.
|
|
150
|
+
* 2. Return a `ZodType` which will be used to validate the subject.
|
|
151
|
+
* 3. Return a `Promise` resolving to either of the above (when called via
|
|
152
|
+
* {@link expectAsync})
|
|
153
|
+
* 4. Throw a {@link AssertionError}; when called via {@link expectAsync}, reject
|
|
154
|
+
* with an {@link AssertionError}
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
export class BupkisAssertionSchemaAsync<
|
|
158
|
+
Parts extends AssertionParts,
|
|
159
|
+
Impl extends AssertionImplSchemaAsync<Parts>,
|
|
160
|
+
Slots extends AssertionSlots<Parts>,
|
|
161
|
+
>
|
|
162
|
+
extends BupkisAssertionAsync<Parts, Impl, Slots>
|
|
163
|
+
implements AssertionSchemaAsync<Parts, Impl, Slots>
|
|
164
|
+
{
|
|
165
|
+
override async executeAsync(
|
|
166
|
+
parsedValues: ParsedValues<Parts>,
|
|
167
|
+
_args: unknown[],
|
|
168
|
+
stackStartFn: (...args: any[]) => any,
|
|
169
|
+
_parseResult?: ParsedResult<Parts>,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
// For async, fall back to standard implementation for now
|
|
172
|
+
const [subject] = parsedValues;
|
|
173
|
+
try {
|
|
174
|
+
await this.impl.parseAsync(subject);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (isA(error, z.ZodError)) {
|
|
177
|
+
throw this.translateZodError(stackStartFn, error, ...parsedValues);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Determines if this assertion can be optimized (simple single-subject
|
|
185
|
+
* schema). Only simple assertions like ['to be a string'] with z.string()
|
|
186
|
+
* qualify.
|
|
187
|
+
*/
|
|
188
|
+
private isSimpleSchemaAssertion(): boolean {
|
|
189
|
+
// Only optimize if we have exactly one subject slot + string literal slots
|
|
190
|
+
// and no complex argument processing
|
|
191
|
+
const hasSubjectSlot =
|
|
192
|
+
this.slots.length > 0 &&
|
|
193
|
+
(this.slots[0]?.def.type === 'unknown' ||
|
|
194
|
+
this.slots[0]?.def.type === 'any');
|
|
195
|
+
|
|
196
|
+
const allOtherSlotsAreLiterals = this.slots.slice(1).every((slot) => {
|
|
197
|
+
const meta = BupkisRegistry.get(slot) ?? {};
|
|
198
|
+
return kStringLiteral in meta;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return hasSubjectSlot && allOtherSlotsAreLiterals;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous assertion subclasses.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @see {@link AssertionFunctionSync} for function-based assertions
|
|
6
|
+
* @see {@link AssertionSchemaSync} for schema-based assertions
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Debug from 'debug';
|
|
10
|
+
import { inspect } from 'util';
|
|
11
|
+
import { type z } from 'zod/v4';
|
|
12
|
+
|
|
13
|
+
import { kStringLiteral } from '../constant.js';
|
|
14
|
+
import { AssertionError } from '../error.js';
|
|
15
|
+
import {
|
|
16
|
+
isAssertionFailure,
|
|
17
|
+
isBoolean,
|
|
18
|
+
isPromiseLike,
|
|
19
|
+
isZodPromise,
|
|
20
|
+
isZodType,
|
|
21
|
+
} from '../guards.js';
|
|
22
|
+
import { BupkisRegistry } from '../metadata.js';
|
|
23
|
+
import {
|
|
24
|
+
type AssertionFunctionSync,
|
|
25
|
+
type AssertionImplFnSync,
|
|
26
|
+
type AssertionImplSchemaSync,
|
|
27
|
+
type AssertionImplSync,
|
|
28
|
+
type AssertionParts,
|
|
29
|
+
type AssertionSchemaSync,
|
|
30
|
+
type AssertionSlots,
|
|
31
|
+
type AssertionSync,
|
|
32
|
+
type ParsedResult,
|
|
33
|
+
type ParsedResultSuccess,
|
|
34
|
+
type ParsedValues,
|
|
35
|
+
} from './assertion-types.js';
|
|
36
|
+
import { BupkisAssertion } from './assertion.js';
|
|
37
|
+
|
|
38
|
+
const debug = Debug('bupkis:assertion:sync');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Abstract class for synchronous assertions.
|
|
42
|
+
*
|
|
43
|
+
* Child classes are expected to implement {@link execute}.
|
|
44
|
+
*/
|
|
45
|
+
export abstract class BupkisAssertionSync<
|
|
46
|
+
Parts extends AssertionParts,
|
|
47
|
+
Impl extends AssertionImplSync<Parts>,
|
|
48
|
+
Slots extends AssertionSlots<Parts>,
|
|
49
|
+
>
|
|
50
|
+
extends BupkisAssertion<Parts, Impl, Slots>
|
|
51
|
+
implements AssertionSync<Parts, Impl, Slots>
|
|
52
|
+
{
|
|
53
|
+
/**
|
|
54
|
+
* Parses raw arguments synchronously against this `Assertion`'s Slots to
|
|
55
|
+
* determine if they match this `Assertion`.
|
|
56
|
+
*
|
|
57
|
+
* @param args Raw arguments provided to `expect()`
|
|
58
|
+
* @returns Result of parsing attempt
|
|
59
|
+
*/
|
|
60
|
+
abstract execute(
|
|
61
|
+
parsedValues: ParsedValues<Parts>,
|
|
62
|
+
args: unknown[],
|
|
63
|
+
stackStartFn: (...args: any[]) => any,
|
|
64
|
+
parseResult?: ParsedResult<Parts>,
|
|
65
|
+
): void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parses raw arguments against the slots of this assertion to determine if
|
|
69
|
+
* this assertion should be executed against those arguments.
|
|
70
|
+
*
|
|
71
|
+
* For example, if an assertion wants the subject to be a `z.string()`, then
|
|
72
|
+
* this will validate that the first raw arg parses as a string. It will also
|
|
73
|
+
* validate Phrase Literals as well, such as "to be a string". If all slots
|
|
74
|
+
* match and none of the slots are "unknown" or "any", then `exactMatch` will
|
|
75
|
+
* be true.
|
|
76
|
+
*
|
|
77
|
+
* If any slot does not match, this returns `success: false`.
|
|
78
|
+
*
|
|
79
|
+
* @param args Raw arguments
|
|
80
|
+
* @returns Result of parsing attempt
|
|
81
|
+
*/
|
|
82
|
+
parseValues<Args extends readonly unknown[]>(
|
|
83
|
+
args: Args,
|
|
84
|
+
): ParsedResult<Parts> {
|
|
85
|
+
const { slots } = this;
|
|
86
|
+
const parsedValues: any[] = [];
|
|
87
|
+
|
|
88
|
+
const mismatch = this.maybeParseValuesArgMismatch(args);
|
|
89
|
+
if (mismatch) {
|
|
90
|
+
return mismatch;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let exactMatch = true;
|
|
94
|
+
for (let i = 0; i < slots.length; i++) {
|
|
95
|
+
const slot = slots[i]!;
|
|
96
|
+
const arg = args[i];
|
|
97
|
+
|
|
98
|
+
const parsedLiteralResult = this.parseSlotForLiteral(slot, i, arg);
|
|
99
|
+
if (parsedLiteralResult === true) {
|
|
100
|
+
continue;
|
|
101
|
+
} else if (parsedLiteralResult !== false) {
|
|
102
|
+
return parsedLiteralResult;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// unknown/any accept anything
|
|
106
|
+
// IMPORTANT: do not use a type guard here
|
|
107
|
+
if (slot.def.type === 'unknown' || slot.def.type === 'any') {
|
|
108
|
+
// debug('Skipping unknown/any slot validation for arg', arg);
|
|
109
|
+
parsedValues.push(arg);
|
|
110
|
+
exactMatch = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// low-effort check
|
|
114
|
+
if (isZodPromise(slot)) {
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
`${this} expects a Promise for slot ${i}; use expectAsync() instead of expect()`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const result = slot.safeParse(arg);
|
|
120
|
+
if (!result.success) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
parsedValues.push(arg);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
exactMatch,
|
|
130
|
+
parsedValues: parsedValues as unknown as ParsedValues<Parts>,
|
|
131
|
+
success: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class BupkisAssertionFunctionSync<
|
|
137
|
+
Parts extends AssertionParts,
|
|
138
|
+
Impl extends AssertionImplFnSync<Parts>,
|
|
139
|
+
Slots extends AssertionSlots<Parts>,
|
|
140
|
+
>
|
|
141
|
+
extends BupkisAssertionSync<Parts, Impl, Slots>
|
|
142
|
+
implements AssertionFunctionSync<Parts, Impl, Slots>
|
|
143
|
+
{
|
|
144
|
+
override execute(
|
|
145
|
+
parsedValues: ParsedValues<Parts>,
|
|
146
|
+
args: unknown[],
|
|
147
|
+
stackStartFn: (...args: any[]) => any,
|
|
148
|
+
_parseResult?: ParsedResult<Parts>,
|
|
149
|
+
): void {
|
|
150
|
+
const result = (this.impl as AssertionImplFnSync<Parts>).call(
|
|
151
|
+
null,
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
153
|
+
...(parsedValues as any),
|
|
154
|
+
);
|
|
155
|
+
if (isPromiseLike(result)) {
|
|
156
|
+
// Avoid unhandled promise rejection
|
|
157
|
+
Promise.resolve(result).catch((err) => {
|
|
158
|
+
debug(`Ate unhandled rejection from assertion %s: %O`, this, err);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
throw new TypeError(
|
|
162
|
+
`Assertion ${this} returned a Promise; use expectAsync() instead of expect()`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (isZodType(result)) {
|
|
166
|
+
const zodResult = result.safeParse(parsedValues[0]);
|
|
167
|
+
if (!zodResult.success) {
|
|
168
|
+
throw this.translateZodError(
|
|
169
|
+
stackStartFn,
|
|
170
|
+
zodResult.error,
|
|
171
|
+
...parsedValues,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
} else if (isBoolean(result)) {
|
|
175
|
+
if (!result) {
|
|
176
|
+
throw new AssertionError({
|
|
177
|
+
message: `Assertion ${this} failed for arguments: ${inspect(args)}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
} else if (isAssertionFailure(result)) {
|
|
181
|
+
throw new AssertionError({
|
|
182
|
+
actual: result.actual,
|
|
183
|
+
expected: result.expected,
|
|
184
|
+
message: result.message ?? `Assertion ${this} failed`,
|
|
185
|
+
});
|
|
186
|
+
} else if (result as unknown) {
|
|
187
|
+
throw new TypeError(
|
|
188
|
+
`Invalid return type from assertion ${this}; expected boolean, ZodType, or AssertionFailure`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A class representing an assertion implemented as a function.
|
|
196
|
+
*
|
|
197
|
+
* This function may:
|
|
198
|
+
*
|
|
199
|
+
* 1. Return a `boolean` indicating pass/fail.
|
|
200
|
+
* 2. Return a `ZodType` which will be used to validate the subject.
|
|
201
|
+
* 3. Return a `Promise` resolving to either of the above (when called via
|
|
202
|
+
* {@link expectAsync})
|
|
203
|
+
* 4. Throw a {@link AssertionError}; when called via {@link expectAsync}, reject
|
|
204
|
+
* with an {@link AssertionError}
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
export class BupkisAssertionSchemaSync<
|
|
208
|
+
Parts extends AssertionParts,
|
|
209
|
+
Impl extends AssertionImplSchemaSync<Parts>,
|
|
210
|
+
Slots extends AssertionSlots<Parts>,
|
|
211
|
+
>
|
|
212
|
+
extends BupkisAssertionSync<Parts, Impl, Slots>
|
|
213
|
+
implements AssertionSchemaSync<Parts, Impl, Slots>
|
|
214
|
+
{
|
|
215
|
+
override execute(
|
|
216
|
+
parsedValues: ParsedValues<Parts>,
|
|
217
|
+
args: unknown[],
|
|
218
|
+
stackStartFn: (...args: any[]) => any,
|
|
219
|
+
parseResult?: ParsedResult<Parts>,
|
|
220
|
+
): void {
|
|
221
|
+
// Check if we have cached validation result from parseValues
|
|
222
|
+
const cachedValidation = parseResult?.success
|
|
223
|
+
? parseResult.subjectValidationResult
|
|
224
|
+
: undefined;
|
|
225
|
+
|
|
226
|
+
if (cachedValidation) {
|
|
227
|
+
debug(
|
|
228
|
+
'Using cached subject validation result from parseValues for %s',
|
|
229
|
+
this,
|
|
230
|
+
);
|
|
231
|
+
if (!cachedValidation.success) {
|
|
232
|
+
// Subject validation failed during parseValues, throw the cached error
|
|
233
|
+
throw this.translateZodError(
|
|
234
|
+
stackStartFn,
|
|
235
|
+
cachedValidation.error,
|
|
236
|
+
...parsedValues,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
// Subject validation passed, nothing more to do
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Fall back to standard validation if no cached result
|
|
244
|
+
const [subject] = parsedValues;
|
|
245
|
+
const result = this.impl.safeParse(subject);
|
|
246
|
+
if (!result.success) {
|
|
247
|
+
throw this.translateZodError(stackStartFn, result.error, ...parsedValues);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
override parseValues<Args extends readonly unknown[]>(
|
|
252
|
+
args: Args,
|
|
253
|
+
): ParsedResult<Parts> {
|
|
254
|
+
const { slots } = this;
|
|
255
|
+
const parsedValues: any[] = [];
|
|
256
|
+
const mismatch = this.maybeParseValuesArgMismatch(args);
|
|
257
|
+
if (mismatch) {
|
|
258
|
+
return mismatch;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let exactMatch = true;
|
|
262
|
+
let subjectValidationResult:
|
|
263
|
+
| undefined
|
|
264
|
+
| { data: any; success: true }
|
|
265
|
+
| { error: z.ZodError; success: false };
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < slots.length; i++) {
|
|
268
|
+
const slot = slots[i]!;
|
|
269
|
+
const arg = args[i];
|
|
270
|
+
const parsedLiteralResult = this.parseSlotForLiteral(slot, i, arg);
|
|
271
|
+
if (parsedLiteralResult === true) {
|
|
272
|
+
continue;
|
|
273
|
+
} else if (parsedLiteralResult !== false) {
|
|
274
|
+
return parsedLiteralResult;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// For the subject slot (first slot if it's unknown/any), try optimized validation
|
|
278
|
+
if (
|
|
279
|
+
i === 0 &&
|
|
280
|
+
(slot.def.type === 'unknown' || slot.def.type === 'any') &&
|
|
281
|
+
this.isSimpleSchemaAssertion()
|
|
282
|
+
) {
|
|
283
|
+
const result = this.impl.safeParse(arg);
|
|
284
|
+
if (result.success) {
|
|
285
|
+
subjectValidationResult = { data: result.data, success: true };
|
|
286
|
+
parsedValues.push(result.data); // Use validated data
|
|
287
|
+
} else {
|
|
288
|
+
subjectValidationResult = { error: result.error, success: false };
|
|
289
|
+
parsedValues.push(arg); // Keep original for error reporting
|
|
290
|
+
}
|
|
291
|
+
exactMatch = false; // Subject was validated, so we know the exact type
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Standard slot processing for non-optimized cases
|
|
296
|
+
if (slot.def.type === 'unknown' || slot.def.type === 'any') {
|
|
297
|
+
debug('Skipping unknown/any slot validation for arg', arg);
|
|
298
|
+
parsedValues.push(arg);
|
|
299
|
+
exactMatch = false;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (isZodPromise(slot)) {
|
|
304
|
+
throw new TypeError(
|
|
305
|
+
`${this} expects a Promise for slot ${i}; use expectAsync() instead of expect()`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const result = slot.safeParse(arg);
|
|
309
|
+
if (!result.success) {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
parsedValues.push(arg);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const result: ParsedResultSuccess<Parts> = {
|
|
318
|
+
exactMatch,
|
|
319
|
+
parsedValues: parsedValues as unknown as ParsedValues<Parts>,
|
|
320
|
+
success: true,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Add cached validation result if we performed optimization
|
|
324
|
+
if (subjectValidationResult) {
|
|
325
|
+
result.subjectValidationResult = subjectValidationResult;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Determines if this assertion can be optimized (simple single-subject
|
|
333
|
+
* schema). Only simple assertions like ['to be a string'] with z.string()
|
|
334
|
+
* qualify.
|
|
335
|
+
*/
|
|
336
|
+
private isSimpleSchemaAssertion(): boolean {
|
|
337
|
+
// Only optimize if we have exactly one subject slot + string literal slots
|
|
338
|
+
// and no complex argument processing
|
|
339
|
+
const hasSubjectSlot =
|
|
340
|
+
this.slots.length > 0 &&
|
|
341
|
+
(this.slots[0]?.def.type === 'unknown' ||
|
|
342
|
+
this.slots[0]?.def.type === 'any');
|
|
343
|
+
|
|
344
|
+
const allOtherSlotsAreLiterals = this.slots.slice(1).every((slot) => {
|
|
345
|
+
const meta = BupkisRegistry.get(slot) ?? {};
|
|
346
|
+
return kStringLiteral in meta;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return hasSubjectSlot && allOtherSlotsAreLiterals;
|
|
350
|
+
}
|
|
351
|
+
}
|