diff --git a/integration-tests/typescript-koa/src/generated/api.github.com.yaml/generated.ts b/integration-tests/typescript-koa/src/generated/api.github.com.yaml/generated.ts index 88d70bcd..608a1840 100644 --- a/integration-tests/typescript-koa/src/generated/api.github.com.yaml/generated.ts +++ b/integration-tests/typescript-koa/src/generated/api.github.com.yaml/generated.ts @@ -20081,7 +20081,7 @@ export function createRouter(implementation: Implementation): KoaRouter { before: z.string().optional(), after: z.string().optional(), direction: z.enum(["asc", "desc"]).optional(), - per_page: z.coerce.number().optional(), + per_page: z.coerce.number().min(1).max(100).optional(), sort: z.enum(["updated", "published"]).optional(), }) @@ -21830,8 +21830,8 @@ export function createRouter(implementation: Implementation): KoaRouter { direction: z.enum(["asc", "desc"]).optional(), before: z.string().optional(), after: z.string().optional(), - first: z.coerce.number().optional(), - last: z.coerce.number().optional(), + first: z.coerce.number().min(1).max(100).optional(), + last: z.coerce.number().min(1).max(100).optional(), per_page: z.coerce.number().optional(), }) @@ -29346,8 +29346,8 @@ export function createRouter(implementation: Implementation): KoaRouter { direction: z.enum(["asc", "desc"]).optional(), before: z.string().optional(), after: z.string().optional(), - first: z.coerce.number().optional(), - last: z.coerce.number().optional(), + first: z.coerce.number().min(1).max(100).optional(), + last: z.coerce.number().min(1).max(100).optional(), per_page: z.coerce.number().optional(), }) @@ -34758,7 +34758,7 @@ export function createRouter(implementation: Implementation): KoaRouter { sort: z.enum(["created", "updated", "published"]).optional(), before: z.string().optional(), after: z.string().optional(), - per_page: z.coerce.number().optional(), + per_page: z.coerce.number().min(1).max(100).optional(), state: z.enum(["triage", "draft", "published", "closed"]).optional(), }) @@ -50520,8 +50520,8 @@ export function createRouter(implementation: Implementation): KoaRouter { per_page: z.coerce.number().optional(), before: z.string().optional(), after: z.string().optional(), - first: z.coerce.number().optional(), - last: z.coerce.number().optional(), + first: z.coerce.number().min(1).max(100).optional(), + last: z.coerce.number().min(1).max(100).optional(), }) const dependabotListAlertsForRepoResponseValidator = @@ -63477,7 +63477,7 @@ export function createRouter(implementation: Implementation): KoaRouter { sort: z.enum(["created", "updated", "published"]).optional(), before: z.string().optional(), after: z.string().optional(), - per_page: z.coerce.number().optional(), + per_page: z.coerce.number().min(1).max(100).optional(), state: z.enum(["triage", "draft", "published", "closed"]).optional(), }) diff --git a/integration-tests/typescript-koa/src/generated/api.github.com.yaml/schemas.ts b/integration-tests/typescript-koa/src/generated/api.github.com.yaml/schemas.ts index 3744966b..b9ec5f55 100644 --- a/integration-tests/typescript-koa/src/generated/api.github.com.yaml/schemas.ts +++ b/integration-tests/typescript-koa/src/generated/api.github.com.yaml/schemas.ts @@ -2113,7 +2113,7 @@ export const s_repository_rule_pull_request = z.object({ dismiss_stale_reviews_on_push: z.coerce.boolean(), require_code_owner_review: z.coerce.boolean(), require_last_push_approval: z.coerce.boolean(), - required_approving_review_count: z.coerce.number(), + required_approving_review_count: z.coerce.number().max(10), required_review_thread_resolution: z.coerce.boolean(), }) .optional(), @@ -3137,7 +3137,7 @@ export const s_global_advisory = z.object({ cvss: z .object({ vector_string: z.string().nullable(), - score: z.coerce.number().nullable(), + score: z.coerce.number().max(10).nullable(), }) .nullable(), cwes: z.array(z.object({ cwe_id: z.string(), name: z.string() })).nullable(), @@ -5358,7 +5358,7 @@ export const s_dependabot_alert_security_advisory = z.object({ vulnerabilities: z.array(s_dependabot_alert_security_vulnerability), severity: z.enum(["low", "medium", "high", "critical"]), cvss: z.object({ - score: z.coerce.number(), + score: z.coerce.number().max(10), vector_string: z.string().nullable(), }), cwes: z.array(z.object({ cwe_id: z.string(), name: z.string() })), @@ -6106,7 +6106,7 @@ export const s_protected_branch_pull_request_review = z.object({ .optional(), dismiss_stale_reviews: z.coerce.boolean(), require_code_owner_reviews: z.coerce.boolean(), - required_approving_review_count: z.coerce.number().optional(), + required_approving_review_count: z.coerce.number().max(6).optional(), require_last_push_approval: z.coerce.boolean().optional(), }) @@ -6626,7 +6626,7 @@ export const s_repository_advisory = z.object({ cvss: z .object({ vector_string: z.string().nullable(), - score: z.coerce.number().nullable(), + score: z.coerce.number().max(10).nullable(), }) .nullable(), cwes: z.array(z.object({ cwe_id: z.string(), name: z.string() })).nullable(), diff --git a/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/generated.ts b/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/generated.ts index 398bb94e..d43489dd 100644 --- a/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/generated.ts +++ b/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/generated.ts @@ -843,7 +843,7 @@ export function createRouter(implementation: Implementation): KoaRouter { const listClientsQuerySchema = z.object({ after: z.string().optional(), - limit: z.coerce.number().optional(), + limit: z.coerce.number().min(1).max(200).optional(), q: z.string().optional(), }) diff --git a/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/schemas.ts b/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/schemas.ts index 7f3358b8..d9897c7b 100644 --- a/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/schemas.ts +++ b/integration-tests/typescript-koa/src/generated/okta.oauth.yaml/schemas.ts @@ -39,7 +39,7 @@ export const s_BackchannelAuthorizeRequest = z.intersection( id_token_hint: z.string().optional(), login_hint: z.string().optional(), request: z.string().optional(), - request_expiry: z.coerce.number().optional(), + request_expiry: z.coerce.number().min(1).max(300).optional(), scope: z.string(), }), z.record(z.any()), @@ -47,7 +47,7 @@ export const s_BackchannelAuthorizeRequest = z.intersection( export const s_BackchannelAuthorizeResponse = z.object({ auth_req_id: z.string().optional(), - expires_in: z.coerce.number().optional(), + expires_in: z.coerce.number().min(1).max(300).optional(), interval: z.coerce.number().optional(), }) diff --git a/jest.base.js b/jest.base.js index c6c06c2b..7e8eaa86 100644 --- a/jest.base.js +++ b/jest.base.js @@ -8,6 +8,8 @@ const config = { }, resetMocks: true, testMatch: ["**/*.spec.ts"], + // Note: prettier is required for inline snapshot indentation to work correctly + prettierPath: require.resolve("prettier"), } module.exports = config diff --git a/package.json b/package.json index be708017..d92d128d 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "lerna": "^8.1.2", "lint-staged": "^15.2.2", "markdown-toc": "^1.2.0", + "prettier": "^3.2.5", "source-map-support": "^0.5.21", "typescript": "~5.3.3" }, diff --git a/packages/openapi-code-generator/src/core/input.ts b/packages/openapi-code-generator/src/core/input.ts index 75b2854e..94d2a3ce 100644 --- a/packages/openapi-code-generator/src/core/input.ts +++ b/packages/openapi-code-generator/src/core/input.ts @@ -436,6 +436,8 @@ function normalizeSchemaObject( // todo: https://github.com/mnahkies/openapi-code-generator/issues/51 format: schemaObject.format, enum: enumValues.length ? enumValues : undefined, + minimum: schemaObject.minimum, + maximum: schemaObject.maximum, } satisfies IRModelNumeric } case "string": { diff --git a/packages/openapi-code-generator/src/core/openapi-types-normalized.ts b/packages/openapi-code-generator/src/core/openapi-types-normalized.ts index 5526a26b..86863a5b 100644 --- a/packages/openapi-code-generator/src/core/openapi-types-normalized.ts +++ b/packages/openapi-code-generator/src/core/openapi-types-normalized.ts @@ -14,6 +14,8 @@ export interface IRModelNumeric extends IRModelBase { type: "number" format?: IRModelNumericFormat | string | undefined enum?: number[] | undefined + minimum?: number | undefined + maximum?: number | undefined } export type IRModelStringFormat = diff --git a/packages/openapi-code-generator/src/test/input.test-utils.ts b/packages/openapi-code-generator/src/test/input.test-utils.ts index 928378de..ac4f12b7 100644 --- a/packages/openapi-code-generator/src/test/input.test-utils.ts +++ b/packages/openapi-code-generator/src/test/input.test-utils.ts @@ -1,6 +1,7 @@ import path from "path" import {jest} from "@jest/globals" import yaml from "js-yaml" +import _ from "lodash" import {Input} from "../core/input" import {logger} from "../core/logger" import {OpenapiLoader} from "../core/openapi-loader" @@ -30,13 +31,19 @@ function fileForVersion(version: Version) { } } -export async function unitTestInput(version: Version, skipValidation = false) { +const getValidator = _.memoize(async (skipValidation: boolean) => { const validator = await OpenapiValidator.create() if (skipValidation) { jest.spyOn(validator, "validate").mockResolvedValue() } + return validator +}) + +export async function unitTestInput(version: Version, skipValidation = false) { + const validator = await getValidator(skipValidation) + const file = fileForVersion(version) const loader = await OpenapiLoader.create(file, validator) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.spec.ts index 4e4790c1..c9731370 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.spec.ts @@ -1,4 +1,11 @@ +import * as vm from "node:vm" import {describe, expect, it} from "@jest/globals" +import {Input} from "../../../core/input" +import { + IRModel, + IRModelNumeric, + MaybeIRModel, +} from "../../../core/openapi-types-normalized" import {testVersions, unitTestInput} from "../../../test/input.test-utils" import {ImportBuilder} from "../import-builder" import {formatOutput} from "../output-utils" @@ -11,25 +18,23 @@ describe.each(testVersions)( const {code, schemas} = await getActual("components/schemas/SimpleObject") expect(code).toMatchInlineSnapshot(` - "import { s_SimpleObject } from "./unit-test.schemas" + "import { s_SimpleObject } from "./unit-test.schemas" - const x = s_SimpleObject - " + const x = s_SimpleObject" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" - - export const s_SimpleObject = z.object({ - str: z.string(), - num: z.coerce.number(), - date: z.string(), - datetime: z.string().datetime({ offset: true }), - optional_str: z.string().optional(), - required_nullable: z.string().nullable(), - }) - " - `) + "import { z } from "zod" + + export const s_SimpleObject = z.object({ + str: z.string(), + num: z.coerce.number(), + date: z.string(), + datetime: z.string().datetime({ offset: true }), + optional_str: z.string().optional(), + required_nullable: z.string().nullable(), + })" + `) }) it("supports the ObjectWithComplexProperties", async () => { @@ -38,32 +43,30 @@ describe.each(testVersions)( ) expect(code).toMatchInlineSnapshot(` - "import { s_ObjectWithComplexProperties } from "./unit-test.schemas" + "import { s_ObjectWithComplexProperties } from "./unit-test.schemas" - const x = s_ObjectWithComplexProperties - " + const x = s_ObjectWithComplexProperties" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" - - export const s_AString = z.string() - - export const s_OneOf = z.union([ - z.object({ strs: z.array(z.string()) }), - z.array(z.string()), - z.string(), - ]) - - export const s_ObjectWithComplexProperties = z.object({ - requiredOneOf: z.union([z.string(), z.coerce.number()]), - requiredOneOfRef: s_OneOf, - optionalOneOf: z.union([z.string(), z.coerce.number()]).optional(), - optionalOneOfRef: s_OneOf.optional(), - nullableSingularOneOf: z.coerce.boolean().nullable().optional(), - nullableSingularOneOfRef: s_AString.nullable().optional(), - }) - " + "import { z } from "zod" + + export const s_AString = z.string() + + export const s_OneOf = z.union([ + z.object({ strs: z.array(z.string()) }), + z.array(z.string()), + z.string(), + ]) + + export const s_ObjectWithComplexProperties = z.object({ + requiredOneOf: z.union([z.string(), z.coerce.number()]), + requiredOneOfRef: s_OneOf, + optionalOneOf: z.union([z.string(), z.coerce.number()]).optional(), + optionalOneOfRef: s_OneOf.optional(), + nullableSingularOneOf: z.coerce.boolean().nullable().optional(), + nullableSingularOneOfRef: s_AString.nullable().optional(), + })" `) }) @@ -71,136 +74,124 @@ describe.each(testVersions)( const {code, schemas} = await getActual("components/schemas/OneOf") expect(code).toMatchInlineSnapshot(` - "import { s_OneOf } from "./unit-test.schemas" + "import { s_OneOf } from "./unit-test.schemas" - const x = s_OneOf - " + const x = s_OneOf" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" - - export const s_OneOf = z.union([ - z.object({ strs: z.array(z.string()) }), - z.array(z.string()), - z.string(), - ]) - " - `) + "import { z } from "zod" + + export const s_OneOf = z.union([ + z.object({ strs: z.array(z.string()) }), + z.array(z.string()), + z.string(), + ])" + `) }) it("supports unions / anyOf", async () => { const {code, schemas} = await getActual("components/schemas/AnyOf") expect(code).toMatchInlineSnapshot(` - "import { s_AnyOf } from "./unit-test.schemas" + "import { s_AnyOf } from "./unit-test.schemas" - const x = s_AnyOf - " + const x = s_AnyOf" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_AnyOf = z.union([z.coerce.number(), z.string()]) - " - `) + export const s_AnyOf = z.union([z.coerce.number(), z.string()])" + `) }) it("supports allOf", async () => { const {code, schemas} = await getActual("components/schemas/AllOf") expect(code).toMatchInlineSnapshot(` - "import { s_AllOf } from "./unit-test.schemas" + "import { s_AllOf } from "./unit-test.schemas" - const x = s_AllOf - " + const x = s_AllOf" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_Base = z.object({ - name: z.string(), - breed: z.string().optional(), - }) + export const s_Base = z.object({ + name: z.string(), + breed: z.string().optional(), + }) - export const s_AllOf = s_Base.merge(z.object({ id: z.coerce.number() })) - " - `) + export const s_AllOf = s_Base.merge(z.object({ id: z.coerce.number() }))" + `) }) it("supports recursion", async () => { const {code, schemas} = await getActual("components/schemas/Recursive") expect(code).toMatchInlineSnapshot(` - "import { s_Recursive } from "./unit-test.schemas" + "import { s_Recursive } from "./unit-test.schemas" - const x = z.lazy(() => s_Recursive) - " + const x = z.lazy(() => s_Recursive)" `) expect(schemas).toMatchInlineSnapshot(` - "import { t_Recursive } from "./models" - import { z } from "zod" + "import { t_Recursive } from "./models" + import { z } from "zod" - export const s_Recursive: z.ZodType = z.object({ - child: z.lazy(() => s_Recursive.optional()), - }) - " - `) + export const s_Recursive: z.ZodType = z.object({ + child: z.lazy(() => s_Recursive.optional()), + })" + `) }) it("orders schemas such that dependencies are defined first", async () => { const {code, schemas} = await getActual("components/schemas/Ordering") expect(code).toMatchInlineSnapshot(` - "import { s_Ordering } from "./unit-test.schemas" + "import { s_Ordering } from "./unit-test.schemas" - const x = s_Ordering - " + const x = s_Ordering" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_AOrdering = z.object({ name: z.string().optional() }) + export const s_AOrdering = z.object({ name: z.string().optional() }) - export const s_ZOrdering = z.object({ - name: z.string().optional(), - dependency1: s_AOrdering, - }) + export const s_ZOrdering = z.object({ + name: z.string().optional(), + dependency1: s_AOrdering, + }) - export const s_Ordering = z.object({ - dependency1: s_ZOrdering, - dependency2: s_AOrdering, - }) - " - `) + export const s_Ordering = z.object({ + dependency1: s_ZOrdering, + dependency2: s_AOrdering, + })" + `) }) it("supports string and numeric enums", async () => { const {code, schemas} = await getActual("components/schemas/Enums") expect(code).toMatchInlineSnapshot(` - "import { s_Enums } from "./unit-test.schemas" + "import { s_Enums } from "./unit-test.schemas" - const x = s_Enums - " + const x = s_Enums" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" - - export const s_Enums = z.object({ - str: z.enum(["foo", "bar"]).nullable().optional(), - num: z - .union([z.literal(10), z.literal(20)]) - .nullable() - .optional(), - }) - " - `) + "import { z } from "zod" + + export const s_Enums = z.object({ + str: z.enum(["foo", "bar"]).nullable().optional(), + num: z + .union([z.literal(10), z.literal(20)]) + .nullable() + .optional(), + })" + `) }) describe("additionalProperties", () => { @@ -210,18 +201,16 @@ describe.each(testVersions)( ) expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesBool } from "./unit-test.schemas" + "import { s_AdditionalPropertiesBool } from "./unit-test.schemas" - const x = s_AdditionalPropertiesBool - " + const x = s_AdditionalPropertiesBool" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_AdditionalPropertiesBool = z.record(z.any()) - " - `) + export const s_AdditionalPropertiesBool = z.record(z.any())" + `) }) it("handles additionalProperties set to {}", async () => { @@ -230,18 +219,16 @@ describe.each(testVersions)( ) expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptySchema } from "./unit-test.schemas" + "import { s_AdditionalPropertiesUnknownEmptySchema } from "./unit-test.schemas" - const x = s_AdditionalPropertiesUnknownEmptySchema - " + const x = s_AdditionalPropertiesUnknownEmptySchema" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_AdditionalPropertiesUnknownEmptySchema = z.record(z.any()) - " - `) + export const s_AdditionalPropertiesUnknownEmptySchema = z.record(z.any())" + `) }) it("handles additionalProperties set to {type: 'object'}", async () => { @@ -250,20 +237,18 @@ describe.each(testVersions)( ) expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptyObjectSchema } from "./unit-test.schemas" + "import { s_AdditionalPropertiesUnknownEmptyObjectSchema } from "./unit-test.schemas" - const x = s_AdditionalPropertiesUnknownEmptyObjectSchema - " + const x = s_AdditionalPropertiesUnknownEmptyObjectSchema" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_AdditionalPropertiesUnknownEmptyObjectSchema = z.record( - z.record(z.any()), - ) - " - `) + export const s_AdditionalPropertiesUnknownEmptyObjectSchema = z.record( + z.record(z.any()), + )" + `) }) it("handles additionalProperties specifying a schema", async () => { @@ -272,25 +257,23 @@ describe.each(testVersions)( ) expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesSchema } from "./unit-test.schemas" + "import { s_AdditionalPropertiesSchema } from "./unit-test.schemas" - const x = s_AdditionalPropertiesSchema - " + const x = s_AdditionalPropertiesSchema" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_NamedNullableStringEnum = z - .enum(["", "one", "two", "three"]) - .nullable() + export const s_NamedNullableStringEnum = z + .enum(["", "one", "two", "three"]) + .nullable() - export const s_AdditionalPropertiesSchema = z.intersection( - z.object({ name: z.string().optional() }), - z.record(s_NamedNullableStringEnum), - ) - " - `) + export const s_AdditionalPropertiesSchema = z.intersection( + z.object({ name: z.string().optional() }), + z.record(s_NamedNullableStringEnum), + )" + `) }) it("handles additionalProperties in conjunction with properties", async () => { @@ -299,54 +282,170 @@ describe.each(testVersions)( ) expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesMixed } from "./unit-test.schemas" + "import { s_AdditionalPropertiesMixed } from "./unit-test.schemas" - const x = s_AdditionalPropertiesMixed - " + const x = s_AdditionalPropertiesMixed" `) expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod" + "import { z } from "zod" - export const s_AdditionalPropertiesMixed = z.intersection( - z.object({ id: z.string().optional(), name: z.string().optional() }), - z.record(z.any()), + export const s_AdditionalPropertiesMixed = z.intersection( + z.object({ id: z.string().optional(), name: z.string().optional() }), + z.record(z.any()), + )" + `) + }) + }) + + describe("numbers", () => { + const base: IRModelNumeric = { + nullable: false, + readOnly: false, + type: "number", + } + + it("supports plain number", async () => { + const {code} = await getActualFromModel({ + ...base, + }) + + expect(code).toMatchInlineSnapshot('"const x = z.coerce.number()"') + await expect(executeParseSchema(code, 123)).resolves.toBe(123) + await expect( + executeParseSchema(code, "not a number 123"), + ).rejects.toThrow("Expected number, received nan") + }) + + it("supports enum number", async () => { + const {code} = await getActualFromModel({ + ...base, + enum: [200, 301, 404], + }) + + expect(code).toMatchInlineSnapshot( + '"const x = z.union([z.literal(200), z.literal(301), z.literal(404)])"', ) - " - `) + + await expect(executeParseSchema(code, 123)).rejects.toThrow( + "Invalid literal value, expected 404", + ) + await expect(executeParseSchema(code, 404)).resolves.toBe(404) + }) + + it("supports minimum", async () => { + const {code} = await getActualFromModel({ + ...base, + minimum: 10, + }) + + expect(code).toMatchInlineSnapshot( + '"const x = z.coerce.number().min(10)"', + ) + + await expect(executeParseSchema(code, 5)).rejects.toThrow( + "Number must be greater than or equal to 10", + ) + await expect(executeParseSchema(code, 20)).resolves.toBe(20) + }) + + it("supports maximum", async () => { + const {code} = await getActualFromModel({ + ...base, + maximum: 16, + }) + + expect(code).toMatchInlineSnapshot( + '"const x = z.coerce.number().max(16)"', + ) + + await expect(executeParseSchema(code, 25)).rejects.toThrow( + "Number must be less than or equal to 16", + ) + await expect(executeParseSchema(code, 8)).resolves.toBe(8) + }) + + it("supports minimum/maximum", async () => { + const {code} = await getActualFromModel({ + ...base, + minimum: 10, + maximum: 24, + }) + + expect(code).toMatchInlineSnapshot( + '"const x = z.coerce.number().min(10).max(24)"', + ) + + await expect(executeParseSchema(code, 5)).rejects.toThrow( + "Number must be greater than or equal to 10", + ) + await expect(executeParseSchema(code, 25)).rejects.toThrow( + "Number must be less than or equal to 24", + ) + await expect(executeParseSchema(code, 20)).resolves.toBe(20) }) }) + async function executeParseSchema(code: string, input: unknown) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const context = {z: require("zod").z} + vm.createContext(context) + return vm.runInContext( + ` + ${code} + + x.parse(${JSON.stringify(input)}) + + `, + context, + ) + } + + async function getActualFromModel(model: IRModel) { + const {input} = await unitTestInput(version) + return getResult(input, model, true) + } + async function getActual(path: string) { const {input, file} = await unitTestInput(version) + return getResult(input, {$ref: `${file}#${path}`}, true) + } + async function getResult( + input: Input, + maybeModel: MaybeIRModel, + required: boolean, + ) { const imports = new ImportBuilder() const builder = await ZodBuilder.fromInput( "./unit-test.schemas.ts", input, ) - const schema = builder .withImports(imports) - .fromModel({$ref: `${file}#${path}`}, true) + .fromModel(maybeModel, required) return { - code: await formatOutput( - ` + code: ( + await formatOutput( + ` ${imports.toString()} const x = ${schema} `, - "unit-test.code.ts", - ), - schemas: await formatOutput( - builder.toCompilationUnit().getRawFileContent({ - allowUnusedImports: false, - includeHeader: false, - }), - "unit-test.schemas.ts", - ), + "unit-test.code.ts", + ) + ).trim(), + schemas: ( + await formatOutput( + builder.toCompilationUnit().getRawFileContent({ + allowUnusedImports: false, + includeHeader: false, + }), + "unit-test.schemas.ts", + ) + ).trim(), } } }, diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.ts index 964ed12b..97da7b7a 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.ts @@ -145,7 +145,14 @@ export class ZodBuilder extends AbstractSchemaBuilder { .join(".") } - return [zod, "coerce.number()"].filter(isDefined).join(".") + return [ + zod, + "coerce.number()", + model.minimum ? `min(${model.minimum})` : undefined, + model.maximum ? `max(${model.maximum})` : undefined, + ] + .filter(isDefined) + .join(".") } protected string(model: IRModelString) { diff --git a/yarn.lock b/yarn.lock index 699d563c..440e4ba2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12151,6 +12151,7 @@ __metadata: lerna: "npm:^8.1.2" lint-staged: "npm:^15.2.2" markdown-toc: "npm:^1.2.0" + prettier: "npm:^3.2.5" source-map-support: "npm:^0.5.21" typescript: "npm:~5.3.3" languageName: unknown @@ -12806,6 +12807,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.2.5": + version: 3.2.5 + resolution: "prettier@npm:3.2.5" + bin: + prettier: bin/prettier.cjs + checksum: 10/d509f9da0b70e8cacc561a1911c0d99ec75117faed27b95cc8534cb2349667dee6351b0ca83fa9d5703f14127faa52b798de40f5705f02d843da133fc3aa416a + languageName: node + linkType: hard + "pretty-format@npm:30.0.0-alpha.3": version: 30.0.0-alpha.3 resolution: "pretty-format@npm:30.0.0-alpha.3"