Skip to content

Commit

Permalink
fix: Cannot be validated correctly when nested pipe schema exists (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
chimame authored Dec 7, 2024
1 parent 2cc0dda commit 663d2f1
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 48 deletions.
139 changes: 94 additions & 45 deletions coercion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,53 +146,67 @@ export function enableTypeCoercion<
: SchemaWithPipeAsync<
[T, ...PipeItem<unknown, unknown, BaseIssue<unknown>>[]]
>),
):
| ReturnType<typeof coerce>
| ReturnType<typeof generateReturnSchema>
| (T extends GenericSchema
? SchemaWithPipe<[T, ...PipeItem<unknown, unknown, BaseIssue<unknown>>[]]>
: SchemaWithPipeAsync<
[T, ...PipeItem<unknown, unknown, BaseIssue<unknown>>[]]
>) {
): {
coerced: boolean;
schema:
| ReturnType<typeof coerce>
| ReturnType<typeof generateReturnSchema>
| (T extends GenericSchema
? SchemaWithPipe<
[T, ...PipeItem<unknown, unknown, BaseIssue<unknown>>[]]
>
: SchemaWithPipeAsync<
[T, ...PipeItem<unknown, unknown, BaseIssue<unknown>>[]]
>);
} {
const originalSchema = "pipe" in type ? type.pipe[0] : type;

switch (type.type) {
case "string":
case "literal":
case "enum":
case "undefined": {
return coerce(type);
return { coerced: true, schema: coerce(type) };
}
case "number": {
return coerce(type, Number);
return { coerced: true, schema: coerce(type, Number) };
}
case "boolean": {
return coerce(type, (text) => (text === "on" ? true : text));
return {
coerced: true,
schema: coerce(type, (text) => (text === "on" ? true : text)),
};
}
case "date": {
return coerce(type, (timestamp) => {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) {
return timestamp;
}
return {
coerced: true,
schema: coerce(type, (timestamp) => {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) {
return timestamp;
}

return date;
});
return date;
}),
};
}
case "bigint": {
return coerce(type, BigInt);
return { coerced: true, schema: coerce(type, BigInt) };
}
case "file":
case "blob": {
return coerce(type);
return { coerced: true, schema: coerce(type) };
}
case "array": {
const arraySchema = {
...originalSchema,
// @ts-expect-error
item: enableTypeCoercion(originalSchema.item),
item: enableTypeCoercion(originalSchema.item).schema,
};
return {
coerced: false,
schema: generateReturnSchema(type, arraySchema),
};
return generateReturnSchema(type, arraySchema);
}
case "optional":
case "nullish":
Expand All @@ -201,63 +215,92 @@ export function enableTypeCoercion<
case "non_nullish":
case "non_nullable": {
// @ts-expect-error
const wrapSchema = enableTypeCoercion(type.wrapped);
const { coerced, schema: wrapSchema } = enableTypeCoercion(type.wrapped);

if ("pipe" in wrapSchema) {
if (coerced) {
// `expects` is required to generate error messages for `TupleSchema`, so it is passed to `UnkonwSchema` for coercion.
const unknown = { ...valibotUnknown(), expects: type.expects };
if (type.async) {
return pipeAsync(unknown, wrapSchema.pipe[1], type);
return {
coerced,
schema: pipeAsync(unknown, wrapSchema.pipe[1], type),
};
}
return pipe(unknown, wrapSchema.pipe[1], type);
return {
coerced,
schema: pipe(unknown, wrapSchema.pipe[1], type),
};
}

const wrappedSchema = {
...originalSchema,
// @ts-expect-error
wrapped: enableTypeCoercion(originalSchema.wrapped),
wrapped: enableTypeCoercion(originalSchema.wrapped).schema,
};

return generateReturnSchema(type, wrappedSchema);
return {
coerced: false,
schema: generateReturnSchema(type, wrappedSchema),
};
}
case "union":
case "intersect": {
const unionSchema = {
...originalSchema,
// @ts-expect-error
options: originalSchema.options.map((option) =>
enableTypeCoercion(option as GenericSchema),
options: originalSchema.options.map(
// @ts-expect-error
(option) => enableTypeCoercion(option as GenericSchema).schema,
),
};
return generateReturnSchema(type, unionSchema);
return {
coerced: false,
schema: generateReturnSchema(type, unionSchema),
};
}
case "variant": {
const variantSchema = {
...originalSchema,
// @ts-expect-error
options: originalSchema.options.map((option) =>
enableTypeCoercion(option as GenericSchema),
options: originalSchema.options.map(
// @ts-expect-error
(option) => enableTypeCoercion(option as GenericSchema).schema,
),
};
return generateReturnSchema(type, variantSchema);
return {
coerced: false,
schema: generateReturnSchema(type, variantSchema),
};
}
case "tuple": {
const tupleSchema = {
...originalSchema,
// @ts-expect-error
items: originalSchema.items.map((option) => enableTypeCoercion(option)),
items: originalSchema.items.map(
// @ts-expect-error
(option) => enableTypeCoercion(option).schema,
),
};
return {
coerced: false,
schema: generateReturnSchema(type, tupleSchema),
};
return generateReturnSchema(type, tupleSchema);
}
case "tuple_with_rest": {
const tupleWithRestSchema = {
...originalSchema,
// @ts-expect-error
items: originalSchema.items.map((option) => enableTypeCoercion(option)),
items: originalSchema.items.map(
// @ts-expect-error
(option) => enableTypeCoercion(option).schema,
),
// @ts-expect-error
rest: enableTypeCoercion(originalSchema.rest),
rest: enableTypeCoercion(originalSchema.rest).schema,
};
return {
coerced: false,
schema: generateReturnSchema(type, tupleWithRestSchema),
};
return generateReturnSchema(type, tupleWithRestSchema);
}
case "loose_object":
case "strict_object":
Expand All @@ -268,12 +311,15 @@ export function enableTypeCoercion<
// @ts-expect-error
Object.entries(originalSchema.entries).map(([key, def]) => [
key,
enableTypeCoercion(def as GenericSchema),
enableTypeCoercion(def as GenericSchema).schema,
]),
),
};

return generateReturnSchema(type, objectSchema);
return {
coerced: false,
schema: generateReturnSchema(type, objectSchema),
};
}
case "object_with_rest": {
const objectWithRestSchema = {
Expand All @@ -282,16 +328,19 @@ export function enableTypeCoercion<
// @ts-expect-error
Object.entries(originalSchema.entries).map(([key, def]) => [
key,
enableTypeCoercion(def as GenericSchema),
enableTypeCoercion(def as GenericSchema).schema,
]),
),
// @ts-expect-error
rest: enableTypeCoercion(originalSchema.rest),
rest: enableTypeCoercion(originalSchema.rest).schema,
};

return generateReturnSchema(type, objectWithRestSchema);
return {
coerced: false,
schema: generateReturnSchema(type, objectWithRestSchema),
};
}
}

return coerce(type);
return { coerced: true, schema: coerce(type) };
}
2 changes: 1 addition & 1 deletion parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function parseWithValibot<
typeof config.schema === "function"
? config.schema(intent)
: config.schema;
const schema = enableTypeCoercion(originalSchema);
const { schema } = enableTypeCoercion(originalSchema);

const resolveResult = (
result: SafeParseResult<Schema>,
Expand Down
36 changes: 35 additions & 1 deletion tests/coercion/schema/wrap.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { nullable, object, optional, string } from "valibot";
import {
check,
isoDate,
nullable,
object,
optional,
pipe,
string,
} from "valibot";
import { describe, expect, test } from "vitest";
import { parseWithValibot } from "../../../parse";
import { createFormData } from "../../helpers/FormData";
Expand All @@ -9,4 +17,30 @@ describe("wrap", () => {
const output = parseWithValibot(createFormData("name", ""), { schema });
expect(output).toMatchObject({ status: "success", value: { name: null } });
});

test("should pass with nested pipe object", () => {
const schema = object({
key1: string(),
key2: optional(
pipe(
object({
date: optional(pipe(string(), isoDate())),
}),
check((input) => input?.date !== "2000-01-01", "Bad date"),
),
),
});

const input = createFormData("key1", "valid");
input.append("key2.date", "");

const output = parseWithValibot(input, {
schema,
});

expect(output).toMatchObject({
status: "success",
value: { key1: "valid", key2: {} },
});
});
});
36 changes: 35 additions & 1 deletion tests/coercion/schema/wrapAsync.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { nullableAsync, objectAsync, optionalAsync, string } from "valibot";
import {
checkAsync,
isoDate,
nullableAsync,
objectAsync,
optionalAsync,
pipeAsync,
string,
} from "valibot";
import { describe, expect, test } from "vitest";
import { parseWithValibot } from "../../../parse";
import { createFormData } from "../../helpers/FormData";
Expand All @@ -13,4 +21,30 @@ describe("wrapAsync", () => {
});
expect(output).toMatchObject({ status: "success", value: { name: null } });
});

test("should pass with nested pipe object", async () => {
const schema = objectAsync({
key1: string(),
key2: optionalAsync(
pipeAsync(
objectAsync({
date: optionalAsync(pipeAsync(string(), isoDate())),
}),
checkAsync((input) => input?.date !== "2000-01-01", "Bad date"),
),
),
});

const input = createFormData("key1", "valid");
input.append("key2.date", "");

const output = await parseWithValibot(input, {
schema,
});

expect(output).toMatchObject({
status: "success",
value: { key1: "valid", key2: {} },
});
});
});

0 comments on commit 663d2f1

Please sign in to comment.