From 1ddcae3a37e98fd77b82fd9169e16c41d8c40032 Mon Sep 17 00:00:00 2001 From: Darius Jankauskas Date: Fri, 28 Jul 2023 13:04:45 -0400 Subject: [PATCH] feat: support default schema values --- demo/api/posts/models.ts | 7 ++- packages/stainless/src/z.ts | 10 +++++ .../ts-to-zod/src/__tests__/basics.test.ts | 28 ++++++++++++ .../src/__tests__/transform-refine.test.ts | 5 ++- packages/ts-to-zod/src/convertSchema.ts | 18 +++++++- packages/ts-to-zod/src/convertType.ts | 45 +++++++++++++------ packages/ts-to-zod/src/typeSchemaUtils.ts | 3 ++ 7 files changed, 95 insertions(+), 21 deletions(-) diff --git a/demo/api/posts/models.ts b/demo/api/posts/models.ts index 2ccb34e5..b8ace017 100644 --- a/demo/api/posts/models.ts +++ b/demo/api/posts/models.ts @@ -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 @@ -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 }>; diff --git a/packages/stainless/src/z.ts b/packages/stainless/src/z.ts index 61ca0a35..c995f2c0 100644 --- a/packages/stainless/src/z.ts +++ b/packages/stainless/src/z.ts @@ -832,6 +832,8 @@ export abstract class BaseSchema { export class Schema extends BaseSchema { declare input: I; declare output: O; + declare default: O | (() => O); + validate(value: Out, ctx: StlContext): void {} transform( value: Out, @@ -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< @@ -1016,6 +1022,8 @@ export interface NumberSchemaProps { nonpositive?: OptionalMessage; finite?: OptionalMessage; safe?: OptionalMessage; + + default?: number; } export const NumberSchemaSymbol = Symbol("NumberSchema"); @@ -1042,6 +1050,8 @@ export interface BigIntSchemaProps { nonnegative?: OptionalMessage; negative?: OptionalMessage; nonpositive?: OptionalMessage; + + default?: bigint; } export const BigIntSchemaSymbol = Symbol("BigIntSchema"); diff --git a/packages/ts-to-zod/src/__tests__/basics.test.ts b/packages/ts-to-zod/src/__tests__/basics.test.ts index 33d5dd0e..632678c8 100644 --- a/packages/ts-to-zod/src/__tests__/basics.test.ts +++ b/packages/ts-to-zod/src/__tests__/basics.test.ts @@ -459,3 +459,31 @@ export const ValidateTransformSchema: z.ZodTypeAny = z.number().refine(new Basic ", } `)); + +export class ValidateTransformDefaultSchema extends z.Schema { + 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); +", +} +`)); diff --git a/packages/ts-to-zod/src/__tests__/transform-refine.test.ts b/packages/ts-to-zod/src/__tests__/transform-refine.test.ts index 4c41aa99..ab165839 100644 --- a/packages/ts-to-zod/src/__tests__/transform-refine.test.ts +++ b/packages/ts-to-zod/src/__tests__/transform-refine.test.ts @@ -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; @@ -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)) }); ", } `)); diff --git a/packages/ts-to-zod/src/convertSchema.ts b/packages/ts-to-zod/src/convertSchema.ts index e3e31068..4f6faf14 100644 --- a/packages/ts-to-zod/src/convertSchema.ts +++ b/packages/ts-to-zod/src/convertSchema.ts @@ -53,13 +53,18 @@ 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") ), ]); @@ -67,11 +72,20 @@ export function convertSchema( 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; } diff --git a/packages/ts-to-zod/src/convertType.ts b/packages/ts-to-zod/src/convertType.ts index c05f6ccc..77786ce7 100644 --- a/packages/ts-to-zod/src/convertType.ts +++ b/packages/ts-to-zod/src/convertType.ts @@ -12,6 +12,7 @@ import { stringSchemaProperties, } from "./typeSchemaUtils"; import { convertSchema } from "./convertSchema"; +import { number } from "zod"; export interface Diagnostics { errors: Incident[]; @@ -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"; @@ -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; } @@ -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(""); } diff --git a/packages/ts-to-zod/src/typeSchemaUtils.ts b/packages/ts-to-zod/src/typeSchemaUtils.ts index 89156fe7..ca5e1015 100644 --- a/packages/ts-to-zod/src/typeSchemaUtils.ts +++ b/packages/ts-to-zod/src/typeSchemaUtils.ts @@ -14,6 +14,7 @@ export const numberSchemaProperties = new Set([ "nonpositive", "finite", "safe", + "default", ]); export const stringSchemaProperties = new Set([ @@ -35,6 +36,7 @@ export const stringSchemaProperties = new Set([ "trim", "toLowerCase", "toUpperCase", + "default", ]); export const bigIntSchemaProperties = new Set([ @@ -49,6 +51,7 @@ export const bigIntSchemaProperties = new Set([ "nonnegative", "negative", "nonpositive", + "default", ]); export const dateSchemaProperties = new Set(["min", "max"]);