From da20fc94c33649f5ba26651ea3c2841ba1f9dfac Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 08:03:00 -0800 Subject: [PATCH 01/14] Simplify minItems / maxItems tuple generation Closes #2048 --- .changeset/clean-phones-deliver.md | 5 +++ .../src/transform/schema-object.ts | 40 ++++++------------- .../transform/schema-object/array.test.ts | 16 ++++++++ 3 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 .changeset/clean-phones-deliver.md diff --git a/.changeset/clean-phones-deliver.md b/.changeset/clean-phones-deliver.md new file mode 100644 index 000000000..878466f42 --- /dev/null +++ b/.changeset/clean-phones-deliver.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": patch +--- + +Simplify minItems / maxItems tuple generation. diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 80ec56b51..6b9836a00 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -339,40 +339,26 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems ? schemaObject.maxItems : undefined; - const estimateCodeSize = typeof max !== "number" ? min : (max * (max + 1) - min * (min - 1)) / 2; + const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2; if ( options.ctx.arrayLength && (min !== 0 || max !== undefined) && estimateCodeSize < 30 // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice ) { - if (min === max) { - const elements: ts.TypeNode[] = []; - for (let i = 0; i < min; i++) { - elements.push(itemType); - } - return tsUnion([ts.factory.createTupleTypeNode(elements)]); - } else if ((schemaObject.maxItems as number) > 0) { - // if maxItems is set, then return a union of all permutations of possible tuple types - const members: ts.TypeNode[] = []; - // populate 1 short of min … - for (let i = 0; i <= (max ?? 0) - min; i++) { - const elements: ts.TypeNode[] = []; - for (let j = min; j < i + min; j++) { - elements.push(itemType); - } - members.push(ts.factory.createTupleTypeNode(elements)); - } - return tsUnion(members); - } // if maxItems not set, then return a simple tuple type the length of `min` - else { - const elements: ts.TypeNode[] = []; - for (let i = 0; i < min; i++) { - elements.push(itemType); - } - elements.push(ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType))); - return ts.factory.createTupleTypeNode(elements); + if (max === undefined) { + return ts.factory.createTupleTypeNode([ + ...Array.from({ length: min }).map(() => itemType), + ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)), + ]); } + + // if maxItems is set, then return a union of all permutations of possible tuple types + return tsUnion( + Array.from({ length: max === undefined ? min : max - min + 1 }).map((_, index) => + ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)), + ), + ); } const finalType = diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 59a2fddb7..8b48b0b0e 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -128,6 +128,22 @@ describe("transformSchemaObject > array", () => { ] | [ string, string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > minItems: 1, maxItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 2 }, + want: `[ + string +] | [ + string, + string ]`, options: { ...DEFAULT_OPTIONS, From d5673f5be208b6993ced87ca9d1085395c5dad9f Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 09:13:37 -0800 Subject: [PATCH 02/14] fixup! Simplify minItems / maxItems tuple generation Account for immutable: true --- .../src/transform/schema-object.ts | 40 ++++--- .../transform/schema-object/array.test.ts | 110 ++++++++++++++++++ 2 files changed, 131 insertions(+), 19 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 6b9836a00..492dd1a4c 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -340,31 +340,33 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor ? schemaObject.maxItems : undefined; const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2; - if ( - options.ctx.arrayLength && - (min !== 0 || max !== undefined) && - estimateCodeSize < 30 // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice - ) { - // if maxItems not set, then return a simple tuple type the length of `min` - if (max === undefined) { - return ts.factory.createTupleTypeNode([ - ...Array.from({ length: min }).map(() => itemType), - ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)), - ]); - } - // if maxItems is set, then return a union of all permutations of possible tuple types + // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice + const MAX_CODE_SIZE = 30; + const shouldGeneratePermutations = + options.ctx.arrayLength && (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE; + + if (shouldGeneratePermutations && max !== undefined) { return tsUnion( - Array.from({ length: max === undefined ? min : max - min + 1 }).map((_, index) => - ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)), - ), + Array.from({ length: max === undefined ? min : max - min + 1 }).map((_, index) => { + const tupleType = ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)); + return options.ctx.immutable + ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) + : tupleType; + }), ); } - const finalType = - ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) + const finalType = shouldGeneratePermutations + ? // if maxItems not set, then return a simple tuple type the length of `min` + ts.factory.createTupleTypeNode([ + ...Array.from({ length: min }).map(() => itemType), + ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)), + ]) + : // wrap itemType in array type, but only if not a tuple or array already + ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) ? itemType - : ts.factory.createArrayTypeNode(itemType); // wrap itemType in array type, but only if not a tuple or array already + : ts.factory.createArrayTypeNode(itemType); return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType) diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 8b48b0b0e..bad2f2d5f 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -104,6 +104,28 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true > minItems: 0", + { + given: { type: "array", items: { type: "string" }, minItems: 0 }, + want: "string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > minItems: 0", + { + given: { type: "array", items: { type: "string" }, minItems: 0 }, + want: "readonly string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > minItems: 1", { @@ -118,6 +140,50 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > minItems: 1", + { + given: { type: "array", items: { type: "string" }, minItems: 1 }, + want: `readonly [ + string, + ...string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: true > minItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 2 }, + want: `[ + string, + string, + ...string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > minItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 2 }, + want: `readonly [ + string, + string, + ...string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > maxItems: 2", { @@ -135,6 +201,23 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > maxItems: 2", + { + given: { type: "array", items: { type: "string" }, maxItems: 2 }, + want: `readonly [ +] | readonly [ + string +] | readonly [ + string, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > minItems: 1, maxItems: 2", { @@ -151,6 +234,22 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > minItems: 1, maxItems: 2", + { + given: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 2 }, + want: `readonly [ + string +] | readonly [ + string, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > maxItems: 20", { @@ -162,6 +261,17 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > maxItems: 20", + { + given: { type: "array", items: { type: "string" }, maxItems: 20 }, + want: "readonly string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > arrayLength: true > minItems: 2, maxItems: 2", { From d9be90f07a17b6ba1c62c10bf8caaa4313ebf04a Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 09:24:38 -0800 Subject: [PATCH 03/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 14 ++++++++++---- .../test/transform/schema-object/array.test.ts | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 492dd1a4c..a1e1a541f 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -357,16 +357,22 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor ); } + // if maxItems not set, then return a simple tuple type the length of `min` + const restType = ts.factory.createArrayTypeNode(itemType); + const finalType = shouldGeneratePermutations - ? // if maxItems not set, then return a simple tuple type the length of `min` - ts.factory.createTupleTypeNode([ + ? ts.factory.createTupleTypeNode([ ...Array.from({ length: min }).map(() => itemType), - ts.factory.createRestTypeNode(ts.factory.createArrayTypeNode(itemType)), + ts.factory.createRestTypeNode( + options.ctx.immutable + ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, restType) + : restType, + ), ]) : // wrap itemType in array type, but only if not a tuple or array already ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) ? itemType - : ts.factory.createArrayTypeNode(itemType); + : restType; return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType) diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index bad2f2d5f..17e3a0fb5 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -146,7 +146,7 @@ describe("transformSchemaObject > array", () => { given: { type: "array", items: { type: "string" }, minItems: 1 }, want: `readonly [ string, - ...string[] + ...readonly string[] ]`, options: { ...DEFAULT_OPTIONS, @@ -176,7 +176,7 @@ describe("transformSchemaObject > array", () => { want: `readonly [ string, string, - ...string[] + ...readonly string[] ]`, options: { ...DEFAULT_OPTIONS, From 00437509de81818d70c99aa67927c1583db43d5b Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 09:26:11 -0800 Subject: [PATCH 04/14] fixup! Simplify minItems / maxItems tuple generation --- packages/openapi-typescript/src/transform/schema-object.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index a1e1a541f..9744d932d 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -348,7 +348,7 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor if (shouldGeneratePermutations && max !== undefined) { return tsUnion( - Array.from({ length: max === undefined ? min : max - min + 1 }).map((_, index) => { + Array.from({ length: max - min + 1 }).map((_, index) => { const tupleType = ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)); return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) From bdd4dc50cc387f26e3d4a141dab9146a9aed7867 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 09:43:19 -0800 Subject: [PATCH 05/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 131 +++++++++--------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 9744d932d..9c81c0149 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -25,7 +25,7 @@ import { tsWithRequired, } from "../lib/ts.js"; import { createDiscriminatorProperty, createRef, getEntries } from "../lib/utils.js"; -import type { ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js"; +import type { ArraySubtype, ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js"; /** * Transform SchemaObject nodes (4.8.24) @@ -277,6 +277,72 @@ export function transformSchemaObjectWithComposition( } } +type ArraySchemaObject = SchemaObject & ArraySubtype; +function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): schemaObject is ArraySchemaObject { + return schemaObject.type === "array"; +} + +function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode { + // default to `unknown[]` + let itemType: ts.TypeNode = UNKNOWN; + // tuple type + if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) { + const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]); + itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options))); + } + // standard array type + else if (schemaObject.items) { + if ("type" in schemaObject.items && schemaObject.items.type === "array") { + itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options)); + } else { + itemType = transformSchemaObject(schemaObject.items, options); + } + } + + const min: number = + typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0; + const max: number | undefined = + typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems + ? schemaObject.maxItems + : undefined; + const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2; + + // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice + const MAX_CODE_SIZE = 30; + const shouldGeneratePermutations = + options.ctx.arrayLength && (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE; + + if (shouldGeneratePermutations && max !== undefined) { + return tsUnion( + Array.from({ length: max - min + 1 }).map((_, index) => { + const tupleType = ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)); + return options.ctx.immutable + ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) + : tupleType; + }), + ); + } + + // if maxItems not set, then return a simple tuple type the length of `min` + const restType = ts.factory.createArrayTypeNode(itemType); + + const finalType = shouldGeneratePermutations + ? ts.factory.createTupleTypeNode([ + ...Array.from({ length: min }).map(() => itemType), + ts.factory.createRestTypeNode( + options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, restType) : restType, + ), + ]) + : // wrap itemType in array type, but only if not a tuple or array already + ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) + ? itemType + : restType; + + return options.ctx.immutable + ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType) + : finalType; +} + /** * Handle SchemaObject minus composition (anyOf/allOf/oneOf) */ @@ -316,67 +382,8 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor } // type: array (with support for tuples) - if (schemaObject.type === "array") { - // default to `unknown[]` - let itemType: ts.TypeNode = UNKNOWN; - // tuple type - if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) { - const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]); - itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options))); - } - // standard array type - else if (schemaObject.items) { - if ("type" in schemaObject.items && schemaObject.items.type === "array") { - itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options)); - } else { - itemType = transformSchemaObject(schemaObject.items, options); - } - } - - const min: number = - typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0; - const max: number | undefined = - typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems - ? schemaObject.maxItems - : undefined; - const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2; - - // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice - const MAX_CODE_SIZE = 30; - const shouldGeneratePermutations = - options.ctx.arrayLength && (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE; - - if (shouldGeneratePermutations && max !== undefined) { - return tsUnion( - Array.from({ length: max - min + 1 }).map((_, index) => { - const tupleType = ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)); - return options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) - : tupleType; - }), - ); - } - - // if maxItems not set, then return a simple tuple type the length of `min` - const restType = ts.factory.createArrayTypeNode(itemType); - - const finalType = shouldGeneratePermutations - ? ts.factory.createTupleTypeNode([ - ...Array.from({ length: min }).map(() => itemType), - ts.factory.createRestTypeNode( - options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, restType) - : restType, - ), - ]) - : // wrap itemType in array type, but only if not a tuple or array already - ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) - ? itemType - : restType; - - return options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType) - : finalType; + if (isArraySchemaObject(schemaObject)) { + return transformArraySchemaObject(schemaObject, options); } // polymorphic, or 3.1 nullable From ebf298c0ccf62a1da75fec8525e6bdd1fe94dc5d Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 09:51:45 -0800 Subject: [PATCH 06/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 9c81c0149..7e97806fe 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -282,6 +282,7 @@ function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): sc return schemaObject.type === "array"; } +/* Transform Array schema object */ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode { // default to `unknown[]` let itemType: ts.TypeNode = UNKNOWN; @@ -312,7 +313,20 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr const shouldGeneratePermutations = options.ctx.arrayLength && (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE; - if (shouldGeneratePermutations && max !== undefined) { + if (!shouldGeneratePermutations) { + // wrap itemType in array type, but only if not a tuple or array already + const tupleType = + ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) + ? itemType + : ts.factory.createArrayTypeNode(itemType); + + return options.ctx.immutable + ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) + : tupleType; + } + + // if maxItems is set, then return a union of all permutations of possible tuple types + if (max !== undefined) { return tsUnion( Array.from({ length: max - min + 1 }).map((_, index) => { const tupleType = ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)); @@ -325,22 +339,16 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr // if maxItems not set, then return a simple tuple type the length of `min` const restType = ts.factory.createArrayTypeNode(itemType); - - const finalType = shouldGeneratePermutations - ? ts.factory.createTupleTypeNode([ - ...Array.from({ length: min }).map(() => itemType), - ts.factory.createRestTypeNode( - options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, restType) : restType, - ), - ]) - : // wrap itemType in array type, but only if not a tuple or array already - ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) - ? itemType - : restType; + const tupleType = ts.factory.createTupleTypeNode([ + ...Array.from({ length: min }).map(() => itemType), + ts.factory.createRestTypeNode( + options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, restType) : restType, + ), + ]); return options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, finalType) - : finalType; + ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) + : tupleType; } /** From 1b26413d927e8294cbbfbcfc227f086c27d1b9f6 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 12:11:53 -0800 Subject: [PATCH 07/14] fixup! Simplify minItems / maxItems tuple generation --- .changeset/clean-phones-deliver.md | 8 ++- .../src/transform/schema-object.ts | 71 ++++++++----------- .../transform/schema-object/array.test.ts | 60 +++++++++++++--- 3 files changed, 85 insertions(+), 54 deletions(-) diff --git a/.changeset/clean-phones-deliver.md b/.changeset/clean-phones-deliver.md index 878466f42..e630133c8 100644 --- a/.changeset/clean-phones-deliver.md +++ b/.changeset/clean-phones-deliver.md @@ -1,5 +1,9 @@ --- -"openapi-typescript": patch +"openapi-typescript": major --- -Simplify minItems / maxItems tuple generation. +Generate heterogeneous array types. +Generate readonly tuple spread when. +Generate tuple types with prefix items. +Stop generating fixed-length tuple types when no minItems or maxItems is present. +Stop generating empty tuple type when arrayLength: true and minItems: 1. diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 7e97806fe..f49373aa4 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -284,52 +284,39 @@ function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): sc /* Transform Array schema object */ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode { - // default to `unknown[]` - let itemType: ts.TypeNode = UNKNOWN; - // tuple type - if (schemaObject.prefixItems || Array.isArray(schemaObject.items)) { - const prefixItems = schemaObject.prefixItems ?? (schemaObject.items as (SchemaObject | ReferenceObject)[]); - itemType = ts.factory.createTupleTypeNode(prefixItems.map((item) => transformSchemaObject(item, options))); - } - // standard array type - else if (schemaObject.items) { - if ("type" in schemaObject.items && schemaObject.items.type === "array") { - itemType = ts.factory.createArrayTypeNode(transformSchemaObject(schemaObject.items, options)); - } else { - itemType = transformSchemaObject(schemaObject.items, options); - } - } - - const min: number = - typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0; + const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options)); + + const itemType = + (Array.isArray(schemaObject.items) + ? tsUnion(schemaObject.items.map((item) => transformSchemaObject(item, options))) + : schemaObject.items + ? transformSchemaObject(schemaObject.items, options) + : undefined) ?? UNKNOWN; + + const min: number = Math.max( + typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0, + prefixTypes.length, + ); const max: number | undefined = typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems ? schemaObject.maxItems : undefined; - const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2; // "30" is an arbitrary number but roughly around when TS starts to struggle with tuple inference in practice const MAX_CODE_SIZE = 30; - const shouldGeneratePermutations = - options.ctx.arrayLength && (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE; - - if (!shouldGeneratePermutations) { - // wrap itemType in array type, but only if not a tuple or array already - const tupleType = - ts.isTupleTypeNode(itemType) || ts.isArrayTypeNode(itemType) - ? itemType - : ts.factory.createArrayTypeNode(itemType); - - return options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) - : tupleType; - } + const estimateCodeSize = max === undefined ? min : (max * (max + 1) - min * (min - 1)) / 2; + const shouldGeneratePermutations = (min !== 0 || max !== undefined) && estimateCodeSize < MAX_CODE_SIZE; // if maxItems is set, then return a union of all permutations of possible tuple types - if (max !== undefined) { + if (shouldGeneratePermutations && max !== undefined) { return tsUnion( - Array.from({ length: max - min + 1 }).map((_, index) => { - const tupleType = ts.factory.createTupleTypeNode(Array.from({ length: index + min }).map(() => itemType)); + Array.from({ length: max - min + 1 }).map((_, index1) => { + const tupleType = ts.factory.createTupleTypeNode( + Array.from({ length: index1 + min }).map((_, index2) => { + return prefixTypes[index2] ?? itemType; + }), + ); + return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) : tupleType; @@ -338,13 +325,15 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr } // if maxItems not set, then return a simple tuple type the length of `min` - const restType = ts.factory.createArrayTypeNode(itemType); - const tupleType = ts.factory.createTupleTypeNode([ - ...Array.from({ length: min }).map(() => itemType), + const spreadType = ts.factory.createArrayTypeNode(itemType); + const tupleType = shouldGeneratePermutations ? ts.factory.createTupleTypeNode([ + ...Array.from({ length: min }).map((_, index) => { + return prefixTypes[index] ?? itemType; + }), ts.factory.createRestTypeNode( - options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, restType) : restType, + options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, spreadType) : spreadType, ), - ]); + ]) : spreadType; return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 17e3a0fb5..a5cfaf389 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -19,19 +19,13 @@ describe("transformSchemaObject > array", () => { }, ], [ - "tuple > tuple items", + "array > heterogeneous items", { given: { type: "array", - items: [{ type: "string" }, { type: "number" }], - minItems: 2, - maxItems: 2, + items: [{ type: "number" }, { type: "string" }], }, - want: `[ - string, - number -]`, - // options: DEFAULT_OPTIONS, + want: "(number | string)[]", }, ], [ @@ -45,7 +39,8 @@ describe("transformSchemaObject > array", () => { want: `[ number, number, - number + number, + ...number[] ]`, // options: DEFAULT_OPTIONS, }, @@ -286,6 +281,48 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: true, immutable: true > prefixItems, minItems: 3", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + }, + want: `readonly [ + string, + number, + string, + ...readonly string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], + [ + "options > arrayLength: true, immutable: true > prefixItems, minItems: 3, maxItems: 3", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + maxItems: 3, + }, + want: `readonly [ + string, + number, + string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true, immutable: true }, + }, + }, + ], [ "options > immutable: true", { @@ -311,7 +348,8 @@ describe("transformSchemaObject > array", () => { want: `readonly [ number, number, - number + number, + ...readonly number[] ]`, options: { ...DEFAULT_OPTIONS, From e85c00475bf3d8962bde09ed722b7b3040474c52 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 12:45:19 -0800 Subject: [PATCH 08/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 31 ++++++++++--------- .../transform/schema-object/array.test.ts | 23 +++++++++++++- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index f49373aa4..251c88653 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -286,12 +286,11 @@ function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): sc function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode { const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options)); - const itemType = - (Array.isArray(schemaObject.items) - ? tsUnion(schemaObject.items.map((item) => transformSchemaObject(item, options))) - : schemaObject.items - ? transformSchemaObject(schemaObject.items, options) - : undefined) ?? UNKNOWN; + if (Array.isArray(schemaObject.items)) { + throw new Error(`${options.path}: invalid property items. Expected Schema Object, got Array`); + } + + const itemType = (schemaObject.items ? transformSchemaObject(schemaObject.items, options) : undefined) ?? UNKNOWN; const min: number = Math.max( typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0, @@ -326,14 +325,18 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr // if maxItems not set, then return a simple tuple type the length of `min` const spreadType = ts.factory.createArrayTypeNode(itemType); - const tupleType = shouldGeneratePermutations ? ts.factory.createTupleTypeNode([ - ...Array.from({ length: min }).map((_, index) => { - return prefixTypes[index] ?? itemType; - }), - ts.factory.createRestTypeNode( - options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, spreadType) : spreadType, - ), - ]) : spreadType; + const tupleType = shouldGeneratePermutations + ? ts.factory.createTupleTypeNode([ + ...Array.from({ length: min }).map((_, index) => { + return prefixTypes[index] ?? itemType; + }), + ts.factory.createRestTypeNode( + options.ctx.immutable + ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, spreadType) + : spreadType, + ), + ]) + : spreadType; return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index a5cfaf389..2a6059582 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -23,7 +23,7 @@ describe("transformSchemaObject > array", () => { { given: { type: "array", - items: [{ type: "number" }, { type: "string" }], + items: { anyOf: [{ type: "number" }, { type: "string" }] }, }, want: "(number | string)[]", }, @@ -373,4 +373,25 @@ describe("transformSchemaObject > array", () => { ci?.timeout, ); } + + const invalidTests: TestCase[] = [ + [ + "error > items is array", + { + given: { + type: "array", + items: [{ type: "number" }, { type: "string" }], + }, + want: '#/components/schemas/schema-object: invalid property items. Expected Schema Object, got Array', + }, + ], + ]; + + for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of invalidTests) { + test.skipIf(ci?.skipIf)(testName, () => { + expect(() => { + transformSchemaObject(given, options); + }).toThrowError(want.toString()); + }); + } }); From a99c69ed0b1c5d8dab233e61eac650c3d45d8624 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 12:53:34 -0800 Subject: [PATCH 09/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 251c88653..c27d2015d 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -282,6 +282,12 @@ function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): sc return schemaObject.type === "array"; } +function padTupleMembers(length: number, itemType: ts.TypeNode, prefixTypes: readonly ts.TypeNode[]) { + return Array.from({ length }).map((_, index2) => { + return prefixTypes[index2] ?? itemType; + }); +} + /* Transform Array schema object */ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode { const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options)); @@ -310,12 +316,7 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr if (shouldGeneratePermutations && max !== undefined) { return tsUnion( Array.from({ length: max - min + 1 }).map((_, index1) => { - const tupleType = ts.factory.createTupleTypeNode( - Array.from({ length: index1 + min }).map((_, index2) => { - return prefixTypes[index2] ?? itemType; - }), - ); - + const tupleType = ts.factory.createTupleTypeNode(padTupleMembers(index1 + min, itemType, prefixTypes)); return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) : tupleType; @@ -327,9 +328,7 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr const spreadType = ts.factory.createArrayTypeNode(itemType); const tupleType = shouldGeneratePermutations ? ts.factory.createTupleTypeNode([ - ...Array.from({ length: min }).map((_, index) => { - return prefixTypes[index] ?? itemType; - }), + ...padTupleMembers(min, itemType, prefixTypes), ts.factory.createRestTypeNode( options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, spreadType) From 3ec89cbbf7ea901b67f37b28cf98139824f91f8a Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 13:45:11 -0800 Subject: [PATCH 10/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 35 ++++++++++--------- .../transform/schema-object/array.test.ts | 22 ++++++++++++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index c27d2015d..b0ebb0124 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -283,11 +283,18 @@ function isArraySchemaObject(schemaObject: SchemaObject | ArraySchemaObject): sc } function padTupleMembers(length: number, itemType: ts.TypeNode, prefixTypes: readonly ts.TypeNode[]) { - return Array.from({ length }).map((_, index2) => { - return prefixTypes[index2] ?? itemType; + return Array.from({ length }).map((_, index) => { + return prefixTypes[index] ?? itemType; }); } +function toOptionsReadonly( + members: TMembers, + options: TransformNodeOptions, +): TMembers | ts.TypeOperatorNode { + return options.ctx.immutable ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, members) : members; +} + /* Transform Array schema object */ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: TransformNodeOptions): ts.TypeNode { const prefixTypes = (schemaObject.prefixItems ?? []).map((item) => transformSchemaObject(item, options)); @@ -298,8 +305,11 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr const itemType = (schemaObject.items ? transformSchemaObject(schemaObject.items, options) : undefined) ?? UNKNOWN; + // The minimum number of tuple members to return const min: number = Math.max( - typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems : 0, + options.ctx.arrayLength && typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 + ? schemaObject.minItems + : 0, prefixTypes.length, ); const max: number | undefined = @@ -315,12 +325,9 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr // if maxItems is set, then return a union of all permutations of possible tuple types if (shouldGeneratePermutations && max !== undefined) { return tsUnion( - Array.from({ length: max - min + 1 }).map((_, index1) => { - const tupleType = ts.factory.createTupleTypeNode(padTupleMembers(index1 + min, itemType, prefixTypes)); - return options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) - : tupleType; - }), + Array.from({ length: max - min + 1 }).map((_, index) => + toOptionsReadonly(ts.factory.createTupleTypeNode(padTupleMembers(index + min, itemType, prefixTypes)), options), + ), ); } @@ -329,17 +336,11 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr const tupleType = shouldGeneratePermutations ? ts.factory.createTupleTypeNode([ ...padTupleMembers(min, itemType, prefixTypes), - ts.factory.createRestTypeNode( - options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, spreadType) - : spreadType, - ), + ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)), ]) : spreadType; - return options.ctx.immutable - ? ts.factory.createTypeOperatorNode(ts.SyntaxKind.ReadonlyKeyword, tupleType) - : tupleType; + return toOptionsReadonly(tupleType, options); } /** diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 2a6059582..4692fd96a 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -88,6 +88,28 @@ describe("transformSchemaObject > array", () => { // options: DEFAULT_OPTIONS, }, ], + [ + "options > arrayLength: false > minItems: 0", + { + given: { type: "array", items: { type: "string" }, minItems: 0 }, + want: "string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: false }, + }, + }, + ], + [ + "options > arrayLength: false > minItems: 1", + { + given: { type: "array", items: { type: "string" }, minItems: 1 }, + want: "string[]", + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: false }, + }, + }, + ], [ "options > arrayLength: true > default", { From c7c8f83e594f68a07ddebc878beafdad3033c0b0 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 14:35:18 -0800 Subject: [PATCH 11/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 5 +++- .../transform/schema-object/array.test.ts | 23 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index b0ebb0124..1bc50cdf1 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -313,7 +313,10 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr prefixTypes.length, ); const max: number | undefined = - typeof schemaObject.maxItems === "number" && schemaObject.maxItems >= 0 && min <= schemaObject.maxItems + options.ctx.arrayLength && + typeof schemaObject.maxItems === "number" && + schemaObject.maxItems >= 0 && + min <= schemaObject.maxItems ? schemaObject.maxItems : undefined; diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 4692fd96a..8906f937e 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -345,6 +345,27 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: false, immutable: true > prefixItems, minItems: 3, maxItems: 5", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [{ type: "string" }, { type: "number" }], + minItems: 3, + maxItems: 5, + }, + want: `readonly [ + string, + number, + ...readonly string[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: false, immutable: true }, + }, + }, + ], [ "options > immutable: true", { @@ -404,7 +425,7 @@ describe("transformSchemaObject > array", () => { type: "array", items: [{ type: "number" }, { type: "string" }], }, - want: '#/components/schemas/schema-object: invalid property items. Expected Schema Object, got Array', + want: "#/components/schemas/schema-object: invalid property items. Expected Schema Object, got Array", }, ], ]; From 6a8c6aaccb524809a8470a557abf8fc1b7b1fd66 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 15:01:57 -0800 Subject: [PATCH 12/14] fixup! Simplify minItems / maxItems tuple generation --- .../openapi-typescript/examples/simple-example.ts | 3 ++- .../src/transform/schema-object.ts | 2 +- .../test/transform/schema-object/array.test.ts | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/openapi-typescript/examples/simple-example.ts b/packages/openapi-typescript/examples/simple-example.ts index 0c2580003..82a2427ea 100644 --- a/packages/openapi-typescript/examples/simple-example.ts +++ b/packages/openapi-typescript/examples/simple-example.ts @@ -245,7 +245,8 @@ export interface operations { }; content: { "application/json": [ - string + string, + ...unknown[] ]; }; }; diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 1bc50cdf1..efc1cdd91 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -303,7 +303,7 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr throw new Error(`${options.path}: invalid property items. Expected Schema Object, got Array`); } - const itemType = (schemaObject.items ? transformSchemaObject(schemaObject.items, options) : undefined) ?? UNKNOWN; + const itemType = schemaObject.items ? transformSchemaObject(schemaObject.items, options) : UNKNOWN; // The minimum number of tuple members to return const min: number = Math.max( diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 8906f937e..284bfabc3 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -366,6 +366,20 @@ describe("transformSchemaObject > array", () => { }, }, ], + [ + "options > arrayLength: false > prefixItems, items: false", + { + given: { + type: "array", + items: false, + prefixItems: [{ type: "string", enum: ["professor"] }], + }, + want: `[ + "professor", + ...unknown[] +]`, + }, + ], [ "options > immutable: true", { From 71ee871731f2aa621be8a50562fbc6a31c8a707b Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 16:09:53 -0800 Subject: [PATCH 13/14] fixup! Simplify minItems / maxItems tuple generation --- .../src/transform/schema-object.ts | 20 ++-- .../transform/schema-object/array.test.ts | 111 ++++++++++++++++++ .../transform/schema-object/object.test.ts | 6 +- 3 files changed, 124 insertions(+), 13 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index efc1cdd91..db75306fb 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -10,6 +10,7 @@ import { UNDEFINED, UNKNOWN, addJSDocComment, + astToString, oapiRef, tsArrayLiteralExpression, tsEnum, @@ -306,12 +307,10 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr const itemType = schemaObject.items ? transformSchemaObject(schemaObject.items, options) : UNKNOWN; // The minimum number of tuple members to return - const min: number = Math.max( + const min: number = options.ctx.arrayLength && typeof schemaObject.minItems === "number" && schemaObject.minItems >= 0 ? schemaObject.minItems - : 0, - prefixTypes.length, - ); + : 0; const max: number | undefined = options.ctx.arrayLength && typeof schemaObject.maxItems === "number" && @@ -336,12 +335,13 @@ function transformArraySchemaObject(schemaObject: ArraySchemaObject, options: Tr // if maxItems not set, then return a simple tuple type the length of `min` const spreadType = ts.factory.createArrayTypeNode(itemType); - const tupleType = shouldGeneratePermutations - ? ts.factory.createTupleTypeNode([ - ...padTupleMembers(min, itemType, prefixTypes), - ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)), - ]) - : spreadType; + const tupleType = + shouldGeneratePermutations || prefixTypes.length + ? ts.factory.createTupleTypeNode([ + ...padTupleMembers(Math.max(min, prefixTypes.length), itemType, prefixTypes), + ts.factory.createRestTypeNode(toOptionsReadonly(spreadType, options)), + ]) + : spreadType; return toOptionsReadonly(tupleType, options); } diff --git a/packages/openapi-typescript/test/transform/schema-object/array.test.ts b/packages/openapi-typescript/test/transform/schema-object/array.test.ts index 284bfabc3..454ef4ff0 100644 --- a/packages/openapi-typescript/test/transform/schema-object/array.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/array.test.ts @@ -296,6 +296,116 @@ describe("transformSchemaObject > array", () => { want: `[ string, string +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > prefixItems, minItems: 2, maxItems: 2", + { + given: { + type: "array", + items: { type: "string" }, + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + ], + minItems: 2, + maxItems: 2, + }, + want: `[ + "calcium", + "magnesium" +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > no items, prefixItems, minItems: 2, maxItems: 3", + { + given: { + type: "array", + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + ], + minItems: 2, + maxItems: 3, + }, + want: `[ + "calcium", + "magnesium" +] | [ + "calcium", + "magnesium", + unknown +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > no items, prefixItems, minItems: 3", + { + given: { + type: "array", + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + ], + minItems: 3, + }, + want: `[ + "calcium", + "magnesium", + unknown, + ...unknown[] +]`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, + }, + }, + ], + [ + "options > arrayLength: true > no items, prefixItems, minItems: 2, maxItems: 5", + { + given: { + type: "array", + prefixItems: [ + { type: "string", enum: ["calcium"] }, + { type: "string", enum: ["magnesium"] }, + { type: "string", enum: ["tungsten"] }, + ], + minItems: 2, + maxItems: 5, + }, + want: `[ + "calcium", + "magnesium" +] | [ + "calcium", + "magnesium", + "tungsten" +] | [ + "calcium", + "magnesium", + "tungsten", + unknown +] | [ + "calcium", + "magnesium", + "tungsten", + unknown, + unknown ]`, options: { ...DEFAULT_OPTIONS, @@ -421,6 +531,7 @@ describe("transformSchemaObject > array", () => { testName, async () => { const result = astToString(transformSchemaObject(given, options)); + // console.log(result); if (want instanceof URL) { expect(result).toMatchFileSnapshot(fileURLToPath(want)); } else { diff --git a/packages/openapi-typescript/test/transform/schema-object/object.test.ts b/packages/openapi-typescript/test/transform/schema-object/object.test.ts index 60290fc57..801dadb9e 100644 --- a/packages/openapi-typescript/test/transform/schema-object/object.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/object.test.ts @@ -376,7 +376,7 @@ describe("transformSchemaObject > object", () => { }, ], [ - "options > two-dimensional array", + "options > array of tuples", { given: { type: "object", @@ -384,7 +384,7 @@ describe("transformSchemaObject > object", () => { array: { type: "array", items: { - items: [ + prefixItems: [ { type: "string", }, @@ -407,7 +407,7 @@ describe("transformSchemaObject > object", () => { }`, options: { ...DEFAULT_OPTIONS, - ctx: { ...DEFAULT_OPTIONS.ctx }, + ctx: { ...DEFAULT_OPTIONS.ctx, arrayLength: true }, }, }, ], From 68d91912fff9418364005255d251664e4d20583a Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 16 Dec 2024 16:18:24 -0800 Subject: [PATCH 14/14] fixup! Simplify minItems / maxItems tuple generation --- .changeset/clean-phones-deliver.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.changeset/clean-phones-deliver.md b/.changeset/clean-phones-deliver.md index e630133c8..953a0d10e 100644 --- a/.changeset/clean-phones-deliver.md +++ b/.changeset/clean-phones-deliver.md @@ -2,8 +2,10 @@ "openapi-typescript": major --- -Generate heterogeneous array types. -Generate readonly tuple spread when. -Generate tuple types with prefix items. -Stop generating fixed-length tuple types when no minItems or maxItems is present. -Stop generating empty tuple type when arrayLength: true and minItems: 1. +Extract types generation for Array-type schemas to `transformArraySchemaObject` method. +Throw error when OpenAPI `items` is array. +Generate correct number of union members for `minItems` * `maxItems` unions. +Generate readonly tuple members for `minItems` & `maxItems` unions. +Generate readonly spread member for `prefixItems` tuple. +Preserve `prefixItems` type members in `minItems` & `maxItems` tuples. +Generate spread member for `prefixItems` tuple with no `minItems` / `maxItems` constraints.