From 48ffe109e839fe85e089e83ea87e39f5aedf36c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:56:11 +0000 Subject: [PATCH 1/3] Initial plan From 2b82dd54f8b110945b8645d942da57063e653586 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:10:56 +0000 Subject: [PATCH 2/3] Add support for non-empty array types with minItems constraint Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../src/plugins/@hey-api/typescript/plugin.ts | 11 +++++++ packages/openapi-ts/src/tsc/index.ts | 1 + packages/openapi-ts/src/tsc/typedef.ts | 31 +++++++++++++++++++ packages/openapi-ts/src/tsc/types.ts | 3 ++ packages/openapi-ts/src/utils/type.ts | 15 +++++++++ 5 files changed, 61 insertions(+) diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts index 78ac210c8f..a119c6cab4 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts @@ -42,6 +42,17 @@ const arrayTypeToIdentifier = ({ itemTypes.push(type); } + // Handle non-empty arrays with minItems >= 1 and no maxItems + if ( + schema.minItems && + schema.minItems >= 1 && + !schema.maxItems && + schema.minItems <= 100 && + itemTypes.length === 1 + ) { + return tsc.nonEmptyArrayTupleNode(itemTypes[0]!, schema.minItems); + } + if (itemTypes.length === 1) { return tsc.typeArrayNode(itemTypes[0]!); } diff --git a/packages/openapi-ts/src/tsc/index.ts b/packages/openapi-ts/src/tsc/index.ts index 916a571a3e..7f087a112c 100644 --- a/packages/openapi-ts/src/tsc/index.ts +++ b/packages/openapi-ts/src/tsc/index.ts @@ -44,6 +44,7 @@ export const tsc = { namespaceDeclaration: types.createNamespaceDeclaration, newExpression: types.createNewExpression, nodeToString: utils.tsNodeToString, + nonEmptyArrayTupleNode: typedef.createNonEmptyArrayTupleNode, null: types.createNull, objectExpression: types.createObjectType, ots: utils.ots, diff --git a/packages/openapi-ts/src/tsc/typedef.ts b/packages/openapi-ts/src/tsc/typedef.ts index 600e1179c9..9fbac31a3e 100644 --- a/packages/openapi-ts/src/tsc/typedef.ts +++ b/packages/openapi-ts/src/tsc/typedef.ts @@ -5,6 +5,7 @@ import { createKeywordTypeNode, createMappedTypeNode, createParameterDeclaration, + createRestTypeNode, createStringLiteral, createTypeNode, createTypeParameterDeclaration, @@ -297,3 +298,33 @@ export const createTypeArrayNode = ( }); return maybeNullable({ isNullable, node }); }; + +/** + * Create non-empty array tuple type node. Example `[number, ...number[]]` for minItems=1 + * @param itemType - the item type + * @param minItems - minimum number of items (must be >= 1) + * @param isNullable - if the whole type can be null + * @returns ts.TupleTypeNode | ts.UnionTypeNode + */ +export const createNonEmptyArrayTupleNode = ( + itemType: any | ts.TypeNode, + minItems: number, + isNullable: boolean = false, +) => { + const typeNode = createTypeNode(itemType); + const arrayType = createTypeReferenceNode({ + typeArguments: [typeNode], + typeName: 'Array', + }); + const restElement = createRestTypeNode({ type: arrayType }); + + // Create the required elements: [T, T, ..., ...T[]] + const elements: Array = []; + for (let i = 0; i < minItems; i++) { + elements.push(typeNode); + } + elements.push(restElement); + + const node = ts.factory.createTupleTypeNode(elements); + return maybeNullable({ isNullable, node }); +}; diff --git a/packages/openapi-ts/src/tsc/types.ts b/packages/openapi-ts/src/tsc/types.ts index af869048b1..b4c8fb2b36 100644 --- a/packages/openapi-ts/src/tsc/types.ts +++ b/packages/openapi-ts/src/tsc/types.ts @@ -1130,3 +1130,6 @@ export const createTemplateLiteralType = ({ ); return templateLiteralType; }; + +export const createRestTypeNode = ({ type }: { type: ts.TypeNode }) => + ts.factory.createRestTypeNode(type); diff --git a/packages/openapi-ts/src/utils/type.ts b/packages/openapi-ts/src/utils/type.ts index 351817a0e0..928a2812dc 100644 --- a/packages/openapi-ts/src/utils/type.ts +++ b/packages/openapi-ts/src/utils/type.ts @@ -86,6 +86,21 @@ const typeArray = (model: Model) => { return tuple; } + // Special case for non-empty arrays with minItems >= 1 and no maxItems + if ( + model.export === 'array' && + model.minItems && + model.minItems >= 1 && + !model.maxItems && + model.minItems <= 100 + ) { + return tsc.nonEmptyArrayTupleNode( + toType(model.link), + model.minItems, + model.isNullable, + ); + } + return tsc.typeArrayNode([toType(model.link)], model.isNullable); } From 90f6020f9a877f41e8440d3a2f3fb9d3d533bdeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:16:07 +0000 Subject: [PATCH 3/3] Add test specification and snapshots for minItems array feature Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../openapi-ts-tests/main/test/3.1.x.test.ts | 8 +++ .../3.1.x/min-items-array/index.ts | 3 + .../3.1.x/min-items-array/types.gen.ts | 52 +++++++++++++++++ specs/3.1.x/min-items-array.yaml | 58 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/types.gen.ts create mode 100644 specs/3.1.x/min-items-array.yaml diff --git a/packages/openapi-ts-tests/main/test/3.1.x.test.ts b/packages/openapi-ts-tests/main/test/3.1.x.test.ts index 04f22d55f9..ff551d4852 100644 --- a/packages/openapi-ts-tests/main/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.1.x.test.ts @@ -970,6 +970,14 @@ describe(`OpenAPI ${version}`, () => { }), description: 'anyOf string and binary string', }, + { + config: createConfig({ + input: 'min-items-array.yaml', + output: 'min-items-array', + plugins: ['@hey-api/typescript'], + }), + description: 'generates non-empty array types with minItems constraint', + }, ]; it.each(scenarios)('$description', async ({ config }) => { diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/index.ts new file mode 100644 index 0000000000..b43a5238d8 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type * from './types.gen'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/types.gen.ts new file mode 100644 index 0000000000..2e6e46943e --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/min-items-array/types.gen.ts @@ -0,0 +1,52 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type GetItemsData = { + body?: never; + path?: never; + query?: { + /** + * Non-empty array with minItems=1 + */ + nodeIds?: [ + number, + ...Array + ]; + /** + * Non-empty array with minItems=3 + */ + tags?: [ + string, + string, + string, + ...Array + ]; + /** + * Normal array without minItems + */ + categories?: Array; + }; + url: '/items'; +}; + +export type GetItemsResponses = { + /** + * Success + */ + 200: { + requiredItems?: [ + string, + ...Array + ]; + optionalItems?: Array; + fixedItems?: [ + boolean, + boolean + ]; + }; +}; + +export type GetItemsResponse = GetItemsResponses[keyof GetItemsResponses]; diff --git a/specs/3.1.x/min-items-array.yaml b/specs/3.1.x/min-items-array.yaml new file mode 100644 index 0000000000..0c570cdb41 --- /dev/null +++ b/specs/3.1.x/min-items-array.yaml @@ -0,0 +1,58 @@ +openapi: 3.1.0 +info: + title: MinItems Array Test + version: 1.0.0 +paths: + /items: + get: + operationId: getItems + parameters: + - name: nodeIds + in: query + required: false + schema: + type: array + items: + type: integer + title: Non-empty array with minItems=1 + minItems: 1 + - name: tags + in: query + required: false + schema: + type: array + items: + type: string + title: Non-empty array with minItems=3 + minItems: 3 + - name: categories + in: query + required: false + schema: + type: array + items: + type: string + title: Normal array without minItems + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + requiredItems: + type: array + items: + type: string + minItems: 1 + optionalItems: + type: array + items: + type: number + fixedItems: + type: array + items: + type: boolean + minItems: 2 + maxItems: 2