Skip to content

Commit 29de6c2

Browse files
maxholmanclaude
andcommitted
feat!: generate dual exact/coerced valibot schemas
Every schema now has two variants: exactFooSchema (spec-literal, strict validation) and fooSchema (coerced, accepts realistic wire types). This enables clean separation between server validation (exact, no cleanup) and client-side DX (coerced, trims strings, handles int64 from JSON, coerces HTTP param strings to native types). - Hono middleware uses exact schemas — server validates strictly - Coerced schemas use v.optional() instead of v.exactOptional() - String trimming in coerced mode (not UUIDs, patterns, enums) - int64 coercion via v.toBigint() only where JSON can't represent it - HTTP param coercion via v.toNumber() for query/header strings - Command classes gain static schema properties (bodySchema, paramsSchema, querySchema, responseSchema) for future rest-client integration - Identical schemas alias to exact (export const foo = exactFoo) - --exact-only CLI flag to skip coerced variants Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9cf97a4 commit 29de6c2

27 files changed

Lines changed: 43600 additions & 16860 deletions

__tests__/__snapshots__/nullables.test.ts.snap

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ export type NullConst = null;
1111
exports[`const values 2`] = `
1212
"import * as v from "valibot";
1313
14-
export const stringConstSchema = v.literal("hello");
15-
export const numberConstSchema = v.literal(42);
16-
export const booleanConstSchema = v.literal(true);
17-
export const nullConstSchema = v.null();
14+
export const exactStringConstSchema = v.literal("hello");
15+
export const stringConstSchema = exactStringConstSchema;
16+
export const exactNumberConstSchema = v.literal(42);
17+
export const numberConstSchema = exactNumberConstSchema;
18+
export const exactBooleanConstSchema = v.literal(true);
19+
export const booleanConstSchema = exactBooleanConstSchema;
20+
export const exactNullConstSchema = v.null();
21+
export const nullConstSchema = exactNullConstSchema;
1822
"
1923
`;
2024

@@ -35,6 +39,7 @@ export type UploadDataCommandInput = UploadDataCommandParams;
3539
exports[`header parameters 2`] = `
3640
"import { Command } from "@block65/rest-client";
3741
import type { UploadDataCommandHeader, UploadDataCommandInput, UploadStatus } from "./types.js";
42+
import { uploadDataCommandParamsSchema, uploadStatusSchema } from "./valibot.js";
3843
3944
/**
4045
* Tagged template literal that applies encodeURIComponent to all interpolated
@@ -51,6 +56,8 @@ function encodePath(strings: TemplateStringsArray, ...values: string[]): string
5156
*/
5257
export class UploadDataCommand extends Command<UploadDataCommandInput, UploadStatus, never, UploadDataCommandHeader> {
5358
public override method = "post" as const;
59+
static paramsSchema = uploadDataCommandParamsSchema;
60+
static responseSchema = uploadStatusSchema;
5461
5562
constructor(input: UploadDataCommandInput, headers: UploadDataCommandHeader) {
5663
const {uploadId } = input;
@@ -63,23 +70,32 @@ export class UploadDataCommand extends Command<UploadDataCommandInput, UploadSta
6370
exports[`header parameters 3`] = `
6471
"import * as v from "valibot";
6572
66-
export const uploadStatusSchema = v.picklist(["pending", "complete"]);
67-
export const uploadDataCommandParamsSchema = v.strictObject({
73+
export const exactUploadStatusSchema = v.picklist(["pending", "complete"]);
74+
export const uploadStatusSchema = exactUploadStatusSchema;
75+
export const exactUploadDataCommandParamsSchema = v.strictObject({
6876
"uploadId": v.string()
6977
});
70-
export const uploadDataCommandHeaderSchema = v.object({
78+
export const uploadDataCommandParamsSchema = v.strictObject({
79+
"uploadId": v.pipe(v.string(), v.trim())
80+
});
81+
export const exactUploadDataCommandHeaderSchema = v.object({
7182
"content-type": v.picklist(["application/json", "text/csv", "application/xml"]),
72-
"content-length": v.pipe(v.string(), v.digits(), v.transform((n) => Number.parseInt(n, 10)), v.number(), v.integer()),
83+
"content-length": v.bigint(),
7384
"x-idempotency-key": v.exactOptional(v.pipe(v.string(), v.uuid()))
7485
});
86+
export const uploadDataCommandHeaderSchema = v.object({
87+
"content-type": v.picklist(["application/json", "text/csv", "application/xml"]),
88+
"content-length": v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.bigint()), v.pipe(v.number(), v.integer(), v.toBigint(), v.bigint()), v.bigint()]),
89+
"x-idempotency-key": v.optional(v.pipe(v.string(), v.uuid()))
90+
});
7591
"
7692
`;
7793
7894
exports[`header parameters 4`] = `
7995
"import { validator } from "hono/validator";
8096
import * as v from "valibot";
8197
import { PublicValibotHonoError } from "@block65/rest-client";
82-
import { uploadDataCommandParamsSchema } from "./valibot.js";
98+
import { exactUploadDataCommandParamsSchema } from "./valibot.js";
8399
84100
function toPublicValibotHonoError(err: unknown): never {
85101
@@ -92,7 +108,7 @@ function toPublicValibotHonoError(err: unknown): never {
92108
93109
export const uploadData = [
94110
validator("param", (value) => {
95-
return v.parseAsync(uploadDataCommandParamsSchema, value).catch(toPublicValibotHonoError);
111+
return v.parseAsync(exactUploadDataCommandParamsSchema, value).catch(toPublicValibotHonoError);
96112
}),
97113
] as const;
98114
"
@@ -106,7 +122,8 @@ exports[`nullables 1`] = `
106122
exports[`oneOf with type null generates v.null() 1`] = `
107123
"import * as v from "valibot";
108124
109-
export const nullableImageSchema = v.union([v.string(), v.null()]);
125+
export const exactNullableImageSchema = v.union([v.string(), v.null()]);
126+
export const nullableImageSchema = v.union([v.pipe(v.string(), v.trim()), v.null()]);
110127
"
111128
`;
112129
@@ -127,14 +144,23 @@ export type ListFilesCommandInput = ListFilesCommandQuery;
127144
exports[`query and header integer params coerce strings to numbers 2`] = `
128145
"import * as v from "valibot";
129146
130-
export const dummySchema = v.string();
131-
export const expireTimeSchema = v.pipe(v.number(), v.integer(), v.minValue(0));
147+
export const exactDummySchema = v.string();
148+
export const dummySchema = v.pipe(v.string(), v.trim());
149+
export const exactExpireTimeSchema = v.pipe(v.bigint(), v.minValue(BigInt(0)));
150+
export const expireTimeSchema = v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.pipe(v.bigint(), v.minValue(BigInt(0)))), v.pipe(v.number(), v.integer(), v.toBigint(), v.pipe(v.bigint(), v.minValue(BigInt(0)))), v.pipe(v.bigint(), v.minValue(BigInt(0)))]);
151+
export const exactListFilesCommandQuerySchema = v.strictObject({
152+
"exp": v.pipe(v.bigint(), v.minValue(BigInt(0))),
153+
"limit": v.exactOptional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100)))
154+
});
132155
export const listFilesCommandQuerySchema = v.strictObject({
133-
"exp": v.pipe(v.string(), v.digits(), v.transform((n) => Number.parseInt(n, 10)), v.number(), v.integer(), v.minValue(0)),
134-
"limit": v.exactOptional(v.pipe(v.string(), v.digits(), v.transform((n) => Number.parseInt(n, 10)), v.number(), v.integer(), v.minValue(1), v.maxValue(100)))
156+
"exp": v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.pipe(v.bigint(), v.minValue(BigInt(0)))), v.pipe(v.number(), v.integer(), v.toBigint(), v.pipe(v.bigint(), v.minValue(BigInt(0)))), v.pipe(v.bigint(), v.minValue(BigInt(0)))]),
157+
"limit": v.optional(v.union([v.pipe(v.string(), v.decimal(), v.toNumber(), v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))), v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))]))
158+
});
159+
export const exactListFilesCommandHeaderSchema = v.object({
160+
"x-rate-limit": v.pipe(v.number(), v.integer(), v.minValue(0))
135161
});
136162
export const listFilesCommandHeaderSchema = v.object({
137-
"x-rate-limit": v.pipe(v.string(), v.digits(), v.transform((n) => Number.parseInt(n, 10)), v.number(), v.integer(), v.minValue(0))
163+
"x-rate-limit": v.union([v.pipe(v.string(), v.decimal(), v.toNumber(), v.pipe(v.number(), v.integer(), v.minValue(0))), v.pipe(v.number(), v.integer(), v.minValue(0))])
138164
});
139165
"
140166
`;
@@ -150,10 +176,14 @@ export type MultiType = string | number;
150176
exports[`top-level type array with null 2`] = `
151177
"import * as v from "valibot";
152178
153-
export const nullableStringSchema = v.nullable(v.string());
154-
export const nullableStringEnumSchema = v.nullable(v.picklist(["active", "inactive"]));
155-
export const nullableIntegerSchema = v.nullable(v.pipe(v.number(), v.integer()));
156-
export const multiTypeSchema = v.union([v.string(), v.number()]);
179+
export const exactNullableStringSchema = v.nullable(v.string());
180+
export const nullableStringSchema = exactNullableStringSchema;
181+
export const exactNullableStringEnumSchema = v.nullable(v.picklist(["active", "inactive"]));
182+
export const nullableStringEnumSchema = exactNullableStringEnumSchema;
183+
export const exactNullableIntegerSchema = v.nullable(v.pipe(v.number(), v.integer()));
184+
export const nullableIntegerSchema = exactNullableIntegerSchema;
185+
export const exactMultiTypeSchema = v.union([v.string(), v.number()]);
186+
export const multiTypeSchema = exactMultiTypeSchema;
157187
"
158188
`;
159189

0 commit comments

Comments
 (0)