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
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"

- name: Install dependencies
run: pnpm install

- name: Run linting
run: pnpm run lint

- name: Build project
run: pnpm run build

- name: Run tests
run: pnpm run test

type-check:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: "pnpm"

- name: Install dependencies
run: pnpm install

- name: Type check
run: pnpm exec tsc --noEmit
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm exec lint-staged
99 changes: 51 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Generate [fast-check](https://github.com/dubzzz/fast-check) arbitraries from [Valibot](https://github.com/fabian-hiller/valibot) schemas for property-based testing.

[![npm version](https://badge.fury.io/js/valibot-fast-check.svg)](https://www.npmjs.com/package/valibot-fast-check)
[![CI](https://github.com/Eronmmer/valibot-fast-check/workflows/CI/badge.svg)](https://github.com/Eronmmer/valibot-fast-check/actions)

## Installation

```bash
Expand All @@ -19,21 +22,21 @@ import fc from "fast-check";

// Define a Valibot schema
const UserSchema = v.object({
name: v.pipe(v.string(), v.minLength(1), v.maxLength(50)),
age: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(120)),
email: v.pipe(v.string(), v.email()),
name: v.pipe(v.string(), v.minLength(1), v.maxLength(50)),
age: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(120)),
email: v.pipe(v.string(), v.email()),
});

// Generate fast-check arbitraries
const userArbitrary = vfc().inputOf(UserSchema);

// Use in property-based tests
fc.assert(
fc.property(userArbitrary, (user) => {
// Your test logic here
const result = v.safeParse(UserSchema, user);
expect(result.success).toBe(true);
})
fc.property(userArbitrary, (user) => {
// Your test logic here
const result = v.safeParse(UserSchema, user);
expect(result.success).toBe(true);
}),
);
```

Expand Down Expand Up @@ -164,23 +167,23 @@ For complex or custom constraints, falls back to schema validation with efficien
```typescript
// Custom validations automatically use filterBySchema
const schema = v.pipe(
v.number(),
v.check((x) => isPrime(x)) // Custom validation
v.number(),
v.check((x) => isPrime(x)), // Custom validation
);

// Constraints that can't be optimized
const schema2 = v.pipe(
v.number(),
v.notValue(5), // Uses filterBySchema
v.notValues([1, 2, 3]) // Uses filterBySchema
v.number(),
v.notValue(5), // Uses filterBySchema
v.notValues([1, 2, 3]), // Uses filterBySchema
);

// Multiple string content constraints
const schema3 = v.pipe(
v.string(),
v.startsWith("hello"),
v.endsWith("world"),
v.includes("test") // Falls back to filterBySchema
v.string(),
v.startsWith("hello"),
v.endsWith("world"),
v.includes("test"), // Falls back to filterBySchema
);
```

Expand All @@ -191,21 +194,21 @@ The library provides detailed error messages for unsupported schemas and generat
```typescript
// Unsupported schema type
try {
vfc().inputOf(unsupportedSchema);
vfc().inputOf(unsupportedSchema);
} catch (error) {
// VFCUnsupportedSchemaError: Unable to generate valid values for Valibot schema. CustomType schemas are not supported.
// VFCUnsupportedSchemaError: Unable to generate valid values for Valibot schema. CustomType schemas are not supported.
}

// Low success rate from filterBySchema
try {
const restrictiveSchema = v.pipe(
v.number(),
v.check((x) => x === Math.PI) // Extremely low success rate
);
const samples = fc.sample(vfc().inputOf(restrictiveSchema), 10);
const restrictiveSchema = v.pipe(
v.number(),
v.check((x) => x === Math.PI), // Extremely low success rate
);
const samples = fc.sample(vfc().inputOf(restrictiveSchema), 10);
} catch (error) {
// VFCGenerationError: Unable to generate valid values for the passed Valibot schema.
// Please provide an override for the schema at path '.'.
// VFCGenerationError: Unable to generate valid values for the passed Valibot schema.
// Please provide an override for the schema at path '.'.
}
```

Expand All @@ -219,37 +222,37 @@ import * as v from "valibot";
import fc from "fast-check";

const schema = v.object({
username: v.pipe(v.string(), v.minLength(3), v.maxLength(20)),
password: v.pipe(v.string(), v.minLength(8)),
age: v.pipe(v.number(), v.integer(), v.minValue(13)),
username: v.pipe(v.string(), v.minLength(3), v.maxLength(20)),
password: v.pipe(v.string(), v.minLength(8)),
age: v.pipe(v.number(), v.integer(), v.minValue(13)),
});

fc.assert(
fc.property(vfc().inputOf(schema), (data) => {
// Test that all generated data is valid
const result = v.safeParse(schema, data);
expect(result.success).toBe(true);

// Test business logic
expect(data.username.length).toBeGreaterThanOrEqual(3);
expect(data.age).toBeGreaterThanOrEqual(13);
})
fc.property(vfc().inputOf(schema), (data) => {
// Test that all generated data is valid
const result = v.safeParse(schema, data);
expect(result.success).toBe(true);

// Test business logic
expect(data.username.length).toBeGreaterThanOrEqual(3);
expect(data.age).toBeGreaterThanOrEqual(13);
}),
);
```

### Complex Nested Schemas

```typescript
const AddressSchema = v.object({
street: v.string(),
city: v.string(),
zipCode: v.pipe(v.string(), v.regex(/^\d{5}$/)),
street: v.string(),
city: v.string(),
zipCode: v.pipe(v.string(), v.regex(/^\d{5}$/)),
});

const PersonSchema = v.object({
name: v.string(),
addresses: v.array(AddressSchema),
primaryAddress: v.optional(AddressSchema),
name: v.string(),
addresses: v.array(AddressSchema),
primaryAddress: v.optional(AddressSchema),
});

const personArbitrary = vfc().inputOf(PersonSchema);
Expand All @@ -260,13 +263,13 @@ const personArbitrary = vfc().inputOf(PersonSchema);

```typescript
const schema = v.object({
id: v.string(), // We want specific ID format
data: v.any(),
id: v.string(), // We want specific ID format
data: v.any(),
});

const customGenerator = vfc().override(
v.string(),
fc.uuid() // All strings will be UUIDs
v.string(),
fc.uuid(), // All strings will be UUIDs
);

const arbitrary = customGenerator.inputOf(schema);
Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "eslint \"src/**/*.ts\"",
"test": "vitest run"
"test": "vitest run",
"prepare": "husky"
},
"keywords": [
"property-based testing",
Expand All @@ -35,7 +36,9 @@
"eslint": "^9.33.0",
"fast-check": "^4.2.0",
"globals": "^16.3.0",
"husky": "^9.1.7",
"jiti": "^2.5.1",
"lint-staged": "^16.1.5",
"prettier": "^3.6.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1",
Expand All @@ -45,5 +48,14 @@
"peerDependencies": {
"fast-check": "^4.2.0",
"valibot": "^1.1.0"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"prettier --write",
"eslint --fix"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
}
Loading