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 90c38f8e..213e65ed 100644 --- a/packages/openapi-code-generator/src/test/input.test-utils.ts +++ b/packages/openapi-code-generator/src/test/input.test-utils.ts @@ -12,7 +12,7 @@ import {OpenapiValidator} from "../core/openapi-validator" export type OpenApiVersion = "3.0.x" | "3.1.x" -function getTestVersions(): OpenApiVersion[] { +function getTestVersions(): [OpenApiVersion, ...OpenApiVersion[]] { if (process.argv.find((arg) => ["--updateSnapshot", "-u"].includes(arg))) { logger.warn("Running with --updateSnapshot - only testing one version") return ["3.0.x"] 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 45dbba5d..2ee5c709 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 @@ -459,1214 +459,4 @@ describe.each( `) }) }) - - describe("numbers", () => { - const base: SchemaNumber = { - nullable: false, - readOnly: false, - type: "number", - } - - it("supports plain number", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - }) - - 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', - ) - }) - - it("supports closed number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "closed", - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().valid(200, 301, 404).required()"', - ) - - await expect(execute(123)).rejects.toThrow( - '"value" must be one of [200, 301, 404]', - ) - await expect(execute(404)).resolves.toBe(404) - }) - - it("supports open number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "open", - }) - - expect(code).toMatchInlineSnapshot(`"const x = joi.number().required()"`) - - await expect(execute(123)).resolves.toBe(123) - await expect(execute(404)).resolves.toBe(404) - await expect(execute("not a number")).rejects.toThrow( - '"value" must be a number', - ) - }) - - it("supports minimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().min(10).required()"', - ) - - await expect(execute(5)).rejects.toThrow( - '"value" must be greater than or equal to 10', - ) - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maximum: 16, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().max(16).required()"', - ) - - await expect(execute(25)).rejects.toThrow( - '"value" must be less than or equal to 16', - ) - await expect(execute(8)).resolves.toBe(8) - }) - - it("supports minimum/maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - maximum: 24, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().min(10).max(24).required()"', - ) - - await expect(execute(5)).rejects.toThrow( - '"value" must be greater than or equal to 10', - ) - await expect(execute(25)).rejects.toThrow( - '"value" must be less than or equal to 24', - ) - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports exclusiveMinimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMinimum: 4, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().greater(4).required()"', - ) - - await expect(execute(4)).rejects.toThrow('"value" must be greater than 4') - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports exclusiveMaximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMaximum: 4, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().less(4).required()"', - ) - - await expect(execute(4)).rejects.toThrow('"value" must be less than 4') - await expect(execute(3)).resolves.toBe(3) - }) - - it("supports multipleOf", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().multiple(4).required()"', - ) - - await expect(execute(11)).rejects.toThrow( - '"value" must be a multiple of 4', - ) - 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, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().multiple(4).min(10).max(20).required()"', - ) - - await expect(execute(11)).rejects.toThrow( - '"value" must be a multiple of 4', - ) - await expect(execute(8)).rejects.toThrow( - '"value" must be greater than or equal to 10', - ) - await expect(execute(24)).rejects.toThrow( - '"value" must be less than or equal to 20', - ) - await expect(execute(16)).resolves.toBe(16) - }) - - it("supports 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 0, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.number().min(0).required()"', - ) - - await expect(execute(-1)).rejects.toThrow( - '"value" must be greater than or equal to 0', - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 42, - }) - - expect(code).toMatchInlineSnapshot(`"const x = joi.number().default(42)"`) - - await expect(execute(undefined)).resolves.toBe(42) - }) - - it("supports default values of 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 0, - }) - - expect(code).toMatchInlineSnapshot(`"const x = joi.number().default(0)"`) - - await expect(execute(undefined)).resolves.toBe(0) - }) - - it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.number().allow(null).default(null)"`, - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - }) - - describe("strings", () => { - const base: SchemaString = { - nullable: false, - readOnly: false, - type: "string", - } - - it("supports plain string", async () => { - const {code, execute} = await getActualFromModel({...base}) - - expect(code).toMatchInlineSnapshot('"const x = joi.string().required()"') - - await expect(execute("a string")).resolves.toBe("a string") - 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", - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.string().valid("red", "blue", "green").required()"`, - ) - - for (const value of enumValues) { - await expect(execute(value)).resolves.toBe(value) - } - - await expect(execute("orange")).rejects.toThrow( - '"value" must be one of [red, blue, green]', - ) - }) - - it("supports open string enums", async () => { - const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "open", - }) - - expect(code).toMatchInlineSnapshot(`"const x = joi.string().required()"`) - - for (const value of enumValues) { - await expect(execute(value)).resolves.toBe(value) - } - await expect(execute("orange")).resolves.toBe("orange") - await expect(execute(404)).rejects.toThrow('"value" must be a string') - }) - - it("supports minLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minLength: 8, - }) - expect(code).toMatchInlineSnapshot( - '"const x = joi.string().min(8).required()"', - ) - - await expect(execute("12345678")).resolves.toBe("12345678") - await expect(execute("1234567")).rejects.toThrow( - '"value" length must be at least 8 characters long', - ) - }) - - it("supports maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxLength: 8, - }) - expect(code).toMatchInlineSnapshot( - '"const x = joi.string().max(8).required()"', - ) - - await expect(execute("12345678")).resolves.toBe("12345678") - await expect(execute("123456789")).rejects.toThrow( - '"value" length must be less than or equal to 8 characters long', - ) - }) - - it("supports pattern", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: '"pk/\\d+"', - }) - expect(code).toMatchInlineSnapshot( - '"const x = joi.string().pattern(new RegExp(\'"pk/\\\\d+"\')).required()"', - ) - - await expect(execute('"pk/1234"')).resolves.toBe('"pk/1234"') - await expect(execute("pk/abcd")).rejects.toThrow( - '"value" with value "pk/abcd" fails to match the required pattern: /"pk\\/\\d+"/', - ) - }) - - it("supports pattern with minLength / maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: "pk-\\d+", - minLength: 5, - maxLength: 8, - }) - expect(code).toMatchInlineSnapshot( - '"const x = joi.string().min(5).max(8).pattern(new RegExp("pk-\\\\d+")).required()"', - ) - - await expect(execute("pk-12")).resolves.toBe("pk-12") - await expect(execute("pk-ab")).rejects.toThrow( - '"value" with value "pk-ab" fails to match the required pattern: /pk-\\d+/', - ) - await expect(execute("pk-1")).rejects.toThrow( - '"value" length must be at least 5 characters long', - ) - await expect(execute("pk-123456")).rejects.toThrow( - '"value" length must be less than or equal to 8 characters long', - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "example", - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.string().default("example")"`, - ) - - await expect(execute(undefined)).resolves.toBe("example") - }) - - it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.string().allow(null).default(null)"`, - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - - it("supports empty string default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "", - }) - - expect(code).toMatchInlineSnapshot(`"const x = joi.string().default("")"`) - - await expect(execute(undefined)).resolves.toBe("") - }) - - it("supports default values with quotes", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 'this is an "example", it\'s got lots of `quotes`', - }) - - expect(code).toMatchInlineSnapshot(` - "const x = joi - .string() - .default('this is an "example", it\\'s got lots of \`quotes\`')" - `) - - await expect(execute(undefined)).resolves.toBe( - 'this is an "example", it\'s got lots of `quotes`', - ) - }) - - it("coerces incorrectly typed default values to be strings", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.string().default("false")"`, - ) - - await expect(execute(undefined)).resolves.toBe("false") - }) - - describe("formats", () => { - it("supports email", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "email", - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.string().email().required()"`, - ) - - await expect(execute("test@example.com")).resolves.toBe( - "test@example.com", - ) - await expect(execute("some string")).rejects.toThrow( - '"value" must be a valid email', - ) - }) - it("supports date-time", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "date-time", - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.string().isoDate().required()"`, - ) - - await expect(execute("2024-05-25T08:20:00.000Z")).resolves.toBe( - "2024-05-25T08:20:00.000Z", - ) - await expect(execute("some string")).rejects.toThrow( - '"value" must be in iso format', - ) - }) - }) - }) - - describe("booleans", () => { - const base: SchemaBoolean = { - nullable: false, - readOnly: false, - type: "boolean", - } - - it("supports plain boolean", async () => { - const {code, execute} = await getActualFromModel({...base}) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").required()"`, - ) - - await expect(execute(true)).resolves.toBe(true) - await expect(execute(false)).resolves.toBe(false) - - await expect(execute("false")).resolves.toBe(false) - await expect(execute("true")).resolves.toBe(true) - - await expect(execute(0)).resolves.toBe(false) - await expect(execute("0")).resolves.toBe(false) - await expect(execute(1)).resolves.toBe(true) - await expect(execute("1")).resolves.toBe(true) - - await expect(execute(12)).rejects.toThrow('"value" must be a boolean') - await expect(execute("yup")).rejects.toThrow('"value" must be a boolean') - await expect(execute([])).rejects.toThrow('"value" must be a boolean') - await expect(execute({})).rejects.toThrow('"value" must be a boolean') - }) - - it("supports default values of false", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").default(false)"`, - ) - - await expect(execute(undefined)).resolves.toBe(false) - }) - - it("supports default values of true", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: true, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").default(true)"`, - ) - - await expect(execute(undefined)).resolves.toBe(true) - }) - - it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.boolean().truthy(1, "1").falsy(0, "0").allow(null).default(null)"`, - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - - it("support enum of 'true'", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: ["true"], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.boolean().truthy(1, "1").valid(true).required()"`, - ) - - await expect(execute(true)).resolves.toBe(true) - await expect(execute(false)).rejects.toThrow('"value" must be [true]') - }) - - it("support enum of 'false'", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: ["false"], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.boolean().falsy(0, "0").valid(false).required()"`, - ) - - await expect(execute(false)).resolves.toBe(false) - await expect(execute(true)).rejects.toThrow('"value" must be [false]') - }) - }) - - 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}) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.array().items(joi.string()).required()"`, - ) - - await expect(execute([])).resolves.toStrictEqual([]) - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute([1, 2])).rejects.toThrow('"[0]" must be a string') - }) - - it("supports uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - uniqueItems: true, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.array().items(joi.string()).unique().required()"`, - ) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo", "foo"])).rejects.toThrow( - '"[1]" contains a duplicate value', - ) - }) - - it("supports minItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minItems: 2, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.array().items(joi.string()).min(2).required()"`, - ) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo"])).rejects.toThrow( - '"value" must contain at least 2 items', - ) - }) - - it("supports maxItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxItems: 2, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.array().items(joi.string()).max(2).required()"`, - ) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo", "bar", "foobar"])).rejects.toThrow( - '"value" must contain less than or equal to 2 items', - ) - }) - - it("supports minItems / maxItems / uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - items: schemaNumber(), - minItems: 1, - maxItems: 3, - uniqueItems: true, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.array().items(joi.number()).unique().min(1).max(3).required()"`, - ) - - await expect(execute([1, 2])).resolves.toStrictEqual([1, 2]) - await expect(execute([])).rejects.toThrow( - '"value" must contain at least 1 items', - ) - await expect(execute([1, 2, 3, 4])).rejects.toThrow( - '"value" must contain less than or equal to 3 items', - ) - await expect(execute([3, 3, 3])).rejects.toThrow( - '"[1]" contains a duplicate value', - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: ["example"], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.array().items(joi.string()).default(["example"])"`, - ) - - await expect(execute(undefined)).resolves.toStrictEqual(["example"]) - }) - - it("supports empty array default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: [], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.array().items(joi.string()).default([])"`, - ) - - await expect(execute(undefined)).resolves.toStrictEqual([]) - }) - }) - - 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"], - }) - - expect(code).toMatchInlineSnapshot(` - "const x = joi - .object() - .keys({ name: joi.string().required(), age: joi.number().required() }) - .options({ stripUnknown: true }) - .required()" - `) - - await expect(execute({name: "John", age: 35})).resolves.toEqual({ - name: "John", - age: 35, - }) - - 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, - }, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = joi.object().pattern(joi.any(), joi.number().required()).required()"', - ) - - await expect(execute({key: 1})).resolves.toEqual({ - key: 1, - }) - await expect(execute({key: "string"})).rejects.toThrow( - '"key" must be a number', - ) - }) - - 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}, - }) - - expect(code).toMatchInlineSnapshot(` - "const x = joi - .object() - .keys({ name: joi.string().required(), age: joi.number().required() }) - .options({ stripUnknown: true }) - .default({ name: "example", age: 22 })" - `) - - // HACK: If we do a toStrictEqual, we get 'Received: serializes to the same string' - // presumably due to the use of global that differs inside the VM to outside. - // Passing through `Object` doesn't fix it, so just use toEqual ¯\_(ツ)_/¯ - await expect(execute(undefined)).resolves.toEqual({ - name: "example", - age: 22, - }) - }) - }) - - describe("unions", () => { - it("can union a string and number", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [schemaString(), schemaNumber()], - }), - ) - - expect(code).toMatchInlineSnapshot(` - "const x = joi - .alternatives() - .try(joi.string().required(), joi.number().required()) - .required()" - `) - - await expect(execute("some string")).resolves.toEqual("some string") - await expect(execute(1234)).resolves.toEqual(1234) - await expect(execute(undefined)).rejects.toThrow('"value" is required') - }) - - it("can union an intersected object and string", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [ - schemaString(), - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot(` - "const x = joi - .alternatives() - .try( - joi.string().required(), - joi - .object() - .keys({ foo: joi.string().required() }) - .options({ stripUnknown: true }) - .required() - .concat( - joi - .object() - .keys({ bar: joi.string().required() }) - .options({ stripUnknown: true }) - .required(), - ) - .required(), - ) - .required()" - `) - - await expect(execute("some string")).resolves.toEqual("some string") - await expect(execute({foo: "bla", bar: "foobar"})).resolves.toEqual({ - foo: "bla", - bar: "foobar", - }) - await expect(execute({foo: "bla"})).rejects.toThrow('"bar" is required') - }) - }) - - describe("intersections", () => { - it("can intersect objects", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot(` - "const x = joi - .object() - .keys({ foo: joi.string().required() }) - .options({ stripUnknown: true }) - .required() - .concat( - joi - .object() - .keys({ bar: joi.string().required() }) - .options({ stripUnknown: true }) - .required(), - ) - .required()" - `) - - await expect(execute({foo: "bla", bar: "foobar"})).resolves.toEqual({ - foo: "bla", - bar: "foobar", - }) - await expect(execute({foo: "bla"})).rejects.toThrow('"bar" is required') - }) - - it("can intersect unions", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - oneOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - schemaObject({ - properties: {id: schemaString()}, - required: ["id"], - }), - ], - }), - ) - - 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 - .alternatives() - .try( - joi - .object() - .keys({ foo: joi.string().required() }) - .options({ stripUnknown: true }) - .required(), - joi - .object() - .keys({ bar: joi.string().required() }) - .options({ stripUnknown: true }) - .required(), - ) - .required(), - joi - .object() - .keys({ id: joi.string().required() }) - .options({ stripUnknown: true }) - .required(), - ).required()" - `) - - await expect(execute({id: "1234", foo: "bla"})).resolves.toEqual({ - id: "1234", - foo: "bla", - }) - await expect(execute({id: "1234", bar: "bla"})).resolves.toEqual({ - id: "1234", - bar: "bla", - }) - await expect(execute({foo: "bla"})).rejects.toThrow( - '"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, - ) - - 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, - ) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.object().pattern(joi.any(), joi.any()).required()"`, - ) - - await expect(execute({key: 1})).resolves.toEqual({ - key: 1, - }) - await expect(execute({key: "string"})).resolves.toEqual({ - key: "string", - }) - await expect(execute(123)).rejects.toThrow( - '"value" must be of type object', - ) - }) - - 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, - ) - - expect(code).toMatchInlineSnapshot( - `"const x = joi.object().pattern(joi.any(), joi.any()).required()"`, - ) - - await expect(execute({key: 1})).resolves.toEqual({ - key: 1, - }) - await expect(execute({key: "string"})).resolves.toEqual({ - key: "string", - }) - await expect(execute(123)).rejects.toThrow( - '"value" must be of type object', - ) - }) - - 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', - ) - }) - }) }) 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 45dbba5d..d7dfc588 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 @@ -16,9 +16,7 @@ import { schemaString, } from "./schema-builder.test-utils" -describe.each( - testVersions, -)("%s - typescript/common/schema-builders/joi-schema-builder", (version) => { +describe("typescript/common/schema-builders/joi-schema-builder - unit tests", () => { const executeParseSchema = async (code: string) => { return vm.runInNewContext( code, @@ -34,432 +32,10 @@ describe.each( const {getActual, getActualFromModel} = schemaBuilderTestHarness( "joi", - version, + testVersions[0], executeParseSchema, ) - it("supports the SimpleObject", async () => { - const {code, schemas} = await getActual("components/schemas/SimpleObject") - - expect(code).toMatchInlineSnapshot(` - "import { s_SimpleObject } from "./unit-test.schemas" - - const x = s_SimpleObject.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_SimpleObject = joi - .object() - .keys({ - str: joi.string().required(), - num: joi.number().required(), - date: joi.string().required(), - datetime: joi.string().isoDate().required(), - optional_str: joi.string(), - required_nullable: joi.string().allow(null).required(), - $ref: joi.string(), - }) - .options({ stripUnknown: true }) - .required() - .id("s_SimpleObject")" - `) - }) - - it("supports the ObjectWithComplexProperties", async () => { - const {code, schemas} = await getActual( - "components/schemas/ObjectWithComplexProperties", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_ObjectWithComplexProperties } from "./unit-test.schemas" - - const x = s_ObjectWithComplexProperties.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_AString = joi.string().required().id("s_AString") - - export const s_OneOf = joi - .alternatives() - .try( - joi - .object() - .keys({ strs: joi.array().items(joi.string()).required() }) - .options({ stripUnknown: true }) - .required(), - joi.array().items(joi.string()).required(), - joi.string().required(), - ) - .required() - .id("s_OneOf") - - export const s_ObjectWithComplexProperties = joi - .object() - .keys({ - requiredOneOf: joi - .alternatives() - .try(joi.string().required(), joi.number().required()) - .required(), - requiredOneOfRef: s_OneOf.required(), - optionalOneOf: joi - .alternatives() - .try(joi.string().required(), joi.number().required()), - optionalOneOfRef: s_OneOf, - nullableSingularOneOf: joi - .boolean() - .truthy(1, "1") - .falsy(0, "0") - .required() - .allow(null), - nullableSingularOneOfRef: s_AString.required().allow(null), - }) - .options({ stripUnknown: true }) - .required() - .id("s_ObjectWithComplexProperties")" - `) - }) - - it("supports unions / oneOf", async () => { - const {code, schemas} = await getActual("components/schemas/OneOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_OneOf } from "./unit-test.schemas" - - const x = s_OneOf.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_OneOf = joi - .alternatives() - .try( - joi - .object() - .keys({ strs: joi.array().items(joi.string()).required() }) - .options({ stripUnknown: true }) - .required(), - joi.array().items(joi.string()).required(), - joi.string().required(), - ) - .required() - .id("s_OneOf")" - `) - }) - - it("supports unions / anyOf", async () => { - const {code, schemas} = await getActual("components/schemas/AnyOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_AnyOf } from "./unit-test.schemas" - - const x = s_AnyOf.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_AnyOf = joi - .alternatives() - .try(joi.number().required(), joi.string().required()) - .required() - .id("s_AnyOf")" - `) - }) - - it("supports allOf", async () => { - const {code, schemas} = await getActual("components/schemas/AllOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_AllOf } from "./unit-test.schemas" - - const x = s_AllOf.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_Base = joi - .object() - .keys({ name: joi.string().required(), breed: joi.string() }) - .options({ stripUnknown: true }) - .required() - .id("s_Base") - - export const s_AllOf = s_Base - .required() - .concat( - joi - .object() - .keys({ id: joi.number().required() }) - .options({ stripUnknown: true }) - .required(), - ) - .required() - .id("s_AllOf")" - `) - }) - - it("supports recursion", async () => { - const {code, schemas} = await getActual("components/schemas/Recursive") - - expect(code).toMatchInlineSnapshot(` - "import { s_Recursive } from "./unit-test.schemas" - - const x = joi.link("#s_Recursive.required()")" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_Recursive = joi - .object() - .keys({ child: joi.link("#s_Recursive") }) - .options({ stripUnknown: true }) - .required() - .id("s_Recursive")" - `) - }) - - it("orders schemas such that dependencies are defined first", async () => { - const {code, schemas} = await getActual("components/schemas/Ordering") - - expect(code).toMatchInlineSnapshot(` - "import { s_Ordering } from "./unit-test.schemas" - - const x = s_Ordering.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_AOrdering = joi - .object() - .keys({ name: joi.string() }) - .options({ stripUnknown: true }) - .required() - .id("s_AOrdering") - - export const s_ZOrdering = joi - .object() - .keys({ name: joi.string(), dependency1: s_AOrdering.required() }) - .options({ stripUnknown: true }) - .required() - .id("s_ZOrdering") - - export const s_Ordering = joi - .object() - .keys({ - dependency1: s_ZOrdering.required(), - dependency2: s_AOrdering.required(), - }) - .options({ stripUnknown: true }) - .required() - .id("s_Ordering")" - `) - }) - - it("supports string and numeric enums", async () => { - const {code, schemas} = await getActual("components/schemas/Enums") - - expect(code).toMatchInlineSnapshot(` - "import { s_Enums } from "./unit-test.schemas" - - const x = s_Enums.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_Enums = joi - .object() - .keys({ - str: joi.string().valid("foo", "bar").allow(null), - num: joi.number().valid(10, 20).allow(null), - }) - .options({ stripUnknown: true }) - .required() - .id("s_Enums")" - `) - }) - - describe("additionalProperties", () => { - it("handles additionalProperties set to true", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesBool", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesBool } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesBool.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_AdditionalPropertiesBool = joi - .object() - .pattern(joi.any(), joi.any()) - .required() - .id("s_AdditionalPropertiesBool")" - `) - }) - - it("handles additionalProperties set to {}", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesUnknownEmptySchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptySchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesUnknownEmptySchema.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_AdditionalPropertiesUnknownEmptySchema = joi - .object() - .pattern(joi.any(), joi.any()) - .required() - .id("s_AdditionalPropertiesUnknownEmptySchema")" - `) - }) - - it("handles additionalProperties set to {type: 'object'}", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesUnknownEmptyObjectSchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptyObjectSchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesUnknownEmptyObjectSchema.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_AdditionalPropertiesUnknownEmptyObjectSchema = joi - .object() - .pattern(joi.any(), joi.object().pattern(joi.any(), joi.any()).required()) - .required() - .id("s_AdditionalPropertiesUnknownEmptyObjectSchema")" - `) - }) - - it("handles additionalProperties specifying a schema", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesSchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesSchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesSchema.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - export const s_NamedNullableStringEnum = joi - .string() - .valid("", "one", "two", "three") - .allow(null) - .required() - .id("s_NamedNullableStringEnum") - - export const s_AdditionalPropertiesSchema = joi - .object() - .pattern(joi.any(), s_NamedNullableStringEnum.required()) - .required() - .id("s_AdditionalPropertiesSchema")" - `) - }) - - it("handles additionalProperties in conjunction with properties", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesMixed", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesMixed } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesMixed.required()" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import joi from "joi" - - /** - * 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 - */ - export 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) - } - } - export const s_AdditionalPropertiesMixed = joiIntersect( - joi - .object() - .keys({ id: joi.string(), name: joi.string() }) - .options({ stripUnknown: true }), - joi.object().pattern(joi.any(), joi.any()).required(), - ) - .required() - .id("s_AdditionalPropertiesMixed")" - `) - }) - }) - describe("numbers", () => { const base: SchemaNumber = { nullable: false, 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 b41bf78a..a6742047 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 @@ -324,1262 +324,4 @@ describe.each( `) }) }) - - describe("numbers", () => { - const base: SchemaNumber = { - nullable: false, - readOnly: false, - type: "number", - } - - it("supports plain number", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - }) - - 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", - ) - }) - - it("supports closed number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "closed", - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.union([z.literal(200), z.literal(301), z.literal(404)])"', - ) - - await expect(execute(123)).rejects.toThrow( - "Invalid literal value, expected 404", - ) - await expect(execute(404)).resolves.toBe(404) - }) - - it("supports open number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "open", - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z.union([ - z.literal(200), - z.literal(301), - z.literal(404), - z.number().transform((it) => it as typeof it & UnknownEnumNumberValue), - ])" - `) - - await expect(execute(123)).resolves.toBe(123) - await expect(execute(404)).resolves.toBe(404) - await expect(execute("not a number")).rejects.toThrow( - "Expected number, received string", - ) - }) - - it("supports minimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().min(10)"', - ) - - await expect(execute(5)).rejects.toThrow( - "Number must be greater than or equal to 10", - ) - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maximum: 16, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().max(16)"', - ) - - await expect(execute(25)).rejects.toThrow( - "Number must be less than or equal to 16", - ) - await expect(execute(8)).resolves.toBe(8) - }) - - it("supports minimum/maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - maximum: 24, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().min(10).max(24)"', - ) - - await expect(execute(5)).rejects.toThrow( - "Number must be greater than or equal to 10", - ) - await expect(execute(25)).rejects.toThrow( - "Number must be less than or equal to 24", - ) - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports exclusiveMinimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMinimum: 4, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().gt(4)"') - - await expect(execute(4)).rejects.toThrow("Number must be greater than 4") - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports exclusiveMaximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMaximum: 4, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().lt(4)"') - - await expect(execute(4)).rejects.toThrow("Number must be less than 4") - await expect(execute(3)).resolves.toBe(3) - }) - - it("supports multipleOf", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().multipleOf(4)"', - ) - - await expect(execute(11)).rejects.toThrow( - "Number must be a multiple of 4", - ) - 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, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().multipleOf(4).min(10).max(20)"', - ) - - await expect(execute(11)).rejects.toThrow( - "Number must be a multiple of 4", - ) - await expect(execute(8)).rejects.toThrow( - "Number must be greater than or equal to 10", - ) - await expect(execute(24)).rejects.toThrow( - "Number must be less than or equal to 20", - ) - await expect(execute(16)).resolves.toBe(16) - }) - - it("supports 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 0, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().min(0)"') - - await expect(execute(-1)).rejects.toThrow( - "Number must be greater than or equal to 0", - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 42, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().default(42)"', - ) - - await expect(execute(undefined)).resolves.toBe(42) - }) - - it("supports default values of 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 0, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().default(0)"', - ) - - await expect(execute(undefined)).resolves.toBe(0) - }) - - it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().nullable().default(null)"', - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - }) - - describe("strings", () => { - const base: SchemaString = { - nullable: false, - readOnly: false, - type: "string", - } - - it("supports plain string", async () => { - const {code, execute} = await getActualFromModel({...base}) - - expect(code).toMatchInlineSnapshot('"const x = z.string()"') - - await expect(execute("a string")).resolves.toBe("a string") - 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", - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.enum(["red", "blue", "green"])"`, - ) - - for (const value of enumValues) { - await expect(execute(value)).resolves.toBe(value) - } - - await expect(execute("orange")).rejects.toThrow( - "Invalid enum value. Expected 'red' | 'blue' | 'green', received 'orange'", - ) - }) - - it("supports open string enums", async () => { - const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "open", - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z.union([ - z.enum(["red", "blue", "green"]), - z.string().transform((it) => it as typeof it & UnknownEnumStringValue), - ])" - `) - - for (const value of enumValues) { - await expect(execute(value)).resolves.toBe(value) - } - await expect(execute("orange")).resolves.toBe("orange") - await expect(execute(404)).rejects.toThrow( - "Expected string, received number", - ) - }) - - 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, - }) - expect(code).toMatchInlineSnapshot('"const x = z.string().min(8)"') - - await expect(execute("12345678")).resolves.toBe("12345678") - await expect(execute("1234567")).rejects.toThrow( - "String must contain at least 8 character(s)", - ) - }) - - it("supports maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxLength: 8, - }) - expect(code).toMatchInlineSnapshot('"const x = z.string().max(8)"') - - await expect(execute("12345678")).resolves.toBe("12345678") - await expect(execute("123456789")).rejects.toThrow( - "String must contain at most 8 character(s)", - ) - }) - - it("supports pattern", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: '"pk/\\d+"', - }) - expect(code).toMatchInlineSnapshot( - '"const x = z.string().regex(new RegExp(\'"pk/\\\\d+"\'))"', - ) - - await expect(execute('"pk/1234"')).resolves.toBe('"pk/1234"') - await expect(execute("pk/abcd")).rejects.toThrow("invalid_string") - }) - - it("supports pattern with minLength / maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: "pk-\\d+", - minLength: 5, - maxLength: 8, - }) - expect(code).toMatchInlineSnapshot( - '"const x = z.string().min(5).max(8).regex(new RegExp("pk-\\\\d+"))"', - ) - - await expect(execute("pk-12")).resolves.toBe("pk-12") - await expect(execute("pk-ab")).rejects.toThrow("invalid_string") - await expect(execute("pk-1")).rejects.toThrow( - "String must contain at least 5 character(s)", - ) - await expect(execute("pk-123456")).rejects.toThrow( - "String must contain at most 8 character(s)", - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "example", - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.string().default("example")"', - ) - - await expect(execute(undefined)).resolves.toBe("example") - }) - - it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.string().nullable().default(null)"', - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - - it("supports empty string default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "", - }) - - expect(code).toMatchInlineSnapshot('"const x = z.string().default("")"') - - await expect(execute(undefined)).resolves.toBe("") - }) - - it("supports default values with quotes", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - 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\`')"`, - ) - - await expect(execute(undefined)).resolves.toBe( - 'this is an "example", it\'s got lots of `quotes`', - ) - }) - - it("coerces incorrectly typed default values to be strings", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.string().default("false")"', - ) - - await expect(execute(undefined)).resolves.toBe("false") - }) - - describe("formats", () => { - it("supports email", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "email", - }) - - expect(code).toMatchInlineSnapshot('"const x = z.string().email()"') - - await expect(execute("test@example.com")).resolves.toBe( - "test@example.com", - ) - await expect(execute("some string")).rejects.toThrow("Invalid email") - }) - it("supports date-time", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "date-time", - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.string().datetime({ offset: true })"', - ) - - await expect(execute("2024-05-25T08:20:00.000Z")).resolves.toBe( - "2024-05-25T08:20:00.000Z", - ) - await expect(execute("some string")).rejects.toThrow("Invalid datetime") - }) - }) - }) - - describe("booleans", () => { - const executeBooleanTest = (code: string, input: unknown) => { - return executeParseSchema(`(async function () { - ${code} - return x.parse(${JSON.stringify(input)}) - })()`) - } - - const base: SchemaBoolean = { - nullable: false, - readOnly: false, - type: "boolean", - } - - function inlineStaticSchemas(code: string) { - const importRegex = - /import {([^}]+)} from "\.\/unit-test\.schemas(?:\.ts)?"\n/ - - const match = code.match(importRegex)?.[1] - - if (match) { - const definitions = match - .split(",") - .map((s) => s.trim()) - .map((it) => { - const definition = Reflect.get(staticSchemas, it) - - if (definition) { - return `const ${it} = ${definition}` - } - return undefined - }) - .filter(isDefined) - .join("\n") - - return `${definitions}\n${code.replace(importRegex, "")}` - } - - return code - } - - it("supports plain boolean", async () => { - const {code} = await getActualFromModel({...base}) - - expect(code).toMatchInlineSnapshot(` - "import { PermissiveBoolean } from "./unit-test.schemas" - - const x = PermissiveBoolean" - `) - }) - - it("supports default values of false", async () => { - const {code} = await getActualFromModel({ - ...base, - default: false, - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - const x = PermissiveBoolean.default(false)" - `) - - await expect( - executeBooleanTest(codeWithoutImport, undefined), - ).resolves.toBe(false) - }) - - it("supports default values of true", async () => { - const {code} = await getActualFromModel({ - ...base, - default: true, - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - const x = PermissiveBoolean.default(true)" - `) - - await expect( - executeBooleanTest(codeWithoutImport, undefined), - ).resolves.toBe(true) - }) - - it("supports default values of null when nullable", async () => { - const {code} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - const x = PermissiveBoolean.nullable().default(null)" - `) - - await expect( - executeBooleanTest(codeWithoutImport, undefined), - ).resolves.toBeNull() - }) - - it("support enum of 'true'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["true"], - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - const PermissiveLiteralTrue = z.preprocess((value) => { - return PermissiveBoolean.parse(value) - }, z.literal(true)) - - const x = PermissiveLiteralTrue" - `) - - await expect(executeBooleanTest(codeWithoutImport, true)).resolves.toBe( - true, - ) - await expect( - executeBooleanTest(codeWithoutImport, false), - ).rejects.toThrow("Invalid literal value, expected true") - }) - - it("support enum of 'false'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["false"], - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - const PermissiveLiteralFalse = z.preprocess((value) => { - return PermissiveBoolean.parse(value) - }, z.literal(false)) - - const x = PermissiveLiteralFalse" - `) - - await expect(executeBooleanTest(codeWithoutImport, false)).resolves.toBe( - false, - ) - await expect(executeBooleanTest(codeWithoutImport, true)).rejects.toThrow( - "Invalid literal value, expected false", - ) - }) - - it("PermissiveBoolean works as expected", async () => { - const code = ` - const x = ${staticSchemas.PermissiveBoolean} - ` - - await expect(executeBooleanTest(code, true)).resolves.toBe(true) - await expect(executeBooleanTest(code, false)).resolves.toBe(false) - - await expect(executeBooleanTest(code, "false")).resolves.toBe(false) - await expect(executeBooleanTest(code, "true")).resolves.toBe(true) - - await expect(executeBooleanTest(code, 0)).resolves.toBe(false) - await expect(executeBooleanTest(code, 1)).resolves.toBe(true) - - await expect(executeBooleanTest(code, 12)).rejects.toThrow( - "Expected boolean, received number", - ) - await expect(executeBooleanTest(code, "yup")).rejects.toThrow( - "Expected boolean, received string", - ) - await expect(executeBooleanTest(code, [])).rejects.toThrow( - "Expected boolean, received array", - ) - await expect(executeBooleanTest(code, {})).rejects.toThrow( - "Expected boolean, received object", - ) - }) - }) - - 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}) - - expect(code).toMatchInlineSnapshot('"const x = z.array(z.string())"') - - await expect(execute([])).resolves.toStrictEqual([]) - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute([1, 2])).rejects.toThrow( - "Expected string, received number", - ) - }) - - it("supports uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - uniqueItems: true, - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z - .array(z.string()) - .refine((array) => new Set([...array]).size === array.length, { - message: "Array must contain unique element(s)", - })" - `) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo", "foo"])).rejects.toThrow( - "Array must contain unique element(s)", - ) - }) - - it("supports minItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minItems: 2, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.array(z.string()).min(2)"', - ) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo"])).rejects.toThrow( - "Array must contain at least 2 element(s)", - ) - }) - - it("supports maxItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxItems: 2, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.array(z.string()).max(2)"', - ) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo", "bar", "foobar"])).rejects.toThrow( - "Array must contain at most 2 element(s)", - ) - }) - - it("supports minItems / maxItems / uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - items: schemaNumber(), - minItems: 1, - maxItems: 3, - uniqueItems: true, - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z - .array(z.coerce.number()) - .min(1) - .max(3) - .refine((array) => new Set([...array]).size === array.length, { - message: "Array must contain unique element(s)", - })" - `) - - await expect(execute([1, 2])).resolves.toStrictEqual([1, 2]) - await expect(execute([])).rejects.toThrow( - "Array must contain at least 1 element(s)", - ) - await expect(execute([1, 2, 3, 4])).rejects.toThrow( - "Array must contain at most 3 element(s)", - ) - await expect(execute([3, 3, 3])).rejects.toThrow( - "Array must contain unique element(s)", - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: ["example"], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.string()).default(["example"])"`, - ) - - await expect(execute(undefined)).resolves.toStrictEqual(["example"]) - }) - - it("supports empty array default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: [], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.string()).default([])"`, - ) - - await expect(execute(undefined)).resolves.toStrictEqual([]) - }) - }) - - 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"], - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.object({ name: z.string(), age: z.coerce.number() })"', - ) - - await expect(execute({name: "John", age: 35})).resolves.toEqual({ - name: "John", - age: 35, - }) - - 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, - }, - }) - - expect(code).toMatchInlineSnapshot( - '"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 - "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}, - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z - .object({ name: z.string(), age: z.coerce.number() }) - .default({ name: "example", age: 22 })" - `) - - await expect(execute(undefined)).resolves.toStrictEqual({ - name: "example", - age: 22, - }) - }) - - it("supports null default when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.record(z.unknown()).nullable().default(null)"`, - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - }) - - describe("unions", () => { - it("can union a string and number", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [schemaString(), schemaNumber()], - }), - ) - - expect(code).toMatchInlineSnapshot( - `"const x = z.union([z.string(), z.coerce.number()])"`, - ) - - await expect(execute("some string")).resolves.toEqual("some string") - await expect(execute(1234)).resolves.toEqual(1234) - await expect(execute(undefined)).rejects.toThrow("Required") - }) - - it("can union an intersected object and string", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [ - schemaString(), - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot(` - "const x = z.union([ - z.string(), - z.object({ foo: z.string() }).merge(z.object({ bar: z.string() })), - ])" - `) - - await expect(execute("some string")).resolves.toEqual("some string") - await expect(execute({foo: "bla", bar: "foobar"})).resolves.toEqual({ - foo: "bla", - bar: "foobar", - }) - await expect(execute({foo: "bla"})).rejects.toThrow("Required") - }) - }) - - describe("intersections", () => { - it("can intersect objects", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot( - `"const x = z.object({ foo: z.string() }).merge(z.object({ bar: z.string() }))"`, - ) - - await expect(execute({foo: "bla", bar: "foobar"})).resolves.toEqual({ - foo: "bla", - bar: "foobar", - }) - await expect(execute({foo: "bla"})).rejects.toThrow("Required") - }) - - it("can intersect unions", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - oneOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - schemaObject({ - properties: {id: schemaString()}, - required: ["id"], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot(` - "const x = z.intersection( - z.union([z.object({ foo: z.string() }), z.object({ bar: z.string() })]), - z.object({ id: z.string() }), - )" - `) - - await expect(execute({id: "1234", foo: "bla"})).resolves.toEqual({ - id: "1234", - foo: "bla", - }) - await expect(execute({id: "1234", bar: "bla"})).resolves.toEqual({ - id: "1234", - bar: "bla", - }) - 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("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, - ) - - expect(code).toMatchInlineSnapshot('"const x = z.any()"') - - 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, - ) - - expect(code).toMatchInlineSnapshot('"const x = z.record(z.any())"') - - await expect(execute({key: 1})).resolves.toEqual({ - key: 1, - }) - await expect(execute({key: "string"})).resolves.toEqual({ - key: "string", - }) - await expect(execute(123)).rejects.toThrow( - "Expected object, received number", - ) - }) - - 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, - ) - - expect(code).toMatchInlineSnapshot(`"const x = z.record(z.unknown())"`) - - await expect(execute({key: 1})).resolves.toEqual({ - key: 1, - }) - await expect(execute({key: "string"})).resolves.toEqual({ - key: "string", - }) - await expect(execute(123)).rejects.toThrow( - "Expected object, received number", - ) - }) - - 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.unknown()))"`, - ) - - 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", - ) - }) - }) }) 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 b41bf78a..db429a27 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 @@ -18,9 +18,7 @@ import { } from "./schema-builder.test-utils" import {staticSchemas} from "./zod-v3-schema-builder" -describe.each( - testVersions, -)("%s - typescript/common/schema-builders/zod-v3-schema-builder", (version) => { +describe("typescript/common/schema-builders/zod-v3-schema-builder - unit tests", () => { const executeParseSchema = async (code: string) => { return vm.runInNewContext( code, @@ -31,300 +29,10 @@ describe.each( const {getActualFromModel, getActual} = schemaBuilderTestHarness( "zod-v3", - version, + testVersions[0], executeParseSchema, ) - it("supports the SimpleObject", async () => { - const {code, schemas} = await getActual("components/schemas/SimpleObject") - - expect(code).toMatchInlineSnapshot(` - "import { s_SimpleObject } from "./unit-test.schemas" - - const x = s_SimpleObject" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_SimpleObject = z.object({ - str: z.string(), - num: z.coerce.number(), - date: z.string(), - datetime: z.string().datetime({ offset: true }), - optional_str: z.string().optional(), - required_nullable: z.string().nullable(), - $ref: z.string().optional(), - })" - `) - }) - - it("supports the ObjectWithComplexProperties", async () => { - const {code, schemas} = await getActual( - "components/schemas/ObjectWithComplexProperties", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_ObjectWithComplexProperties } from "./unit-test.schemas" - - const x = s_ObjectWithComplexProperties" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const PermissiveBoolean = z.preprocess((value) => { - if (typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if (typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - export const s_AString = z.string() - - export const s_OneOf = z.union([ - z.object({ strs: z.array(z.string()) }), - z.array(z.string()), - z.string(), - ]) - - export const s_ObjectWithComplexProperties = z.object({ - requiredOneOf: z.union([z.string(), z.coerce.number()]), - requiredOneOfRef: s_OneOf, - optionalOneOf: z.union([z.string(), z.coerce.number()]).optional(), - optionalOneOfRef: s_OneOf.optional(), - nullableSingularOneOf: PermissiveBoolean.nullable().optional(), - nullableSingularOneOfRef: s_AString.nullable().optional(), - })" - `) - }) - - it("supports unions / oneOf", async () => { - const {code, schemas} = await getActual("components/schemas/OneOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_OneOf } from "./unit-test.schemas" - - const x = s_OneOf" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_OneOf = z.union([ - z.object({ strs: z.array(z.string()) }), - z.array(z.string()), - z.string(), - ])" - `) - }) - - it("supports unions / anyOf", async () => { - const {code, schemas} = await getActual("components/schemas/AnyOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_AnyOf } from "./unit-test.schemas" - - const x = s_AnyOf" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_AnyOf = z.union([z.coerce.number(), z.string()])" - `) - }) - - it("supports allOf", async () => { - const {code, schemas} = await getActual("components/schemas/AllOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_AllOf } from "./unit-test.schemas" - - const x = s_AllOf" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_Base = z.object({ - name: z.string(), - breed: z.string().optional(), - }) - - export const s_AllOf = s_Base.merge(z.object({ id: z.coerce.number() }))" - `) - }) - - it("supports recursion", async () => { - const {code, schemas} = await getActual("components/schemas/Recursive") - - expect(code).toMatchInlineSnapshot(` - "import { s_Recursive } from "./unit-test.schemas" - - const x = z.lazy(() => s_Recursive)" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - import type { t_Recursive } from "./unit-test.types" - - export const s_Recursive: z.ZodType = - z.object({ child: z.lazy(() => s_Recursive.optional()) })" - `) - }) - - it("orders schemas such that dependencies are defined first", async () => { - const {code, schemas} = await getActual("components/schemas/Ordering") - - expect(code).toMatchInlineSnapshot(` - "import { s_Ordering } from "./unit-test.schemas" - - const x = s_Ordering" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_AOrdering = z.object({ name: z.string().optional() }) - - export const s_ZOrdering = z.object({ - name: z.string().optional(), - dependency1: s_AOrdering, - }) - - export const s_Ordering = z.object({ - dependency1: s_ZOrdering, - dependency2: s_AOrdering, - })" - `) - }) - - it("supports string and numeric enums", async () => { - const {code, schemas} = await getActual("components/schemas/Enums") - - expect(code).toMatchInlineSnapshot(` - "import { s_Enums } from "./unit-test.schemas" - - const x = s_Enums" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_Enums = z.object({ - str: z.enum(["foo", "bar"]).nullable().optional(), - num: z - .union([z.literal(10), z.literal(20)]) - .nullable() - .optional(), - })" - `) - }) - - describe("additionalProperties", () => { - it("handles additionalProperties set to true", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesBool", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesBool } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesBool" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_AdditionalPropertiesBool = z.record(z.unknown())" - `) - }) - - it("handles additionalProperties set to {}", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesUnknownEmptySchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptySchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesUnknownEmptySchema" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_AdditionalPropertiesUnknownEmptySchema = z.record(z.unknown())" - `) - }) - - it("handles additionalProperties set to {type: 'object'}", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesUnknownEmptyObjectSchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptyObjectSchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesUnknownEmptyObjectSchema" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_AdditionalPropertiesUnknownEmptyObjectSchema = z.record( - z.record(z.unknown()), - )" - `) - }) - - it("handles additionalProperties specifying a schema", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesSchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesSchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesSchema" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_NamedNullableStringEnum = z - .enum(["", "one", "two", "three"]) - .nullable() - - export const s_AdditionalPropertiesSchema = z.record(s_NamedNullableStringEnum)" - `) - }) - - it("handles additionalProperties in conjunction with properties", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesMixed", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesMixed } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesMixed" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v3" - - export const s_AdditionalPropertiesMixed = z.intersection( - z.object({ id: z.string().optional(), name: z.string().optional() }), - z.record(z.unknown()), - )" - `) - }) - }) - describe("numbers", () => { const base: SchemaNumber = { nullable: false, 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 dfa7c162..307c1e83 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 @@ -328,1285 +328,4 @@ describe.each( `) }) }) - - describe("numbers", () => { - const base: SchemaNumber = { - nullable: false, - readOnly: false, - type: "number", - } - - it("supports plain number", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number()"') - await expect(execute(123)).resolves.toBe(123) - await expect(execute("not a number 123")).rejects.toThrow( - "Invalid input: expected number, received NaN", - ) - }) - - it("supports closed number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "closed", - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.union([z.literal(200), z.literal(301), z.literal(404)])"', - ) - - await expect(execute(123)).rejects.toThrow("Invalid input: expected 404") - await expect(execute(404)).resolves.toBe(404) - }) - - it("supports open number enums", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - enum: [200, 301, 404], - "x-enum-extensibility": "open", - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z.union([ - z.literal(200), - z.literal(301), - z.literal(404), - z.number().transform((it) => it as typeof it & UnknownEnumNumberValue), - ])" - `) - - await expect(execute(123)).resolves.toBe(123) - await expect(execute(404)).resolves.toBe(404) - await expect(execute("not a number")).rejects.toThrow( - "Invalid input: expected number, received string", - ) - }) - - it("supports minimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().min(10)"', - ) - - await expect(execute(5)).rejects.toThrow( - "Too small: expected number to be >=10", - ) - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maximum: 16, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().max(16)"', - ) - - await expect(execute(25)).rejects.toThrow( - "Too big: expected number to be <=16", - ) - await expect(execute(8)).resolves.toBe(8) - }) - - it("supports minimum/maximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 10, - maximum: 24, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().min(10).max(24)"', - ) - - await expect(execute(5)).rejects.toThrow( - "Too small: expected number to be >=10", - ) - await expect(execute(25)).rejects.toThrow( - "Too big: expected number to be <=24", - ) - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports exclusiveMinimum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMinimum: 4, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().gt(4)"') - - await expect(execute(4)).rejects.toThrow( - "Too small: expected number to be >4", - ) - await expect(execute(20)).resolves.toBe(20) - }) - - it("supports exclusiveMaximum", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - exclusiveMaximum: 4, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().lt(4)"') - - await expect(execute(4)).rejects.toThrow( - "Too big: expected number to be <4", - ) - await expect(execute(3)).resolves.toBe(3) - }) - - it("supports multipleOf", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - multipleOf: 4, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().multipleOf(4)"', - ) - - await expect(execute(11)).rejects.toThrow( - "Invalid number: must be a multiple of 4", - ) - 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, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().multipleOf(4).min(10).max(20)"', - ) - - await expect(execute(11)).rejects.toThrow( - "Invalid number: must be a multiple of 4", - ) - await expect(execute(8)).rejects.toThrow( - "Too small: expected number to be >=10", - ) - await expect(execute(24)).rejects.toThrow( - "Too big: expected number to be <=20", - ) - await expect(execute(16)).resolves.toBe(16) - }) - - it("supports 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minimum: 0, - }) - - expect(code).toMatchInlineSnapshot('"const x = z.coerce.number().min(0)"') - - await expect(execute(-1)).rejects.toThrow( - "Too small: expected number to be >=0", - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 42, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().default(42)"', - ) - - await expect(execute(undefined)).resolves.toBe(42) - }) - - it("supports default values of 0", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: 0, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().default(0)"', - ) - - await expect(execute(undefined)).resolves.toBe(0) - }) - - it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.coerce.number().nullable().default(null)"', - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - }) - - describe("strings", () => { - const base: SchemaString = { - nullable: false, - readOnly: false, - type: "string", - } - - it("supports plain string", async () => { - const {code, execute} = await getActualFromModel({...base}) - - expect(code).toMatchInlineSnapshot('"const x = z.string()"') - - await expect(execute("a string")).resolves.toBe("a string") - 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", - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.enum(["red", "blue", "green"])"`, - ) - - for (const value of enumValues) { - await expect(execute(value)).resolves.toBe(value) - } - - await expect(execute("orange")).rejects.toThrow( - `Invalid option: expected one of \\"red\\"|\\"blue\\"|\\"green\\"`, - ) - }) - - it("supports open string enums", async () => { - const enumValues = ["red", "blue", "green"] - const {code, execute} = await getActualFromModel({ - ...base, - enum: enumValues, - "x-enum-extensibility": "open", - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z.union([ - z.enum(["red", "blue", "green"]), - z.string().transform((it) => it as typeof it & UnknownEnumStringValue), - ])" - `) - - for (const value of enumValues) { - await expect(execute(value)).resolves.toBe(value) - } - await expect(execute("orange")).resolves.toBe("orange") - await expect(execute(404)).rejects.toThrow( - "Invalid input: expected string, received number", - ) - }) - - 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, - }) - expect(code).toMatchInlineSnapshot('"const x = z.string().min(8)"') - - await expect(execute("12345678")).resolves.toBe("12345678") - await expect(execute("1234567")).rejects.toThrow( - "Too small: expected string to have >=8 characters", - ) - }) - - it("supports maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxLength: 8, - }) - expect(code).toMatchInlineSnapshot('"const x = z.string().max(8)"') - - await expect(execute("12345678")).resolves.toBe("12345678") - await expect(execute("123456789")).rejects.toThrow( - "Too big: expected string to have <=8 characters", - ) - }) - - it("supports pattern", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: '"pk/\\d+"', - }) - expect(code).toMatchInlineSnapshot( - '"const x = z.string().regex(new RegExp(\'"pk/\\\\d+"\'))"', - ) - - await expect(execute('"pk/1234"')).resolves.toBe('"pk/1234"') - await expect(execute("pk/abcd")).rejects.toThrow( - `Invalid string: must match pattern /\\"pk\\\\/\\\\d+\\"/`, - ) - }) - - it("supports pattern with minLength / maxLength", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - pattern: "pk-\\d+", - minLength: 5, - maxLength: 8, - }) - expect(code).toMatchInlineSnapshot( - '"const x = z.string().min(5).max(8).regex(new RegExp("pk-\\\\d+"))"', - ) - - await expect(execute("pk-12")).resolves.toBe("pk-12") - await expect(execute("pk-ab")).rejects.toThrow( - `Invalid string: must match pattern /pk-\\\\d+/`, - ) - await expect(execute("pk-1")).rejects.toThrow( - "Too small: expected string to have >=5 characters", - ) - await expect(execute("pk-123456")).rejects.toThrow( - "Too big: expected string to have <=8 characters", - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "example", - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.string().default("example")"', - ) - - await expect(execute(undefined)).resolves.toBe("example") - }) - - it("supports default values of null when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.string().nullable().default(null)"', - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - - it("supports empty string default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: "", - }) - - expect(code).toMatchInlineSnapshot('"const x = z.string().default("")"') - - await expect(execute(undefined)).resolves.toBe("") - }) - - it("supports default values with quotes", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - 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\`')"`, - ) - - await expect(execute(undefined)).resolves.toBe( - 'this is an "example", it\'s got lots of `quotes`', - ) - }) - - it("coerces incorrectly typed default values to be strings", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: false, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.string().default("false")"', - ) - - await expect(execute(undefined)).resolves.toBe("false") - }) - - describe("formats", () => { - it("supports email", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "email", - }) - - expect(code).toMatchInlineSnapshot('"const x = z.email()"') - - await expect(execute("test@example.com")).resolves.toBe( - "test@example.com", - ) - await expect(execute("some string")).rejects.toThrow("Invalid email") - }) - it("supports date-time", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - format: "date-time", - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.iso.datetime({ offset: true })"', - ) - - await expect(execute("2024-05-25T08:20:00.000Z")).resolves.toBe( - "2024-05-25T08:20:00.000Z", - ) - await expect(execute("some string")).rejects.toThrow( - "Invalid ISO datetime", - ) - }) - }) - }) - - describe("booleans", () => { - const executeBooleanTest = (code: string, input: unknown) => { - return executeParseSchema(`(async function () { - ${code} - return x.parse(${JSON.stringify(input)}) - })()`) - } - - const base: SchemaBoolean = { - nullable: false, - readOnly: false, - type: "boolean", - } - - function inlineStaticSchemas(code: string) { - const importRegex = - /import {([^}]+)} from "\.\/unit-test\.schemas(?:\.ts)?"\n/ - - const match = code.match(importRegex)?.[1] - - if (match) { - const definitions = match - .split(",") - .map((s) => s.trim()) - .map((it) => { - const definition = Reflect.get(staticSchemas, it) - - if (definition) { - return `const ${it} = ${definition}` - } - return undefined - }) - .filter(isDefined) - .join("\n") - - return `${definitions}\n${code.replace(importRegex, "")}` - } - - return code - } - - it("supports plain boolean", async () => { - const {code} = await getActualFromModel({...base}) - - expect(code).toMatchInlineSnapshot(` - "import { PermissiveBoolean } from "./unit-test.schemas" - - const x = PermissiveBoolean" - `) - }) - - it("supports default values of false", async () => { - const {code} = await getActualFromModel({ - ...base, - default: false, - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - const x = PermissiveBoolean.default(false)" - `) - - await expect( - executeBooleanTest(codeWithoutImport, undefined), - ).resolves.toBe(false) - }) - - it("supports default values of true", async () => { - const {code} = await getActualFromModel({ - ...base, - default: true, - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - const x = PermissiveBoolean.default(true)" - `) - - await expect( - executeBooleanTest(codeWithoutImport, undefined), - ).resolves.toBe(true) - }) - - it("supports default values of null when nullable", async () => { - const {code} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - const x = PermissiveBoolean.nullable().default(null)" - `) - - await expect( - executeBooleanTest(codeWithoutImport, undefined), - ).resolves.toBeNull() - }) - - it("support enum of 'true'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["true"], - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - const PermissiveLiteralTrue = z.preprocess((value) => { - return PermissiveBoolean.parse(value) - }, z.literal(true)) - - const x = PermissiveLiteralTrue" - `) - - await expect(executeBooleanTest(codeWithoutImport, true)).resolves.toBe( - true, - ) - await expect( - executeBooleanTest(codeWithoutImport, false), - ).rejects.toThrow("Invalid input: expected true") - }) - - it("support enum of 'false'", async () => { - const {code} = await getActualFromModel({ - ...base, - enum: ["false"], - }) - - const codeWithoutImport = inlineStaticSchemas(code) - - expect(codeWithoutImport).toMatchInlineSnapshot(` - "const PermissiveBoolean = z.preprocess((value) => { - if(typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if(typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - const PermissiveLiteralFalse = z.preprocess((value) => { - return PermissiveBoolean.parse(value) - }, z.literal(false)) - - const x = PermissiveLiteralFalse" - `) - - await expect(executeBooleanTest(codeWithoutImport, false)).resolves.toBe( - false, - ) - await expect(executeBooleanTest(codeWithoutImport, true)).rejects.toThrow( - "Invalid input: expected false", - ) - }) - - it("PermissiveBoolean works as expected", async () => { - const code = ` - const x = ${staticSchemas.PermissiveBoolean} - ` - - await expect(executeBooleanTest(code, true)).resolves.toBe(true) - await expect(executeBooleanTest(code, false)).resolves.toBe(false) - - await expect(executeBooleanTest(code, "false")).resolves.toBe(false) - await expect(executeBooleanTest(code, "true")).resolves.toBe(true) - - await expect(executeBooleanTest(code, 0)).resolves.toBe(false) - await expect(executeBooleanTest(code, 1)).resolves.toBe(true) - - await expect(executeBooleanTest(code, 12)).rejects.toThrow( - "Invalid input: expected boolean, received number", - ) - await expect(executeBooleanTest(code, "yup")).rejects.toThrow( - "Invalid input: expected boolean, received string", - ) - await expect(executeBooleanTest(code, [])).rejects.toThrow( - "Invalid input: expected boolean, received array", - ) - await expect(executeBooleanTest(code, {})).rejects.toThrow( - "Invalid input: expected boolean, received Object", - ) - }) - }) - - 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}) - - expect(code).toMatchInlineSnapshot('"const x = z.array(z.string())"') - - await expect(execute([])).resolves.toStrictEqual([]) - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute([1, 2])).rejects.toThrow( - "Invalid input: expected string, received number", - ) - }) - - it("supports uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - uniqueItems: true, - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z - .array(z.string()) - .refine((array) => new Set([...array]).size === array.length, { - message: "Array must contain unique element(s)", - })" - `) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo", "foo"])).rejects.toThrow( - "Array must contain unique element(s)", - ) - }) - - it("supports minItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - minItems: 2, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.array(z.string()).min(2)"', - ) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo"])).rejects.toThrow( - "Too small: expected array to have >=2 items", - ) - }) - - it("supports maxItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - maxItems: 2, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.array(z.string()).max(2)"', - ) - - await expect(execute(["foo", "bar"])).resolves.toStrictEqual([ - "foo", - "bar", - ]) - await expect(execute(["foo", "bar", "foobar"])).rejects.toThrow( - "Too big: expected array to have <=2 items", - ) - }) - - it("supports minItems / maxItems / uniqueItems", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - items: schemaNumber(), - minItems: 1, - maxItems: 3, - uniqueItems: true, - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z - .array(z.coerce.number()) - .min(1) - .max(3) - .refine((array) => new Set([...array]).size === array.length, { - message: "Array must contain unique element(s)", - })" - `) - - await expect(execute([1, 2])).resolves.toStrictEqual([1, 2]) - await expect(execute([])).rejects.toThrow( - "Too small: expected array to have >=1 items", - ) - await expect(execute([1, 2, 3, 4])).rejects.toThrow( - "Too big: expected array to have <=3 items", - ) - await expect(execute([3, 3, 3])).rejects.toThrow( - "Array must contain unique element(s)", - ) - }) - - it("supports default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: ["example"], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.string()).default(["example"])"`, - ) - - await expect(execute(undefined)).resolves.toStrictEqual(["example"]) - }) - - it("supports empty array default values", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - default: [], - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.array(z.string()).default([])"`, - ) - - await expect(execute(undefined)).resolves.toStrictEqual([]) - }) - }) - - 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"], - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.object({ name: z.string(), age: z.coerce.number() })"', - ) - - await expect(execute({name: "John", age: 35})).resolves.toEqual({ - name: "John", - age: 35, - }) - - await expect(execute({age: 35})).rejects.toThrow( - "Invalid input: expected string, received undefined", - ) - }) - - it("supports record objects", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - additionalProperties: { - type: "number", - nullable: false, - readOnly: false, - }, - }) - - expect(code).toMatchInlineSnapshot( - '"const x = z.record(z.string(), 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 - "Invalid input: 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}, - }) - - expect(code).toMatchInlineSnapshot(` - "const x = z - .object({ name: z.string(), age: z.coerce.number() }) - .default({ name: "example", age: 22 })" - `) - - await expect(execute(undefined)).resolves.toEqual({ - name: "example", - age: 22, - }) - }) - - it("supports null default when nullable", async () => { - const {code, execute} = await getActualFromModel({ - ...base, - nullable: true, - default: null, - }) - - expect(code).toMatchInlineSnapshot( - `"const x = z.record(z.string(), z.unknown()).nullable().default(null)"`, - ) - - await expect(execute(undefined)).resolves.toBeNull() - }) - }) - - describe("unions", () => { - it("can union a string and number", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [schemaString(), schemaNumber()], - }), - ) - - expect(code).toMatchInlineSnapshot( - `"const x = z.union([z.string(), z.coerce.number()])"`, - ) - - await expect(execute("some string")).resolves.toEqual("some string") - await expect(execute(1234)).resolves.toEqual(1234) - await expect(execute(undefined)).rejects.toThrow( - "Invalid input: expected string, received undefined", - ) - }) - - it("can union an intersected object and string", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - anyOf: [ - schemaString(), - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot(` - "const x = z.union([ - z.string(), - z.object({ foo: z.string() }).merge(z.object({ bar: z.string() })), - ])" - `) - - await expect(execute("some string")).resolves.toEqual("some string") - await expect(execute({foo: "bla", bar: "foobar"})).resolves.toEqual({ - foo: "bla", - bar: "foobar", - }) - // todo: the error here is not great, zod doesn't mention that the received object doesn't match the possible object - await expect(execute({foo: "bla"})).rejects.toThrow( - "Invalid input: expected string, received undefined", - ) - }) - }) - - describe("intersections", () => { - it("can intersect objects", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot( - `"const x = z.object({ foo: z.string() }).merge(z.object({ bar: z.string() }))"`, - ) - - await expect(execute({foo: "bla", bar: "foobar"})).resolves.toEqual({ - foo: "bla", - bar: "foobar", - }) - await expect(execute({foo: "bla"})).rejects.toThrow( - "Invalid input: expected string, received undefined", - ) - }) - - it("can intersect unions", async () => { - const {code, execute} = await getActualFromModel( - schemaObject({ - allOf: [ - schemaObject({ - oneOf: [ - schemaObject({ - properties: {foo: schemaString()}, - required: ["foo"], - }), - schemaObject({ - properties: {bar: schemaString()}, - required: ["bar"], - }), - ], - }), - schemaObject({ - properties: {id: schemaString()}, - required: ["id"], - }), - ], - }), - ) - - expect(code).toMatchInlineSnapshot(` - "const x = z.intersection( - z.union([z.object({ foo: z.string() }), z.object({ bar: z.string() })]), - z.object({ id: z.string() }), - )" - `) - - await expect(execute({id: "1234", foo: "bla"})).resolves.toEqual({ - id: "1234", - foo: "bla", - }) - await expect(execute({id: "1234", bar: "bla"})).resolves.toEqual({ - id: "1234", - bar: "bla", - }) - await expect(execute({foo: "bla"})).rejects.toThrow( - "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("supports any objects", async () => { - const {code, execute} = await getActualFromModel( - {...base, type: "any"}, - config, - ) - - expect(code).toMatchInlineSnapshot('"const x = z.any()"') - - 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, - ) - - expect(code).toMatchInlineSnapshot( - '"const x = z.record(z.string(), z.any())"', - ) - - await expect(execute({key: 1})).resolves.toEqual({ - key: 1, - }) - await expect(execute({key: "string"})).resolves.toEqual({ - key: "string", - }) - await expect(execute(123)).rejects.toThrow( - "Invalid input: expected record, received number", - ) - }) - - 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, - ) - - expect(code).toMatchInlineSnapshot( - `"const x = z.record(z.string(), z.unknown())"`, - ) - - await expect(execute({key: 1})).resolves.toEqual({ - key: 1, - }) - await expect(execute({key: "string"})).resolves.toEqual({ - key: "string", - }) - await expect(execute(123)).rejects.toThrow( - "Invalid input: expected record, received number", - ) - }) - - 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.unknown()))"`, - ) - - 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", - ) - }) - }) }) 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 dfa7c162..466028ea 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 @@ -18,317 +18,17 @@ import { } from "./schema-builder.test-utils" import {staticSchemas} from "./zod-v4-schema-builder" -describe.each( - testVersions, -)("%s - typescript/common/schema-builders/zod-v4-schema-builder", (version) => { +describe("typescript/common/schema-builders/zod-v4-schema-builder - unit tests", () => { const executeParseSchema = async (code: string) => { return vm.runInNewContext(code, {z: require("zod/v4").z, RegExp}) } const {getActualFromModel, getActual} = schemaBuilderTestHarness( "zod-v4", - version, + testVersions[0], executeParseSchema, ) - it("supports the SimpleObject", async () => { - const {code, schemas} = await getActual("components/schemas/SimpleObject") - - expect(code).toMatchInlineSnapshot(` - "import { s_SimpleObject } from "./unit-test.schemas" - - const x = s_SimpleObject" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_SimpleObject = z.object({ - str: z.string(), - num: z.coerce.number(), - date: z.string(), - datetime: z.iso.datetime({ offset: true }), - optional_str: z.string().optional(), - required_nullable: z.string().nullable(), - $ref: z.string().optional(), - })" - `) - }) - - it("supports the ObjectWithComplexProperties", async () => { - const {code, schemas} = await getActual( - "components/schemas/ObjectWithComplexProperties", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_ObjectWithComplexProperties } from "./unit-test.schemas" - - const x = s_ObjectWithComplexProperties" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const PermissiveBoolean = z.preprocess((value) => { - if (typeof value === "string" && (value === "true" || value === "false")) { - return value === "true" - } else if (typeof value === "number" && (value === 1 || value === 0)) { - return value === 1 - } - return value - }, z.boolean()) - - export const s_AString = z.string() - - export const s_OneOf = z.union([ - z.object({ strs: z.array(z.string()) }), - z.array(z.string()), - z.string(), - ]) - - export const s_ObjectWithComplexProperties = z.object({ - requiredOneOf: z.union([z.string(), z.coerce.number()]), - requiredOneOfRef: s_OneOf, - optionalOneOf: z.union([z.string(), z.coerce.number()]).optional(), - optionalOneOfRef: s_OneOf.optional(), - nullableSingularOneOf: PermissiveBoolean.nullable().optional(), - nullableSingularOneOfRef: s_AString.nullable().optional(), - })" - `) - }) - - it("supports unions / oneOf", async () => { - const {code, schemas} = await getActual("components/schemas/OneOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_OneOf } from "./unit-test.schemas" - - const x = s_OneOf" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_OneOf = z.union([ - z.object({ strs: z.array(z.string()) }), - z.array(z.string()), - z.string(), - ])" - `) - }) - - it("supports unions / anyOf", async () => { - const {code, schemas} = await getActual("components/schemas/AnyOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_AnyOf } from "./unit-test.schemas" - - const x = s_AnyOf" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_AnyOf = z.union([z.coerce.number(), z.string()])" - `) - }) - - it("supports allOf", async () => { - const {code, schemas} = await getActual("components/schemas/AllOf") - - expect(code).toMatchInlineSnapshot(` - "import { s_AllOf } from "./unit-test.schemas" - - const x = s_AllOf" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_Base = z.object({ - name: z.string(), - breed: z.string().optional(), - }) - - export const s_AllOf = s_Base.merge(z.object({ id: z.coerce.number() }))" - `) - }) - - it("supports recursion", async () => { - const {code, schemas} = await getActual("components/schemas/Recursive") - - expect(code).toMatchInlineSnapshot(` - "import { s_Recursive } from "./unit-test.schemas" - - const x = z.lazy(() => s_Recursive)" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - import type { t_Recursive } from "./unit-test.types" - - export const s_Recursive: z.ZodType = z.object({ - child: z.lazy(() => s_Recursive.optional()), - })" - `) - }) - - it("orders schemas such that dependencies are defined first", async () => { - const {code, schemas} = await getActual("components/schemas/Ordering") - - expect(code).toMatchInlineSnapshot(` - "import { s_Ordering } from "./unit-test.schemas" - - const x = s_Ordering" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_AOrdering = z.object({ name: z.string().optional() }) - - export const s_ZOrdering = z.object({ - name: z.string().optional(), - dependency1: s_AOrdering, - }) - - export const s_Ordering = z.object({ - dependency1: s_ZOrdering, - dependency2: s_AOrdering, - })" - `) - }) - - it("supports string and numeric enums", async () => { - const {code, schemas} = await getActual("components/schemas/Enums") - - expect(code).toMatchInlineSnapshot(` - "import { s_Enums } from "./unit-test.schemas" - - const x = s_Enums" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_Enums = z.object({ - str: z.enum(["foo", "bar"]).nullable().optional(), - num: z - .union([z.literal(10), z.literal(20)]) - .nullable() - .optional(), - })" - `) - }) - - describe("additionalProperties", () => { - it("handles additionalProperties set to true", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesBool", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesBool } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesBool" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_AdditionalPropertiesBool = z.record(z.string(), z.unknown())" - `) - }) - - it("handles additionalProperties set to {}", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesUnknownEmptySchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptySchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesUnknownEmptySchema" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_AdditionalPropertiesUnknownEmptySchema = z.record( - z.string(), - z.unknown(), - )" - `) - }) - - it("handles additionalProperties set to {type: 'object'}", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesUnknownEmptyObjectSchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesUnknownEmptyObjectSchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesUnknownEmptyObjectSchema" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_AdditionalPropertiesUnknownEmptyObjectSchema = z.record( - z.string(), - z.record(z.string(), z.unknown()), - )" - `) - }) - - it("handles additionalProperties specifying a schema", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesSchema", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesSchema } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesSchema" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_NamedNullableStringEnum = z - .enum(["", "one", "two", "three"]) - .nullable() - - export const s_AdditionalPropertiesSchema = z.record( - z.string(), - s_NamedNullableStringEnum, - )" - `) - }) - - it("handles additionalProperties in conjunction with properties", async () => { - const {code, schemas} = await getActual( - "components/schemas/AdditionalPropertiesMixed", - ) - - expect(code).toMatchInlineSnapshot(` - "import { s_AdditionalPropertiesMixed } from "./unit-test.schemas" - - const x = s_AdditionalPropertiesMixed" - `) - - expect(schemas).toMatchInlineSnapshot(` - "import { z } from "zod/v4" - - export const s_AdditionalPropertiesMixed = z.intersection( - z.object({ id: z.string().optional(), name: z.string().optional() }), - z.record(z.string(), z.unknown()), - )" - `) - }) - }) - describe("numbers", () => { const base: SchemaNumber = { nullable: false,