Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- `expect.ts` - Main entry points (`expect`, `expectAsync`)
- `bootstrap.ts` - Factory functions for creating assertion engines
- `guards.ts` - Runtime type guards and validation utilities
- `schema.ts` - Reusable Zod schemas (`ClassSchema`, `FunctionSchema`, etc.)
- `schema.ts` - Reusable Zod schemas (`ConstructibleSchema`, `FunctionSchema`, etc.)
- `types.ts` - Complex TypeScript type definitions and inference system
- `util.ts` - Object matching utilities (`satisfies`, `exhaustivelySatisfies`)
- `error.ts` - Custom error classes (`AssertionError`, `NegatedAssertionError`)
Expand Down Expand Up @@ -158,7 +158,7 @@ createAssertion([z.number(), 'is even'], (n) => n % 2 === 0);
**Module Boundaries**:

- `guards.ts` - runtime type checking (used throughout)
- `schema.ts` - reusable Zod schemas (`ClassSchema`, `FunctionSchema`, etc.)
- `schema.ts` - reusable Zod schemas (`ConstructibleSchema`, `FunctionSchema`, etc.)
- `util.ts` - object matching utilities (`satisfies`, `exhaustivelySatisfies`)
- `bootstrap.ts` - factory functions for creating assertion engines
- Clear separation between sync/async assertion implementations
Expand Down
10 changes: 5 additions & 5 deletions site/reference/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ const { expect } = use([asyncFnAssertion]);
expect(async () => {}, 'to be an async function');
```

### ClassSchema
### ConstructibleSchema

{@link schema!ClassSchema ClassSchema} matches JavaScript functions which may be constructed with the [new keyword][]. This includes any function which was defined using the `function` keyword in addition to classes (created with `class` or otherwise).
{@link schema!ConstructibleSchema ConstructibleSchema} matches JavaScript functions which may be constructed with the [new keyword][]. This includes any function which was defined using the `function` keyword in addition to classes (created with `class` or otherwise).

> **Important:** We can only know if a function is _constructible_; we cannot know if it was specifically created using the `class` keyword or is otherwise intended to be a "class". This is a language-level limitation. If someone knows a way around this, please [open an issue][issue-tracker]!

**Example:**

```ts
import { createAssertion, use } from 'bupkis';
import { ClassSchema } from 'bupkis/schema';
import { ConstructibleSchema } from 'bupkis/schema';

const classAssertion = createAssertion(
[ClassSchema, 'to be a subclass of Error'],
ClassSchema.refine((subject) => subject.prototype instanceof Error),
[ConstructibleSchema, 'to be a subclass of Error'],
ConstructibleSchema.refine((subject) => subject.prototype instanceof Error),
);

const { expect } = use([classAssertion]);
Expand Down
10 changes: 7 additions & 3 deletions src/assertion/impl/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { z } from 'zod/v4';

import { isA, isNonNullObject, isString } from '../../guards.js';
import {
ClassSchema,
ConstructibleSchema,
FunctionSchema,
WrappedPromiseLikeSchema,
} from '../../schema.js';
Expand Down Expand Up @@ -97,7 +97,11 @@ export const PromiseAssertions = [
),
// Parameterized "to reject" with class constructor
createAsyncAssertion(
[FunctionSchema, ['to reject with a', 'to reject with an'], ClassSchema],
[
FunctionSchema,
['to reject with a', 'to reject with an'],
ConstructibleSchema,
],
async (subject, ctor) => {
const error = await trapAsyncFnError(subject);
if (!error) {
Expand All @@ -110,7 +114,7 @@ export const PromiseAssertions = [
[
WrappedPromiseLikeSchema,
['to reject with a', 'to reject with an'],
ClassSchema,
ConstructibleSchema,
],
async (subject, ctor) => {
const error = await trapPromiseError(subject);
Expand Down
6 changes: 3 additions & 3 deletions src/assertion/impl/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { inspect } from 'node:util';
import { z } from 'zod/v4';

import { isA, isNonNullObject, isString } from '../../guards.js';
import { ClassSchema, FunctionSchema } from '../../schema.js';
import { ConstructibleSchema, FunctionSchema } from '../../schema.js';
import {
valueToSchema,
type ValueToSchemaOptions,
Expand Down Expand Up @@ -509,7 +509,7 @@ export const CallbackSyncAssertions = [
'to invoke nodeback with a',
'to invoke nodeback with an',
],
ClassSchema,
ConstructibleSchema,
],
(subject, ctor) => {
const { called, error } = trapNodebackInvocation(subject);
Expand Down Expand Up @@ -776,7 +776,7 @@ export const CallbackAsyncAssertions = [
'to eventually invoke nodeback with a',
'to eventually invoke nodeback with an',
],
ClassSchema,
ConstructibleSchema,
],
async (subject, ctor) => {
const { called, error } = await trapAsyncNodebackInvocation(subject);
Expand Down
7 changes: 5 additions & 2 deletions src/assertion/impl/sync-basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from 'zod/v4';
import { BupkisRegistry } from '../../metadata.js';
import {
AsyncFunctionSchema,
ClassSchema,
ConstructibleSchema,
FalsySchema,
FunctionSchema,
PrimitiveSchema,
Expand Down Expand Up @@ -64,7 +64,10 @@ export const BasicAssertions = [
createAssertion(['to be undefined'], z.undefined()),
createAssertion([['to be an array', 'to be array']], z.array(z.any())),
createAssertion([['to be a date', 'to be a Date']], z.date()),
createAssertion([['to be a class', 'to be a constructor']], ClassSchema),
createAssertion(
[['to be a class', 'to be a constructor']],
ConstructibleSchema,
),
createAssertion(['to be a primitive'], PrimitiveSchema),

createAssertion(
Expand Down
8 changes: 4 additions & 4 deletions src/assertion/impl/sync-parametric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { z } from 'zod/v4';
import { isA, isError, isNonNullObject, isString } from '../../guards.js';
import {
ArrayLikeSchema,
ClassSchema,
ConstructibleSchema,
FunctionSchema,
RegExpSchema,
StrongMapSchema,
Expand Down Expand Up @@ -55,7 +55,7 @@ const knownTypes = Object.freeze(

export const ParametricAssertions = [
createAssertion(
[['to be an instance of', 'to be a'], ClassSchema],
[['to be an instance of', 'to be a'], ConstructibleSchema],
(_, ctor) => z.instanceof(ctor),
),
createAssertion(
Expand Down Expand Up @@ -332,7 +332,7 @@ export const ParametricAssertions = [
}
}),
createAssertion(
[FunctionSchema, ['to throw a', 'to thrown an'], ClassSchema],
[FunctionSchema, ['to throw a', 'to thrown an'], ConstructibleSchema],
(subject, ctor) => {
const error = trapError(subject);
if (!error) {
Expand Down Expand Up @@ -391,7 +391,7 @@ export const ParametricAssertions = [
[
FunctionSchema,
['to throw a', 'to thrown an'],
ClassSchema,
ConstructibleSchema,
'satisfying',
z.union([z.string(), z.instanceof(RegExp), z.looseObject({})]),
],
Expand Down
22 changes: 11 additions & 11 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { BupkisRegistry } from './metadata.js';
import { type Constructor, type MutableOrReadonly } from './types.js';

/**
* A Zod schema that validates JavaScript classes or constructor functions.
* A Zod schema that validates JavaScript constructible functions.
*
* This schema validates values that can be used as constructors, including ES6
* classes, traditional constructor functions, and built-in constructors. It
Expand All @@ -54,28 +54,28 @@ import { type Constructor, type MutableOrReadonly } from './types.js';
*
* @privateRemarks
* The schema is registered in the {@link BupkisRegistry} with the name
* `ClassSchema` for later reference and type checking purposes.
* `ConstructibleSchema` for later reference and type checking purposes.
* @example
*
* ```typescript
* class MyClass {}
* function MyConstructor() {}
*
* ClassSchema.parse(MyClass); // ✓ Valid
* ClassSchema.parse(MyConstructor); // ✓ Valid
* ClassSchema.parse(Array); // ✓ Valid
* ClassSchema.parse(Date); // ✓ Valid
* ClassSchema.parse(() => {}); // ✗ Throws validation error
* ClassSchema.parse({}); // ✗ Throws validation error
* ConstructibleSchema.parse(MyClass); // ✓ Valid
* ConstructibleSchema.parse(MyConstructor); // ✓ Valid
* ConstructibleSchema.parse(Array); // ✓ Valid
* ConstructibleSchema.parse(Date); // ✓ Valid
* ConstructibleSchema.parse(() => {}); // ✗ Throws validation error
* ConstructibleSchema.parse({}); // ✗ Throws validation error
* ```
*
* @group Schema
*/

export const ClassSchema = z
export const ConstructibleSchema = z
.custom<Constructor>(isConstructible)
.register(BupkisRegistry, { name: 'ClassSchema' })
.describe('Class / Constructor');
.register(BupkisRegistry, { name: 'ConstructibleSchema' })
.describe('Constructible Function');

/**
* A Zod schema that validates any JavaScript function.
Expand Down
75 changes: 38 additions & 37 deletions test/property/async.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,46 +92,47 @@ const testConfigs = {
},

// Test functions rejecting with specific error class
'functionschema-to-reject-with-a-to-reject-with-an-classschema-3s3p': {
invalid: {
async: true,
generators: [
// Generate function that throws wrong error type
fc.oneof(
'functionschema-to-reject-with-a-to-reject-with-an-constructibleschema-3s3p':
{
invalid: {
async: true,
generators: [
// Generate function that throws wrong error type
fc.oneof(
fc.func(fc.anything()).map((fn) => async (..._args: unknown[]) => {
fn(); // Generate some behavior
throw new TypeError('type error');
}),
fc.func(fc.anything()).map((fn) => async (..._args: unknown[]) => {
// just resolve
fn();
}),
),
fc.constantFrom(
...extractPhrases(
'functionschema-to-reject-with-a-to-reject-with-an-constructibleschema-3s3p',
),
),
fc.constant(RangeError), // Expecting RangeError but function throws TypeError
],
},
valid: {
async: true,
generators: [
// Generate function that throws correct error type
fc.func(fc.anything()).map((fn) => async (..._args: unknown[]) => {
fn(); // Generate some behavior
throw new TypeError('type error');
}),
fc.func(fc.anything()).map((fn) => async (..._args: unknown[]) => {
// just resolve
fn();
}),
),
fc.constantFrom(
...extractPhrases(
'functionschema-to-reject-with-a-to-reject-with-an-classschema-3s3p',
),
),
fc.constant(RangeError), // Expecting RangeError but function throws TypeError
],
},
valid: {
async: true,
generators: [
// Generate function that throws correct error type
fc.func(fc.anything()).map((fn) => async (..._args: unknown[]) => {
fn(); // Generate some behavior
throw new TypeError('type error');
}),
fc.constantFrom(
...extractPhrases(
'functionschema-to-reject-with-a-to-reject-with-an-classschema-3s3p',
fc.constantFrom(
...extractPhrases(
'functionschema-to-reject-with-a-to-reject-with-an-constructibleschema-3s3p',
),
),
),
fc.constant(TypeError), // Expecting TypeError and function throws TypeError
],
fc.constant(TypeError), // Expecting TypeError and function throws TypeError
],
},
},
},
// Test functions rejecting with string patterns
'functionschema-to-reject-with-error-satisfying-string-regexp-object-3s3p': {
invalid: {
Expand Down Expand Up @@ -273,7 +274,7 @@ const testConfigs = {
},

// Test promises rejecting with specific error class
'wrappedpromiselikeschema-to-reject-with-a-to-reject-with-an-classschema-3s3p':
'wrappedpromiselikeschema-to-reject-with-a-to-reject-with-an-constructibleschema-3s3p':
{
invalid: {
async: true,
Expand All @@ -290,7 +291,7 @@ const testConfigs = {
),
fc.constantFrom(
...extractPhrases(
'wrappedpromiselikeschema-to-reject-with-a-to-reject-with-an-classschema-3s3p',
'wrappedpromiselikeschema-to-reject-with-a-to-reject-with-an-constructibleschema-3s3p',
),
),
fc.constant(RangeError), // Promise rejects with TypeError, not RangeError
Expand All @@ -302,7 +303,7 @@ const testConfigs = {
fc.string().map((msg) => Promise.reject(new TypeError(msg))),
fc.constantFrom(
...extractPhrases(
'wrappedpromiselikeschema-to-reject-with-a-to-reject-with-an-classschema-3s3p',
'wrappedpromiselikeschema-to-reject-with-a-to-reject-with-an-constructibleschema-3s3p',
),
),
fc.constant(TypeError), // Promise rejects with TypeError, expecting TypeError
Expand Down
12 changes: 6 additions & 6 deletions test/property/callback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const syncTestConfigs = {
],
},
},
'functionschema-to-call-nodeback-with-a-to-call-nodeback-with-an-to-invoke-nodeback-with-a-to-invoke-nodeback-with-an-classschema-3s3p':
'functionschema-to-call-nodeback-with-a-to-call-nodeback-with-an-to-invoke-nodeback-with-a-to-invoke-nodeback-with-an-constructibleschema-3s3p':
{
invalid: {
generators: [
Expand All @@ -293,7 +293,7 @@ const syncTestConfigs = {
}),
fc.constantFrom(
...extractSyncPhrases(
'functionschema-to-call-nodeback-with-a-to-call-nodeback-with-an-to-invoke-nodeback-with-a-to-invoke-nodeback-with-an-classschema-3s3p',
'functionschema-to-call-nodeback-with-a-to-call-nodeback-with-an-to-invoke-nodeback-with-a-to-invoke-nodeback-with-an-constructibleschema-3s3p',
),
),
fc.constant(Error),
Expand All @@ -306,7 +306,7 @@ const syncTestConfigs = {
}),
fc.constantFrom(
...extractSyncPhrases(
'functionschema-to-call-nodeback-with-a-to-call-nodeback-with-an-to-invoke-nodeback-with-a-to-invoke-nodeback-with-an-classschema-3s3p',
'functionschema-to-call-nodeback-with-a-to-call-nodeback-with-an-to-invoke-nodeback-with-a-to-invoke-nodeback-with-an-constructibleschema-3s3p',
),
),
fc.constant(Error),
Expand Down Expand Up @@ -928,7 +928,7 @@ const asyncTestConfigs = {
timeout: 2000, // Longer timeout to accommodate assertion library timeout
},
},
'functionschema-to-eventually-call-nodeback-with-a-to-eventually-call-nodeback-with-an-to-eventually-invoke-nodeback-with-a-to-eventually-invoke-nodeback-with-an-classschema-3s3p':
'functionschema-to-eventually-call-nodeback-with-a-to-eventually-call-nodeback-with-an-to-eventually-invoke-nodeback-with-a-to-eventually-invoke-nodeback-with-an-constructibleschema-3s3p':
{
invalid: {
async: true,
Expand All @@ -941,7 +941,7 @@ const asyncTestConfigs = {
}),
fc.constantFrom(
...extractAsyncPhrases(
'functionschema-to-eventually-call-nodeback-with-a-to-eventually-call-nodeback-with-an-to-eventually-invoke-nodeback-with-a-to-eventually-invoke-nodeback-with-an-classschema-3s3p',
'functionschema-to-eventually-call-nodeback-with-a-to-eventually-call-nodeback-with-an-to-eventually-invoke-nodeback-with-a-to-eventually-invoke-nodeback-with-an-constructibleschema-3s3p',
),
),
fc.constant(Error),
Expand All @@ -956,7 +956,7 @@ const asyncTestConfigs = {
}),
fc.constantFrom(
...extractAsyncPhrases(
'functionschema-to-eventually-call-nodeback-with-a-to-eventually-call-nodeback-with-an-to-eventually-invoke-nodeback-with-a-to-eventually-invoke-nodeback-with-an-classschema-3s3p',
'functionschema-to-eventually-call-nodeback-with-a-to-eventually-call-nodeback-with-an-to-eventually-invoke-nodeback-with-a-to-eventually-invoke-nodeback-with-an-constructibleschema-3s3p',
),
),
fc.constant(Error),
Expand Down
4 changes: 3 additions & 1 deletion test/property/property-test.macro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ export const assertExhaustiveTestConfig = (
it(`should test all available assertions in ${collectionName}`, () => {
const allCollectionIds = new Set(Object.keys(assertions));
const testedIds = new Set(Object.keys(testConfigs));
const diff = allCollectionIds.difference(testedIds);
const diff = new Set(
[...allCollectionIds].filter((id) => !testedIds.has(id)),
);
try {
expect(diff, 'to be empty');
} catch {
Expand Down
Loading
Loading