Skip to content

Commit

Permalink
Merge pull request #383 from scythewyvern/valibot-adapter
Browse files Browse the repository at this point in the history
Valibot Adapter
  • Loading branch information
airjp73 authored Aug 28, 2024
2 parents 9cd2e5b + 0afd6ca commit ba1e106
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 4 deletions.
9 changes: 7 additions & 2 deletions apps/docs-v2/app/routes/_docs.installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,27 @@ export const meta = () => [

<Row>
<Col>
RVF is validation library agnostic, but there are official adapters available for `zod` and `yup`.
RVF is validation library agnostic, but there are official adapters available for `zod`, `yup`, and `valibot`.

- `@rvf/zod`
- `@rvf/yup`
- `@rvf/valibot`

</Col>

<Col>
<CodeExamples title="Install validation adapter" tabs={["Zod", "Yup"]}>
<CodeExamples title="Install validation adapter" tabs={["Zod", "Yup", "Valibot"]}>
```bash
npm install @rvf/zod
```

```bash
npm install @rvf/yup
```

```bash
npm install @rvf/valibot
```
</CodeExamples>

</Col>
Expand Down
4 changes: 2 additions & 2 deletions apps/docs-v2/app/routes/_docs.validation-library-support.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const meta = () => [

# Validation Library Support

There are official adapters available for `zod` and `yup` ,
There are official adapters available for `zod`, `yup`, and `valibot`,
but you can easily support whatever library you want by creating your own adapter.

And if you create an adapter for a library, feel free to make a PR to add official support 😊
Expand All @@ -15,7 +15,7 @@ And if you create an adapter for a library, feel free to make a PR to add offici

In order to make an adapter for your validation library of choice,
you can create a function that accepts a schema from the validation library and turns it into a validator using `createValidator`.
For more on this you can check the implementations for `withZod` and `withYup`.
For more on this you can check the implementations for `withZod`, `withYup`, and `withValibot`.

The out-of-the-box support for `yup` in this library works like this:

Expand Down
Binary file modified bun.lockb
Binary file not shown.
7 changes: 7 additions & 0 deletions packages/valibot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# RVF Valibot

The Valibot adapter for [RVF](https://github.com/airjp73/remix-validated-form).

## Docs

The best place to learn about RVF is the [documentation](https://rvf-js.io).
33 changes: 33 additions & 0 deletions packages/valibot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@rvf/valibot",
"version": "6.0.0",
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/airjp73/remix-validated-form"
},
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"prepublishOnly": "npm run build",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@rvf/core": ">= 0.0.0 < 7.0.0",
"@rvf/set-get": ">= 0.0.0 < 1.0.0",
"valibot": ">= 0.39.0"
},
"devDependencies": {
"@types/semver": "^7.5.0",
"@rvf/core": "*",
"semver": "^7.5.1",
"@rvf/set-get": "*",
"tsconfig": "*",
"tsup-config": "*",
"typescript": "^5.4.5",
"valibot": "^0.39.0"
}
}
53 changes: 53 additions & 0 deletions packages/valibot/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { type FieldErrors, type Validator, createValidator } from "@rvf/core";
import { pathArrayToString } from '@rvf/set-get';
import {
type BaseIssue,
type Config,
type GenericSchema,
type GenericSchemaAsync,
type InferIssue,
type InferOutput,
safeParseAsync,
} from "valibot";

type MaybeAsyncSchema = GenericSchema | GenericSchemaAsync

function formatIssuePath(issue: BaseIssue<unknown>): string | null {
if (!issue.path || issue.path.length === 0) return null;
const pathArray = issue.path.map(path => path.key as string | number);

return pathArrayToString(pathArray);
}

function parseIssues<Schema extends GenericSchema>(
issues: [InferIssue<Schema>, ...InferIssue<Schema>[]]
): FieldErrors {
const parsedIssues: FieldErrors = {};

for (const issue of issues) {
// More about unions: https://valibot.dev/guides/unions/
if (issue.type === 'union' && issue.issues) {
const unionIssues = parseIssues(issue.issues);
Object.assign(parsedIssues, unionIssues);
}

const path = formatIssuePath(issue);
if (path) parsedIssues[path] = issue.message;
}

return parsedIssues;
}

export function withValibot<Schema extends MaybeAsyncSchema>(
schema: Schema,
config?: Config<InferIssue<Schema>>,
): Validator<InferOutput<Schema>> {
return createValidator({
validate: async (input) => {
const result = await safeParseAsync(schema, input, config);

if (result.success) return { data: result.output, error: undefined };
return { data: undefined, error: parseIssues(result.issues) };
},
});
}
83 changes: 83 additions & 0 deletions packages/valibot/src/with-valibot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as fs from "fs/promises";
import * as path from "path";
import * as semver from "semver";
import { anyString } from "@remix-validated-form/test-utils";
import { describe, it, expect } from "vitest";
import * as v from "valibot";

import { withValibot } from "./";

const packageDir = path.join(__dirname, "..");
const packageJsonPath = path.join(packageDir, "package.json");
const corePackageJsonPath = path.join(packageDir, "../core/package.json");

describe("withValibot", () => {
it("returns coherent errors for complex schemas", async () => {
const schema = v.union( [
v.object({
type: v.literal("foo"),
foo: v.string(),
}),
v.object({
type: v.literal("bar"),
bar: v.string(),
}),
]);
const obj = {
type: "foo",
bar: 123,
foo: 123,
};

expect(await withValibot(schema).validate(obj)).toEqual({
data: undefined,
error: {
fieldErrors: {
type: anyString,
bar: anyString,
foo: anyString,
},
subaction: undefined,
},
submittedData: obj,
});
});

it("returns errors for fields that are unions", async () => {
const schema = v.object({
field1: v.union([v.literal("foo"), v.literal("bar")]),
field2: v.union([v.literal("foo"), v.literal("bar")]),
});
const obj = {
field1: "a value",
// field2 missing
};

const validator = withValibot(schema);
expect(await validator.validate(obj)).toEqual({
data: undefined,
error: {
fieldErrors: {
field1: anyString,
field2: anyString,
},
subaction: undefined,
},
submittedData: obj,
});
});
});


describe("peer dependecy version", () => {
it("should have a peer dependency version that matches the lastet version of RVF", async () => {
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
const peerDependencyVersion = packageJson.peerDependencies["@rvf/core"];
const rvfPackageJson = JSON.parse(
await fs.readFile(corePackageJsonPath, "utf-8"),
);
const rvfVersion = rvfPackageJson.version;

expect(semver.satisfies(rvfVersion, peerDependencyVersion)).toBe(true);
});
});
5 changes: 5 additions & 0 deletions packages/valibot/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "tsconfig/tsconfig.json",
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
3 changes: 3 additions & 0 deletions packages/valibot/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { config } from "tsup-config";

export default config;
29 changes: 29 additions & 0 deletions packages/zod/src/validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { anyString, TestFormData } from "@remix-validated-form/test-utils";
import { withYup } from "@rvf/yup";
import { withValibot } from "@rvf/valibot";
import { Validator, objectFromPathEntries } from "@rvf/core";
import { describe, it, expect } from "vitest";
import * as yup from "yup";
import { z } from "zod";
import * as v from 'valibot';
import { withZod } from ".";

// If adding an adapter, write a validator that validates this shape
Expand Down Expand Up @@ -76,6 +78,33 @@ const validationTestCases: ValidationTestCase[] = [
}),
),
},
{
name: 'valibot',
validator: withValibot(
v.object({
firstName: v.string(),
lastName: v.string(),
age: v.optional(v.number()),
address: v.pipe(
v.unknown(),
v.transform((input) => input ?? {}),
v.object({
streetAddress: v.string(),
city: v.string(),
country: v.string(),
}),
),
pets: v.optional(
v.array(
v.object({
animal: v.string(),
name: v.string(),
}),
),
),
})
)
}
];

describe("Validation", () => {
Expand Down

0 comments on commit ba1e106

Please sign in to comment.