Skip to content

Commit

Permalink
fix: Validation correctly even when the optional and nullish keys…
Browse files Browse the repository at this point in the history
… are not specified (#54)
  • Loading branch information
chimame authored Jan 30, 2025
1 parent 8288719 commit 4e1e57b
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 34 deletions.
109 changes: 79 additions & 30 deletions coercion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
type PipeItem,
type SchemaWithPipe,
type SchemaWithPipeAsync,
nullish,
nullishAsync,
optional,
optionalAsync,
pipe,
pipeAsync,
transform as vTransform,
Expand Down Expand Up @@ -152,6 +156,74 @@ function generateReturnSchema<
return coercionSchema;
}

/**
* Generate a wrapped schema with coercion
* @param type The schema to be coerced
* @param originalSchema The original schema
* @param schemaType The schema type
* @returns The coerced schema
*/
function generateWrappedSchema<T extends GenericSchema | GenericSchemaAsync>(
type: T,
originalSchema: T,
schemaType?: "nullish" | "optional",
) {
// @ts-expect-error
const { coerced, schema: wrapSchema } = enableTypeCoercion(type.wrapped);

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) {
switch (schemaType) {
case "nullish":
return {
coerced: false,
schema: nullishAsync(pipeAsync(unknown, wrapSchema.pipe[1], type)),
};
case "optional":
return {
coerced: false,
schema: optionalAsync(pipeAsync(unknown, wrapSchema.pipe[1], type)),
};
default:
return {
coerced,
schema: pipeAsync(unknown, wrapSchema.pipe[1], type),
};
}
}
switch (schemaType) {
case "nullish":
return {
coerced: false,
schema: nullish(pipe(unknown, wrapSchema.pipe[1], type)),
};
case "optional":
return {
coerced: false,
schema: optional(pipe(unknown, wrapSchema.pipe[1], type)),
};
default:
return {
coerced,
schema: pipe(unknown, wrapSchema.pipe[1], type),
};
}
}

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

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

/**
* Reconstruct the provided schema with additional preprocessing steps
* This coerce empty values to undefined and transform strings to the correct type
Expand Down Expand Up @@ -244,41 +316,18 @@ export function enableTypeCoercion<
schema: generateReturnSchema(type, exactOptionalSchema),
};
}
case "nullish": {
return generateWrappedSchema(type, originalSchema, type.type);
}
case "optional": {
return generateWrappedSchema(type, originalSchema, type.type);
}
case "undefinedable":
case "optional":
case "nullish":
case "nullable":
case "non_optional":
case "non_nullish":
case "non_nullable": {
// @ts-expect-error
const { coerced, schema: wrapSchema } = enableTypeCoercion(type.wrapped);

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 {
coerced,
schema: pipeAsync(unknown, wrapSchema.pipe[1], type),
};
}
return {
coerced,
schema: pipe(unknown, wrapSchema.pipe[1], type),
};
}

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

return {
coerced: false,
schema: generateReturnSchema(type, wrappedSchema),
};
return generateWrappedSchema(type, originalSchema);
}
case "union":
case "intersect": {
Expand Down
2 changes: 1 addition & 1 deletion tests/coercion/schema/nullish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createFormData } from "../../helpers/FormData";

describe("nullish", () => {
test("should pass also undefined", () => {
const schema = object({ age: nullish(number()) });
const schema = object({ name: nullish(string()), age: nullish(number()) });
const output = parseWithValibot(createFormData("age", ""), { schema });

expect(output).toMatchObject({
Expand Down
5 changes: 4 additions & 1 deletion tests/coercion/schema/nullishAsync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { createFormData } from "../../helpers/FormData";

describe("nullishAsync", () => {
test("should pass also undefined", async () => {
const schema = objectAsync({ age: nullishAsync(number()) });
const schema = objectAsync({
name: nullishAsync(string()),
age: nullishAsync(number()),
});
const output = await parseWithValibot(createFormData("age", ""), {
schema,
});
Expand Down
5 changes: 4 additions & 1 deletion tests/coercion/schema/optional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { createFormData } from "../../helpers/FormData";

describe("optional", () => {
test("should pass also undefined", () => {
const schema = object({ age: optional(number()) });
const schema = object({
name: optional(string()),
age: optional(number()),
});
const output = parseWithValibot(createFormData("age", ""), { schema });

expect(output).toMatchObject({
Expand Down
5 changes: 4 additions & 1 deletion tests/coercion/schema/optionalAsync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { createFormData } from "../../helpers/FormData";

describe("optionalAsync", () => {
test("should pass also undefined", async () => {
const schema = objectAsync({ age: optionalAsync(number()) });
const schema = objectAsync({
name: optionalAsync(string()),
age: optionalAsync(number()),
});
const output = await parseWithValibot(createFormData("age", ""), {
schema,
});
Expand Down

0 comments on commit 4e1e57b

Please sign in to comment.