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/src/use.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { BupkisAssertionAsync } from './assertion/assertion-async.js';
2
+ import { BupkisAssertionSync } from './assertion/assertion-sync.js';
3
+ import {
4
+ type AnyAssertion,
5
+ type AnyAsyncAssertions,
6
+ type AnySyncAssertions,
7
+ } from './assertion/assertion-types.js';
8
+ import {
9
+ createBaseExpect,
10
+ createExpectAsyncFunction,
11
+ createExpectSyncFunction,
12
+ } from './expect.js';
13
+ import {
14
+ type Concat,
15
+ type FilterAsyncAssertions,
16
+ type FilterSyncAssertions,
17
+ type UseFn,
18
+ } from './types.js';
19
+
20
+ export function createUse<
21
+ const T extends AnySyncAssertions,
22
+ const U extends AnyAsyncAssertions,
23
+ >(syncAssertions: T, asyncAssertions: U): UseFn<T, U> {
24
+ const use: UseFn<T, U> = <
25
+ V extends readonly AnyAssertion[],
26
+ W extends FilterSyncAssertions<V>,
27
+ X extends FilterAsyncAssertions<V>,
28
+ >(
29
+ assertions: V,
30
+ ) => {
31
+ const newSyncAssertions = assertions.filter(
32
+ (a) => a instanceof BupkisAssertionSync,
33
+ ) as unknown as W;
34
+ const newAsyncAssertions = assertions.filter(
35
+ (a) => a instanceof BupkisAssertionAsync,
36
+ ) as unknown as X;
37
+ const allSyncAssertions = [
38
+ ...syncAssertions,
39
+ ...newSyncAssertions,
40
+ ] as unknown as Concat<typeof syncAssertions, typeof newSyncAssertions>;
41
+ const allAsyncAssertions = [
42
+ ...asyncAssertions,
43
+ ...newAsyncAssertions,
44
+ ] as unknown as Concat<typeof asyncAssertions, typeof newAsyncAssertions>;
45
+ const expectFunction = createExpectSyncFunction(allSyncAssertions);
46
+ const expectAsyncFunction = createExpectAsyncFunction(allAsyncAssertions);
47
+
48
+ const expect = Object.assign(
49
+ expectFunction,
50
+ createBaseExpect(allSyncAssertions, allAsyncAssertions, 'sync'),
51
+ );
52
+ const expectAsync = Object.assign(
53
+ expectAsyncFunction,
54
+ createBaseExpect(allSyncAssertions, allAsyncAssertions, 'async'),
55
+ );
56
+
57
+ return {
58
+ expect,
59
+ expectAsync: expectAsync,
60
+ };
61
+ };
62
+ return use;
63
+ }
package/src/util.ts ADDED
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Utility functions for object satisfaction and shape validation.
3
+ *
4
+ * This module provides core utility functions for checking if objects satisfy
5
+ * expected shapes, including `satisfies` for partial matching,
6
+ * `exhaustivelySatisfies` for exact matching, and `shallowSatisfiesShape` for
7
+ * converting shapes to Zod schemas. All functions handle circular references
8
+ * safely.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ import { type StringKeyOf } from 'type-fest';
14
+ import { z } from 'zod/v4';
15
+
16
+ import { isNonNullObject, isPromiseLike, isString } from './guards.js';
17
+ import {
18
+ FunctionSchema,
19
+ RegExpSchema,
20
+ StrongMapSchema,
21
+ StrongSetSchema,
22
+ WrappedPromiseLikeSchema,
23
+ } from './schema.js';
24
+
25
+ export function keyBy<
26
+ const T extends readonly Record<PropertyKey, any>[],
27
+ K extends StringKeyOf<T[number]>,
28
+ >(collection: T, key: K): Record<string, T[number]> {
29
+ const result = {} as Record<string, T[number]>;
30
+
31
+ for (const item of collection) {
32
+ const keyValue = item[key];
33
+ if (
34
+ typeof keyValue === 'string' ||
35
+ typeof keyValue === 'number' ||
36
+ typeof keyValue === 'symbol'
37
+ ) {
38
+ result[String(keyValue)] = item;
39
+ }
40
+ }
41
+
42
+ return result;
43
+ }
44
+
45
+ /**
46
+ * Recursively converts an arbitrary value to a Zod v4 schema that would
47
+ * validate values with the same structure.
48
+ *
49
+ * This function analyzes the runtime value and generates a corresponding Zod
50
+ * schema that captures the value's structure and type information. It handles
51
+ * primitives, objects, arrays, functions, and various built-in types, with
52
+ * support for circular reference detection.
53
+ *
54
+ * @example
55
+ *
56
+ * ```typescript
57
+ * // Primitive types
58
+ * valueToSchema('hello'); // z.string()
59
+ * valueToSchema(42); // z.number()
60
+ * valueToSchema(true); // z.boolean()
61
+ *
62
+ * // Objects
63
+ * valueToSchema({ name: 'John', age: 30 });
64
+ * // z.object({ name: z.string(), age: z.number() })
65
+ *
66
+ * // Arrays
67
+ * valueToSchema(['a', 'b', 'c']); // z.array(z.string())
68
+ * valueToSchema([1, 'mixed']); // z.array(z.union([z.number(), z.string()]))
69
+ *
70
+ * // Nested structures
71
+ * valueToSchema({ users: [{ name: 'John' }] });
72
+ * // z.object({ users: z.array(z.object({ name: z.string() })) })
73
+ * ```
74
+ *
75
+ * @param value - The value to convert to a schema
76
+ * @param options - Configuration options for schema generation
77
+ * @param visited - Internal WeakSet for circular reference detection
78
+ * @returns A Zod schema that validates values matching the input's structure
79
+ */
80
+ export const valueToSchema = (
81
+ value: unknown,
82
+ options: {
83
+ /** Current depth (internal) */
84
+ _currentDepth?: number;
85
+ /** Whether to allow mixed types in arrays (default: true) */
86
+ allowMixedArrays?: boolean;
87
+ /** If `true`, use `z.literal()` for primitive values instead of type schemas */
88
+ literalPrimitives?: boolean;
89
+ /**
90
+ * If `true`, treat `RegExp` literals as `RegExp` literals; otherwise treat
91
+ * as strings and attempt match
92
+ */
93
+ literalRegExp?: boolean;
94
+ /** Maximum nesting depth to prevent stack overflow (default: 10) */
95
+ maxDepth?: number;
96
+ /** If `true`, will disallow unknown properties in objects */
97
+ strict?: boolean;
98
+ } = {},
99
+ visited = new WeakSet<object>(),
100
+ ): z.ZodType => {
101
+ const {
102
+ _currentDepth = 0,
103
+ allowMixedArrays = true,
104
+ literalPrimitives = false,
105
+ literalRegExp = false,
106
+ maxDepth = 10,
107
+ strict = false,
108
+ } = options;
109
+
110
+ // Prevent infinite recursion
111
+ if (_currentDepth >= maxDepth) {
112
+ return z.unknown();
113
+ }
114
+
115
+ // Handle primitives
116
+ if (value === null) {
117
+ return z.null();
118
+ }
119
+
120
+ if (value === undefined) {
121
+ return z.undefined();
122
+ }
123
+ if (Number.isNaN(value as number)) {
124
+ return z.nan();
125
+ }
126
+ if (value === Infinity || value === -Infinity) {
127
+ return z.literal(value as any);
128
+ }
129
+
130
+ const valueType = typeof value;
131
+
132
+ switch (valueType) {
133
+ case 'bigint':
134
+ return literalPrimitives ? z.literal(value as bigint) : z.bigint();
135
+ case 'boolean':
136
+ return literalPrimitives ? z.literal(value as boolean) : z.boolean();
137
+ case 'function':
138
+ return FunctionSchema;
139
+ case 'number':
140
+ return literalPrimitives ? z.literal(value as number) : z.number();
141
+ case 'string':
142
+ return literalPrimitives ? z.literal(value as string) : z.string();
143
+ case 'symbol':
144
+ return z.symbol();
145
+ }
146
+
147
+ // Handle objects
148
+ if (typeof value === 'object' && value !== null) {
149
+ // Check for circular references
150
+ if (visited.has(value)) {
151
+ // Return a recursive schema reference or unknown for circular refs
152
+ return z.unknown();
153
+ }
154
+
155
+ visited.add(value);
156
+
157
+ try {
158
+ // Handle built-in object types
159
+ if (value instanceof Date) {
160
+ return z.date();
161
+ }
162
+
163
+ if (value instanceof RegExp) {
164
+ if (literalRegExp) {
165
+ return RegExpSchema;
166
+ }
167
+ return z.coerce.string().regex(value);
168
+ }
169
+
170
+ if (value instanceof Map) {
171
+ return StrongMapSchema;
172
+ }
173
+
174
+ if (value instanceof Set) {
175
+ return StrongSetSchema;
176
+ }
177
+
178
+ if (value instanceof WeakMap) {
179
+ return z.instanceof(WeakMap);
180
+ }
181
+
182
+ if (value instanceof WeakSet) {
183
+ return z.instanceof(WeakSet);
184
+ }
185
+
186
+ if (value instanceof Error) {
187
+ return z.instanceof(Error);
188
+ }
189
+
190
+ if (isPromiseLike(value)) {
191
+ return WrappedPromiseLikeSchema;
192
+ }
193
+
194
+ // Handle arrays
195
+ if (Array.isArray(value)) {
196
+ if (value.length === 0) {
197
+ return z.array(z.unknown());
198
+ }
199
+
200
+ const elementSchemas = value.map((item) =>
201
+ valueToSchema(
202
+ item,
203
+ {
204
+ ...options,
205
+ _currentDepth: _currentDepth + 1,
206
+ },
207
+ visited,
208
+ ),
209
+ );
210
+
211
+ if (allowMixedArrays) {
212
+ // Create a union of all unique element types
213
+ const uniqueSchemas = Array.from(
214
+ new Set(elementSchemas.map((schema) => schema.constructor.name)),
215
+ ).map((_, index) => elementSchemas[index]);
216
+
217
+ if (uniqueSchemas.length === 1) {
218
+ return z.array(uniqueSchemas[0]!);
219
+ } else {
220
+ return z.array(
221
+ z.union(uniqueSchemas as [z.ZodType, z.ZodType, ...z.ZodType[]]),
222
+ );
223
+ }
224
+ } else {
225
+ // Use the first element's schema for all elements
226
+ return z.array(elementSchemas[0]!);
227
+ }
228
+ }
229
+
230
+ // Handle plain objects
231
+ if (isNonNullObject(value)) {
232
+ const schemaShape: Record<string, z.ZodType<any>> = {};
233
+
234
+ for (const [key, val] of Object.entries(value)) {
235
+ if (isString(key)) {
236
+ schemaShape[key] = valueToSchema(
237
+ val,
238
+ {
239
+ ...options,
240
+ _currentDepth: _currentDepth + 1,
241
+ },
242
+ visited,
243
+ );
244
+ }
245
+ }
246
+
247
+ return strict
248
+ ? z.strictObject(schemaShape)
249
+ : z.looseObject(schemaShape);
250
+ }
251
+
252
+ // Handle other object types (ArrayBuffer, etc.)
253
+ return z.custom<object>(
254
+ (val) => typeof val === 'object' && val !== null,
255
+ { message: 'Expected an object' },
256
+ );
257
+ } finally {
258
+ visited.delete(value);
259
+ }
260
+ }
261
+
262
+ // Fallback for unknown types
263
+ return z.unknown();
264
+ };