diff --git a/.changeset/gentle-rings-dream.md b/.changeset/gentle-rings-dream.md new file mode 100644 index 0000000..65cadff --- /dev/null +++ b/.changeset/gentle-rings-dream.md @@ -0,0 +1,5 @@ +--- +"formgator": minor +--- + +Formgator is now standard-schema compliant! Read more on https://standardschema.dev/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 48b31c6..f429009 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,5 @@ "prettier.singleQuote": false, "[svelte]": { "editor.defaultFormatter": "svelte.svelte-vscode" - }, + } } diff --git a/package.json b/package.json index 93f1b9e..c1d96e0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "devDependencies": { "@biomejs/biome": "1.9.4", "@changesets/cli": "^2.27.12", + "@standard-schema/spec": "^1.0.0", "@sveltejs/kit": "^2.16.1", "@types/node": "^22.12.0", "pkgroll": "^2.6.1", diff --git a/src/index.test.ts b/src/index.test.ts index 0e1d5a2..7c3f954 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,6 +1,7 @@ import { describe, it } from "node:test"; +import type { StandardSchemaV1 } from "@standard-schema/spec"; import assert from "./assert.ts"; -import { fail, failures } from "./definitions.ts"; +import { type ReadonlyFormData, fail, failures } from "./definitions.ts"; import * as fg from "./index.ts"; describe("form()", () => { @@ -58,4 +59,50 @@ describe("form()", () => { } }); }); + + describe(".~standard()", () => { + it("should accept valid inputs", () => { + const schema = fg.form({ + text: fg.text(), + number: fg.range(), + }) satisfies StandardSchemaV1; + + const data = new FormData(); + data.append("text", "Hello World!"); + data.append("number", "50"); + assert.deepEqualTyped(schema["~standard"].validate(data), { + value: { text: "Hello World!", number: 50 }, + }); + }); + + it("should reject invalid inputs", () => { + const schema = fg.form({ + text: fg.text(), + number: fg.range(), + }) satisfies StandardSchemaV1; + + const data = new FormData(); + data.append("number", "123"); + assert.deepEqualTyped(schema["~standard"].validate(data), { + issues: [ + { + message: "Invalid type", + path: ["text"], + }, + { + message: "Too big, maximum value is 100", + path: ["number"], + }, + ], + }); + + assert.deepEqualTyped(schema["~standard"].validate({}), { + issues: [ + { + message: "value must be FormData or URLSearchParams", + }, + ], + }); + }); + }); }); diff --git a/src/index.ts b/src/index.ts index 46bdb76..9cfd28c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,14 +32,15 @@ * @module */ +import type { StandardSchemaV1 } from "@standard-schema/spec"; import { type FormInput, type ReadonlyFormData, type Result, type ValidationIssue, fail, - safeParse, succeed, + safeParse as symbol, } from "./definitions.ts"; export { checkbox } from "./validators/checkbox.ts"; @@ -159,29 +160,46 @@ export function form>>( accepted: Partial>; } >; + "~standard": StandardSchemaV1.Props>; } { + const safeParse = (data: ReadonlyFormData) => { + const entries: Array<[string, unknown]> = []; + const errorEntries: Array<[string, ValidationIssue | null]> = []; + for (const [name, input] of Object.entries(inputs)) { + const result = input[symbol](data, name); + if (result.success === false) errorEntries.push([name, result.error]); + else entries.push([name, result.data]); + } + return errorEntries.length === 0 + ? succeed(Object.fromEntries(entries) as Output) + : fail({ + issues: Object.fromEntries(errorEntries) as Issues, + accepted: Object.fromEntries(entries) as Partial>, + }); + }; return { inputs, - safeParse: (data) => { - const entries: Array<[string, unknown]> = []; - const errorEntries: Array<[string, ValidationIssue | null]> = []; - for (const [name, input] of Object.entries(inputs)) { - const result = input[safeParse](data, name); - if (result.success === false) errorEntries.push([name, result.error]); - else entries.push([name, result.data]); - } - return errorEntries.length === 0 - ? succeed(Object.fromEntries(entries) as Output) - : fail({ - issues: Object.fromEntries(errorEntries) as Issues, - accepted: Object.fromEntries(entries) as Partial>, - }); - }, - parse(data) { - const result = this.safeParse(data); + safeParse, + parse: (data) => { + const result = safeParse(data); if (result.success === false) throw new FormgatorError(result.error.issues, result.error.accepted); return result.data; }, + "~standard": { + version: 1, + vendor: "formgator", + validate: (value) => { + if (!(value instanceof URLSearchParams) && !(value instanceof FormData)) + return { issues: [{ message: "value must be FormData or URLSearchParams" }] }; + const result = safeParse(value); + if (result.success) return { value: result.data }; + return { + issues: Object.entries(result.error.issues).map( + ([key, { message }]) => ({ message, path: [key] }), + ), + }; + }, + }, }; } diff --git a/yarn.lock b/yarn.lock index 5f80c05..8949f69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -949,6 +949,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.0.0": + version: 1.0.0 + resolution: "@standard-schema/spec@npm:1.0.0" + checksum: 10c0/a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f + languageName: node + linkType: hard + "@sveltejs/kit@npm:^2.16.1": version: 2.16.1 resolution: "@sveltejs/kit@npm:2.16.1" @@ -1646,6 +1653,7 @@ __metadata: dependencies: "@biomejs/biome": "npm:1.9.4" "@changesets/cli": "npm:^2.27.12" + "@standard-schema/spec": "npm:^1.0.0" "@sveltejs/kit": "npm:^2.16.1" "@types/node": "npm:^22.12.0" pkgroll: "npm:^2.6.1"