Skip to content

Commit

Permalink
feat: support default schema values
Browse files Browse the repository at this point in the history
  • Loading branch information
DJankauskas committed Jul 28, 2023
1 parent ce411c1 commit 1ddcae3
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 21 deletions.
7 changes: 3 additions & 4 deletions demo/api/posts/models.ts
Expand Up @@ -7,7 +7,6 @@ import { PrismaModel, PrismaModelLoader } from "@stl-api/prisma";
import { PostResponse as PostResponseSchema } from "../../.stl-codegen/api/posts/models";
import { Post as RawPrismaPost } from "@prisma/client";

type Uuid = z.StringSchema<{ uuid: true }>;
export const IncludableUserSchema = z.lazy(() => User).includable();
export const SelectableUserSchema = z.lazy(() => UserSelection).selectable();
export const IncludableCommentsSchema = z
Expand All @@ -18,12 +17,12 @@ export const IncludableCommentsFieldSchema = z
.selectable();

type PostProps = {
id: Uuid;
id: z.UUID;
body: string;
createdAt: Date;
updatedAt: Date;
userId: Uuid;
likedIds: Uuid[];
userId: z.UUID;
likedIds: z.UUID[];
image?: string | null;
user: z.ZodSchema<{ schema: typeof IncludableUserSchema }>;
user_fields: z.ZodSchema<{ schema: typeof SelectableUserSchema }>;
Expand Down
10 changes: 10 additions & 0 deletions packages/stainless/src/z.ts
Expand Up @@ -832,6 +832,8 @@ export abstract class BaseSchema {
export class Schema<O, I = O> extends BaseSchema {
declare input: I;
declare output: O;
declare default: O | (() => O);

validate(value: Out<I>, ctx: StlContext<any>): void {}
transform(
value: Out<I>,
Expand Down Expand Up @@ -986,8 +988,12 @@ export interface StringSchemaProps {
trim?: true;
toLowerCase?: true;
toUpperCase?: true;

default?: string;
}

export type UUID = StringSchema<{ uuid: true }>;

export const StringSchemaSymbol = Symbol("StringSchema");

export class StringSchema<
Expand Down Expand Up @@ -1016,6 +1022,8 @@ export interface NumberSchemaProps {
nonpositive?: OptionalMessage<true>;
finite?: OptionalMessage<true>;
safe?: OptionalMessage<true>;

default?: number;
}

export const NumberSchemaSymbol = Symbol("NumberSchema");
Expand All @@ -1042,6 +1050,8 @@ export interface BigIntSchemaProps {
nonnegative?: OptionalMessage<true>;
negative?: OptionalMessage<true>;
nonpositive?: OptionalMessage<true>;

default?: bigint;
}

export const BigIntSchemaSymbol = Symbol("BigIntSchema");
Expand Down
28 changes: 28 additions & 0 deletions packages/ts-to-zod/src/__tests__/basics.test.ts
Expand Up @@ -459,3 +459,31 @@ export const ValidateTransformSchema: z.ZodTypeAny = z.number().refine(new Basic
",
}
`));

export class ValidateTransformDefaultSchema extends z.Schema<string, number> {
default = "42";

validate(input: number): boolean {
return input % 2 === 0;
}

transform(input: number): string {
return String(input);
}
}

it(`schema with validate, transform, default`, async () =>
expect(
await multiFileTestCase({
__filename,
getNode: (sourceFile) =>
sourceFile.getClass("ValidateTransformDefaultSchema"),
})
).toMatchInlineSnapshot(`
{
"src/__tests__/basics.test.codegen.ts": "import { z } from "zod";
import * as BasicsTest from "./basics.test";
export const ValidateTransformDefaultSchema: z.ZodTypeAny = z.number().refine(new BasicsTest.ValidateTransformDefaultSchema().validate).stlTransform(new BasicsTest.ValidateTransformDefaultSchema().transform).default(new BasicsTest.ValidateTransformDefaultSchema().default);
",
}
`));
5 changes: 3 additions & 2 deletions packages/ts-to-zod/src/__tests__/transform-refine.test.ts
Expand Up @@ -23,6 +23,7 @@ type T = {
finite: true;
safe: "too big";
gt: [5, "5 and below too small!"];
default: 10;
}>;
object: z.ObjectSchema<{}, { passthrough: true }>;
aliasedObject: z.ObjectSchema<Aliased, { strict: true }>;
Expand All @@ -46,11 +47,11 @@ it(`transform`, async () =>
})
).toMatchInlineSnapshot(`
{
"src/__tests__/transform-refine.tesz.codegen.ts": "import { z } from "zod";
"src/__tests__/transform-refine.test.codegen.ts": "import { z } from "zod";
import * as TransformRefineTest from "./transform-refine.test";
export const ParsePet: z.ZodTypeAny = z.string().refine(new TransformRefineTest.ParsePet().validate);
const Aliased: z.ZodTypeAny = z.object({ x: z.string() });
const T: z.ZodTypeAny = z.object({ a: z.date().transform(new TransformRefineTesz.ToString().transform).transform(new TransformRefineTesz.ParseFloat().transform), b: z.string().transform(new TransformRefineTesz.Coerce().transform), c: z.string().refine(new TransformRefineTesz.ParsePet().refine, new TransformRefineTesz.ParsePet().message), d: z.number().superRefine(new TransformRefineTesz.Even().superRefine), date: z.date().min(new Date("2023-01-10")), number: z.number().finite().safe("too big").gt(5, "5 and below too small!"), object: z.object({}).passthrough(), aliasedObject: z.object({ x: z.string() }).strict(), array: z.array(z.number().nullable()).nonempty().min(5, "at least five elements needed"), datetime: z.string().datetime({ offset: true }), catchall: z.object({ a: z.number() }).catchall(z.string()), includes: z.includes(z.lazy(() => Aliased), 3), deepIncludes: z.includes(z.lazy(() => Aliased), 5), selectable: z.lazy(() => Aliased).selectable(), selection: z.lazy(() => Aliased).selection(), pageResponse: z.pageResponse(z.lazy(() => Aliased)) });
const T: z.ZodTypeAny = z.object({ c: z.lazy(() => ParsePetSchema), date: z.date().min(new Date("2023-01-10")), number: z.number().finite().safe("too big").gt(5, "5 and below too small!").default(10), object: z.object({}).passthrough(), aliasedObject: z.object({ x: z.string() }).strict(), array: z.array(z.number().nullable()).nonempty().min(5, "at least five elements needed"), datetime: z.string().datetime({ offset: true }), catchall: z.object({ a: z.number() }).catchall(z.string()), includes: z.includes(z.lazy(() => Aliased), 3), deepIncludes: z.includes(z.lazy(() => Aliased), 5), selectable: z.lazy(() => Aliased).selectable(), selection: z.lazy(() => Aliased).selection(), pageResponse: z.pageResponse(z.lazy(() => Aliased)) });
",
}
`));
18 changes: 16 additions & 2 deletions packages/ts-to-zod/src/convertSchema.ts
Expand Up @@ -53,25 +53,39 @@ export function convertSchema(
typeFilePath,
true
);
const newClassExpression = factory.createNewExpression(
classExpression,
undefined,
[]
);

let schemaExpression = convertType(ctx, inputType, diagnosticItem);

if (declaration.getMethod("validate")) {
schemaExpression = methodCall(schemaExpression, "refine", [
factory.createPropertyAccessExpression(
factory.createNewExpression(classExpression, undefined, []),
newClassExpression,
factory.createIdentifier("validate")
),
]);
}
if (declaration.getMethod("transform")) {
schemaExpression = methodCall(schemaExpression, "stlTransform", [
factory.createPropertyAccessExpression(
factory.createNewExpression(classExpression, undefined, []),
newClassExpression,
factory.createIdentifier("transform")
),
]);
}

if (declaration.getProperty("default")) {
schemaExpression = methodCall(schemaExpression, "default", [
factory.createPropertyAccessExpression(
newClassExpression,
factory.createIdentifier("default")
),
]);
}

return schemaExpression;
}
45 changes: 32 additions & 13 deletions packages/ts-to-zod/src/convertType.ts
Expand Up @@ -12,6 +12,7 @@ import {
stringSchemaProperties,
} from "./typeSchemaUtils";
import { convertSchema } from "./convertSchema";
import { number } from "zod";

export interface Diagnostics {
errors: Incident[];
Expand Down Expand Up @@ -1586,6 +1587,9 @@ function convertSchemaType(
continue;
}

// default is generated after `z.schemaTypeName`
if (name === "default") continue;

// ip and datetime are special in that they can take an object as a
// parameter. This requires special logic in a few cases.
const ipDatetimeSpecialCase = name === "ip" || name === "datetime";
Expand Down Expand Up @@ -1712,6 +1716,22 @@ function convertSchemaType(
}
}

const defaultProperty = typeArgs.getProperty("default");
if (defaultProperty) {
const defaultLiteral = defaultProperty
.getTypeAtLocation(ctx.node)
.getLiteralValue();
if (!defaultLiteral) {
ctx.addError(diagnosticItem, {
message: "`default` must be provided a literal value.",
});
} else {
expression = methodCall(expression, "default", [
convertLiteralValueToExpression(defaultLiteral),
]);
}
}

return expression;
}

Expand Down Expand Up @@ -1831,18 +1851,17 @@ export function getPropertyDeclaration(
}
}

/** Converts a path into an identifier suitable for local use within a file */
function mangleString(str: string): string {
const unicodeLetterRegex = /\p{L}/u;
const escapedStringBuilder = ["__"];
for (const codePointString of str) {
if (codePointString === Path.sep) {
escapedStringBuilder.push("$");
} else if (unicodeLetterRegex.test(codePointString)) {
escapedStringBuilder.push(codePointString);
} else {
escapedStringBuilder.push(`u${codePointString.codePointAt(0)}`);
}
function convertLiteralValueToExpression(
literal: string | number | ts.PseudoBigInt | undefined
) {
switch (typeof literal) {
case "string":
return factory.createStringLiteral(literal);
case "number":
return factory.createNumericLiteral(literal);
case "undefined":
return factory.createIdentifier("undefined");
default:
return factory.createBigIntLiteral(literal);
}
return escapedStringBuilder.join("");
}
3 changes: 3 additions & 0 deletions packages/ts-to-zod/src/typeSchemaUtils.ts
Expand Up @@ -14,6 +14,7 @@ export const numberSchemaProperties = new Set([
"nonpositive",
"finite",
"safe",
"default",
]);

export const stringSchemaProperties = new Set([
Expand All @@ -35,6 +36,7 @@ export const stringSchemaProperties = new Set([
"trim",
"toLowerCase",
"toUpperCase",
"default",
]);

export const bigIntSchemaProperties = new Set([
Expand All @@ -49,6 +51,7 @@ export const bigIntSchemaProperties = new Set([
"nonnegative",
"negative",
"nonpositive",
"default",
]);

export const dateSchemaProperties = new Set(["min", "max"]);
Expand Down

0 comments on commit 1ddcae3

Please sign in to comment.