diff --git a/packages/openapi-code-generator/src/core/dependency-graph.ts b/packages/openapi-code-generator/src/core/dependency-graph.ts index 4ce0fb5b..db9f3d7b 100644 --- a/packages/openapi-code-generator/src/core/dependency-graph.ts +++ b/packages/openapi-code-generator/src/core/dependency-graph.ts @@ -1,4 +1,4 @@ -import type {Input} from "./input" +import type {ISchemaProvider} from "./input" import {logger} from "./logger" import type {Reference} from "./openapi-types" import type {MaybeIRModel} from "./openapi-types-normalized" @@ -94,11 +94,11 @@ export type DependencyGraph = {order: string[]; circular: Set} * * It's not perfect though: * - doesn't discover schemas declared in external specifications (eg: shared definition files) - * @param input + * @param schemaProvider * @param getNameForRef */ export function buildDependencyGraph( - input: Input, + schemaProvider: ISchemaProvider, getNameForRef: (reference: Reference) => string, ): DependencyGraph { logger.time("calculate schema dependency graph") @@ -110,7 +110,7 @@ export function buildDependencyGraph( const order: string[] = [] // TODO: this may miss extracted in-line schemas - for (const [name, schema] of Object.entries(input.allSchemas())) { + for (const [name, schema] of Object.entries(schemaProvider.allSchemas())) { remaining.set( getNameForRef({$ref: name}), getDependenciesFromSchema(schema, getNameForRef), diff --git a/packages/openapi-code-generator/src/core/input.ts b/packages/openapi-code-generator/src/core/input.ts index b35abcdb..4dedf4df 100644 --- a/packages/openapi-code-generator/src/core/input.ts +++ b/packages/openapi-code-generator/src/core/input.ts @@ -38,7 +38,9 @@ export type InputConfig = { } export interface ISchemaProvider { - schema(maybeRef: MaybeIRModel): IRModel + schema(maybeRef: MaybeIRModel | Reference): IRModel + allSchemas(): Record + preprocess(maybePreprocess: Reference | xInternalPreproccess): IRPreprocess } export class Input implements ISchemaProvider { diff --git a/packages/openapi-code-generator/src/core/normalization/parameter-normalizer.spec.ts b/packages/openapi-code-generator/src/core/normalization/parameter-normalizer.spec.ts index e7fa4262..f52ed158 100644 --- a/packages/openapi-code-generator/src/core/normalization/parameter-normalizer.spec.ts +++ b/packages/openapi-code-generator/src/core/normalization/parameter-normalizer.spec.ts @@ -86,15 +86,14 @@ describe("ParameterNormalizer", () => { }) it("throws on unsupported style", () => { - const param = { - name: "id", - in: "path", - style: "form", - schema: {type: "string"}, - } as any - expect(() => parameterNormalizer.normalizeParameter(param)).toThrow( - "unsupported parameter style: 'form' for in: 'path'", - ) + expect(() => + parameterNormalizer.normalizeParameter({ + name: "id", + in: "path", + style: "form", + schema: {type: "string"}, + }), + ).toThrow("unsupported parameter style: 'form' for in: 'path'") }) }) @@ -226,7 +225,7 @@ describe("ParameterNormalizer", () => { } loader.parameter.mockImplementation((it) => it as Parameter) - loader.schema.mockImplementation((it) => it as any) + loader.schema.mockImplementation((it) => it) loader.addVirtualType.mockImplementation((_opId, name) => ir.ref(name, "virtual"), ) @@ -300,7 +299,7 @@ describe("ParameterNormalizer", () => { } loader.parameter.mockReturnValue(queryParam) - loader.schema.mockReturnValue({type: "string"} as any) + loader.schema.mockReturnValue({type: "string"}) loader.addVirtualType.mockImplementation((_operationId, name) => ir.ref(name, "virtual"), ) @@ -328,7 +327,7 @@ describe("ParameterNormalizer", () => { loader.schema.mockReturnValue({ type: "array", items: {type: "string"}, - } as any) + }) loader.addVirtualType.mockImplementation((_opId, name) => ir.ref(name, "virtual"), ) diff --git a/packages/openapi-code-generator/src/core/openapi-utils.spec.ts b/packages/openapi-code-generator/src/core/openapi-utils.spec.ts index 43a4fbac..210ecb72 100644 --- a/packages/openapi-code-generator/src/core/openapi-utils.spec.ts +++ b/packages/openapi-code-generator/src/core/openapi-utils.spec.ts @@ -1,5 +1,10 @@ import {describe, expect, it} from "@jest/globals" -import {extractPlaceholders, getNameFromRef, isRef} from "./openapi-utils" +import { + extractPlaceholders, + getNameFromRef, + getRawNameFromRef, + isRef, +} from "./openapi-utils" describe("core/openapi-utils", () => { describe("#isRef", () => { @@ -17,6 +22,20 @@ describe("core/openapi-utils", () => { }) }) + describe("#getRawNameFromRef", () => { + it("returns the raw name", () => { + expect( + getRawNameFromRef({$ref: "#/components/schemas/Something"}), + ).toEqual("Something") + }) + + it("throws on an invalid $ref", () => { + expect(() => getRawNameFromRef({$ref: "#/"})).toThrow( + "no name found in $ref: '#/'", + ) + }) + }) + describe("#getNameFromRef", () => { it("includes the given prefix", () => { expect(getNameFromRef({$ref: "#/components/schemas/Foo"}, "t_")).toBe( diff --git a/packages/openapi-code-generator/src/core/openapi-utils.ts b/packages/openapi-code-generator/src/core/openapi-utils.ts index 035c7a3f..d6115ff5 100644 --- a/packages/openapi-code-generator/src/core/openapi-utils.ts +++ b/packages/openapi-code-generator/src/core/openapi-utils.ts @@ -7,13 +7,18 @@ export function isRef(it: unknown | Reference): it is Reference { return Reflect.has(it, "$ref") } -export function getNameFromRef({$ref}: Reference, prefix: string): string { +export function getRawNameFromRef({$ref}: Reference): string { const name = $ref.split("/").pop() if (!name) { throw new Error(`no name found in $ref: '${$ref}'`) } + return name +} + +export function getNameFromRef({$ref}: Reference, prefix: string): string { + const name = getRawNameFromRef({$ref}) // todo: this is a hack to workaround reserved words being used as names // can likely improve to selectively apply when a reserved word is used. return prefix + name.replace(/[-.]+/g, "_") diff --git a/packages/openapi-code-generator/src/test/fake-schema-provider.ts b/packages/openapi-code-generator/src/test/fake-schema-provider.ts new file mode 100644 index 00000000..6c46311c --- /dev/null +++ b/packages/openapi-code-generator/src/test/fake-schema-provider.ts @@ -0,0 +1,44 @@ +import type {ISchemaProvider} from "../core/input" +import type { + IRModel, + IRPreprocess, + IRRef, + MaybeIRModel, +} from "../core/openapi-types-normalized" +import {getRawNameFromRef, isRef} from "../core/openapi-utils" + +export class FakeSchemaProvider implements ISchemaProvider { + private readonly testRefs: Record = {} + + registerTestRef(ref: IRRef, model: IRModel) { + this.testRefs[ref.$ref] = model + } + + schema(maybeRef: MaybeIRModel): IRModel { + if (isRef(maybeRef)) { + const result = this.testRefs[maybeRef.$ref] + + if (!result) { + throw new Error( + `FakeSchemaProvider: $ref '${maybeRef.$ref}' is not registered`, + ) + } + + return result + } + + return maybeRef + } + + allSchemas(): Record { + return Object.fromEntries( + Object.entries(this.testRefs).map(([$ref, value]) => { + return [getRawNameFromRef({$ref}), value] + }), + ) + } + + preprocess(): IRPreprocess { + return {} + } +} 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 213e65ed..260442c7 100644 --- a/packages/openapi-code-generator/src/test/input.test-utils.ts +++ b/packages/openapi-code-generator/src/test/input.test-utils.ts @@ -7,7 +7,6 @@ import {GenericLoader} from "../core/loaders/generic.loader" import {OpenapiLoader} from "../core/loaders/openapi-loader" import {TypespecLoader} from "../core/loaders/typespec.loader" import {logger} from "../core/logger" -import {SchemaNormalizer} from "../core/normalization/schema-normalizer" import {OpenapiValidator} from "../core/openapi-validator" export type OpenApiVersion = "3.0.x" | "3.1.x" @@ -59,7 +58,6 @@ export async function unitTestInput( return { input: new Input(loader, config), - schemaNormalizer: new SchemaNormalizer(config), file, } } diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts index ce6e5248..1612d3cb 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts @@ -2,7 +2,7 @@ import { buildDependencyGraph, type DependencyGraph, } from "../../../core/dependency-graph" -import type {Input} from "../../../core/input" +import type {ISchemaProvider} from "../../../core/input" import {logger} from "../../../core/logger" import type {Reference} from "../../../core/openapi-types" import type { @@ -38,7 +38,7 @@ export abstract class AbstractSchemaBuilder< protected constructor( public readonly filename: string, - protected readonly input: Input, + protected readonly schemaProvider: ISchemaProvider, protected readonly config: SchemaBuilderConfig, protected readonly schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, @@ -50,7 +50,9 @@ export abstract class AbstractSchemaBuilder< ) { this.graph = parent?.graph ?? - buildDependencyGraph(this.input, (it) => this.getSchemaNameFromRef(it)) + buildDependencyGraph(this.schemaProvider, (it) => + this.getSchemaNameFromRef(it), + ) this.importHelpers(this.schemaBuilderImports) this.typeBuilder = typeBuilder.withImports(this.schemaBuilderImports) } @@ -123,6 +125,7 @@ export abstract class AbstractSchemaBuilder< }) } + /* istanbul ignore next */ throw new Error("unreachable") }) ) @@ -184,7 +187,7 @@ export abstract class AbstractSchemaBuilder< } if (maybeModel["x-internal-preprocess"]) { - const dereferenced = this.input.preprocess( + const dereferenced = this.schemaProvider.preprocess( maybeModel["x-internal-preprocess"], ) if (dereferenced.deserialize) { @@ -211,6 +214,7 @@ export abstract class AbstractSchemaBuilder< // todo: byte is base64 encoded string, https://spec.openapis.org/registry/format/byte.html // model.format === "byte" if (model.format === "binary") { + // todo: check instanceof Blob? result = this.any() } else { result = this.string(model) @@ -234,7 +238,7 @@ export abstract class AbstractSchemaBuilder< // Note: for zod in particular it's desirable to use merge over intersection // where possible, as it returns a more malleable schema const isMergable = model.schemas - .map((it) => this.input.schema(it)) + .map((it) => this.schemaProvider.schema(it)) .every((it) => it.type === "object" && !it.additionalProperties) result = isMergable ? this.merge(schemas) : this.intersect(schemas) @@ -304,7 +308,9 @@ export abstract class AbstractSchemaBuilder< } if (model["x-internal-preprocess"]) { - const dereferenced = this.input.preprocess(model["x-internal-preprocess"]) + const dereferenced = this.schemaProvider.preprocess( + model["x-internal-preprocess"], + ) if (dereferenced.deserialize) { result = this.preprocess(result, dereferenced.deserialize.fn) } diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.integration.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.integration.spec.ts index 2ee5c709..33c00478 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.integration.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.integration.spec.ts @@ -1,19 +1,10 @@ import vm from "node:vm" -import {describe, expect, it} from "@jest/globals" -import type { - SchemaArray, - SchemaBoolean, - SchemaNumber, - SchemaObject, - SchemaString, -} from "../../../core/openapi-types" +import {beforeAll, describe, expect, it} from "@jest/globals" import {testVersions} from "../../../test/input.test-utils" -import type {SchemaBuilderConfig} from "./abstract-schema-builder" +import {TypescriptFormatterBiome} from "../typescript-formatter.biome" import { - schemaBuilderTestHarness, - schemaNumber, - schemaObject, - schemaString, + type SchemaBuilderIntegrationTestHarness, + schemaBuilderIntegrationTestHarness, } from "./schema-builder.test-utils" describe.each( @@ -32,11 +23,19 @@ describe.each( ) } - const {getActual, getActualFromModel} = schemaBuilderTestHarness( - "joi", - version, - executeParseSchema, - ) + let getActual: SchemaBuilderIntegrationTestHarness["getActual"] + + beforeAll(async () => { + const formatter = await TypescriptFormatterBiome.createNodeFormatter() + const harness = schemaBuilderIntegrationTestHarness( + "joi", + formatter, + version, + executeParseSchema, + ) + + getActual = harness.getActual + }) it("supports the SimpleObject", async () => { const {code, schemas} = await getActual("components/schemas/SimpleObject") diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts index e8120d98..007c8a8e 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.ts @@ -1,4 +1,4 @@ -import type {Input} from "../../../core/input" +import type {ISchemaProvider} from "../../../core/input" import type {Reference} from "../../../core/openapi-types" import type { IRModel, @@ -34,16 +34,16 @@ export class JoiBuilder extends AbstractSchemaBuilder< private includeIntersectHelper = false - static async fromInput( + static async fromSchemaProvider( filename: string, - input: Input, + schemaProvider: ISchemaProvider, schemaBuilderConfig: SchemaBuilderConfig, schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, ): Promise { return new JoiBuilder( filename, - input, + schemaProvider, schemaBuilderConfig, schemaBuilderImports, typeBuilder, @@ -54,7 +54,7 @@ export class JoiBuilder extends AbstractSchemaBuilder< override withImports(imports: ImportBuilder): JoiBuilder { return new JoiBuilder( this.filename, - this.input, + this.schemaProvider, this.config, this.schemaBuilderImports, this.typeBuilder, @@ -89,7 +89,7 @@ export class JoiBuilder extends AbstractSchemaBuilder< protected schemaFromRef(reference: Reference): ExportDefinition { const name = this.getSchemaNameFromRef(reference) - const schemaObject = this.input.schema(reference) + const schemaObject = this.schemaProvider.schema(reference) const value = this.fromModel(schemaObject, true) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.unit.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.unit.spec.ts index d7dfc588..f14cbd91 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.unit.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/joi-schema-builder.unit.spec.ts @@ -1,22 +1,21 @@ -import vm from "node:vm" -import {describe, expect, it} from "@jest/globals" -import type { - SchemaArray, - SchemaBoolean, - SchemaNumber, - SchemaObject, - SchemaString, -} from "../../../core/openapi-types" -import {testVersions} from "../../../test/input.test-utils" +import * as vm from "node:vm" +import {beforeAll, beforeEach, describe, expect, it} from "@jest/globals" +import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" +import type {IRModel} from "../../../core/openapi-types-normalized" +import {FakeSchemaProvider} from "../../../test/fake-schema-provider" +import {irFixture as ir} from "../../../test/ir-model.fixtures.test-utils" +import {TypescriptFormatterBiome} from "../typescript-formatter.biome" import type {SchemaBuilderConfig} from "./abstract-schema-builder" import { + type SchemaBuilderTestHarness, schemaBuilderTestHarness, - schemaNumber, - schemaObject, - schemaString, } from "./schema-builder.test-utils" describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () => { + let formatter: TypescriptFormatterBiome + let schemaProvider: FakeSchemaProvider + let testHarness: SchemaBuilderTestHarness + const executeParseSchema = async (code: string) => { return vm.runInNewContext( code, @@ -30,25 +29,21 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () ) } - const {getActual, getActualFromModel} = schemaBuilderTestHarness( - "joi", - testVersions[0], - executeParseSchema, - ) + beforeAll(async () => { + formatter = await TypescriptFormatterBiome.createNodeFormatter() + testHarness = schemaBuilderTestHarness("joi", formatter, executeParseSchema) + }) - describe("numbers", () => { - const base: SchemaNumber = { - nullable: false, - readOnly: false, - type: "number", - } + beforeEach(() => { + schemaProvider = new FakeSchemaProvider() + }) + describe("numbers", () => { it("supports plain number", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - }) + const {code, execute} = await getActual(ir.number()) expect(code).toMatchInlineSnapshot('"const x = joi.number().required()"') + await expect(execute(123)).resolves.toBe(123) await expect(execute("not a number 123")).rejects.toThrow( '"value" must be a number', @@ -56,11 +51,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports closed number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "closed", - }) + const {code, execute} = await getActual( + ir.number({ + enum: [200, 301, 404], + "x-enum-extensibility": "closed", + }), + ) expect(code).toMatchInlineSnapshot( '"const x = joi.number().valid(200, 301, 404).required()"', @@ -73,11 +69,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports open number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "open", - }) + const {code, execute} = await getActual( + ir.number({ + enum: [200, 301, 404], + "x-enum-extensibility": "open", + }), + ) expect(code).toMatchInlineSnapshot(`"const x = joi.number().required()"`) @@ -88,11 +85,8 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () ) }) - it("supports minimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - }) + it("supports inclusiveMinimum", async () => { + const {code, execute} = await getActual(ir.number({inclusiveMinimum: 10})) expect(code).toMatchInlineSnapshot( '"const x = joi.number().min(10).required()"', @@ -101,14 +95,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () await expect(execute(5)).rejects.toThrow( '"value" must be greater than or equal to 10', ) + await expect(execute(10)).resolves.toBe(10) await expect(execute(20)).resolves.toBe(20) }) - it("supports maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maximum: 16, - }) + it("supports inclusiveMaximum", async () => { + const {code, execute} = await getActual(ir.number({inclusiveMaximum: 16})) expect(code).toMatchInlineSnapshot( '"const x = joi.number().max(16).required()"', @@ -117,15 +109,14 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () await expect(execute(25)).rejects.toThrow( '"value" must be less than or equal to 16', ) + await expect(execute(16)).resolves.toBe(16) await expect(execute(8)).resolves.toBe(8) }) - it("supports minimum/maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - maximum: 24, - }) + it("supports inclusiveMinimum/inclusiveMaximum", async () => { + const {code, execute} = await getActual( + ir.number({inclusiveMinimum: 10, inclusiveMaximum: 24}), + ) expect(code).toMatchInlineSnapshot( '"const x = joi.number().min(10).max(24).required()"', @@ -141,10 +132,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports exclusiveMinimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMinimum: 4, - }) + const {code, execute} = await getActual(ir.number({exclusiveMinimum: 4})) expect(code).toMatchInlineSnapshot( '"const x = joi.number().greater(4).required()"', @@ -155,10 +143,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports exclusiveMaximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMaximum: 4, - }) + const {code, execute} = await getActual(ir.number({exclusiveMaximum: 4})) expect(code).toMatchInlineSnapshot( '"const x = joi.number().less(4).required()"', @@ -169,10 +154,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports multipleOf", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - }) + const {code, execute} = await getActual(ir.number({multipleOf: 4})) expect(code).toMatchInlineSnapshot( '"const x = joi.number().multiple(4).required()"', @@ -184,13 +166,14 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () await expect(execute(16)).resolves.toBe(16) }) - it("supports combining multipleOf and min/max", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - minimum: 10, - maximum: 20, - }) + it("supports combining multipleOf and inclusiveMinimum/inclusiveMaximum", async () => { + const {code, execute} = await getActual( + ir.number({ + multipleOf: 4, + inclusiveMinimum: 10, + inclusiveMaximum: 20, + }), + ) expect(code).toMatchInlineSnapshot( '"const x = joi.number().multiple(4).min(10).max(20).required()"', @@ -209,25 +192,25 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 0, - }) + const {code, execute} = await getActual( + ir.number({inclusiveMinimum: 0, inclusiveMaximum: 0}), + ) expect(code).toMatchInlineSnapshot( - '"const x = joi.number().min(0).required()"', + '"const x = joi.number().min(0).max(0).required()"', ) await expect(execute(-1)).rejects.toThrow( '"value" must be greater than or equal to 0', ) + await expect(execute(1)).rejects.toThrow( + '"value" must be less than or equal to 0', + ) + await expect(execute(0)).resolves.toBe(0) }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 42, - }) + const {code, execute} = await getActual(ir.number({default: 42})) expect(code).toMatchInlineSnapshot(`"const x = joi.number().default(42)"`) @@ -235,10 +218,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values of 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 0, - }) + const {code, execute} = await getActual(ir.number({default: 0})) expect(code).toMatchInlineSnapshot(`"const x = joi.number().default(0)"`) @@ -246,11 +226,9 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.number({nullable: true, default: null}), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.number().allow(null).default(null)"`, @@ -261,14 +239,8 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) describe("strings", () => { - const base: SchemaString = { - nullable: false, - readOnly: false, - type: "string", - } - it("supports plain string", async () => { - const {code, execute} = await getActualFromModel({...base}) + const {code, execute} = await getActual(ir.string()) expect(code).toMatchInlineSnapshot('"const x = joi.string().required()"') @@ -276,13 +248,27 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () await expect(execute(123)).rejects.toThrow('"value" must be a string') }) + it("supports nullable string", async () => { + const {code, execute} = await getActual(ir.string({nullable: true})) + + expect(code).toMatchInlineSnapshot( + `"const x = joi.string().allow(null).required()"`, + ) + + await expect(execute("a string")).resolves.toBe("a string") + await expect(execute(null)).resolves.toBe(null) + await expect(execute(123)).rejects.toThrow('"value" must be a string') + }) + it("supports closed string enums", async () => { const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "closed", - }) + + const {code, execute} = await getActual( + ir.string({ + enum: enumValues, + "x-enum-extensibility": "closed", + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.string().valid("red", "blue", "green").required()"`, @@ -299,11 +285,13 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () it("supports open string enums", async () => { const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "open", - }) + + const {code, execute} = await getActual( + ir.string({ + enum: enumValues, + "x-enum-extensibility": "open", + }), + ) expect(code).toMatchInlineSnapshot(`"const x = joi.string().required()"`) @@ -315,10 +303,8 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports minLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minLength: 8, - }) + const {code, execute} = await getActual(ir.string({minLength: 8})) + expect(code).toMatchInlineSnapshot( '"const x = joi.string().min(8).required()"', ) @@ -330,10 +316,8 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxLength: 8, - }) + const {code, execute} = await getActual(ir.string({maxLength: 8})) + expect(code).toMatchInlineSnapshot( '"const x = joi.string().max(8).required()"', ) @@ -345,10 +329,8 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports pattern", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: '"pk/\\d+"', - }) + const {code, execute} = await getActual(ir.string({pattern: '"pk/\\d+"'})) + expect(code).toMatchInlineSnapshot( '"const x = joi.string().pattern(new RegExp(\'"pk/\\\\d+"\')).required()"', ) @@ -360,12 +342,14 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports pattern with minLength / maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: "pk-\\d+", - minLength: 5, - maxLength: 8, - }) + const {code, execute} = await getActual( + ir.string({ + pattern: "pk-\\d+", + minLength: 5, + maxLength: 8, + }), + ) + expect(code).toMatchInlineSnapshot( '"const x = joi.string().min(5).max(8).pattern(new RegExp("pk-\\\\d+")).required()"', ) @@ -383,10 +367,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "example", - }) + const {code, execute} = await getActual(ir.string({default: "example"})) expect(code).toMatchInlineSnapshot( `"const x = joi.string().default("example")"`, @@ -396,11 +377,9 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.string({nullable: true, default: null}), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.string().allow(null).default(null)"`, @@ -410,10 +389,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports empty string default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "", - }) + const {code, execute} = await getActual(ir.string({default: ""})) expect(code).toMatchInlineSnapshot(`"const x = joi.string().default("")"`) @@ -421,10 +397,11 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values with quotes", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 'this is an "example", it\'s got lots of `quotes`', - }) + const {code, execute} = await getActual( + ir.string({ + default: 'this is an "example", it\'s got lots of `quotes`', + }), + ) expect(code).toMatchInlineSnapshot(` "const x = joi @@ -438,10 +415,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("coerces incorrectly typed default values to be strings", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) + const {code, execute} = await getActual(ir.string({default: false})) expect(code).toMatchInlineSnapshot( `"const x = joi.string().default("false")"`, @@ -452,10 +426,7 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () describe("formats", () => { it("supports email", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "email", - }) + const {code, execute} = await getActual(ir.string({format: "email"})) expect(code).toMatchInlineSnapshot( `"const x = joi.string().email().required()"`, @@ -469,10 +440,9 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () ) }) it("supports date-time", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "date-time", - }) + const {code, execute} = await getActual( + ir.string({format: "date-time"}), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.string().isoDate().required()"`, @@ -485,18 +455,18 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () '"value" must be in iso format', ) }) + it("supports binary", async () => { + const {code} = await getActual(ir.string({format: "binary"})) + + expect(code).toMatchInlineSnapshot(`"const x = joi.any().required()"`) + // todo: JSON.stringify doesn't work for passing a Blob into the VM, so can't execute + }) }) }) describe("booleans", () => { - const base: SchemaBoolean = { - nullable: false, - readOnly: false, - type: "boolean", - } - it("supports plain boolean", async () => { - const {code, execute} = await getActualFromModel({...base}) + const {code, execute} = await getActual(ir.boolean()) expect(code).toMatchInlineSnapshot( `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").required()"`, @@ -520,10 +490,11 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values of false", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) + const {code, execute} = await getActual( + ir.boolean({ + default: false, + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").default(false)"`, @@ -533,10 +504,11 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values of true", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: true, - }) + const {code, execute} = await getActual( + ir.boolean({ + default: true, + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").default(true)"`, @@ -546,11 +518,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.boolean({ + nullable: true, + default: null, + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").allow(null).default(null)"`, @@ -560,10 +533,11 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("support enum of 'true'", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: ["true"], - }) + const {code, execute} = await getActual( + ir.boolean({ + enum: ["true"], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.boolean().truthy(1, "1").valid(true).required()"`, @@ -574,10 +548,11 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("support enum of 'false'", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: ["false"], - }) + const {code, execute} = await getActual( + ir.boolean({ + enum: ["false"], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.boolean().falsy(0, "0").valid(false).required()"`, @@ -589,16 +564,8 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) describe("arrays", () => { - const base: SchemaArray = { - nullable: false, - readOnly: false, - type: "array", - items: {nullable: false, readOnly: false, type: "string"}, - uniqueItems: false, - } - it("supports arrays", async () => { - const {code, execute} = await getActualFromModel({...base}) + const {code, execute} = await getActual(ir.array({items: ir.string()})) expect(code).toMatchInlineSnapshot( `"const x = joi.array().items(joi.string()).required()"`, @@ -613,10 +580,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - uniqueItems: true, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + uniqueItems: true, + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.array().items(joi.string()).unique().required()"`, @@ -632,10 +601,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports minItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minItems: 2, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + minItems: 2, + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.array().items(joi.string()).min(2).required()"`, @@ -651,10 +622,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports maxItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxItems: 2, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + maxItems: 2, + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.array().items(joi.string()).max(2).required()"`, @@ -670,13 +643,14 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports minItems / maxItems / uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - items: schemaNumber(), - minItems: 1, - maxItems: 3, - uniqueItems: true, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.number(), + minItems: 1, + maxItems: 3, + uniqueItems: true, + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.array().items(joi.number()).unique().min(1).max(3).required()"`, @@ -695,10 +669,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: ["example"], - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + default: ["example"], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.array().items(joi.string()).default(["example"])"`, @@ -708,10 +684,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports empty array default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: [], - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + default: [], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = joi.array().items(joi.string()).default([])"`, @@ -722,27 +700,16 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) describe("objects", () => { - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: false, - required: [], - nullable: false, - readOnly: false, - } - it("supports general objects", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - properties: { - name: {type: "string", nullable: false, readOnly: false}, - age: {type: "number", nullable: false, readOnly: false}, - }, - required: ["name", "age"], - }) + const {code, execute} = await getActual( + ir.object({ + properties: { + name: ir.string(), + age: ir.number(), + }, + required: ["name", "age"], + }), + ) expect(code).toMatchInlineSnapshot(` "const x = joi @@ -760,18 +727,99 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () await expect(execute({age: 35})).rejects.toThrow('"name" is required') }) - it("supports record objects", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - additionalProperties: { - type: "number", - nullable: false, - readOnly: false, - }, + it("supports objects with a properties + a record property", async () => { + const {code, execute} = await getActual( + ir.object({ + required: ["well_defined"], + properties: { + well_defined: ir.number(), + }, + additionalProperties: ir.record({value: ir.number()}), + }), + ) + + expect(code).toMatchInlineSnapshot(` + "/** + * Recursively re-distribute the type union/intersection such that joi can support it + * Eg: from A & (B | C) to (A & B) | (A & C) + * https://github.com/hapijs/joi/issues/3057 + */ + function joiIntersect( + left: joi.Schema, + right: joi.Schema, + ): joi.ObjectSchema | joi.AlternativesSchema { + if (isAlternativesSchema(left)) { + return joi + .alternatives() + .match(left.$_getFlag("match") ?? "any") + .try(...getAlternatives(left).map((it) => joiIntersect(it, right))) + } + + if (isAlternativesSchema(right)) { + return joi + .alternatives() + .match(right.$_getFlag("match") ?? "any") + .try(...getAlternatives(right).map((it) => joiIntersect(left, it))) + } + + if (!isObjectSchema(left) || !isObjectSchema(right)) { + throw new Error( + "only objects, or unions of objects can be intersected together.", + ) + } + + return (left as joi.ObjectSchema).concat(right) + + function isAlternativesSchema(it: joi.Schema): it is joi.AlternativesSchema { + return it.type === "alternatives" + } + + function isObjectSchema(it: joi.Schema): it is joi.ObjectSchema { + return it.type === "object" + } + + function getAlternatives(it: joi.AlternativesSchema): joi.Schema[] { + const terms = it.$_terms + const matches = terms.matches + + if (!Array.isArray(matches)) { + throw new Error("$_terms.matches is not an array of schemas") + } + + return matches.map((it) => it.schema) + } + } + const x = joiIntersect( + joi + .object() + .keys({ well_defined: joi.number().required() }) + .options({ stripUnknown: true }), + joi.object().pattern(joi.any(), joi.number().required()).required(), + ).required()" + `) + + await expect(execute({well_defined: 0, key: 1})).resolves.toEqual({ + well_defined: 0, + key: 1, }) + await expect(execute({key: 1})).rejects.toThrow( + '"well_defined" is required', + ) + await expect(execute({well_defined: 0, key: "string"})).rejects.toThrow( + '"key" must be a number', + ) + }) + + it("supports objects with just a record property", async () => { + const {code, execute} = await getActual( + ir.object({ + properties: {}, + additionalProperties: ir.record({value: ir.number()}), + }), + ) expect(code).toMatchInlineSnapshot( - '"const x = joi.object().pattern(joi.any(), joi.number().required()).required()"', + `"const x = joi.object().pattern(joi.any(), joi.number().required()).required()"`, ) await expect(execute({key: 1})).resolves.toEqual({ @@ -783,15 +831,16 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - properties: { - name: {type: "string", nullable: false, readOnly: false}, - age: {type: "number", nullable: false, readOnly: false}, - }, - required: ["name", "age"], - default: {name: "example", age: 22}, - }) + const {code, execute} = await getActual( + ir.object({ + properties: { + name: ir.string(), + age: ir.number(), + }, + required: ["name", "age"], + default: {name: "example", age: 22}, + }), + ) expect(code).toMatchInlineSnapshot(` "const x = joi @@ -809,13 +858,95 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () age: 22, }) }) + + it("supports null default when nullable", async () => { + const {code, execute} = await getActual( + ir.object({ + required: ["name"], + properties: { + name: ir.string(), + }, + nullable: true, + default: null, + }), + ) + + expect(code).toMatchInlineSnapshot(` + "const x = joi + .object() + .keys({ name: joi.string().required() }) + .options({ stripUnknown: true }) + .allow(null) + .default(null)" + `) + + await expect(execute(undefined)).resolves.toBeNull() + }) + }) + + describe("records", () => { + it("supports a Record", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.number(), + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = joi.object().pattern(joi.any(), joi.number().required()).required()"`, + ) + + await expect(execute({foo: 1})).resolves.toEqual({foo: 1}) + await expect(execute({foo: "string"})).rejects.toThrow( + '"foo" must be a number', + ) + }) + + it("supports a nullable Record with default null", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.number(), + nullable: true, + default: null, + }), + ) + + expect(code).toMatchInlineSnapshot(` + "const x = joi + .object() + .pattern(joi.any(), joi.number().required()) + .allow(null) + .default(null)" + `) + + await expect(execute({foo: 1})).resolves.toEqual({foo: 1}) + await expect(execute(undefined)).resolves.toBeNull() + await expect(execute({foo: "string"})).rejects.toThrow( + '"foo" must be a number', + ) + }) + + it("supports a Record", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.never(), + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = joi.object().keys({}).options({ stripUnknown: true }).required()"`, + ) + + await expect(execute({})).resolves.toEqual({}) + await expect(execute({foo: "string"})).resolves.toEqual({}) + }) }) describe("unions", () => { it("can union a string and number", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [schemaString(), schemaNumber()], + const {code, execute} = await getActual( + ir.union({ + schemas: [ir.string(), ir.number()], }), ) @@ -832,18 +963,18 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("can union an intersected object and string", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [ - schemaString(), - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.union({ + schemas: [ + ir.string(), + ir.intersection({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], @@ -881,19 +1012,31 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) await expect(execute({foo: "bla"})).rejects.toThrow('"bar" is required') }) + + it("unwraps a single element union", async () => { + const {code, execute} = await getActual( + ir.union({ + schemas: [ir.string()], + }), + ) + + expect(code).toMatchInlineSnapshot(`"const x = joi.string().required()"`) + + await expect(execute("some string")).resolves.toEqual("some string") + }) }) describe("intersections", () => { it("can intersect objects", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], @@ -924,23 +1067,23 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () }) it("can intersect unions", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - oneOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ + ir.union({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], }), - schemaObject({ - properties: {id: schemaString()}, + ir.object({ + properties: {id: ir.string()}, required: ["id"], }), ], @@ -1034,28 +1177,44 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () '"value" does not match any of the allowed types', ) }) - }) - describe("unspecified schemas when allowAny: true", () => { - const config: SchemaBuilderConfig = {allowAny: true} - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } - - it("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, + it("unwraps a single element primitive intersection", async () => { + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ir.string()], + }), ) + expect(code).toMatchInlineSnapshot(`"const x = joi.string().required()"`) + + await expect(execute("some string")).resolves.toEqual("some string") + }) + + it("unwraps a single element object intersection", async () => { + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ir.object({properties: {foo: ir.string()}})], + }), + ) + + expect(code).toMatchInlineSnapshot(` + "const x = joi + .object() + .keys({ foo: joi.string() }) + .options({ stripUnknown: true }) + .required()" + `) + + await expect(execute({foo: "bar"})).resolves.toEqual({foo: "bar"}) + }) + }) + + describe("any", () => { + it("supports any when allowAny: true", async () => { + const {code, execute} = await getActual(ir.any(), { + config: {allowAny: true}, + }) + expect(code).toMatchInlineSnapshot(`"const x = joi.any().required()"`) await expect(execute({any: "object"})).resolves.toEqual({ @@ -1067,13 +1226,28 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () await expect(execute("some string")).resolves.toBe("some string") }) - it("supports any record objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: true, - }, - config, + it("supports any when allowAny: false", async () => { + const {code, execute} = await getActual(ir.any(), { + config: {allowAny: false}, + }) + + expect(code).toMatchInlineSnapshot(`"const x = joi.any().required()"`) + + await expect(execute({any: "object"})).resolves.toEqual({ + any: "object", + }) + await expect(execute(["foo", 12])).resolves.toEqual(["foo", 12]) + await expect(execute(null)).resolves.toBeNull() + await expect(execute(123)).resolves.toBe(123) + await expect(execute("some string")).resolves.toBe("some string") + }) + + it("supports any record when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.any(), + }), + {config: {allowAny: true}}, ) expect(code).toMatchInlineSnapshot( @@ -1091,94 +1265,12 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () ) }) - it("supports any arrays", async () => { - const {code, execute} = await getActualFromModel( - { - type: "array", - nullable: false, - readOnly: false, - uniqueItems: false, - items: { - ...base, - additionalProperties: true, - }, - }, - config, - ) - - expect(code).toMatchInlineSnapshot(` - "const x = joi - .array() - .items(joi.object().pattern(joi.any(), joi.any())) - .required()" - `) - - await expect(execute([{key: 1}])).resolves.toEqual([ - { - key: 1, - }, - ]) - await expect(execute({key: "string"})).rejects.toThrow( - '"value" must be an array', - ) - }) - - it("supports empty objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: false, - }, - config, - ) - expect(code).toMatchInlineSnapshot( - `"const x = joi.object().keys({}).options({ stripUnknown: true }).required()"`, - ) - await expect(execute({any: "object"})).resolves.toEqual({}) - await expect(execute("some string")).rejects.toThrow( - '"value" must be of type object', - ) - }) - }) - - describe("unspecified schemas when allowAny: false", () => { - const config: SchemaBuilderConfig = {allowAny: false} - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } - - it("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, - ) - - expect(code).toMatchInlineSnapshot(`"const x = joi.any().required()"`) - - await expect(execute({any: "object"})).resolves.toEqual({ - any: "object", - }) - await expect(execute(["foo", 12])).resolves.toEqual(["foo", 12]) - await expect(execute(null)).resolves.toBeNull() - await expect(execute(123)).resolves.toBe(123) - await expect(execute("some string")).resolves.toBe("some string") - }) - - it("supports any record objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: true, - }, - config, + it("supports any record objects when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.any(), + }), + {config: {allowAny: false}}, ) expect(code).toMatchInlineSnapshot( @@ -1196,27 +1288,17 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () ) }) - it("supports any arrays", async () => { - const {code, execute} = await getActualFromModel( - { - type: "array", - nullable: false, - readOnly: false, - uniqueItems: false, - items: { - ...base, - additionalProperties: true, - }, - }, - config, + it("supports any arrays when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.array({ + items: ir.any(), + }), + {config: {allowAny: true}}, ) - expect(code).toMatchInlineSnapshot(` - "const x = joi - .array() - .items(joi.object().pattern(joi.any(), joi.any())) - .required()" - `) + expect(code).toMatchInlineSnapshot( + `"const x = joi.array().items(joi.any()).required()"`, + ) await expect(execute([{key: 1}])).resolves.toEqual([ { @@ -1228,21 +1310,52 @@ describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () ) }) - it("supports empty objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: false, - }, - config, + it("supports any arrays when allowAny: false", async () => { + const {code, execute} = await getActual( + ir.array({ + items: ir.any(), + }), + {config: {allowAny: false}}, ) + expect(code).toMatchInlineSnapshot( - `"const x = joi.object().keys({}).options({ stripUnknown: true }).required()"`, + `"const x = joi.array().items(joi.any()).required()"`, ) - await expect(execute({any: "object"})).resolves.toEqual({}) - await expect(execute("some string")).rejects.toThrow( - '"value" must be of type object', + + await expect(execute([{key: 1}])).resolves.toEqual([ + { + key: 1, + }, + ]) + await expect(execute({key: "string"})).rejects.toThrow( + '"value" must be an array', ) }) }) + + describe("never", () => { + it.skip("supports never", async () => { + const {code, execute} = await getActual(ir.never()) + + expect(code).toMatchInlineSnapshot(`"const x = joi.any().required()"`) + + await expect(execute("some string")).rejects.toBe("bla") + }) + }) + + async function getActual( + schema: IRModel, + { + config = {allowAny: false}, + compilerOptions = {exactOptionalPropertyTypes: false}, + }: { + config?: SchemaBuilderConfig + compilerOptions?: CompilerOptions + } = {}, + ) { + return testHarness.getActual(schema, schemaProvider, { + config, + compilerOptions, + }) + } }) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts index c08325e5..dd530b80 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.test-utils.ts @@ -1,73 +1,108 @@ import ts from "typescript" -import type {Input} from "../../../core/input" -import type {SchemaNormalizer} from "../../../core/normalization/schema-normalizer" -import type { - Reference, - Schema, - SchemaNumber, - SchemaObject, - SchemaString, -} from "../../../core/openapi-types" -import {isRef} from "../../../core/openapi-utils" +import type {ISchemaProvider} from "../../../core/input" +import type {IFormatter} from "../../../core/interfaces" +import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" +import type {MaybeIRModel} from "../../../core/openapi-types-normalized" import { type OpenApiVersion, unitTestInput, } from "../../../test/input.test-utils" import {ImportBuilder} from "../import-builder" -import {TypeBuilder} from "../type-builder/type-builder" -import {TypescriptFormatterBiome} from "../typescript-formatter.biome" +import {TypeBuilder, type TypeBuilderConfig} from "../type-builder/type-builder" import type {SchemaBuilderConfig} from "./abstract-schema-builder" import {type SchemaBuilderType, schemaBuilderFactory} from "./schema-builder" -export function schemaBuilderTestHarness( +export type SchemaBuilderIntegrationTestHarness = { + getActual( + path: string, + config?: SchemaBuilderConfig, + ): Promise<{ + code: string + schemas: string + execute: (input: unknown) => Promise + }> +} + +export function schemaBuilderIntegrationTestHarness( schemaBuilderType: SchemaBuilderType, + formatter: IFormatter, version: OpenApiVersion, executeParseSchema: (code: string, input?: unknown) => Promise, ) { - async function getActualFromModel( - schema: Schema, - config: SchemaBuilderConfig = {allowAny: false}, - ) { - const {input, schemaNormalizer} = await unitTestInput(version) - return getResult(input, schemaNormalizer, schema, true, config) - } + const innerHarness = schemaBuilderTestHarness( + schemaBuilderType, + formatter, + executeParseSchema, + ) async function getActual( path: string, config: SchemaBuilderConfig = {allowAny: false}, ) { - const {input, schemaNormalizer, file} = await unitTestInput(version) - return getResult( - input, - schemaNormalizer, + const {input, file} = await unitTestInput(version) + + return innerHarness.getActual( {$ref: `${file}#${path}`}, + input, + { + config: {allowAny: config.allowAny}, + compilerOptions: {exactOptionalPropertyTypes: false}, + }, true, - config, ) } - async function getResult( - input: Input, - schemaNormalizer: SchemaNormalizer, - maybeSchema: Schema | Reference, - required: boolean, - config: SchemaBuilderConfig, - ) { - const formatter = await TypescriptFormatterBiome.createNodeFormatter() + return { + getActual, + } +} + +export type SchemaBuilderTestHarness = { + getActual( + maybeIRModel: MaybeIRModel, + schemaProvider: ISchemaProvider, + opts?: { + config?: TypeBuilderConfig + compilerOptions?: CompilerOptions + }, + required?: boolean, + ): Promise<{ + code: string + schemas: string + execute: (input: unknown) => Promise + }> +} +export function schemaBuilderTestHarness( + schemaBuilderType: SchemaBuilderType, + formatter: IFormatter, + executeParseSchema: (code: string, input?: unknown) => Promise, +): SchemaBuilderTestHarness { + async function getActual( + maybeIRModel: MaybeIRModel, + schemaProvider: ISchemaProvider, + { + config = {allowAny: false}, + compilerOptions = {exactOptionalPropertyTypes: false}, + }: { + config?: TypeBuilderConfig + compilerOptions?: CompilerOptions + }, + required: boolean = true, + ) { const imports = new ImportBuilder({includeFileExtensions: false}) const typeBuilder = await TypeBuilder.fromSchemaProvider( "./unit-test.types.ts", - input, - {exactOptionalPropertyTypes: false}, - {allowAny: config.allowAny}, + schemaProvider, + compilerOptions, + config, ) const schemaBuilder = ( await schemaBuilderFactory( "./unit-test.schemas.ts", - input, + schemaProvider, schemaBuilderType, config, new ImportBuilder({includeFileExtensions: false}), @@ -75,12 +110,7 @@ export function schemaBuilderTestHarness( ) ).withImports(imports) - const schema = schemaBuilder.fromModel( - isRef(maybeSchema) - ? maybeSchema - : schemaNormalizer.normalize(maybeSchema), - required, - ) + const schema = schemaBuilder.fromModel(maybeIRModel, required) const code = ( await formatter.format( @@ -131,47 +161,5 @@ export function schemaBuilderTestHarness( } } - return { - getActualFromModel, - getActual, - } -} - -export function schemaObject( - partial: Partial = {}, -): SchemaObject { - return { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - ...partial, - } -} - -export function schemaString( - partial: Partial = {}, -): SchemaString { - return { - type: "string", - nullable: false, - readOnly: false, - ...partial, - } -} - -export function schemaNumber( - partial: Partial = {}, -): SchemaNumber { - return { - type: "number", - nullable: false, - readOnly: false, - ...partial, - } + return {getActual} } diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts index e750f5c6..e6a2447e 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/schema-builder.ts @@ -1,4 +1,4 @@ -import type {Input} from "../../../core/input" +import type {ISchemaProvider} from "../../../core/input" import type {ImportBuilder} from "../import-builder" import type {TypeBuilder} from "../type-builder/type-builder" import type {SchemaBuilderConfig} from "./abstract-schema-builder" @@ -11,7 +11,7 @@ export type SchemaBuilderType = "zod-v3" | "zod-v4" | "joi" export function schemaBuilderFactory( filename: string, - input: Input, + schemaProvider: ISchemaProvider, schemaBuilderType: SchemaBuilderType, schemaBuilderConfig: SchemaBuilderConfig, schemaBuilderImports: ImportBuilder, @@ -19,9 +19,9 @@ export function schemaBuilderFactory( ): Promise { switch (schemaBuilderType) { case "joi": { - return JoiBuilder.fromInput( + return JoiBuilder.fromSchemaProvider( filename, - input, + schemaProvider, schemaBuilderConfig, schemaBuilderImports, typeBuilder, @@ -29,9 +29,9 @@ export function schemaBuilderFactory( } case "zod-v3": { - return ZodV3Builder.fromInput( + return ZodV3Builder.fromSchemaProvider( filename, - input, + schemaProvider, schemaBuilderConfig, schemaBuilderImports, typeBuilder, @@ -39,9 +39,9 @@ export function schemaBuilderFactory( } case "zod-v4": { - return ZodV4Builder.fromInput( + return ZodV4Builder.fromSchemaProvider( filename, - input, + schemaProvider, schemaBuilderConfig, schemaBuilderImports, typeBuilder, @@ -49,6 +49,7 @@ export function schemaBuilderFactory( } default: + /* istanbul ignore next */ throw new Error(`schemaBuilderType '${schemaBuilderType}' not recognized`) } } diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.integration.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.integration.spec.ts index a6742047..d5a28eb1 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.integration.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.integration.spec.ts @@ -1,22 +1,11 @@ import * as vm from "node:vm" -import {describe, expect, it} from "@jest/globals" -import type { - SchemaArray, - SchemaBoolean, - SchemaNumber, - SchemaObject, - SchemaString, -} from "../../../core/openapi-types" -import {isDefined} from "../../../core/utils" +import {beforeAll, describe, expect, it} from "@jest/globals" import {testVersions} from "../../../test/input.test-utils" -import type {SchemaBuilderConfig} from "./abstract-schema-builder" +import {TypescriptFormatterBiome} from "../typescript-formatter.biome" import { - schemaBuilderTestHarness, - schemaNumber, - schemaObject, - schemaString, + type SchemaBuilderIntegrationTestHarness, + schemaBuilderIntegrationTestHarness, } from "./schema-builder.test-utils" -import {staticSchemas} from "./zod-v3-schema-builder" describe.each( testVersions, @@ -29,11 +18,19 @@ describe.each( ) } - const {getActualFromModel, getActual} = schemaBuilderTestHarness( - "zod-v3", - version, - executeParseSchema, - ) + let getActual: SchemaBuilderIntegrationTestHarness["getActual"] + + beforeAll(async () => { + const formatter = await TypescriptFormatterBiome.createNodeFormatter() + const harness = schemaBuilderIntegrationTestHarness( + "zod-v3", + formatter, + version, + executeParseSchema, + ) + + getActual = harness.getActual + }) it("supports the SimpleObject", async () => { const {code, schemas} = await getActual("components/schemas/SimpleObject") diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts index a09e9ab7..38bed1ba 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.ts @@ -1,4 +1,4 @@ -import type {Input} from "../../../core/input" +import type {ISchemaProvider} from "../../../core/input" import type {Reference} from "../../../core/openapi-types" import type { IRModel, @@ -46,16 +46,16 @@ export class ZodV3Builder extends AbstractSchemaBuilder< > { readonly type = "zod-v3" - static async fromInput( + static async fromSchemaProvider( filename: string, - input: Input, + schemaProvider: ISchemaProvider, schemaBuilderConfig: SchemaBuilderConfig, schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, ): Promise { return new ZodV3Builder( filename, - input, + schemaProvider, schemaBuilderConfig, schemaBuilderImports, typeBuilder, @@ -66,7 +66,7 @@ export class ZodV3Builder extends AbstractSchemaBuilder< override withImports(imports: ImportBuilder): ZodV3Builder { return new ZodV3Builder( this.filename, - this.input, + this.schemaProvider, this.config, this.schemaBuilderImports, this.typeBuilder, @@ -92,7 +92,7 @@ export class ZodV3Builder extends AbstractSchemaBuilder< protected schemaFromRef(reference: Reference): ExportDefinition { const name = this.getSchemaNameFromRef(reference) - const schemaObject = this.input.schema(reference) + const schemaObject = this.schemaProvider.schema(reference) const value = this.fromModel(schemaObject, true) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.unit.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.unit.spec.ts index db429a27..ad41b62d 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.unit.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v3-schema-builder.unit.spec.ts @@ -1,24 +1,23 @@ import * as vm from "node:vm" -import {describe, expect, it} from "@jest/globals" -import type { - SchemaArray, - SchemaBoolean, - SchemaNumber, - SchemaObject, - SchemaString, -} from "../../../core/openapi-types" +import {beforeAll, beforeEach, describe, expect, it} from "@jest/globals" +import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" +import type {IRModel} from "../../../core/openapi-types-normalized" import {isDefined} from "../../../core/utils" -import {testVersions} from "../../../test/input.test-utils" +import {FakeSchemaProvider} from "../../../test/fake-schema-provider" +import {irFixture as ir} from "../../../test/ir-model.fixtures.test-utils" +import {TypescriptFormatterBiome} from "../typescript-formatter.biome" import type {SchemaBuilderConfig} from "./abstract-schema-builder" import { + type SchemaBuilderTestHarness, schemaBuilderTestHarness, - schemaNumber, - schemaObject, - schemaString, } from "./schema-builder.test-utils" import {staticSchemas} from "./zod-v3-schema-builder" describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", () => { + let formatter: TypescriptFormatterBiome + let schemaProvider: FakeSchemaProvider + let testHarness: SchemaBuilderTestHarness + const executeParseSchema = async (code: string) => { return vm.runInNewContext( code, @@ -27,25 +26,25 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) } - const {getActualFromModel, getActual} = schemaBuilderTestHarness( - "zod-v3", - testVersions[0], - executeParseSchema, - ) + beforeAll(async () => { + formatter = await TypescriptFormatterBiome.createNodeFormatter() + testHarness = schemaBuilderTestHarness( + "zod-v3", + formatter, + executeParseSchema, + ) + }) - describe("numbers", () => { - const base: SchemaNumber = { - nullable: false, - readOnly: false, - type: "number", - } + beforeEach(() => { + schemaProvider = new FakeSchemaProvider() + }) + describe("numbers", () => { it("supports plain number", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - }) + const {code, execute} = await getActual(ir.number()) expect(code).toMatchInlineSnapshot('"const x = z.coerce.number()"') + await expect(execute(123)).resolves.toBe(123) await expect(execute("not a number 123")).rejects.toThrow( "Expected number, received nan", @@ -53,11 +52,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports closed number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "closed", - }) + const {code, execute} = await getActual( + ir.number({ + enum: [200, 301, 404], + "x-enum-extensibility": "closed", + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.union([z.literal(200), z.literal(301), z.literal(404)])"', @@ -70,11 +70,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports open number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "open", - }) + const {code, execute} = await getActual( + ir.number({ + enum: [200, 301, 404], + "x-enum-extensibility": "open", + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z.union([ @@ -92,11 +93,8 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) }) - it("supports minimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - }) + it("supports inclusiveMinimum", async () => { + const {code, execute} = await getActual(ir.number({inclusiveMinimum: 10})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().min(10)"', @@ -105,14 +103,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", await expect(execute(5)).rejects.toThrow( "Number must be greater than or equal to 10", ) + await expect(execute(10)).resolves.toBe(10) await expect(execute(20)).resolves.toBe(20) }) - it("supports maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maximum: 16, - }) + it("supports inclusiveMaximum", async () => { + const {code, execute} = await getActual(ir.number({inclusiveMaximum: 16})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().max(16)"', @@ -121,15 +117,14 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", await expect(execute(25)).rejects.toThrow( "Number must be less than or equal to 16", ) + await expect(execute(16)).resolves.toBe(16) await expect(execute(8)).resolves.toBe(8) }) - it("supports minimum/maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - maximum: 24, - }) + it("supports inclusiveMinimum/inclusiveMaximum", async () => { + const {code, execute} = await getActual( + ir.number({inclusiveMinimum: 10, inclusiveMaximum: 24}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().min(10).max(24)"', @@ -145,10 +140,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports exclusiveMinimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMinimum: 4, - }) + const {code, execute} = await getActual(ir.number({exclusiveMinimum: 4})) expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().gt(4)"') @@ -157,10 +149,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports exclusiveMaximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMaximum: 4, - }) + const {code, execute} = await getActual(ir.number({exclusiveMaximum: 4})) expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().lt(4)"') @@ -169,10 +158,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports multipleOf", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - }) + const {code, execute} = await getActual(ir.number({multipleOf: 4})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().multipleOf(4)"', @@ -184,13 +170,14 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", await expect(execute(16)).resolves.toBe(16) }) - it("supports combining multipleOf and min/max", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - minimum: 10, - maximum: 20, - }) + it("supports combining multipleOf and inclusiveMinimum/inclusiveMaximum", async () => { + const {code, execute} = await getActual( + ir.number({ + multipleOf: 4, + inclusiveMinimum: 10, + inclusiveMaximum: 20, + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().multipleOf(4).min(10).max(20)"', @@ -209,23 +196,25 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 0, - }) + const {code, execute} = await getActual( + ir.number({inclusiveMinimum: 0, inclusiveMaximum: 0}), + ) - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().min(0)"') + expect(code).toMatchInlineSnapshot( + '"const x = z.coerce.number().min(0).max(0)"', + ) await expect(execute(-1)).rejects.toThrow( "Number must be greater than or equal to 0", ) + await expect(execute(1)).rejects.toThrow( + "Number must be less than or equal to 0", + ) + await expect(execute(0)).resolves.toBe(0) }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 42, - }) + const {code, execute} = await getActual(ir.number({default: 42})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().default(42)"', @@ -235,10 +224,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values of 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 0, - }) + const {code, execute} = await getActual(ir.number({default: 0})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().default(0)"', @@ -248,11 +234,9 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.number({nullable: true, default: null}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().nullable().default(null)"', @@ -263,14 +247,8 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) describe("strings", () => { - const base: SchemaString = { - nullable: false, - readOnly: false, - type: "string", - } - it("supports plain string", async () => { - const {code, execute} = await getActualFromModel({...base}) + const {code, execute} = await getActual(ir.string()) expect(code).toMatchInlineSnapshot('"const x = z.string()"') @@ -280,13 +258,27 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) }) + it("supports nullable string", async () => { + const {code, execute} = await getActual(ir.string({nullable: true})) + + expect(code).toMatchInlineSnapshot('"const x = z.string().nullable()"') + + await expect(execute("a string")).resolves.toBe("a string") + await expect(execute(null)).resolves.toBe(null) + await expect(execute(123)).rejects.toThrow( + "Expected string, received number", + ) + }) + it("supports closed string enums", async () => { const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "closed", - }) + + const {code, execute} = await getActual( + ir.string({ + enum: enumValues, + "x-enum-extensibility": "closed", + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.enum(["red", "blue", "green"])"`, @@ -303,11 +295,13 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", it("supports open string enums", async () => { const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "open", - }) + + const {code, execute} = await getActual( + ir.string({ + enum: enumValues, + "x-enum-extensibility": "open", + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z.union([ @@ -325,36 +319,9 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) }) - it("supports nullable string using allOf", async () => { - const {code, execute} = await getActualFromModel({ - type: "object", - anyOf: [ - {type: "string", nullable: false, readOnly: false}, - {type: "null", nullable: false, readOnly: false}, - ], - allOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.string().nullable()"') - - await expect(execute("a string")).resolves.toBe("a string") - await expect(execute(null)).resolves.toBe(null) - await expect(execute(123)).rejects.toThrow( - "Expected string, received number", - ) - }) - it("supports minLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minLength: 8, - }) + const {code, execute} = await getActual(ir.string({minLength: 8})) + expect(code).toMatchInlineSnapshot('"const x = z.string().min(8)"') await expect(execute("12345678")).resolves.toBe("12345678") @@ -364,10 +331,8 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxLength: 8, - }) + const {code, execute} = await getActual(ir.string({maxLength: 8})) + expect(code).toMatchInlineSnapshot('"const x = z.string().max(8)"') await expect(execute("12345678")).resolves.toBe("12345678") @@ -377,10 +342,8 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports pattern", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: '"pk/\\d+"', - }) + const {code, execute} = await getActual(ir.string({pattern: '"pk/\\d+"'})) + expect(code).toMatchInlineSnapshot( '"const x = z.string().regex(new RegExp(\'"pk/\\\\d+"\'))"', ) @@ -390,12 +353,14 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports pattern with minLength / maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: "pk-\\d+", - minLength: 5, - maxLength: 8, - }) + const {code, execute} = await getActual( + ir.string({ + pattern: "pk-\\d+", + minLength: 5, + maxLength: 8, + }), + ) + expect(code).toMatchInlineSnapshot( '"const x = z.string().min(5).max(8).regex(new RegExp("pk-\\\\d+"))"', ) @@ -411,10 +376,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "example", - }) + const {code, execute} = await getActual(ir.string({default: "example"})) expect(code).toMatchInlineSnapshot( '"const x = z.string().default("example")"', @@ -424,11 +386,9 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.string({nullable: true, default: null}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.string().nullable().default(null)"', @@ -438,10 +398,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports empty string default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "", - }) + const {code, execute} = await getActual(ir.string({default: ""})) expect(code).toMatchInlineSnapshot('"const x = z.string().default("")"') @@ -449,10 +406,11 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values with quotes", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 'this is an "example", it\'s got lots of `quotes`', - }) + const {code, execute} = await getActual( + ir.string({ + default: 'this is an "example", it\'s got lots of `quotes`', + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.string().default('this is an "example", it\\'s got lots of \`quotes\`')"`, @@ -464,10 +422,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("coerces incorrectly typed default values to be strings", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) + const {code, execute} = await getActual(ir.string({default: false})) expect(code).toMatchInlineSnapshot( '"const x = z.string().default("false")"', @@ -478,10 +433,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", describe("formats", () => { it("supports email", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "email", - }) + const {code, execute} = await getActual(ir.string({format: "email"})) expect(code).toMatchInlineSnapshot('"const x = z.string().email()"') @@ -491,10 +443,9 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", await expect(execute("some string")).rejects.toThrow("Invalid email") }) it("supports date-time", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "date-time", - }) + const {code, execute} = await getActual( + ir.string({format: "date-time"}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.string().datetime({ offset: true })"', @@ -505,6 +456,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) await expect(execute("some string")).rejects.toThrow("Invalid datetime") }) + it("supports binary", async () => { + const {code} = await getActual(ir.string({format: "binary"})) + + expect(code).toMatchInlineSnapshot(`"const x = z.any()"`) + // todo: JSON.stringify doesn't work for passing a Blob into the VM, so can't execute + }) }) }) @@ -516,12 +473,6 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", })()`) } - const base: SchemaBoolean = { - nullable: false, - readOnly: false, - type: "boolean", - } - function inlineStaticSchemas(code: string) { const importRegex = /import {([^}]+)} from "\.\/unit-test\.schemas(?:\.ts)?"\n/ @@ -550,7 +501,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", } it("supports plain boolean", async () => { - const {code} = await getActualFromModel({...base}) + const {code} = await getActual(ir.boolean()) expect(code).toMatchInlineSnapshot(` "import { PermissiveBoolean } from "./unit-test.schemas" @@ -560,10 +511,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values of false", async () => { - const {code} = await getActualFromModel({ - ...base, - default: false, - }) + const {code} = await getActual(ir.boolean({default: false})) const codeWithoutImport = inlineStaticSchemas(code) @@ -586,10 +534,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values of true", async () => { - const {code} = await getActualFromModel({ - ...base, - default: true, - }) + const {code} = await getActual(ir.boolean({default: true})) const codeWithoutImport = inlineStaticSchemas(code) @@ -612,11 +557,9 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values of null when nullable", async () => { - const {code} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code} = await getActual( + ir.boolean({nullable: true, default: null}), + ) const codeWithoutImport = inlineStaticSchemas(code) @@ -639,10 +582,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("support enum of 'true'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["true"], - }) + const {code} = await getActual(ir.boolean({enum: ["true"]})) const codeWithoutImport = inlineStaticSchemas(code) @@ -671,10 +611,7 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("support enum of 'false'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["false"], - }) + const {code} = await getActual(ir.boolean({enum: ["false"]})) const codeWithoutImport = inlineStaticSchemas(code) @@ -732,16 +669,8 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) describe("arrays", () => { - const base: SchemaArray = { - nullable: false, - readOnly: false, - type: "array", - items: {nullable: false, readOnly: false, type: "string"}, - uniqueItems: false, - } - it("supports arrays", async () => { - const {code, execute} = await getActualFromModel({...base}) + const {code, execute} = await getActual(ir.array({items: ir.string()})) expect(code).toMatchInlineSnapshot('"const x = z.array(z.string())"') @@ -756,10 +685,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - uniqueItems: true, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + uniqueItems: true, + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z @@ -779,10 +710,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports minItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minItems: 2, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + minItems: 2, + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.array(z.string()).min(2)"', @@ -798,10 +731,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports maxItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxItems: 2, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + maxItems: 2, + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.array(z.string()).max(2)"', @@ -817,13 +752,14 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports minItems / maxItems / uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - items: schemaNumber(), - minItems: 1, - maxItems: 3, - uniqueItems: true, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.number(), + minItems: 1, + maxItems: 3, + uniqueItems: true, + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z @@ -848,10 +784,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: ["example"], - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + default: ["example"], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.array(z.string()).default(["example"])"`, @@ -861,10 +799,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports empty array default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: [], - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + default: [], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.array(z.string()).default([])"`, @@ -875,27 +815,16 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) describe("objects", () => { - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } - it("supports general objects", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - properties: { - name: {type: "string", nullable: false, readOnly: false}, - age: {type: "number", nullable: false, readOnly: false}, - }, - required: ["name", "age"], - }) + const {code, execute} = await getActual( + ir.object({ + properties: { + name: ir.string(), + age: ir.number(), + }, + required: ["name", "age"], + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.object({ name: z.string(), age: z.coerce.number() })"', @@ -909,39 +838,79 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", await expect(execute({age: 35})).rejects.toThrow("Required") }) - it("supports record objects", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - additionalProperties: { - type: "number", - nullable: false, - readOnly: false, - }, + it("supports empty objects", async () => { + const {code, execute} = await getActual(ir.object({properties: {}})) + expect(code).toMatchInlineSnapshot('"const x = z.object({})"') + await expect(execute({any: "object"})).resolves.toEqual({}) + await expect(execute("some string")).rejects.toThrow( + "Expected object, received string", + ) + }) + + it("supports objects with a properties + a record property", async () => { + const {code, execute} = await getActual( + ir.object({ + required: ["well_defined"], + properties: { + well_defined: ir.number(), + }, + additionalProperties: ir.record({value: ir.number()}), + }), + ) + + expect(code).toMatchInlineSnapshot(` + "const x = z.intersection( + z.object({ well_defined: z.coerce.number() }), + z.record(z.coerce.number()), + )" + `) + + await expect(execute({well_defined: 0, key: 1})).resolves.toEqual({ + well_defined: 0, + key: 1, }) + await expect(execute({key: 1})).rejects.toThrow( + // todo: the error here would be better if we avoided using coerce + "Expected number, received nan", + ) + await expect(execute({well_defined: 0, key: "string"})).rejects.toThrow( + // TODO: the error here would be better if we avoided using coerce + "Expected number, received nan", + ) + }) + + it("supports objects with just a record property", async () => { + const {code, execute} = await getActual( + ir.object({ + properties: {}, + additionalProperties: ir.record({value: ir.number()}), + }), + ) expect(code).toMatchInlineSnapshot( - '"const x = z.record(z.coerce.number())"', + `"const x = z.record(z.coerce.number())"`, ) await expect(execute({key: 1})).resolves.toEqual({ key: 1, }) await expect(execute({key: "string"})).rejects.toThrow( - // TODO: the error here would be better if we avoided using coerce + // todo: the error here would be better if we avoided using coerce "Expected number, received nan", ) }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - properties: { - name: {type: "string", nullable: false, readOnly: false}, - age: {type: "number", nullable: false, readOnly: false}, - }, - required: ["name", "age"], - default: {name: "example", age: 22}, - }) + const {code, execute} = await getActual( + ir.object({ + properties: { + name: ir.string(), + age: ir.number(), + }, + required: ["name", "age"], + default: {name: "example", age: 22}, + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z @@ -956,25 +925,82 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("supports null default when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.object({ + required: ["name"], + properties: { + name: ir.string(), + }, + nullable: true, + default: null, + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = z.object({ name: z.string() }).nullable().default(null)"`, + ) + + await expect(execute(undefined)).resolves.toBeNull() + }) + }) + + describe("records", () => { + it("supports a Record", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.number(), + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = z.record(z.coerce.number())"`, + ) + + await expect(execute({foo: 1})).resolves.toStrictEqual({foo: 1}) + await expect(execute({foo: "string"})).rejects.toThrow( + "Expected number, received nan", + ) + }) + + it("supports a nullable Record with default null", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.number(), + nullable: true, + default: null, + }), + ) expect(code).toMatchInlineSnapshot( - `"const x = z.record(z.unknown()).nullable().default(null)"`, + `"const x = z.record(z.coerce.number()).nullable().default(null)"`, ) + await expect(execute({foo: 1})).resolves.toStrictEqual({foo: 1}) await expect(execute(undefined)).resolves.toBeNull() + await expect(execute({foo: "string"})).rejects.toThrow( + "Expected number, received nan", + ) + }) + + it("supports a Record", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.never(), + }), + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.object({})"`) + + await expect(execute({})).resolves.toStrictEqual({}) + await expect(execute({foo: "string"})).resolves.toStrictEqual({}) }) }) describe("unions", () => { it("can union a string and number", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [schemaString(), schemaNumber()], + const {code, execute} = await getActual( + ir.union({ + schemas: [ir.string(), ir.number()], }), ) @@ -988,18 +1014,18 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("can union an intersected object and string", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [ - schemaString(), - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.union({ + schemas: [ + ir.string(), + ir.intersection({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], @@ -1022,19 +1048,31 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) await expect(execute({foo: "bla"})).rejects.toThrow("Required") }) + + it("unwraps a single element union", async () => { + const {code, execute} = await getActual( + ir.union({ + schemas: [ir.string()], + }), + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.string()"`) + + await expect(execute("some string")).resolves.toEqual("some string") + }) }) describe("intersections", () => { it("can intersect objects", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], @@ -1053,23 +1091,23 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) it("can intersect unions", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - oneOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ + ir.union({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], }), - schemaObject({ - properties: {id: schemaString()}, + ir.object({ + properties: {id: ir.string()}, required: ["id"], }), ], @@ -1093,28 +1131,40 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", }) await expect(execute({foo: "bla"})).rejects.toThrow("Required") }) - }) - describe("unspecified schemas when allowAny: true", () => { - const config: SchemaBuilderConfig = {allowAny: true} - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } + it("unwraps a single element primitive intersection", async () => { + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ir.string()], + }), + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.string()"`) - it("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, + await expect(execute("some string")).resolves.toEqual("some string") + }) + + it("unwraps a single element object intersection", async () => { + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ir.object({properties: {foo: ir.string()}})], + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = z.object({ foo: z.string().optional() })"`, ) + await expect(execute({foo: "bar"})).resolves.toEqual({foo: "bar"}) + }) + }) + + describe("any", () => { + it("supports any when allowAny: true", async () => { + const {code, execute} = await getActual(ir.any(), { + config: {allowAny: true}, + }) + expect(code).toMatchInlineSnapshot('"const x = z.any()"') await expect(execute({any: "object"})).resolves.toEqual({ @@ -1126,13 +1176,28 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", await expect(execute("some string")).resolves.toBe("some string") }) - it("supports any record objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: true, - }, - config, + it("supports any when allowAny: false", async () => { + const {code, execute} = await getActual(ir.any(), { + config: {allowAny: false}, + }) + + expect(code).toMatchInlineSnapshot(`"const x = z.unknown()"`) + + await expect(execute({any: "object"})).resolves.toEqual({ + any: "object", + }) + await expect(execute(["foo", 12])).resolves.toEqual(["foo", 12]) + await expect(execute(null)).resolves.toBeNull() + await expect(execute(123)).resolves.toBe(123) + await expect(execute("some string")).resolves.toBe("some string") + }) + + it("supports any record when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.any(), + }), + {config: {allowAny: true}}, ) expect(code).toMatchInlineSnapshot('"const x = z.record(z.any())"') @@ -1148,89 +1213,12 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) }) - it("supports any arrays", async () => { - const {code, execute} = await getActualFromModel( - { - type: "array", - nullable: false, - readOnly: false, - uniqueItems: false, - items: { - ...base, - additionalProperties: true, - }, - }, - config, - ) - - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.record(z.any()))"`, - ) - - await expect(execute([{key: 1}])).resolves.toEqual([ - { - key: 1, - }, - ]) - await expect(execute({key: "string"})).rejects.toThrow( - "Expected array, received object", - ) - }) - - it("supports empty objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: false, - }, - config, - ) - expect(code).toMatchInlineSnapshot('"const x = z.object({})"') - await expect(execute({any: "object"})).resolves.toEqual({}) - await expect(execute("some string")).rejects.toThrow( - "Expected object, received string", - ) - }) - }) - - describe("unspecified schemas when allowAny: false", () => { - const config: SchemaBuilderConfig = {allowAny: false} - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } - - it("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, - ) - - expect(code).toMatchInlineSnapshot(`"const x = z.unknown()"`) - - await expect(execute({any: "object"})).resolves.toEqual({ - any: "object", - }) - await expect(execute(["foo", 12])).resolves.toEqual(["foo", 12]) - await expect(execute(null)).resolves.toBeNull() - await expect(execute(123)).resolves.toBe(123) - await expect(execute("some string")).resolves.toBe("some string") - }) - - it("supports any record objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: true, - }, - config, + it("supports any record objects when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.any(), + }), + {config: {allowAny: false}}, ) expect(code).toMatchInlineSnapshot(`"const x = z.record(z.unknown())"`) @@ -1246,24 +1234,15 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) }) - it("supports any arrays", async () => { - const {code, execute} = await getActualFromModel( - { - type: "array", - nullable: false, - readOnly: false, - uniqueItems: false, - items: { - ...base, - additionalProperties: true, - }, - }, - config, + it("supports any arrays when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.array({ + items: ir.any(), + }), + {config: {allowAny: true}}, ) - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.record(z.unknown()))"`, - ) + expect(code).toMatchInlineSnapshot(`"const x = z.array(z.any())"`) await expect(execute([{key: 1}])).resolves.toEqual([ { @@ -1275,19 +1254,52 @@ describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", ) }) - it("supports empty objects", async () => { - const {code, execute} = await getActualFromModel( + it("supports any arrays when allowAny: false", async () => { + const {code, execute} = await getActual( + ir.array({ + items: ir.any(), + }), + {config: {allowAny: false}}, + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.array(z.unknown())"`) + + await expect(execute([{key: 1}])).resolves.toEqual([ { - ...base, - additionalProperties: false, + key: 1, }, - config, + ]) + await expect(execute({key: "string"})).rejects.toThrow( + "Expected array, received object", ) - expect(code).toMatchInlineSnapshot('"const x = z.object({})"') - await expect(execute({any: "object"})).resolves.toEqual({}) + }) + }) + + describe("never", () => { + it("supports never", async () => { + const {code, execute} = await getActual(ir.never()) + + expect(code).toMatchInlineSnapshot(`"const x = z.never()"`) + await expect(execute("some string")).rejects.toThrow( - "Expected object, received string", + "Expected never, received string", ) }) }) + + async function getActual( + schema: IRModel, + { + config = {allowAny: false}, + compilerOptions = {exactOptionalPropertyTypes: false}, + }: { + config?: SchemaBuilderConfig + compilerOptions?: CompilerOptions + } = {}, + ) { + return testHarness.getActual(schema, schemaProvider, { + config, + compilerOptions, + }) + } }) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.integration.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.integration.spec.ts index 307c1e83..32dcae4a 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.integration.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.integration.spec.ts @@ -1,22 +1,11 @@ import * as vm from "node:vm" -import {describe, expect, it} from "@jest/globals" -import type { - SchemaArray, - SchemaBoolean, - SchemaNumber, - SchemaObject, - SchemaString, -} from "../../../core/openapi-types" -import {isDefined} from "../../../core/utils" +import {beforeAll, describe, expect, it} from "@jest/globals" import {testVersions} from "../../../test/input.test-utils" -import type {SchemaBuilderConfig} from "./abstract-schema-builder" +import {TypescriptFormatterBiome} from "../typescript-formatter.biome" import { - schemaBuilderTestHarness, - schemaNumber, - schemaObject, - schemaString, + type SchemaBuilderIntegrationTestHarness, + schemaBuilderIntegrationTestHarness, } from "./schema-builder.test-utils" -import {staticSchemas} from "./zod-v4-schema-builder" describe.each( testVersions, @@ -25,11 +14,19 @@ describe.each( return vm.runInNewContext(code, {z: require("zod/v4").z, RegExp}) } - const {getActualFromModel, getActual} = schemaBuilderTestHarness( - "zod-v4", - version, - executeParseSchema, - ) + let getActual: SchemaBuilderIntegrationTestHarness["getActual"] + + beforeAll(async () => { + const formatter = await TypescriptFormatterBiome.createNodeFormatter() + const harness = schemaBuilderIntegrationTestHarness( + "zod-v4", + formatter, + version, + executeParseSchema, + ) + + getActual = harness.getActual + }) it("supports the SimpleObject", async () => { const {code, schemas} = await getActual("components/schemas/SimpleObject") diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts index 006b0744..8ccc0587 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.ts @@ -1,4 +1,4 @@ -import type {Input} from "../../../core/input" +import type {ISchemaProvider} from "../../../core/input" import type {Reference} from "../../../core/openapi-types" import type { IRModel, @@ -47,16 +47,16 @@ export class ZodV4Builder extends AbstractSchemaBuilder< > { readonly type = "zod-v4" - static async fromInput( + static async fromSchemaProvider( filename: string, - input: Input, + schemaProvider: ISchemaProvider, schemaBuilderConfig: SchemaBuilderConfig, schemaBuilderImports: ImportBuilder, typeBuilder: TypeBuilder, ): Promise { return new ZodV4Builder( filename, - input, + schemaProvider, schemaBuilderConfig, schemaBuilderImports, typeBuilder, @@ -67,7 +67,7 @@ export class ZodV4Builder extends AbstractSchemaBuilder< override withImports(imports: ImportBuilder): ZodV4Builder { return new ZodV4Builder( this.filename, - this.input, + this.schemaProvider, this.config, this.schemaBuilderImports, this.typeBuilder, @@ -93,7 +93,7 @@ export class ZodV4Builder extends AbstractSchemaBuilder< protected schemaFromRef(reference: Reference): ExportDefinition { const name = this.getSchemaNameFromRef(reference) - const schemaObject = this.input.schema(reference) + const schemaObject = this.schemaProvider.schema(reference) const value = this.fromModel(schemaObject, true) diff --git a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.unit.spec.ts b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.unit.spec.ts index 466028ea..8eb73bf1 100644 --- a/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.unit.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/schema-builders/zod-v4-schema-builder.unit.spec.ts @@ -1,45 +1,78 @@ import * as vm from "node:vm" -import {describe, expect, it} from "@jest/globals" -import type { - SchemaArray, - SchemaBoolean, - SchemaNumber, - SchemaObject, - SchemaString, -} from "../../../core/openapi-types" +import {beforeAll, beforeEach, describe, expect, it} from "@jest/globals" +import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" +import type {IRModel} from "../../../core/openapi-types-normalized" import {isDefined} from "../../../core/utils" -import {testVersions} from "../../../test/input.test-utils" +import {FakeSchemaProvider} from "../../../test/fake-schema-provider" +import {irFixture as ir} from "../../../test/ir-model.fixtures.test-utils" +import {TypescriptFormatterBiome} from "../typescript-formatter.biome" import type {SchemaBuilderConfig} from "./abstract-schema-builder" import { + type SchemaBuilderTestHarness, schemaBuilderTestHarness, - schemaNumber, - schemaObject, - schemaString, } from "./schema-builder.test-utils" import {staticSchemas} from "./zod-v4-schema-builder" describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", () => { + let formatter: TypescriptFormatterBiome + let schemaProvider: FakeSchemaProvider + let testHarness: SchemaBuilderTestHarness + const executeParseSchema = async (code: string) => { - return vm.runInNewContext(code, {z: require("zod/v4").z, RegExp}) + return vm.runInNewContext( + code, + // Note: done this way for consistency with joi tests + {z: require("zod/v4").z, RegExp}, + ) } - const {getActualFromModel, getActual} = schemaBuilderTestHarness( - "zod-v4", - testVersions[0], - executeParseSchema, - ) + beforeAll(async () => { + formatter = await TypescriptFormatterBiome.createNodeFormatter() + testHarness = schemaBuilderTestHarness( + "zod-v4", + formatter, + executeParseSchema, + ) + }) - describe("numbers", () => { - const base: SchemaNumber = { - nullable: false, - readOnly: false, - type: "number", - } + beforeEach(() => { + schemaProvider = new FakeSchemaProvider() + }) + + // todo: figure out how to deal with imports in the vm + describe.skip("$ref", () => { + it("will convert a $ref to a name and emit the referenced type", async () => { + const ref = ir.ref("/components/schemas/User") + schemaProvider.registerTestRef( + ref, + ir.object({properties: {username: ir.string()}}), + ) + + const {code, schemas, execute} = await getActual( + ir.object({properties: {user: ref}}), + ) + + expect(schemas).toMatchInlineSnapshot(` + "import { z } from "zod/v4" + export const s_User = z.object({ username: z.string().optional() })" + `) + expect(code).toMatchInlineSnapshot(` + "import { s_User } from "./unit-test.schemas" + + const x = z.object({ user: s_User.optional() })" + `) + + await expect(execute({user: {username: "admin"}})).resolves.toStrictEqual( + {user: {username: "admin"}}, + ) + await expect(execute({user: {name: "admin"}})).rejects.toThrow("foo") + }) + }) + + describe("numbers", () => { it("supports plain number", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - }) + const {code, execute} = await getActual(ir.number()) expect(code).toMatchInlineSnapshot('"const x = z.coerce.number()"') await expect(execute(123)).resolves.toBe(123) @@ -49,11 +82,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports closed number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "closed", - }) + const {code, execute} = await getActual( + ir.number({ + enum: [200, 301, 404], + "x-enum-extensibility": "closed", + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.union([z.literal(200), z.literal(301), z.literal(404)])"', @@ -64,11 +98,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports open number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "open", - }) + const {code, execute} = await getActual( + ir.number({ + enum: [200, 301, 404], + "x-enum-extensibility": "open", + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z.union([ @@ -86,11 +121,8 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", ) }) - it("supports minimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - }) + it("supports inclusiveMinimum", async () => { + const {code, execute} = await getActual(ir.number({inclusiveMinimum: 10})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().min(10)"', @@ -99,14 +131,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", await expect(execute(5)).rejects.toThrow( "Too small: expected number to be >=10", ) + await expect(execute(10)).resolves.toBe(10) await expect(execute(20)).resolves.toBe(20) }) - it("supports maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maximum: 16, - }) + it("supports inclusiveMaximum", async () => { + const {code, execute} = await getActual(ir.number({inclusiveMaximum: 16})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().max(16)"', @@ -115,15 +145,14 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", await expect(execute(25)).rejects.toThrow( "Too big: expected number to be <=16", ) + await expect(execute(16)).resolves.toBe(16) await expect(execute(8)).resolves.toBe(8) }) - it("supports minimum/maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - maximum: 24, - }) + it("supports inclusiveMinimum/inclusiveMaximum", async () => { + const {code, execute} = await getActual( + ir.number({inclusiveMinimum: 10, inclusiveMaximum: 24}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().min(10).max(24)"', @@ -139,10 +168,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports exclusiveMinimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMinimum: 4, - }) + const {code, execute} = await getActual(ir.number({exclusiveMinimum: 4})) expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().gt(4)"') @@ -153,10 +179,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports exclusiveMaximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMaximum: 4, - }) + const {code, execute} = await getActual(ir.number({exclusiveMaximum: 4})) expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().lt(4)"') @@ -167,10 +190,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports multipleOf", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - }) + const {code, execute} = await getActual(ir.number({multipleOf: 4})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().multipleOf(4)"', @@ -182,13 +202,14 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", await expect(execute(16)).resolves.toBe(16) }) - it("supports combining multipleOf and min/max", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - minimum: 10, - maximum: 20, - }) + it("supports combining multipleOf and inclusiveMinimum/inclusiveMaximum", async () => { + const {code, execute} = await getActual( + ir.number({ + multipleOf: 4, + inclusiveMinimum: 10, + inclusiveMaximum: 20, + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().multipleOf(4).min(10).max(20)"', @@ -207,23 +228,25 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 0, - }) + const {code, execute} = await getActual( + ir.number({inclusiveMinimum: 0, inclusiveMaximum: 0}), + ) - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().min(0)"') + expect(code).toMatchInlineSnapshot( + '"const x = z.coerce.number().min(0).max(0)"', + ) await expect(execute(-1)).rejects.toThrow( "Too small: expected number to be >=0", ) + await expect(execute(1)).rejects.toThrow( + "Too big: expected number to be <=0", + ) + await expect(execute(0)).resolves.toBe(0) }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 42, - }) + const {code, execute} = await getActual(ir.number({default: 42})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().default(42)"', @@ -233,10 +256,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values of 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 0, - }) + const {code, execute} = await getActual(ir.number({default: 0})) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().default(0)"', @@ -246,11 +266,9 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.number({nullable: true, default: null}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.coerce.number().nullable().default(null)"', @@ -261,14 +279,8 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) describe("strings", () => { - const base: SchemaString = { - nullable: false, - readOnly: false, - type: "string", - } - it("supports plain string", async () => { - const {code, execute} = await getActualFromModel({...base}) + const {code, execute} = await getActual(ir.string()) expect(code).toMatchInlineSnapshot('"const x = z.string()"') @@ -278,13 +290,27 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", ) }) + it("supports nullable string", async () => { + const {code, execute} = await getActual(ir.string({nullable: true})) + + expect(code).toMatchInlineSnapshot('"const x = z.string().nullable()"') + + await expect(execute("a string")).resolves.toBe("a string") + await expect(execute(null)).resolves.toBe(null) + await expect(execute(123)).rejects.toThrow( + "Invalid input: expected string, received number", + ) + }) + it("supports closed string enums", async () => { const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "closed", - }) + + const {code, execute} = await getActual( + ir.string({ + enum: enumValues, + "x-enum-extensibility": "closed", + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.enum(["red", "blue", "green"])"`, @@ -301,11 +327,13 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", it("supports open string enums", async () => { const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "open", - }) + + const {code, execute} = await getActual( + ir.string({ + enum: enumValues, + "x-enum-extensibility": "open", + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z.union([ @@ -323,36 +351,9 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", ) }) - it("supports nullable string using allOf", async () => { - const {code, execute} = await getActualFromModel({ - type: "object", - anyOf: [ - {type: "string", nullable: false, readOnly: false}, - {type: "null", nullable: false, readOnly: false}, - ], - allOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.string().nullable()"') - - await expect(execute("a string")).resolves.toBe("a string") - await expect(execute(null)).resolves.toBe(null) - await expect(execute(123)).rejects.toThrow( - "Invalid input: expected string, received number", - ) - }) - it("supports minLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minLength: 8, - }) + const {code, execute} = await getActual(ir.string({minLength: 8})) + expect(code).toMatchInlineSnapshot('"const x = z.string().min(8)"') await expect(execute("12345678")).resolves.toBe("12345678") @@ -362,10 +363,8 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxLength: 8, - }) + const {code, execute} = await getActual(ir.string({maxLength: 8})) + expect(code).toMatchInlineSnapshot('"const x = z.string().max(8)"') await expect(execute("12345678")).resolves.toBe("12345678") @@ -375,10 +374,8 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports pattern", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: '"pk/\\d+"', - }) + const {code, execute} = await getActual(ir.string({pattern: '"pk/\\d+"'})) + expect(code).toMatchInlineSnapshot( '"const x = z.string().regex(new RegExp(\'"pk/\\\\d+"\'))"', ) @@ -390,12 +387,14 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports pattern with minLength / maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: "pk-\\d+", - minLength: 5, - maxLength: 8, - }) + const {code, execute} = await getActual( + ir.string({ + pattern: "pk-\\d+", + minLength: 5, + maxLength: 8, + }), + ) + expect(code).toMatchInlineSnapshot( '"const x = z.string().min(5).max(8).regex(new RegExp("pk-\\\\d+"))"', ) @@ -413,10 +412,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "example", - }) + const {code, execute} = await getActual(ir.string({default: "example"})) expect(code).toMatchInlineSnapshot( '"const x = z.string().default("example")"', @@ -426,11 +422,9 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.string({nullable: true, default: null}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.string().nullable().default(null)"', @@ -440,10 +434,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports empty string default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "", - }) + const {code, execute} = await getActual(ir.string({default: ""})) expect(code).toMatchInlineSnapshot('"const x = z.string().default("")"') @@ -451,10 +442,11 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values with quotes", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 'this is an "example", it\'s got lots of `quotes`', - }) + const {code, execute} = await getActual( + ir.string({ + default: 'this is an "example", it\'s got lots of `quotes`', + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.string().default('this is an "example", it\\'s got lots of \`quotes\`')"`, @@ -466,10 +458,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("coerces incorrectly typed default values to be strings", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) + const {code, execute} = await getActual(ir.string({default: false})) expect(code).toMatchInlineSnapshot( '"const x = z.string().default("false")"', @@ -480,10 +469,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", describe("formats", () => { it("supports email", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "email", - }) + const {code, execute} = await getActual(ir.string({format: "email"})) expect(code).toMatchInlineSnapshot('"const x = z.email()"') @@ -493,10 +479,9 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", await expect(execute("some string")).rejects.toThrow("Invalid email") }) it("supports date-time", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "date-time", - }) + const {code, execute} = await getActual( + ir.string({format: "date-time"}), + ) expect(code).toMatchInlineSnapshot( '"const x = z.iso.datetime({ offset: true })"', @@ -509,6 +494,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", "Invalid ISO datetime", ) }) + it("supports binary", async () => { + const {code} = await getActual(ir.string({format: "binary"})) + + expect(code).toMatchInlineSnapshot(`"const x = z.any()"`) + // todo: JSON.stringify doesn't work for passing a Blob into the VM, so can't execute + }) }) }) @@ -520,12 +511,6 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", })()`) } - const base: SchemaBoolean = { - nullable: false, - readOnly: false, - type: "boolean", - } - function inlineStaticSchemas(code: string) { const importRegex = /import {([^}]+)} from "\.\/unit-test\.schemas(?:\.ts)?"\n/ @@ -554,7 +539,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", } it("supports plain boolean", async () => { - const {code} = await getActualFromModel({...base}) + const {code} = await getActual(ir.boolean()) expect(code).toMatchInlineSnapshot(` "import { PermissiveBoolean } from "./unit-test.schemas" @@ -564,10 +549,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values of false", async () => { - const {code} = await getActualFromModel({ - ...base, - default: false, - }) + const {code} = await getActual(ir.boolean({default: false})) const codeWithoutImport = inlineStaticSchemas(code) @@ -590,10 +572,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values of true", async () => { - const {code} = await getActualFromModel({ - ...base, - default: true, - }) + const {code} = await getActual(ir.boolean({default: true})) const codeWithoutImport = inlineStaticSchemas(code) @@ -616,11 +595,9 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values of null when nullable", async () => { - const {code} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code} = await getActual( + ir.boolean({nullable: true, default: null}), + ) const codeWithoutImport = inlineStaticSchemas(code) @@ -643,10 +620,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("support enum of 'true'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["true"], - }) + const {code} = await getActual(ir.boolean({enum: ["true"]})) const codeWithoutImport = inlineStaticSchemas(code) @@ -675,10 +649,7 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("support enum of 'false'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["false"], - }) + const {code} = await getActual(ir.boolean({enum: ["false"]})) const codeWithoutImport = inlineStaticSchemas(code) @@ -736,16 +707,8 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) describe("arrays", () => { - const base: SchemaArray = { - nullable: false, - readOnly: false, - type: "array", - items: {nullable: false, readOnly: false, type: "string"}, - uniqueItems: false, - } - it("supports arrays", async () => { - const {code, execute} = await getActualFromModel({...base}) + const {code, execute} = await getActual(ir.array({items: ir.string()})) expect(code).toMatchInlineSnapshot('"const x = z.array(z.string())"') @@ -760,10 +723,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - uniqueItems: true, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + uniqueItems: true, + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z @@ -783,10 +748,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports minItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minItems: 2, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + minItems: 2, + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.array(z.string()).min(2)"', @@ -802,10 +769,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports maxItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxItems: 2, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + maxItems: 2, + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.array(z.string()).max(2)"', @@ -821,13 +790,14 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports minItems / maxItems / uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - items: schemaNumber(), - minItems: 1, - maxItems: 3, - uniqueItems: true, - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.number(), + minItems: 1, + maxItems: 3, + uniqueItems: true, + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z @@ -852,10 +822,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: ["example"], - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + default: ["example"], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.array(z.string()).default(["example"])"`, @@ -865,10 +837,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports empty array default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: [], - }) + const {code, execute} = await getActual( + ir.array({ + items: ir.string(), + default: [], + }), + ) expect(code).toMatchInlineSnapshot( `"const x = z.array(z.string()).default([])"`, @@ -879,27 +853,16 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) describe("objects", () => { - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } - it("supports general objects", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - properties: { - name: {type: "string", nullable: false, readOnly: false}, - age: {type: "number", nullable: false, readOnly: false}, - }, - required: ["name", "age"], - }) + const {code, execute} = await getActual( + ir.object({ + properties: { + name: ir.string(), + age: ir.number(), + }, + required: ["name", "age"], + }), + ) expect(code).toMatchInlineSnapshot( '"const x = z.object({ name: z.string(), age: z.coerce.number() })"', @@ -915,18 +878,57 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", ) }) - it("supports record objects", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - additionalProperties: { - type: "number", - nullable: false, - readOnly: false, - }, + it("supports empty objects", async () => { + const {code, execute} = await getActual(ir.object({properties: {}})) + expect(code).toMatchInlineSnapshot('"const x = z.object({})"') + await expect(execute({any: "object"})).resolves.toEqual({}) + await expect(execute("some string")).rejects.toThrow( + "Invalid input: expected object, received string", + ) + }) + + it("supports objects with a properties + a record property", async () => { + const {code, execute} = await getActual( + ir.object({ + required: ["well_defined"], + properties: { + well_defined: ir.number(), + }, + additionalProperties: ir.record({value: ir.number()}), + }), + ) + + expect(code).toMatchInlineSnapshot(` + "const x = z.intersection( + z.object({ well_defined: z.coerce.number() }), + z.record(z.string(), z.coerce.number()), + )" + `) + + await expect(execute({well_defined: 0, key: 1})).resolves.toEqual({ + well_defined: 0, + key: 1, }) + await expect(execute({key: 1})).rejects.toThrow( + // todo: the error here would be better if we avoided using coerce + "Invalid input: expected number, received NaN", + ) + await expect(execute({key: "string"})).rejects.toThrow( + // todo: the error here would be better if we avoided using coerce + "Invalid input: expected number, received NaN", + ) + }) + + it("supports objects with just a record property", async () => { + const {code, execute} = await getActual( + ir.object({ + properties: {}, + additionalProperties: ir.record({value: ir.number()}), + }), + ) expect(code).toMatchInlineSnapshot( - '"const x = z.record(z.string(), z.coerce.number())"', + `"const x = z.record(z.string(), z.coerce.number())"`, ) await expect(execute({key: 1})).resolves.toEqual({ @@ -939,15 +941,16 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - properties: { - name: {type: "string", nullable: false, readOnly: false}, - age: {type: "number", nullable: false, readOnly: false}, - }, - required: ["name", "age"], - default: {name: "example", age: 22}, - }) + const {code, execute} = await getActual( + ir.object({ + properties: { + name: ir.string(), + age: ir.number(), + }, + required: ["name", "age"], + default: {name: "example", age: 22}, + }), + ) expect(code).toMatchInlineSnapshot(` "const x = z @@ -962,25 +965,82 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("supports null default when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) + const {code, execute} = await getActual( + ir.object({ + required: ["name"], + properties: { + name: ir.string(), + }, + nullable: true, + default: null, + }), + ) expect(code).toMatchInlineSnapshot( - `"const x = z.record(z.string(), z.unknown()).nullable().default(null)"`, + `"const x = z.object({ name: z.string() }).nullable().default(null)"`, ) await expect(execute(undefined)).resolves.toBeNull() }) }) + describe("records", () => { + it("supports a Record", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.number(), + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = z.record(z.string(), z.coerce.number())"`, + ) + + await expect(execute({foo: 1})).resolves.toStrictEqual({foo: 1}) + await expect(execute({foo: "string"})).rejects.toThrow( + "Invalid input: expected number, received NaN", + ) + }) + + it("supports a nullable Record with default null", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.number(), + nullable: true, + default: null, + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = z.record(z.string(), z.coerce.number()).nullable().default(null)"`, + ) + + await expect(execute({foo: 1})).resolves.toStrictEqual({foo: 1}) + await expect(execute(undefined)).resolves.toBeNull() + await expect(execute({foo: "string"})).rejects.toThrow( + "Invalid input: expected number, received NaN", + ) + }) + + it("supports a Record", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.never(), + }), + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.object({})"`) + + await expect(execute({})).resolves.toStrictEqual({}) + await expect(execute({foo: "string"})).resolves.toStrictEqual({}) + }) + }) + describe("unions", () => { it("can union a string and number", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [schemaString(), schemaNumber()], + const {code, execute} = await getActual( + ir.union({ + schemas: [ir.string(), ir.number()], }), ) @@ -996,18 +1056,18 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("can union an intersected object and string", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [ - schemaString(), - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.union({ + schemas: [ + ir.string(), + ir.intersection({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], @@ -1033,19 +1093,31 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", "Invalid input: expected string, received undefined", ) }) + + it("unwraps a single element union", async () => { + const {code, execute} = await getActual( + ir.union({ + schemas: [ir.string()], + }), + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.string()"`) + + await expect(execute("some string")).resolves.toEqual("some string") + }) }) describe("intersections", () => { it("can intersect objects", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], @@ -1066,23 +1138,23 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", }) it("can intersect unions", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - oneOf: [ - schemaObject({ - properties: {foo: schemaString()}, + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ + ir.union({ + schemas: [ + ir.object({ + properties: {foo: ir.string()}, required: ["foo"], }), - schemaObject({ - properties: {bar: schemaString()}, + ir.object({ + properties: {bar: ir.string()}, required: ["bar"], }), ], }), - schemaObject({ - properties: {id: schemaString()}, + ir.object({ + properties: {id: ir.string()}, required: ["id"], }), ], @@ -1108,28 +1180,40 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", "Invalid input: expected string, received undefined", ) }) - }) - describe("unspecified schemas when allowAny: true", () => { - const config: SchemaBuilderConfig = {allowAny: true} - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } + it("unwraps a single element primitive intersection", async () => { + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ir.string()], + }), + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.string()"`) + + await expect(execute("some string")).resolves.toEqual("some string") + }) - it("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, + it("unwraps a single element object intersection", async () => { + const {code, execute} = await getActual( + ir.intersection({ + schemas: [ir.object({properties: {foo: ir.string()}})], + }), + ) + + expect(code).toMatchInlineSnapshot( + `"const x = z.object({ foo: z.string().optional() })"`, ) + await expect(execute({foo: "bar"})).resolves.toEqual({foo: "bar"}) + }) + }) + + describe("any", () => { + it("supports any when allowAny: true", async () => { + const {code, execute} = await getActual(ir.any(), { + config: {allowAny: true}, + }) + expect(code).toMatchInlineSnapshot('"const x = z.any()"') await expect(execute({any: "object"})).resolves.toEqual({ @@ -1141,17 +1225,32 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", await expect(execute("some string")).resolves.toBe("some string") }) - it("supports any record objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: true, - }, - config, + it("supports any when allowAny: false", async () => { + const {code, execute} = await getActual(ir.any(), { + config: {allowAny: false}, + }) + + expect(code).toMatchInlineSnapshot(`"const x = z.unknown()"`) + + await expect(execute({any: "object"})).resolves.toEqual({ + any: "object", + }) + await expect(execute(["foo", 12])).resolves.toEqual(["foo", 12]) + await expect(execute(null)).resolves.toBeNull() + await expect(execute(123)).resolves.toBe(123) + await expect(execute("some string")).resolves.toBe("some string") + }) + + it("supports any record when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.any(), + }), + {config: {allowAny: true}}, ) expect(code).toMatchInlineSnapshot( - '"const x = z.record(z.string(), z.any())"', + `"const x = z.record(z.string(), z.any())"`, ) await expect(execute({key: 1})).resolves.toEqual({ @@ -1165,89 +1264,12 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", ) }) - it("supports any arrays", async () => { - const {code, execute} = await getActualFromModel( - { - type: "array", - nullable: false, - readOnly: false, - uniqueItems: false, - items: { - ...base, - additionalProperties: true, - }, - }, - config, - ) - - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.record(z.string(), z.any()))"`, - ) - - await expect(execute([{key: 1}])).resolves.toEqual([ - { - key: 1, - }, - ]) - await expect(execute({key: "string"})).rejects.toThrow( - "Invalid input: expected array, received Object", - ) - }) - - it("supports empty objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: false, - }, - config, - ) - expect(code).toMatchInlineSnapshot('"const x = z.object({})"') - await expect(execute({any: "object"})).resolves.toEqual({}) - await expect(execute("some string")).rejects.toThrow( - "Invalid input: expected object, received string", - ) - }) - }) - - describe("unspecified schemas when allowAny: false", () => { - const config: SchemaBuilderConfig = {allowAny: false} - const base: SchemaObject = { - type: "object", - allOf: [], - anyOf: [], - oneOf: [], - properties: {}, - additionalProperties: undefined, - required: [], - nullable: false, - readOnly: false, - } - - it("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, - ) - - expect(code).toMatchInlineSnapshot(`"const x = z.unknown()"`) - - await expect(execute({any: "object"})).resolves.toEqual({ - any: "object", - }) - await expect(execute(["foo", 12])).resolves.toEqual(["foo", 12]) - await expect(execute(null)).resolves.toBeNull() - await expect(execute(123)).resolves.toBe(123) - await expect(execute("some string")).resolves.toBe("some string") - }) - - it("supports any record objects", async () => { - const {code, execute} = await getActualFromModel( - { - ...base, - additionalProperties: true, - }, - config, + it("supports any record objects when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.record({ + value: ir.any(), + }), + {config: {allowAny: false}}, ) expect(code).toMatchInlineSnapshot( @@ -1265,24 +1287,15 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", ) }) - it("supports any arrays", async () => { - const {code, execute} = await getActualFromModel( - { - type: "array", - nullable: false, - readOnly: false, - uniqueItems: false, - items: { - ...base, - additionalProperties: true, - }, - }, - config, + it("supports any arrays when allowAny: true", async () => { + const {code, execute} = await getActual( + ir.array({ + items: ir.any(), + }), + {config: {allowAny: true}}, ) - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.record(z.string(), z.unknown()))"`, - ) + expect(code).toMatchInlineSnapshot(`"const x = z.array(z.any())"`) await expect(execute([{key: 1}])).resolves.toEqual([ { @@ -1294,19 +1307,52 @@ describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", ) }) - it("supports empty objects", async () => { - const {code, execute} = await getActualFromModel( + it("supports any arrays when allowAny: false", async () => { + const {code, execute} = await getActual( + ir.array({ + items: ir.any(), + }), + {config: {allowAny: false}}, + ) + + expect(code).toMatchInlineSnapshot(`"const x = z.array(z.unknown())"`) + + await expect(execute([{key: 1}])).resolves.toEqual([ { - ...base, - additionalProperties: false, + key: 1, }, - config, + ]) + await expect(execute({key: "string"})).rejects.toThrow( + "Invalid input: expected array, received Object", ) - expect(code).toMatchInlineSnapshot('"const x = z.object({})"') - await expect(execute({any: "object"})).resolves.toEqual({}) + }) + }) + + describe("never", () => { + it("supports never", async () => { + const {code, execute} = await getActual(ir.never()) + + expect(code).toMatchInlineSnapshot(`"const x = z.never()"`) + await expect(execute("some string")).rejects.toThrow( - "Invalid input: expected object, received string", + "Invalid input: expected never, received string", ) }) }) + + async function getActual( + schema: IRModel, + { + config = {allowAny: false}, + compilerOptions = {exactOptionalPropertyTypes: false}, + }: { + config?: SchemaBuilderConfig + compilerOptions?: CompilerOptions + } = {}, + ) { + return testHarness.getActual(schema, schemaProvider, { + config, + compilerOptions, + }) + } }) diff --git a/packages/openapi-code-generator/src/typescript/common/type-builder/type-builder.unit.spec.ts b/packages/openapi-code-generator/src/typescript/common/type-builder/type-builder.unit.spec.ts index 308f652b..9cc4a7bc 100644 --- a/packages/openapi-code-generator/src/typescript/common/type-builder/type-builder.unit.spec.ts +++ b/packages/openapi-code-generator/src/typescript/common/type-builder/type-builder.unit.spec.ts @@ -1,12 +1,7 @@ import {beforeAll, beforeEach, describe, expect, it} from "@jest/globals" -import type {ISchemaProvider} from "../../../core/input" import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" -import type { - IRModel, - IRRef, - MaybeIRModel, -} from "../../../core/openapi-types-normalized" -import {isRef} from "../../../core/openapi-utils" +import type {IRModel} from "../../../core/openapi-types-normalized" +import {FakeSchemaProvider} from "../../../test/fake-schema-provider" import {irFixture as ir} from "../../../test/ir-model.fixtures.test-utils" import {TypescriptFormatterBiome} from "../typescript-formatter.biome" import type {TypeBuilderConfig} from "./type-builder" @@ -15,30 +10,6 @@ import { typeBuilderTestHarness, } from "./type-builder.test-utils" -class FakeSchemaProvider implements ISchemaProvider { - private readonly testRefs: Record = {} - - registerTestRef(ref: IRRef, model: IRModel) { - this.testRefs[ref.$ref] = model - } - - schema(maybeRef: MaybeIRModel): IRModel { - if (isRef(maybeRef)) { - const result = this.testRefs[maybeRef.$ref] - - if (!result) { - throw new Error( - `FakeSchemaProvider: $ref '${maybeRef.$ref}' is not registered`, - ) - } - - return result - } - - return maybeRef - } -} - describe("typescript/common/type-builder - unit tests", () => { let formatter: TypescriptFormatterBiome let schemaProvider: FakeSchemaProvider